diff -Nru snapd-2.40/arch/arch.go snapd-2.42.1/arch/arch.go
--- snapd-2.40/arch/arch.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/arch/arch.go 2019-10-30 12:17:43.000000000 +0000
@@ -33,33 +33,30 @@
// change the architecture. This is important to e.g. install
// armhf snaps onto a armhf image that is generated on an amd64
// machine
-var arch = ArchitectureType(ubuntuArchFromGoArch(runtime.GOARCH))
+var arch = ArchitectureType(dpkgArchFromGoArch(runtime.GOARCH))
// SetArchitecture allows overriding the auto detected Architecture
func SetArchitecture(newArch ArchitectureType) {
arch = newArch
}
-// FIXME: rename all Ubuntu*Architecture() to SnapdArchitecture()
-// (or DpkgArchitecture)
-
-// UbuntuArchitecture returns the debian equivalent architecture for the
+// DpkgArchitecture returns the debian equivalent architecture for the
// currently running architecture.
//
// If the architecture does not map any debian architecture, the
// GOARCH is returned.
-func UbuntuArchitecture() string {
+func DpkgArchitecture() string {
return string(arch)
}
-// ubuntuArchFromGoArch maps a go architecture string to the coresponding
-// Ubuntu architecture string.
+// dpkgArchFromGoArch maps a go architecture string to the coresponding
+// Debian equivalent architecture string.
//
// E.g. the go "386" architecture string maps to the ubuntu "i386"
// architecture.
-func ubuntuArchFromGoArch(goarch string) string {
+func dpkgArchFromGoArch(goarch string) string {
goArchMapping := map[string]string{
- // go ubuntu
+ // go dpkg
"386": "i386",
"amd64": "amd64",
"arm": "armhf",
@@ -82,27 +79,27 @@
}
}
- ubuntuArch := goArchMapping[goarch]
- if ubuntuArch == "" {
+ dpkgArch := goArchMapping[goarch]
+ if dpkgArch == "" {
log.Panicf("unknown goarch %q", goarch)
}
- return ubuntuArch
+ return dpkgArch
}
-// UbuntuKernelArchitecture return the debian equivalent architecture
+// DpkgKernelArchitecture returns the debian equivalent architecture
// for the current running kernel. This is usually the same as the
-// UbuntuArchitecture - however there maybe cases that you run e.g.
+// DpkgArchitecture - however there maybe cases that you run e.g.
// a snapd:i386 on an amd64 kernel.
-func UbuntuKernelArchitecture() string {
- return ubuntuArchFromKernelArch(osutil.MachineName())
+func DpkgKernelArchitecture() string {
+ return dpkgArchFromKernelArch(osutil.MachineName())
}
-// ubuntuArchFromkernelArch maps the kernel architecture as reported
+// dpkgArchFromkernelArch maps the kernel architecture as reported
// via uname() to the dpkg architecture
-func ubuntuArchFromKernelArch(utsMachine string) string {
+func dpkgArchFromKernelArch(utsMachine string) string {
kernelArchMapping := map[string]string{
- // kernel ubuntu
+ // kernel dpkg
"i686": "i386",
"x86_64": "amd64",
"armv7l": "armhf",
@@ -115,12 +112,12 @@
"ppc64": "ppc64",
}
- ubuntuArch := kernelArchMapping[utsMachine]
- if ubuntuArch == "" {
+ dpkgArch := kernelArchMapping[utsMachine]
+ if dpkgArch == "" {
log.Panicf("unknown kernel arch %q", utsMachine)
}
- return ubuntuArch
+ return dpkgArch
}
// IsSupportedArchitecture returns true if the system architecture is in the
diff -Nru snapd-2.40/arch/arch_test.go snapd-2.42.1/arch/arch_test.go
--- snapd-2.40/arch/arch_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/arch/arch_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -33,22 +33,24 @@
type ArchTestSuite struct {
}
-func (ts *ArchTestSuite) TestUbuntuArchitecture(c *C) {
- c.Check(ubuntuArchFromGoArch("386"), Equals, "i386")
- c.Check(ubuntuArchFromGoArch("amd64"), Equals, "amd64")
- c.Check(ubuntuArchFromGoArch("arm"), Equals, "armhf")
- c.Check(ubuntuArchFromGoArch("arm64"), Equals, "arm64")
- c.Check(ubuntuArchFromGoArch("ppc64le"), Equals, "ppc64el")
- c.Check(ubuntuArchFromGoArch("ppc64"), Equals, "ppc64")
- c.Check(ubuntuArchFromGoArch("s390x"), Equals, "s390x")
+func (ts *ArchTestSuite) TestArchDpkgArchitecture(c *C) {
+ c.Check(dpkgArchFromGoArch("386"), Equals, "i386")
+ c.Check(dpkgArchFromGoArch("amd64"), Equals, "amd64")
+ c.Check(dpkgArchFromGoArch("arm"), Equals, "armhf")
+ c.Check(dpkgArchFromGoArch("arm64"), Equals, "arm64")
+ c.Check(dpkgArchFromGoArch("ppc64le"), Equals, "ppc64el")
+ c.Check(dpkgArchFromGoArch("ppc64"), Equals, "ppc64")
+ c.Check(dpkgArchFromGoArch("s390x"), Equals, "s390x")
+ c.Check(dpkgArchFromGoArch("ppc"), Equals, "powerpc")
+ c.Check(dpkgArchFromGoArch("ppc64"), Equals, "ppc64")
}
-func (ts *ArchTestSuite) TestSetArchitecture(c *C) {
+func (ts *ArchTestSuite) TestArchSetArchitecture(c *C) {
SetArchitecture("armhf")
- c.Assert(UbuntuArchitecture(), Equals, "armhf")
+ c.Assert(DpkgArchitecture(), Equals, "armhf")
}
-func (ts *ArchTestSuite) TestSupportedArchitectures(c *C) {
+func (ts *ArchTestSuite) TestArchSupportedArchitectures(c *C) {
arch = "armhf"
c.Check(IsSupportedArchitecture([]string{"all"}), Equals, true)
c.Check(IsSupportedArchitecture([]string{"amd64", "armhf", "powerpc"}), Equals, true)
@@ -56,6 +58,7 @@
c.Check(IsSupportedArchitecture([]string{"amd64", "powerpc"}), Equals, false)
arch = "amd64"
+ c.Check(IsSupportedArchitecture([]string{"all"}), Equals, true)
c.Check(IsSupportedArchitecture([]string{"amd64", "armhf", "powerpc"}), Equals, true)
c.Check(IsSupportedArchitecture([]string{"powerpc"}), Equals, false)
}
diff -Nru snapd-2.40/asserts/asserts.go snapd-2.42.1/asserts/asserts.go
--- snapd-2.40/asserts/asserts.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/asserts/asserts.go 2019-10-30 12:17:43.000000000 +0000
@@ -37,6 +37,17 @@
noAuthority typeFlags = iota + 1
)
+// MetaHeaders is a list of headers in assertions which are about the assertion
+// itself.
+var MetaHeaders = [...]string{
+ "type",
+ "format",
+ "authority-id",
+ "revision",
+ "body-length",
+ "sign-key-sha3-384",
+}
+
// AssertionType describes a known assertion type with its name and metadata.
type AssertionType struct {
// Name of the type.
diff -Nru snapd-2.40/asserts/batch.go snapd-2.42.1/asserts/batch.go
--- snapd-2.40/asserts/batch.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/asserts/batch.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,229 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package asserts
+
+import (
+ "fmt"
+ "io"
+ "strings"
+)
+
+// Batch allows to accumulate a set of assertions possibly out of
+// prerequisite order and then add them in one go to an assertion
+// database.
+// Nothing will be committed if there are missing prerequisites, for a full
+// consistency check beforehand there is the Precheck option.
+type Batch struct {
+ bs Backstore
+ added []Assertion
+ // added is in prereq order
+ inPrereqOrder bool
+
+ unsupported func(u *Ref, err error) error
+}
+
+// NewBatch creates a new Batch to accumulate assertions to add in one
+// go to an assertion database.
+// unsupported can be used to ignore/log assertions with unsupported formats,
+// default behavior is to error on them.
+func NewBatch(unsupported func(u *Ref, err error) error) *Batch {
+ if unsupported == nil {
+ unsupported = func(_ *Ref, err error) error {
+ return err
+ }
+ }
+
+ return &Batch{
+ bs: NewMemoryBackstore(),
+ inPrereqOrder: true, // empty list is trivially so
+ unsupported: unsupported,
+ }
+}
+
+// Add one assertion to the batch.
+func (b *Batch) Add(a Assertion) error {
+ b.inPrereqOrder = false
+
+ if !a.SupportedFormat() {
+ err := &UnsupportedFormatError{Ref: a.Ref(), Format: a.Format()}
+ return b.unsupported(a.Ref(), err)
+ }
+ if err := b.bs.Put(a.Type(), a); err != nil {
+ if revErr, ok := err.(*RevisionError); ok {
+ if revErr.Current >= a.Revision() {
+ // we already got something more recent
+ return nil
+ }
+ }
+ return err
+ }
+ b.added = append(b.added, a)
+ return nil
+}
+
+// AddStream adds a stream of assertions to the batch.
+// Returns references to the assertions effectively added.
+func (b *Batch) AddStream(r io.Reader) ([]*Ref, error) {
+ b.inPrereqOrder = false
+
+ start := len(b.added)
+ dec := NewDecoder(r)
+ for {
+ a, err := dec.Decode()
+ if err == io.EOF {
+ break
+ }
+ if err != nil {
+ return nil, err
+ }
+ if err := b.Add(a); err != nil {
+ return nil, err
+ }
+ }
+ added := b.added[start:]
+ if len(added) == 0 {
+ return nil, nil
+ }
+ refs := make([]*Ref, len(added))
+ for i, a := range added {
+ refs[i] = a.Ref()
+ }
+ return refs, nil
+}
+
+// Fetch adds to the batch by invoking fetching to drive an internal
+// Fetcher that was built with trustedDB and retrieve.
+func (b *Batch) Fetch(trustedDB RODatabase, retrieve func(*Ref) (Assertion, error), fetching func(Fetcher) error) error {
+ f := NewFetcher(trustedDB, retrieve, b.Add)
+ return fetching(f)
+}
+
+func (b *Batch) precheck(db *Database) error {
+ db = db.WithStackedBackstore(NewMemoryBackstore())
+ return b.commitTo(db)
+}
+
+type CommitOptions struct {
+ // Precheck indicates whether to do a full consistency check
+ // before starting adding the batch.
+ Precheck bool
+}
+
+// CommitTo adds the batch of assertions to the given assertion database.
+// Nothing will be committed if there are missing prerequisites, for a full
+// consistency check beforehand there is the Precheck option.
+func (b *Batch) CommitTo(db *Database, opts *CommitOptions) error {
+ if opts == nil {
+ opts = &CommitOptions{}
+ }
+ if opts.Precheck {
+ if err := b.precheck(db); err != nil {
+ return err
+ }
+ }
+
+ return b.commitTo(db)
+}
+
+// commitTo does a best effort of adding all the batch assertions to
+// the target database.
+func (b *Batch) commitTo(db *Database) error {
+ if err := b.prereqSort(db); err != nil {
+ return err
+ }
+
+ // TODO: trigger w. caller a global sanity check if something is revoked
+ // (but try to save as much possible still),
+ // or err is a check error
+
+ var errs []error
+ for _, a := range b.added {
+ err := db.Add(a)
+ if IsUnaccceptedUpdate(err) {
+ // unsupported format case is handled before
+ // be idempotent
+ // system db has already the same or newer
+ continue
+ }
+ if err != nil {
+ errs = append(errs, err)
+ }
+ }
+ if len(errs) != 0 {
+ return &commitError{errs: errs}
+ }
+ return nil
+}
+
+func (b *Batch) prereqSort(db *Database) error {
+ if b.inPrereqOrder {
+ // nothing to do
+ return nil
+ }
+
+ // put in prereq order using a fetcher
+ ordered := make([]Assertion, 0, len(b.added))
+ retrieve := func(ref *Ref) (Assertion, error) {
+ a, err := b.bs.Get(ref.Type, ref.PrimaryKey, ref.Type.MaxSupportedFormat())
+ if IsNotFound(err) {
+ // fallback to pre-existing assertions
+ a, err = ref.Resolve(db.Find)
+ }
+ if err != nil {
+ return nil, resolveError("cannot resolve prerequisite assertion: %s", ref, err)
+ }
+ return a, nil
+ }
+ save := func(a Assertion) error {
+ ordered = append(ordered, a)
+ return nil
+ }
+ f := NewFetcher(db, retrieve, save)
+
+ for _, a := range b.added {
+ if err := f.Fetch(a.Ref()); err != nil {
+ return err
+ }
+ }
+
+ b.added = ordered
+ b.inPrereqOrder = true
+ return nil
+}
+
+func resolveError(format string, ref *Ref, err error) error {
+ if IsNotFound(err) {
+ return fmt.Errorf(format, ref)
+ } else {
+ return fmt.Errorf(format+": %v", ref, err)
+ }
+}
+
+type commitError struct {
+ errs []error
+}
+
+func (e *commitError) Error() string {
+ l := []string{""}
+ for _, e := range e.errs {
+ l = append(l, e.Error())
+ }
+ return fmt.Sprintf("cannot accept some assertions:%s", strings.Join(l, "\n - "))
+}
diff -Nru snapd-2.40/asserts/batch_test.go snapd-2.42.1/asserts/batch_test.go
--- snapd-2.40/asserts/batch_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/asserts/batch_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,475 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package asserts_test
+
+import (
+ "bytes"
+ "fmt"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+)
+
+type batchSuite struct {
+ storeSigning *assertstest.StoreStack
+ dev1Acct *asserts.Account
+
+ db *asserts.Database
+}
+
+var _ = Suite(&batchSuite{})
+
+func (s *batchSuite) SetUpTest(c *C) {
+ s.storeSigning = assertstest.NewStoreStack("can0nical", nil)
+
+ s.dev1Acct = assertstest.NewAccount(s.storeSigning, "developer1", nil, "")
+ err := s.storeSigning.Add(s.dev1Acct)
+ c.Assert(err, IsNil)
+
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: s.storeSigning.Trusted,
+ })
+ c.Assert(err, IsNil)
+ s.db = db
+}
+
+func (s *batchSuite) snapDecl(c *C, name string, extraHeaders map[string]interface{}) *asserts.SnapDeclaration {
+ headers := map[string]interface{}{
+ "series": "16",
+ "snap-id": name + "-id",
+ "snap-name": name,
+ "publisher-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ for h, v := range extraHeaders {
+ headers[h] = v
+ }
+ decl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ err = s.storeSigning.Add(decl)
+ c.Assert(err, IsNil)
+ return decl.(*asserts.SnapDeclaration)
+}
+
+func (s *batchSuite) TestAddStream(c *C) {
+ b := &bytes.Buffer{}
+ enc := asserts.NewEncoder(b)
+ // wrong order is ok
+ err := enc.Encode(s.dev1Acct)
+ c.Assert(err, IsNil)
+ enc.Encode(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ batch := asserts.NewBatch(nil)
+ refs, err := batch.AddStream(b)
+ c.Assert(err, IsNil)
+ c.Check(refs, DeepEquals, []*asserts.Ref{
+ {Type: asserts.AccountType, PrimaryKey: []string{s.dev1Acct.AccountID()}},
+ {Type: asserts.AccountKeyType, PrimaryKey: []string{s.storeSigning.StoreAccountKey("").PublicKeyID()}},
+ })
+
+ // noop
+ err = batch.Add(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ err = batch.CommitTo(s.db, nil)
+ c.Assert(err, IsNil)
+
+ devAcct, err := s.db.Find(asserts.AccountType, map[string]string{
+ "account-id": s.dev1Acct.AccountID(),
+ })
+ c.Assert(err, IsNil)
+ c.Check(devAcct.(*asserts.Account).Username(), Equals, "developer1")
+}
+
+func (s *batchSuite) TestAddEmptyStream(c *C) {
+ b := &bytes.Buffer{}
+
+ batch := asserts.NewBatch(nil)
+ refs, err := batch.AddStream(b)
+ c.Assert(err, IsNil)
+ c.Check(refs, HasLen, 0)
+}
+
+func (s *batchSuite) TestConsiderPreexisting(c *C) {
+ // prereq store key
+ err := s.db.Add(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ batch := asserts.NewBatch(nil)
+ err = batch.Add(s.dev1Acct)
+ c.Assert(err, IsNil)
+
+ err = batch.CommitTo(s.db, nil)
+ c.Assert(err, IsNil)
+
+ devAcct, err := s.db.Find(asserts.AccountType, map[string]string{
+ "account-id": s.dev1Acct.AccountID(),
+ })
+ c.Assert(err, IsNil)
+ c.Check(devAcct.(*asserts.Account).Username(), Equals, "developer1")
+}
+
+func (s *batchSuite) TestAddStreamReturnsEffectivelyAddedRefs(c *C) {
+ batch := asserts.NewBatch(nil)
+
+ err := batch.Add(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ b := &bytes.Buffer{}
+ enc := asserts.NewEncoder(b)
+ // wrong order is ok
+ err = enc.Encode(s.dev1Acct)
+ c.Assert(err, IsNil)
+ // this was already added to the batch
+ enc.Encode(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ // effectively adds only the developer1 account
+ refs, err := batch.AddStream(b)
+ c.Assert(err, IsNil)
+ c.Check(refs, DeepEquals, []*asserts.Ref{
+ {Type: asserts.AccountType, PrimaryKey: []string{s.dev1Acct.AccountID()}},
+ })
+
+ err = batch.CommitTo(s.db, nil)
+ c.Assert(err, IsNil)
+
+ devAcct, err := s.db.Find(asserts.AccountType, map[string]string{
+ "account-id": s.dev1Acct.AccountID(),
+ })
+ c.Assert(err, IsNil)
+ c.Check(devAcct.(*asserts.Account).Username(), Equals, "developer1")
+}
+
+func (s *batchSuite) TestCommitRefusesSelfSignedKey(c *C) {
+ aKey, _ := assertstest.GenerateKey(752)
+ aSignDB := assertstest.NewSigningDB("can0nical", aKey)
+
+ aKeyEncoded, err := asserts.EncodePublicKey(aKey.PublicKey())
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "authority-id": "can0nical",
+ "account-id": "can0nical",
+ "public-key-sha3-384": aKey.PublicKey().ID(),
+ "name": "default",
+ "since": time.Now().UTC().Format(time.RFC3339),
+ }
+ acctKey, err := aSignDB.Sign(asserts.AccountKeyType, headers, aKeyEncoded, "")
+ c.Assert(err, IsNil)
+
+ headers = map[string]interface{}{
+ "authority-id": "can0nical",
+ "brand-id": "can0nical",
+ "repair-id": "2",
+ "summary": "repair two",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }
+ repair, err := aSignDB.Sign(asserts.RepairType, headers, []byte("#script"), "")
+ c.Assert(err, IsNil)
+
+ batch := asserts.NewBatch(nil)
+
+ err = batch.Add(repair)
+ c.Assert(err, IsNil)
+
+ err = batch.Add(acctKey)
+ c.Assert(err, IsNil)
+
+ // this must fail
+ err = batch.CommitTo(s.db, nil)
+ c.Assert(err, ErrorMatches, `circular assertions are not expected:.*`)
+}
+
+func (s *batchSuite) TestAddUnsupported(c *C) {
+ restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 111)
+ defer restore()
+
+ batch := asserts.NewBatch(nil)
+
+ var a asserts.Assertion
+ (func() {
+ restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 999)
+ defer restore()
+ headers := map[string]interface{}{
+ "format": "999",
+ "revision": "1",
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-name": "foo",
+ "publisher-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ var err error
+ a, err = s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ })()
+
+ err := batch.Add(a)
+ c.Check(err, ErrorMatches, `proposed "snap-declaration" assertion has format 999 but 111 is latest supported`)
+}
+
+func (s *batchSuite) TestAddUnsupportedIgnore(c *C) {
+ restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 111)
+ defer restore()
+
+ var uRef *asserts.Ref
+ unsupported := func(ref *asserts.Ref, _ error) error {
+ uRef = ref
+ return nil
+ }
+
+ batch := asserts.NewBatch(unsupported)
+
+ var a asserts.Assertion
+ (func() {
+ restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 999)
+ defer restore()
+ headers := map[string]interface{}{
+ "format": "999",
+ "revision": "1",
+ "series": "16",
+ "snap-id": "snap-id-1",
+ "snap-name": "foo",
+ "publisher-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ var err error
+ a, err = s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
+ c.Assert(err, IsNil)
+ })()
+
+ err := batch.Add(a)
+ c.Check(err, IsNil)
+ c.Check(uRef, DeepEquals, &asserts.Ref{
+ Type: asserts.SnapDeclarationType,
+ PrimaryKey: []string{"16", "snap-id-1"},
+ })
+}
+
+func (s *batchSuite) TestCommitPartial(c *C) {
+ // Commit does add any successful assertion until the first error
+
+ // store key already present
+ err := s.db.Add(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ batch := asserts.NewBatch(nil)
+
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+
+ err = batch.Add(snapDeclFoo)
+ c.Assert(err, IsNil)
+ err = batch.Add(s.dev1Acct)
+ c.Assert(err, IsNil)
+
+ // too old
+ rev := 1
+ headers := map[string]interface{}{
+ "snap-id": "foo-id",
+ "snap-sha3-384": makeDigest(rev),
+ "snap-size": fmt.Sprintf("%d", len(fakeSnap(rev))),
+ "snap-revision": fmt.Sprintf("%d", rev),
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Time{}.Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = batch.Add(snapRev)
+ c.Assert(err, IsNil)
+
+ err = batch.CommitTo(s.db, &asserts.CommitOptions{Precheck: false})
+ c.Check(err, ErrorMatches, `(?ms).*validity.*`)
+
+ // snap-declaration was added anyway
+ _, err = s.db.Find(asserts.SnapDeclarationType, map[string]string{
+ "series": "16",
+ "snap-id": "foo-id",
+ })
+ c.Assert(err, IsNil)
+}
+
+func (s *batchSuite) TestCommitMissing(c *C) {
+ // store key already present
+ err := s.db.Add(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ batch := asserts.NewBatch(nil)
+
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+
+ err = batch.Add(snapDeclFoo)
+ c.Assert(err, IsNil)
+
+ err = batch.CommitTo(s.db, nil)
+ c.Check(err, ErrorMatches, `cannot resolve prerequisite assertion: account.*`)
+}
+
+func (s *batchSuite) TestPrecheckPartial(c *C) {
+ // store key already present
+ err := s.db.Add(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ batch := asserts.NewBatch(nil)
+
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+
+ err = batch.Add(snapDeclFoo)
+ c.Assert(err, IsNil)
+ err = batch.Add(s.dev1Acct)
+ c.Assert(err, IsNil)
+
+ // too old
+ rev := 1
+ headers := map[string]interface{}{
+ "snap-id": "foo-id",
+ "snap-sha3-384": makeDigest(rev),
+ "snap-size": fmt.Sprintf("%d", len(fakeSnap(rev))),
+ "snap-revision": fmt.Sprintf("%d", rev),
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Time{}.Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = batch.Add(snapRev)
+ c.Assert(err, IsNil)
+
+ err = batch.CommitTo(s.db, &asserts.CommitOptions{Precheck: true})
+ c.Check(err, ErrorMatches, `(?ms).*validity.*`)
+
+ // nothing was added
+ _, err = s.db.Find(asserts.SnapDeclarationType, map[string]string{
+ "series": "16",
+ "snap-id": "foo-id",
+ })
+ c.Assert(asserts.IsNotFound(err), Equals, true)
+}
+
+func (s *batchSuite) TestPrecheckHappy(c *C) {
+ // store key already present
+ err := s.db.Add(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ batch := asserts.NewBatch(nil)
+
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+
+ err = batch.Add(snapDeclFoo)
+ c.Assert(err, IsNil)
+ err = batch.Add(s.dev1Acct)
+ c.Assert(err, IsNil)
+
+ rev := 1
+ revDigest := makeDigest(rev)
+ headers := map[string]interface{}{
+ "snap-id": "foo-id",
+ "snap-sha3-384": revDigest,
+ "snap-size": fmt.Sprintf("%d", len(fakeSnap(rev))),
+ "snap-revision": fmt.Sprintf("%d", rev),
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = batch.Add(snapRev)
+ c.Assert(err, IsNil)
+
+ // test precheck on its own
+ err = batch.DoPrecheck(s.db)
+ c.Assert(err, IsNil)
+
+ // nothing was added yet
+ _, err = s.db.Find(asserts.SnapDeclarationType, map[string]string{
+ "series": "16",
+ "snap-id": "foo-id",
+ })
+ c.Assert(asserts.IsNotFound(err), Equals, true)
+
+ // commit (with precheck)
+ err = batch.CommitTo(s.db, &asserts.CommitOptions{Precheck: true})
+ c.Assert(err, IsNil)
+
+ _, err = s.db.Find(asserts.SnapRevisionType, map[string]string{
+ "snap-sha3-384": revDigest,
+ })
+ c.Check(err, IsNil)
+}
+
+func (s *batchSuite) TestFetch(c *C) {
+ err := s.db.Add(s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ s.snapDecl(c, "foo", nil)
+
+ rev := 10
+ revDigest := makeDigest(rev)
+ headers := map[string]interface{}{
+ "snap-id": "foo-id",
+ "snap-sha3-384": revDigest,
+ "snap-size": fmt.Sprintf("%d", len(fakeSnap(rev))),
+ "snap-revision": fmt.Sprintf("%d", rev),
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = s.storeSigning.Add(snapRev)
+ c.Assert(err, IsNil)
+ ref := snapRev.Ref()
+
+ batch := asserts.NewBatch(nil)
+
+ // retrieve from storeSigning
+ retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
+ return ref.Resolve(s.storeSigning.Find)
+ }
+ // fetching the snap-revision
+ fetching := func(f asserts.Fetcher) error {
+ return f.Fetch(ref)
+ }
+
+ err = batch.Fetch(s.db, retrieve, fetching)
+ c.Assert(err, IsNil)
+
+ // nothing was added yet
+ _, err = s.db.Find(asserts.SnapDeclarationType, map[string]string{
+ "series": "16",
+ "snap-id": "foo-id",
+ })
+ c.Assert(asserts.IsNotFound(err), Equals, true)
+
+ // commit
+ err = batch.CommitTo(s.db, nil)
+ c.Assert(err, IsNil)
+
+ _, err = s.db.Find(asserts.SnapRevisionType, map[string]string{
+ "snap-sha3-384": revDigest,
+ })
+ c.Check(err, IsNil)
+}
diff -Nru snapd-2.40/asserts/database.go snapd-2.42.1/asserts/database.go
--- snapd-2.40/asserts/database.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/asserts/database.go 2019-10-30 12:17:43.000000000 +0000
@@ -195,10 +195,15 @@
type Database struct {
bs Backstore
keypairMgr KeypairManager
+
trusted Backstore
predefined Backstore
+ // all backstores to consider for find
backstores []Backstore
- checkers []Checker
+ // backstores of dbs this was built on by stacking
+ stackedOn []Backstore
+
+ checkers []Checker
}
// OpenDatabase opens the assertion database based on the configuration.
@@ -264,6 +269,30 @@
}, nil
}
+// WithStackedBackstore returns a new database that adds to the given backstore
+// only but finds in backstore and the base database backstores and
+// cross-checks against all of them.
+// This is useful to cross-check a set of assertions without adding
+// them to the database.
+func (db *Database) WithStackedBackstore(backstore Backstore) *Database {
+ // original bs goes in front of stacked-on ones
+ stackedOn := []Backstore{db.bs}
+ stackedOn = append(stackedOn, db.stackedOn...)
+ // find order: trusted, predefined, new backstore, stacked-on ones
+ backstores := []Backstore{db.trusted, db.predefined}
+ backstores = append(backstores, backstore)
+ backstores = append(backstores, stackedOn...)
+ return &Database{
+ bs: backstore,
+ keypairMgr: db.keypairMgr,
+ trusted: db.trusted,
+ predefined: db.predefined,
+ backstores: backstores,
+ stackedOn: stackedOn,
+ checkers: db.checkers,
+ }
+}
+
// ImportKey stores the given private/public key pair.
func (db *Database) ImportKey(privKey PrivateKey) error {
return db.keypairMgr.Put(privKey)
@@ -408,6 +437,24 @@
return fmt.Errorf("cannot add %q assertion with primary key clashing with a predefined assertion: %v", ref.Type.Name, ref.PrimaryKey)
}
+ // this is non empty only in the stacked case
+ if len(db.stackedOn) != 0 {
+ headers, err := HeadersFromPrimaryKey(ref.Type, ref.PrimaryKey)
+ if err != nil {
+ return fmt.Errorf("internal error: HeadersFromPrimaryKey for %q failed on prechecked data: %s", ref.Type.Name, ref.PrimaryKey)
+ }
+ cur, err := find(db.stackedOn, ref.Type, headers, -1)
+ if err == nil {
+ curRev := cur.Revision()
+ rev := assert.Revision()
+ if curRev >= rev {
+ return &RevisionError{Current: curRev, Used: rev}
+ }
+ } else if !IsNotFound(err) {
+ return err
+ }
+ }
+
return db.bs.Put(ref.Type, assert)
}
diff -Nru snapd-2.40/asserts/database_test.go snapd-2.42.1/asserts/database_test.go
--- snapd-2.40/asserts/database_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/asserts/database_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -1139,6 +1139,105 @@
c.Check(err, ErrorMatches, `cannot find "test-only" assertions for format 3 higher than supported format 1`)
}
+func (safs *signAddFindSuite) TestWithStackedBackstore(c *C) {
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "one",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Add(a1)
+ c.Assert(err, IsNil)
+
+ headers = map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "two",
+ }
+ a2, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+
+ bs := asserts.NewMemoryBackstore()
+ stacked := safs.db.WithStackedBackstore(bs)
+
+ err = stacked.Add(a2)
+ c.Assert(err, IsNil)
+
+ _, err = stacked.Find(asserts.TestOnlyType, map[string]string{
+ "primary-key": "one",
+ })
+ c.Check(err, IsNil)
+
+ _, err = stacked.Find(asserts.TestOnlyType, map[string]string{
+ "primary-key": "two",
+ })
+ c.Check(err, IsNil)
+
+ _, err = safs.db.Find(asserts.TestOnlyType, map[string]string{
+ "primary-key": "two",
+ })
+ c.Check(asserts.IsNotFound(err), Equals, true)
+
+ _, err = stacked.Find(asserts.AccountKeyType, map[string]string{
+ "public-key-sha3-384": safs.signingKeyID,
+ })
+ c.Check(err, IsNil)
+
+ // stored in backstore
+ _, err = bs.Get(asserts.TestOnlyType, []string{"two"}, 0)
+ c.Check(err, IsNil)
+}
+
+func (safs *signAddFindSuite) TestWithStackedBackstoreSafety(c *C) {
+ stacked := safs.db.WithStackedBackstore(asserts.NewMemoryBackstore())
+
+ // usual add safety
+ pubKey0, err := safs.signingDB.PublicKey(safs.signingKeyID)
+ c.Assert(err, IsNil)
+ pubKey0Encoded, err := asserts.EncodePublicKey(pubKey0)
+ c.Assert(err, IsNil)
+
+ now := time.Now().UTC()
+ headers := map[string]interface{}{
+ "authority-id": "canonical",
+ "account-id": "canonical",
+ "public-key-sha3-384": safs.signingKeyID,
+ "name": "default",
+ "since": now.Format(time.RFC3339),
+ "until": now.AddDate(1, 0, 0).Format(time.RFC3339),
+ }
+ tKey, err := safs.signingDB.Sign(asserts.AccountKeyType, headers, []byte(pubKey0Encoded), safs.signingKeyID)
+ c.Assert(err, IsNil)
+
+ err = stacked.Add(tKey)
+ c.Check(err, ErrorMatches, `cannot add "account-key" assertion with primary key clashing with a trusted assertion: .*`)
+
+ // cannot go back to old revisions
+ headers = map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "one",
+ }
+ a0, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+
+ headers = map[string]interface{}{
+ "authority-id": "canonical",
+ "primary-key": "one",
+ "revision": "1",
+ }
+ a1, err := safs.signingDB.Sign(asserts.TestOnlyType, headers, nil, safs.signingKeyID)
+ c.Assert(err, IsNil)
+
+ err = safs.db.Add(a1)
+ c.Assert(err, IsNil)
+
+ err = stacked.Add(a0)
+ c.Assert(err, DeepEquals, &asserts.RevisionError{
+ Used: 0,
+ Current: 1,
+ })
+}
+
type revisionErrorSuite struct{}
func (res *revisionErrorSuite) TestErrorText(c *C) {
diff -Nru snapd-2.40/asserts/device_asserts.go snapd-2.42.1/asserts/device_asserts.go
--- snapd-2.40/asserts/device_asserts.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/asserts/device_asserts.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,542 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2016 Canonical Ltd
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package asserts
-
-import (
- "fmt"
- "regexp"
- "strings"
- "time"
-
- "github.com/snapcore/snapd/snap/naming"
- "github.com/snapcore/snapd/strutil"
-)
-
-// Model holds a model assertion, which is a statement by a brand
-// about the properties of a device model.
-type Model struct {
- assertionBase
- classic bool
- requiredSnaps []string
- sysUserAuthority []string
- timestamp time.Time
-}
-
-// BrandID returns the brand identifier. Same as the authority id.
-func (mod *Model) BrandID() string {
- return mod.HeaderString("brand-id")
-}
-
-// Model returns the model name identifier.
-func (mod *Model) Model() string {
- return mod.HeaderString("model")
-}
-
-// DisplayName returns the human-friendly name of the model or
-// falls back to Model if this was not set.
-func (mod *Model) DisplayName() string {
- display := mod.HeaderString("display-name")
- if display == "" {
- return mod.Model()
- }
- return display
-}
-
-// Series returns the series of the core software the model uses.
-func (mod *Model) Series() string {
- return mod.HeaderString("series")
-}
-
-// Classic returns whether the model is a classic system.
-func (mod *Model) Classic() bool {
- return mod.classic
-}
-
-// Architecture returns the archicteture the model is based on.
-func (mod *Model) Architecture() string {
- return mod.HeaderString("architecture")
-}
-
-// snapWithTrack represents a snap that includes optional track
-// information like `snapName=trackName`
-type snapWithTrack string
-
-func (s snapWithTrack) Snap() string {
- return strings.SplitN(string(s), "=", 2)[0]
-}
-
-func (s snapWithTrack) Track() string {
- l := strings.SplitN(string(s), "=", 2)
- if len(l) > 1 {
- return l[1]
- }
- return ""
-}
-
-// Gadget returns the gadget snap the model uses.
-func (mod *Model) Gadget() string {
- return snapWithTrack(mod.HeaderString("gadget")).Snap()
-}
-
-// GadgetTrack returns the gadget track the model uses.
-func (mod *Model) GadgetTrack() string {
- return snapWithTrack(mod.HeaderString("gadget")).Track()
-}
-
-// Kernel returns the kernel snap the model uses.
-func (mod *Model) Kernel() string {
- return snapWithTrack(mod.HeaderString("kernel")).Snap()
-}
-
-// KernelTrack returns the kernel track the model uses.
-func (mod *Model) KernelTrack() string {
- return snapWithTrack(mod.HeaderString("kernel")).Track()
-}
-
-// Base returns the base snap the model uses.
-func (mod *Model) Base() string {
- return mod.HeaderString("base")
-}
-
-// Store returns the snap store the model uses.
-func (mod *Model) Store() string {
- return mod.HeaderString("store")
-}
-
-// RequiredSnaps returns the snaps that must be installed at all times and cannot be removed for this model.
-func (mod *Model) RequiredSnaps() []string {
- return mod.requiredSnaps
-}
-
-// SystemUserAuthority returns the authority ids that are accepted as signers of system-user assertions for this model. Empty list means any.
-func (mod *Model) SystemUserAuthority() []string {
- return mod.sysUserAuthority
-}
-
-// Timestamp returns the time when the model assertion was issued.
-func (mod *Model) Timestamp() time.Time {
- return mod.timestamp
-}
-
-// Implement further consistency checks.
-func (mod *Model) checkConsistency(db RODatabase, acck *AccountKey) error {
- // TODO: double check trust level of authority depending on class and possibly allowed-modes
- return nil
-}
-
-// sanity
-var _ consistencyChecker = (*Model)(nil)
-
-// limit model to only lowercase for now
-var validModel = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$")
-
-func checkSnapWithTrackHeader(header string, headers map[string]interface{}) error {
- _, ok := headers[header]
- if !ok {
- return nil
- }
- value, ok := headers[header].(string)
- if !ok {
- return fmt.Errorf(`%q header must be a string`, header)
- }
- l := strings.SplitN(value, "=", 2)
-
- if err := validateSnapName(l[0], header); err != nil {
- return err
- }
- if len(l) == 1 {
- return nil
- }
- track := l[1]
- if strings.Count(track, "/") != 0 {
- return fmt.Errorf(`%q channel selector must be a track name only`, header)
- }
- channelRisks := []string{"stable", "candidate", "beta", "edge"}
- if strutil.ListContains(channelRisks, track) {
- return fmt.Errorf(`%q channel selector must be a track name`, header)
- }
- return nil
-}
-
-func checkModel(headers map[string]interface{}) (string, error) {
- s, err := checkStringMatches(headers, "model", validModel)
- if err != nil {
- return "", err
- }
-
- // TODO: support the concept of case insensitive/preserving string headers
- if strings.ToLower(s) != s {
- return "", fmt.Errorf(`"model" header cannot contain uppercase letters`)
- }
- return s, nil
-}
-
-func checkAuthorityMatchesBrand(a Assertion) error {
- typeName := a.Type().Name
- authorityID := a.AuthorityID()
- brand := a.HeaderString("brand-id")
- if brand != authorityID {
- return fmt.Errorf("authority-id and brand-id must match, %s assertions are expected to be signed by the brand: %q != %q", typeName, authorityID, brand)
- }
- return nil
-}
-
-func checkOptionalSystemUserAuthority(headers map[string]interface{}, brandID string) ([]string, error) {
- const name = "system-user-authority"
- v, ok := headers[name]
- if !ok {
- return []string{brandID}, nil
- }
- switch x := v.(type) {
- case string:
- if x == "*" {
- return nil, nil
- }
- case []interface{}:
- lst, err := checkStringListMatches(headers, name, validAccountID)
- if err == nil {
- return lst, nil
- }
- }
- return nil, fmt.Errorf("%q header must be '*' or a list of account ids", name)
-}
-
-var (
- modelMandatory = []string{"architecture", "gadget", "kernel"}
- classicModelOptional = []string{"architecture", "gadget"}
-)
-
-func validateSnapName(name string, headerName string) error {
- if err := naming.ValidateSnap(name); err != nil {
- return fmt.Errorf("invalid snap name in %q header: %s", headerName, name)
- }
- return nil
-}
-
-func assembleModel(assert assertionBase) (Assertion, error) {
- err := checkAuthorityMatchesBrand(&assert)
- if err != nil {
- return nil, err
- }
-
- _, err = checkModel(assert.headers)
- if err != nil {
- return nil, err
- }
-
- classic, err := checkOptionalBool(assert.headers, "classic")
- if err != nil {
- return nil, err
- }
-
- if classic {
- if _, ok := assert.headers["kernel"]; ok {
- return nil, fmt.Errorf("cannot specify a kernel with a classic model")
- }
- if _, ok := assert.headers["base"]; ok {
- return nil, fmt.Errorf("cannot specify a base with a classic model")
- }
- }
-
- checker := checkNotEmptyString
- toCheck := modelMandatory
- if classic {
- checker = checkOptionalString
- toCheck = classicModelOptional
- }
-
- for _, h := range toCheck {
- if _, err := checker(assert.headers, h); err != nil {
- return nil, err
- }
- }
-
- // kernel/gadget must be valid snap names and can have (optional) tracks
- // - validate those
- if err := checkSnapWithTrackHeader("kernel", assert.headers); err != nil {
- return nil, err
- }
- if err := checkSnapWithTrackHeader("gadget", assert.headers); err != nil {
- return nil, err
- }
- // base, if provided, must be a valid snap name too
- base, err := checkOptionalString(assert.headers, "base")
- if err != nil {
- return nil, err
- }
- if base != "" {
- if err := validateSnapName(base, "base"); err != nil {
- return nil, err
- }
- }
-
- // store is optional but must be a string, defaults to the ubuntu store
- _, err = checkOptionalString(assert.headers, "store")
- if err != nil {
- return nil, err
- }
-
- // display-name is optional but must be a string
- _, err = checkOptionalString(assert.headers, "display-name")
- if err != nil {
- return nil, err
- }
-
- // required snap must be valid snap names
- reqSnaps, err := checkStringList(assert.headers, "required-snaps")
- if err != nil {
- return nil, err
- }
- for _, name := range reqSnaps {
- if err := validateSnapName(name, "required-snaps"); err != nil {
- return nil, err
- }
- }
-
- sysUserAuthority, err := checkOptionalSystemUserAuthority(assert.headers, assert.HeaderString("brand-id"))
- if err != nil {
- return nil, err
- }
-
- timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
- if err != nil {
- return nil, err
- }
-
- // NB:
- // * core is not supported at this time, it defaults to ubuntu-core
- // in prepare-image until rename and/or introduction of the header.
- // * some form of allowed-modes, class are postponed,
- //
- // prepare-image takes care of not allowing them for now
-
- // ignore extra headers and non-empty body for future compatibility
- return &Model{
- assertionBase: assert,
- classic: classic,
- requiredSnaps: reqSnaps,
- sysUserAuthority: sysUserAuthority,
- timestamp: timestamp,
- }, nil
-}
-
-// Serial holds a serial assertion, which is a statement binding a
-// device identity with the device public key.
-type Serial struct {
- assertionBase
- timestamp time.Time
- pubKey PublicKey
-}
-
-// BrandID returns the brand identifier of the device.
-func (ser *Serial) BrandID() string {
- return ser.HeaderString("brand-id")
-}
-
-// Model returns the model name identifier of the device.
-func (ser *Serial) Model() string {
- return ser.HeaderString("model")
-}
-
-// Serial returns the serial identifier of the device, together with
-// brand id and model they form the unique identifier of the device.
-func (ser *Serial) Serial() string {
- return ser.HeaderString("serial")
-}
-
-// DeviceKey returns the public key of the device.
-func (ser *Serial) DeviceKey() PublicKey {
- return ser.pubKey
-}
-
-// Timestamp returns the time when the serial assertion was issued.
-func (ser *Serial) Timestamp() time.Time {
- return ser.timestamp
-}
-
-// TODO: implement further consistency checks for Serial but first review approach
-
-func assembleSerial(assert assertionBase) (Assertion, error) {
- err := checkAuthorityMatchesBrand(&assert)
- if err != nil {
- return nil, err
- }
-
- _, err = checkModel(assert.headers)
- if err != nil {
- return nil, err
- }
-
- encodedKey, err := checkNotEmptyString(assert.headers, "device-key")
- if err != nil {
- return nil, err
- }
- pubKey, err := DecodePublicKey([]byte(encodedKey))
- if err != nil {
- return nil, err
- }
- keyID, err := checkNotEmptyString(assert.headers, "device-key-sha3-384")
- if err != nil {
- return nil, err
- }
- if keyID != pubKey.ID() {
- return nil, fmt.Errorf("device key does not match provided key id")
- }
-
- timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
- if err != nil {
- return nil, err
- }
-
- // ignore extra headers and non-empty body for future compatibility
- return &Serial{
- assertionBase: assert,
- timestamp: timestamp,
- pubKey: pubKey,
- }, nil
-}
-
-// SerialRequest holds a serial-request assertion, which is a self-signed request to obtain a full device identity bound to the device public key.
-type SerialRequest struct {
- assertionBase
- pubKey PublicKey
-}
-
-// BrandID returns the brand identifier of the device making the request.
-func (sreq *SerialRequest) BrandID() string {
- return sreq.HeaderString("brand-id")
-}
-
-// Model returns the model name identifier of the device making the request.
-func (sreq *SerialRequest) Model() string {
- return sreq.HeaderString("model")
-}
-
-// Serial returns the optional proposed serial identifier for the device, the service taking the request might use it or ignore it.
-func (sreq *SerialRequest) Serial() string {
- return sreq.HeaderString("serial")
-}
-
-// RequestID returns the id for the request, obtained from and to be presented to the serial signing service.
-func (sreq *SerialRequest) RequestID() string {
- return sreq.HeaderString("request-id")
-}
-
-// DeviceKey returns the public key of the device making the request.
-func (sreq *SerialRequest) DeviceKey() PublicKey {
- return sreq.pubKey
-}
-
-func assembleSerialRequest(assert assertionBase) (Assertion, error) {
- _, err := checkNotEmptyString(assert.headers, "brand-id")
- if err != nil {
- return nil, err
- }
-
- _, err = checkModel(assert.headers)
- if err != nil {
- return nil, err
- }
-
- _, err = checkNotEmptyString(assert.headers, "request-id")
- if err != nil {
- return nil, err
- }
-
- _, err = checkOptionalString(assert.headers, "serial")
- if err != nil {
- return nil, err
- }
-
- encodedKey, err := checkNotEmptyString(assert.headers, "device-key")
- if err != nil {
- return nil, err
- }
- pubKey, err := DecodePublicKey([]byte(encodedKey))
- if err != nil {
- return nil, err
- }
-
- if pubKey.ID() != assert.SignKeyID() {
- return nil, fmt.Errorf("device key does not match included signing key id")
- }
-
- // ignore extra headers and non-empty body for future compatibility
- return &SerialRequest{
- assertionBase: assert,
- pubKey: pubKey,
- }, nil
-}
-
-// DeviceSessionRequest holds a device-session-request assertion, which is a request wrapping a store-provided nonce to start a session by a device signed with its key.
-type DeviceSessionRequest struct {
- assertionBase
- timestamp time.Time
-}
-
-// BrandID returns the brand identifier of the device making the request.
-func (req *DeviceSessionRequest) BrandID() string {
- return req.HeaderString("brand-id")
-}
-
-// Model returns the model name identifier of the device making the request.
-func (req *DeviceSessionRequest) Model() string {
- return req.HeaderString("model")
-}
-
-// Serial returns the serial identifier of the device making the request,
-// together with brand id and model it forms the unique identifier of
-// the device.
-func (req *DeviceSessionRequest) Serial() string {
- return req.HeaderString("serial")
-}
-
-// Nonce returns the nonce obtained from store and to be presented when requesting a device session.
-func (req *DeviceSessionRequest) Nonce() string {
- return req.HeaderString("nonce")
-}
-
-// Timestamp returns the time when the device-session-request was created.
-func (req *DeviceSessionRequest) Timestamp() time.Time {
- return req.timestamp
-}
-
-func assembleDeviceSessionRequest(assert assertionBase) (Assertion, error) {
- _, err := checkModel(assert.headers)
- if err != nil {
- return nil, err
- }
-
- _, err = checkNotEmptyString(assert.headers, "nonce")
- if err != nil {
- return nil, err
- }
-
- timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
- if err != nil {
- return nil, err
- }
-
- // ignore extra headers and non-empty body for future compatibility
- return &DeviceSessionRequest{
- assertionBase: assert,
- timestamp: timestamp,
- }, nil
-}
diff -Nru snapd-2.40/asserts/device_asserts_test.go snapd-2.42.1/asserts/device_asserts_test.go
--- snapd-2.40/asserts/device_asserts_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/asserts/device_asserts_test.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,714 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2016 Canonical Ltd
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package asserts_test
-
-import (
- "fmt"
- "strings"
- "time"
-
- . "gopkg.in/check.v1"
-
- "github.com/snapcore/snapd/asserts"
- "github.com/snapcore/snapd/asserts/assertstest"
-)
-
-type modelSuite struct {
- ts time.Time
- tsLine string
-}
-
-var (
- _ = Suite(&modelSuite{})
- _ = Suite(&serialSuite{})
-)
-
-func (mods *modelSuite) SetUpSuite(c *C) {
- mods.ts = time.Now().Truncate(time.Second).UTC()
- mods.tsLine = "timestamp: " + mods.ts.Format(time.RFC3339) + "\n"
-}
-
-const (
- reqSnaps = "required-snaps:\n - foo\n - bar\n"
- sysUserAuths = "system-user-authority: *\n"
-)
-
-const (
- modelExample = "type: model\n" +
- "authority-id: brand-id1\n" +
- "series: 16\n" +
- "brand-id: brand-id1\n" +
- "model: baz-3000\n" +
- "display-name: Baz 3000\n" +
- "architecture: amd64\n" +
- "gadget: brand-gadget\n" +
- "base: core18\n" +
- "kernel: baz-linux\n" +
- "store: brand-store\n" +
- sysUserAuths +
- reqSnaps +
- "TSLINE" +
- "body-length: 0\n" +
- "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
- "\n\n" +
- "AXNpZw=="
-
- classicModelExample = "type: model\n" +
- "authority-id: brand-id1\n" +
- "series: 16\n" +
- "brand-id: brand-id1\n" +
- "model: baz-3000\n" +
- "display-name: Baz 3000\n" +
- "classic: true\n" +
- "architecture: amd64\n" +
- "gadget: brand-gadget\n" +
- "store: brand-store\n" +
- reqSnaps +
- "TSLINE" +
- "body-length: 0\n" +
- "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
- "\n\n" +
- "AXNpZw=="
-)
-
-func (mods *modelSuite) TestDecodeOK(c *C) {
- encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- c.Check(a.Type(), Equals, asserts.ModelType)
- model := a.(*asserts.Model)
- c.Check(model.AuthorityID(), Equals, "brand-id1")
- c.Check(model.Timestamp(), Equals, mods.ts)
- c.Check(model.Series(), Equals, "16")
- c.Check(model.BrandID(), Equals, "brand-id1")
- c.Check(model.Model(), Equals, "baz-3000")
- c.Check(model.DisplayName(), Equals, "Baz 3000")
- c.Check(model.Architecture(), Equals, "amd64")
- c.Check(model.Gadget(), Equals, "brand-gadget")
- c.Check(model.GadgetTrack(), Equals, "")
- c.Check(model.Kernel(), Equals, "baz-linux")
- c.Check(model.KernelTrack(), Equals, "")
- c.Check(model.Base(), Equals, "core18")
- c.Check(model.Store(), Equals, "brand-store")
- c.Check(model.RequiredSnaps(), DeepEquals, []string{"foo", "bar"})
- c.Check(model.SystemUserAuthority(), HasLen, 0)
-}
-
-func (mods *modelSuite) TestDecodeStoreIsOptional(c *C) {
- withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
- encoded := strings.Replace(withTimestamp, "store: brand-store\n", "store: \n", 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- model := a.(*asserts.Model)
- c.Check(model.Store(), Equals, "")
-
- encoded = strings.Replace(withTimestamp, "store: brand-store\n", "", 1)
- a, err = asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- model = a.(*asserts.Model)
- c.Check(model.Store(), Equals, "")
-}
-
-func (mods *modelSuite) TestDecodeBaseIsOptional(c *C) {
- withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
- encoded := strings.Replace(withTimestamp, "base: core18\n", "base: \n", 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- model := a.(*asserts.Model)
- c.Check(model.Base(), Equals, "")
-
- encoded = strings.Replace(withTimestamp, "base: core18\n", "", 1)
- a, err = asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- model = a.(*asserts.Model)
- c.Check(model.Base(), Equals, "")
-}
-
-func (mods *modelSuite) TestDecodeDisplayNameIsOptional(c *C) {
- withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
- encoded := strings.Replace(withTimestamp, "display-name: Baz 3000\n", "display-name: \n", 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- model := a.(*asserts.Model)
- // optional but we fallback to Model
- c.Check(model.DisplayName(), Equals, "baz-3000")
-
- encoded = strings.Replace(withTimestamp, "display-name: Baz 3000\n", "", 1)
- a, err = asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- model = a.(*asserts.Model)
- // optional but we fallback to Model
- c.Check(model.DisplayName(), Equals, "baz-3000")
-}
-
-func (mods *modelSuite) TestDecodeRequiredSnapsAreOptional(c *C) {
- withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
- encoded := strings.Replace(withTimestamp, reqSnaps, "", 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- model := a.(*asserts.Model)
- c.Check(model.RequiredSnaps(), HasLen, 0)
-}
-
-func (mods *modelSuite) TestDecodeValidatesSnapNames(c *C) {
- withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
- encoded := strings.Replace(withTimestamp, reqSnaps, "required-snaps:\n - foo_bar\n - bar\n", 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(a, IsNil)
- c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "required-snaps" header: foo_bar`)
-
- encoded = strings.Replace(withTimestamp, reqSnaps, "required-snaps:\n - foo\n - bar-;;''\n", 1)
- a, err = asserts.Decode([]byte(encoded))
- c.Assert(a, IsNil)
- c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "required-snaps" header: bar-;;''`)
-
- encoded = strings.Replace(withTimestamp, "kernel: baz-linux\n", "kernel: baz-linux_instance\n", 1)
- a, err = asserts.Decode([]byte(encoded))
- c.Assert(a, IsNil)
- c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "kernel" header: baz-linux_instance`)
-
- encoded = strings.Replace(withTimestamp, "gadget: brand-gadget\n", "gadget: brand-gadget_instance\n", 1)
- a, err = asserts.Decode([]byte(encoded))
- c.Assert(a, IsNil)
- c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "gadget" header: brand-gadget_instance`)
-
- encoded = strings.Replace(withTimestamp, "base: core18\n", "base: core18_instance\n", 1)
- a, err = asserts.Decode([]byte(encoded))
- c.Assert(a, IsNil)
- c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "base" header: core18_instance`)
-}
-
-func (mods modelSuite) TestDecodeValidSnapNames(c *C) {
- // reuse test cases for snap.ValidateName()
-
- withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
-
- validNames := []string{
- "aa", "aaa", "aaaa",
- "a-a", "aa-a", "a-aa", "a-b-c",
- "a0", "a-0", "a-0a",
- "01game", "1-or-2",
- // a regexp stresser
- "u-94903713687486543234157734673284536758",
- }
- for _, name := range validNames {
- encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", fmt.Sprintf("kernel: %s\n", name), 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- model := a.(*asserts.Model)
- c.Check(model.Kernel(), Equals, name)
- }
- invalidNames := []string{
- // name cannot be empty, never reaches snap name validation
- "",
- // too short (min 2 chars)
- "a",
- // names cannot be too long
- "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
- "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx",
- "1111111111111111111111111111111111111111x",
- "x1111111111111111111111111111111111111111",
- "x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x",
- // a regexp stresser
- "u-9490371368748654323415773467328453675-",
- // dashes alone are not a name
- "-", "--",
- // double dashes in a name are not allowed
- "a--a",
- // name should not end with a dash
- "a-",
- // name cannot have any spaces in it
- "a ", " a", "a a",
- // a number alone is not a name
- "0", "123",
- // identifier must be plain ASCII
- "日本語", "한글", "ру́сский язы́к",
- // instance names are invalid too
- "foo_bar", "x_1",
- }
- for _, name := range invalidNames {
- encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", fmt.Sprintf("kernel: %s\n", name), 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(a, IsNil)
- if name != "" {
- c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "kernel" header: .*`)
- } else {
- c.Assert(err, ErrorMatches, `assertion model: "kernel" header should not be empty`)
- }
- }
-}
-
-func (mods *modelSuite) TestDecodeSystemUserAuthorityIsOptional(c *C) {
- withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
- encoded := strings.Replace(withTimestamp, sysUserAuths, "", 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- model := a.(*asserts.Model)
- // the default is just to accept the brand itself
- c.Check(model.SystemUserAuthority(), DeepEquals, []string{"brand-id1"})
-
- encoded = strings.Replace(withTimestamp, sysUserAuths, "system-user-authority:\n - foo\n - bar\n", 1)
- a, err = asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- model = a.(*asserts.Model)
- c.Check(model.SystemUserAuthority(), DeepEquals, []string{"foo", "bar"})
-}
-
-func (mods *modelSuite) TestDecodeKernelTrack(c *C) {
- withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
- encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", "kernel: baz-linux=18\n", 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- model := a.(*asserts.Model)
- c.Check(model.Kernel(), Equals, "baz-linux")
- c.Check(model.KernelTrack(), Equals, "18")
-}
-
-func (mods *modelSuite) TestDecodeGadgetTrack(c *C) {
- withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
- encoded := strings.Replace(withTimestamp, "gadget: brand-gadget\n", "gadget: brand-gadget=18\n", 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- model := a.(*asserts.Model)
- c.Check(model.Gadget(), Equals, "brand-gadget")
- c.Check(model.GadgetTrack(), Equals, "18")
-}
-
-const (
- modelErrPrefix = "assertion model: "
-)
-
-func (mods *modelSuite) TestDecodeInvalid(c *C) {
- encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
-
- invalidTests := []struct{ original, invalid, expectedErr string }{
- {"series: 16\n", "", `"series" header is mandatory`},
- {"series: 16\n", "series: \n", `"series" header should not be empty`},
- {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`},
- {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
- {"brand-id: brand-id1\n", "brand-id: random\n", `authority-id and brand-id must match, model assertions are expected to be signed by the brand: "brand-id1" != "random"`},
- {"model: baz-3000\n", "", `"model" header is mandatory`},
- {"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
- {"model: baz-3000\n", "model: baz/3000\n", `"model" primary key header cannot contain '/'`},
- // lift this restriction at a later point
- {"model: baz-3000\n", "model: BAZ-3000\n", `"model" header cannot contain uppercase letters`},
- {"display-name: Baz 3000\n", "display-name:\n - xyz\n", `"display-name" header must be a string`},
- {"architecture: amd64\n", "", `"architecture" header is mandatory`},
- {"architecture: amd64\n", "architecture: \n", `"architecture" header should not be empty`},
- {"gadget: brand-gadget\n", "", `"gadget" header is mandatory`},
- {"gadget: brand-gadget\n", "gadget: \n", `"gadget" header should not be empty`},
- {"gadget: brand-gadget\n", "gadget: brand-gadget=x/x/x\n", `"gadget" channel selector must be a track name only`},
- {"gadget: brand-gadget\n", "gadget: brand-gadget=stable\n", `"gadget" channel selector must be a track name`},
- {"gadget: brand-gadget\n", "gadget: brand-gadget=18/beta\n", `"gadget" channel selector must be a track name only`},
- {"gadget: brand-gadget\n", "gadget:\n - xyz \n", `"gadget" header must be a string`},
- {"kernel: baz-linux\n", "", `"kernel" header is mandatory`},
- {"kernel: baz-linux\n", "kernel: \n", `"kernel" header should not be empty`},
- {"kernel: baz-linux\n", "kernel: baz-linux=x/x/x\n", `"kernel" channel selector must be a track name only`},
- {"kernel: baz-linux\n", "kernel: baz-linux=stable\n", `"kernel" channel selector must be a track name`},
- {"kernel: baz-linux\n", "kernel: baz-linux=18/beta\n", `"kernel" channel selector must be a track name only`},
- {"kernel: baz-linux\n", "kernel:\n - xyz \n", `"kernel" header must be a string`},
- {"store: brand-store\n", "store:\n - xyz\n", `"store" header must be a string`},
- {mods.tsLine, "", `"timestamp" header is mandatory`},
- {mods.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
- {mods.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
- {reqSnaps, "required-snaps: foo\n", `"required-snaps" header must be a list of strings`},
- {reqSnaps, "required-snaps:\n -\n - nested\n", `"required-snaps" header must be a list of strings`},
- {sysUserAuths, "system-user-authority:\n a: 1\n", `"system-user-authority" header must be '\*' or a list of account ids`},
- {sysUserAuths, "system-user-authority:\n - 5_6\n", `"system-user-authority" header must be '\*' or a list of account ids`},
- }
-
- for _, test := range invalidTests {
- invalid := strings.Replace(encoded, test.original, test.invalid, 1)
- _, err := asserts.Decode([]byte(invalid))
- c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr)
- }
-}
-
-func (mods *modelSuite) TestModelCheck(c *C) {
- ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)))
- c.Assert(err, IsNil)
-
- storeDB, db := makeStoreAndCheckDB(c)
- brandDB := setup3rdPartySigning(c, "brand-id1", storeDB, db)
-
- headers := ex.Headers()
- headers["brand-id"] = brandDB.AuthorityID
- headers["timestamp"] = time.Now().Format(time.RFC3339)
- model, err := brandDB.Sign(asserts.ModelType, headers, nil, "")
- c.Assert(err, IsNil)
-
- err = db.Check(model)
- c.Assert(err, IsNil)
-}
-
-func (mods *modelSuite) TestModelCheckInconsistentTimestamp(c *C) {
- ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)))
- c.Assert(err, IsNil)
-
- storeDB, db := makeStoreAndCheckDB(c)
- brandDB := setup3rdPartySigning(c, "brand-id1", storeDB, db)
-
- headers := ex.Headers()
- headers["brand-id"] = brandDB.AuthorityID
- headers["timestamp"] = "2011-01-01T14:00:00Z"
- model, err := brandDB.Sign(asserts.ModelType, headers, nil, "")
- c.Assert(err, IsNil)
-
- err = db.Check(model)
- c.Assert(err, ErrorMatches, `model assertion timestamp outside of signing key validity \(key valid since.*\)`)
-}
-
-func (mods *modelSuite) TestClassicDecodeOK(c *C) {
- encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- c.Check(a.Type(), Equals, asserts.ModelType)
- model := a.(*asserts.Model)
- c.Check(model.AuthorityID(), Equals, "brand-id1")
- c.Check(model.Timestamp(), Equals, mods.ts)
- c.Check(model.Series(), Equals, "16")
- c.Check(model.BrandID(), Equals, "brand-id1")
- c.Check(model.Model(), Equals, "baz-3000")
- c.Check(model.DisplayName(), Equals, "Baz 3000")
- c.Check(model.Classic(), Equals, true)
- c.Check(model.Architecture(), Equals, "amd64")
- c.Check(model.Gadget(), Equals, "brand-gadget")
- c.Check(model.Kernel(), Equals, "")
- c.Check(model.KernelTrack(), Equals, "")
- c.Check(model.Store(), Equals, "brand-store")
- c.Check(model.RequiredSnaps(), DeepEquals, []string{"foo", "bar"})
-}
-
-func (mods *modelSuite) TestClassicDecodeInvalid(c *C) {
- encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1)
-
- invalidTests := []struct{ original, invalid, expectedErr string }{
- {"classic: true\n", "classic: foo\n", `"classic" header must be 'true' or 'false'`},
- {"architecture: amd64\n", "architecture:\n - foo\n", `"architecture" header must be a string`},
- {"gadget: brand-gadget\n", "gadget:\n - foo\n", `"gadget" header must be a string`},
- {"gadget: brand-gadget\n", "kernel: brand-kernel\n", `cannot specify a kernel with a classic model`},
- {"gadget: brand-gadget\n", "base: some-base\n", `cannot specify a base with a classic model`},
- }
-
- for _, test := range invalidTests {
- invalid := strings.Replace(encoded, test.original, test.invalid, 1)
- _, err := asserts.Decode([]byte(invalid))
- c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr)
- }
-}
-
-func (mods *modelSuite) TestClassicDecodeGadgetAndArchOptional(c *C) {
- encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1)
- encoded = strings.Replace(encoded, "gadget: brand-gadget\n", "", 1)
- encoded = strings.Replace(encoded, "architecture: amd64\n", "", 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- c.Check(a.Type(), Equals, asserts.ModelType)
- model := a.(*asserts.Model)
- c.Check(model.Classic(), Equals, true)
- c.Check(model.Architecture(), Equals, "")
- c.Check(model.Gadget(), Equals, "")
-}
-
-type serialSuite struct {
- ts time.Time
- tsLine string
- deviceKey asserts.PrivateKey
- encodedDevKey string
-}
-
-func (ss *serialSuite) SetUpSuite(c *C) {
- ss.ts = time.Now().Truncate(time.Second).UTC()
- ss.tsLine = "timestamp: " + ss.ts.Format(time.RFC3339) + "\n"
-
- ss.deviceKey = testPrivKey2
- encodedPubKey, err := asserts.EncodePublicKey(ss.deviceKey.PublicKey())
- c.Assert(err, IsNil)
- ss.encodedDevKey = string(encodedPubKey)
-}
-
-const serialExample = "type: serial\n" +
- "authority-id: brand-id1\n" +
- "brand-id: brand-id1\n" +
- "model: baz-3000\n" +
- "serial: 2700\n" +
- "device-key:\n DEVICEKEY\n" +
- "device-key-sha3-384: KEYID\n" +
- "TSLINE" +
- "body-length: 2\n" +
- "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
- "HW" +
- "\n\n" +
- "AXNpZw=="
-
-func (ss *serialSuite) TestDecodeOK(c *C) {
- encoded := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1)
- encoded = strings.Replace(encoded, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
- encoded = strings.Replace(encoded, "KEYID", ss.deviceKey.PublicKey().ID(), 1)
- a, err := asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
- c.Check(a.Type(), Equals, asserts.SerialType)
- serial := a.(*asserts.Serial)
- c.Check(serial.AuthorityID(), Equals, "brand-id1")
- c.Check(serial.Timestamp(), Equals, ss.ts)
- c.Check(serial.BrandID(), Equals, "brand-id1")
- c.Check(serial.Model(), Equals, "baz-3000")
- c.Check(serial.Serial(), Equals, "2700")
- c.Check(serial.DeviceKey().ID(), Equals, ss.deviceKey.PublicKey().ID())
-}
-
-const (
- deviceSessReqErrPrefix = "assertion device-session-request: "
- serialErrPrefix = "assertion serial: "
- serialReqErrPrefix = "assertion serial-request: "
-)
-
-func (ss *serialSuite) TestDecodeInvalid(c *C) {
- encoded := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1)
-
- invalidTests := []struct{ original, invalid, expectedErr string }{
- {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`},
- {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
- {"authority-id: brand-id1\n", "authority-id: random\n", `authority-id and brand-id must match, serial assertions are expected to be signed by the brand: "random" != "brand-id1"`},
- {"model: baz-3000\n", "", `"model" header is mandatory`},
- {"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
- {"model: baz-3000\n", "model: _what\n", `"model" header contains invalid characters: "_what"`},
- {"serial: 2700\n", "", `"serial" header is mandatory`},
- {"serial: 2700\n", "serial: \n", `"serial" header should not be empty`},
- {ss.tsLine, "", `"timestamp" header is mandatory`},
- {ss.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
- {ss.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
- {"device-key:\n DEVICEKEY\n", "", `"device-key" header is mandatory`},
- {"device-key:\n DEVICEKEY\n", "device-key: \n", `"device-key" header should not be empty`},
- {"device-key:\n DEVICEKEY\n", "device-key: $$$\n", `cannot decode public key: .*`},
- {"device-key-sha3-384: KEYID\n", "", `"device-key-sha3-384" header is mandatory`},
- }
-
- for _, test := range invalidTests {
- invalid := strings.Replace(encoded, test.original, test.invalid, 1)
- invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
- invalid = strings.Replace(invalid, "KEYID", ss.deviceKey.PublicKey().ID(), 1)
- _, err := asserts.Decode([]byte(invalid))
- c.Check(err, ErrorMatches, serialErrPrefix+test.expectedErr)
- }
-}
-
-func (ss *serialSuite) TestDecodeKeyIDMismatch(c *C) {
- invalid := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1)
- invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
- invalid = strings.Replace(invalid, "KEYID", "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", 1)
-
- _, err := asserts.Decode([]byte(invalid))
- c.Check(err, ErrorMatches, serialErrPrefix+"device key does not match provided key id")
-}
-
-func (ss *serialSuite) TestSerialCheck(c *C) {
- encoded := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1)
- encoded = strings.Replace(encoded, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
- encoded = strings.Replace(encoded, "KEYID", ss.deviceKey.PublicKey().ID(), 1)
- ex, err := asserts.Decode([]byte(encoded))
- c.Assert(err, IsNil)
-
- storeDB, db := makeStoreAndCheckDB(c)
- brandDB := setup3rdPartySigning(c, "brand1", storeDB, db)
-
- tests := []struct {
- signDB assertstest.SignerDB
- brandID string
- authID string
- keyID string
- }{
- {brandDB, brandDB.AuthorityID, "", brandDB.KeyID},
- }
-
- for _, test := range tests {
- headers := ex.Headers()
- headers["brand-id"] = test.brandID
- if test.authID != "" {
- headers["authority-id"] = test.authID
- } else {
- headers["authority-id"] = test.brandID
- }
- headers["timestamp"] = time.Now().Format(time.RFC3339)
- serial, err := test.signDB.Sign(asserts.SerialType, headers, nil, test.keyID)
- c.Assert(err, IsNil)
-
- err = db.Check(serial)
- c.Check(err, IsNil)
- }
-}
-
-func (ss *serialSuite) TestSerialRequestHappy(c *C) {
- sreq, err := asserts.SignWithoutAuthority(asserts.SerialRequestType,
- map[string]interface{}{
- "brand-id": "brand-id1",
- "model": "baz-3000",
- "device-key": ss.encodedDevKey,
- "request-id": "REQID",
- }, []byte("HW-DETAILS"), ss.deviceKey)
- c.Assert(err, IsNil)
-
- // roundtrip
- a, err := asserts.Decode(asserts.Encode(sreq))
- c.Assert(err, IsNil)
-
- sreq2, ok := a.(*asserts.SerialRequest)
- c.Assert(ok, Equals, true)
-
- // standalone signature check
- err = asserts.SignatureCheck(sreq2, sreq2.DeviceKey())
- c.Check(err, IsNil)
-
- c.Check(sreq2.BrandID(), Equals, "brand-id1")
- c.Check(sreq2.Model(), Equals, "baz-3000")
- c.Check(sreq2.RequestID(), Equals, "REQID")
-
- c.Check(sreq2.Serial(), Equals, "")
-}
-
-func (ss *serialSuite) TestSerialRequestHappyOptionalSerial(c *C) {
- sreq, err := asserts.SignWithoutAuthority(asserts.SerialRequestType,
- map[string]interface{}{
- "brand-id": "brand-id1",
- "model": "baz-3000",
- "serial": "pserial",
- "device-key": ss.encodedDevKey,
- "request-id": "REQID",
- }, []byte("HW-DETAILS"), ss.deviceKey)
- c.Assert(err, IsNil)
-
- // roundtrip
- a, err := asserts.Decode(asserts.Encode(sreq))
- c.Assert(err, IsNil)
-
- sreq2, ok := a.(*asserts.SerialRequest)
- c.Assert(ok, Equals, true)
-
- c.Check(sreq2.Model(), Equals, "baz-3000")
- c.Check(sreq2.Serial(), Equals, "pserial")
-}
-
-func (ss *serialSuite) TestSerialRequestDecodeInvalid(c *C) {
- encoded := "type: serial-request\n" +
- "brand-id: brand-id1\n" +
- "model: baz-3000\n" +
- "device-key:\n DEVICEKEY\n" +
- "request-id: REQID\n" +
- "serial: S\n" +
- "body-length: 2\n" +
- "sign-key-sha3-384: " + ss.deviceKey.PublicKey().ID() + "\n\n" +
- "HW" +
- "\n\n" +
- "AXNpZw=="
-
- invalidTests := []struct{ original, invalid, expectedErr string }{
- {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`},
- {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
- {"model: baz-3000\n", "", `"model" header is mandatory`},
- {"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
- {"request-id: REQID\n", "", `"request-id" header is mandatory`},
- {"request-id: REQID\n", "request-id: \n", `"request-id" header should not be empty`},
- {"device-key:\n DEVICEKEY\n", "", `"device-key" header is mandatory`},
- {"device-key:\n DEVICEKEY\n", "device-key: \n", `"device-key" header should not be empty`},
- {"device-key:\n DEVICEKEY\n", "device-key: $$$\n", `cannot decode public key: .*`},
- {"serial: S\n", "serial:\n - xyz\n", `"serial" header must be a string`},
- }
-
- for _, test := range invalidTests {
- invalid := strings.Replace(encoded, test.original, test.invalid, 1)
- invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
-
- _, err := asserts.Decode([]byte(invalid))
- c.Check(err, ErrorMatches, serialReqErrPrefix+test.expectedErr)
- }
-}
-
-func (ss *serialSuite) TestSerialRequestDecodeKeyIDMismatch(c *C) {
- invalid := "type: serial-request\n" +
- "brand-id: brand-id1\n" +
- "model: baz-3000\n" +
- "device-key:\n " + strings.Replace(ss.encodedDevKey, "\n", "\n ", -1) + "\n" +
- "request-id: REQID\n" +
- "body-length: 2\n" +
- "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
- "HW" +
- "\n\n" +
- "AXNpZw=="
-
- _, err := asserts.Decode([]byte(invalid))
- c.Check(err, ErrorMatches, "assertion serial-request: device key does not match included signing key id")
-}
-
-func (ss *serialSuite) TestDeviceSessionRequest(c *C) {
- ts := time.Now().UTC().Round(time.Second)
- sessReq, err := asserts.SignWithoutAuthority(asserts.DeviceSessionRequestType,
- map[string]interface{}{
- "brand-id": "brand-id1",
- "model": "baz-3000",
- "serial": "99990",
- "nonce": "NONCE",
- "timestamp": ts.Format(time.RFC3339),
- }, nil, ss.deviceKey)
- c.Assert(err, IsNil)
-
- // roundtrip
- a, err := asserts.Decode(asserts.Encode(sessReq))
- c.Assert(err, IsNil)
-
- sessReq2, ok := a.(*asserts.DeviceSessionRequest)
- c.Assert(ok, Equals, true)
-
- // standalone signature check
- err = asserts.SignatureCheck(sessReq2, ss.deviceKey.PublicKey())
- c.Check(err, IsNil)
-
- c.Check(sessReq2.BrandID(), Equals, "brand-id1")
- c.Check(sessReq2.Model(), Equals, "baz-3000")
- c.Check(sessReq2.Serial(), Equals, "99990")
- c.Check(sessReq2.Nonce(), Equals, "NONCE")
- c.Check(sessReq2.Timestamp().Equal(ts), Equals, true)
-}
-
-func (ss *serialSuite) TestDeviceSessionRequestDecodeInvalid(c *C) {
- tsLine := "timestamp: " + time.Now().Format(time.RFC3339) + "\n"
- encoded := "type: device-session-request\n" +
- "brand-id: brand-id1\n" +
- "model: baz-3000\n" +
- "serial: 99990\n" +
- "nonce: NONCE\n" +
- tsLine +
- "body-length: 0\n" +
- "sign-key-sha3-384: " + ss.deviceKey.PublicKey().ID() + "\n\n" +
- "AXNpZw=="
-
- invalidTests := []struct{ original, invalid, expectedErr string }{
- {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
- {"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
- {"serial: 99990\n", "", `"serial" header is mandatory`},
- {"nonce: NONCE\n", "nonce: \n", `"nonce" header should not be empty`},
- {tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
- }
-
- for _, test := range invalidTests {
- invalid := strings.Replace(encoded, test.original, test.invalid, 1)
- _, err := asserts.Decode([]byte(invalid))
- c.Check(err, ErrorMatches, deviceSessReqErrPrefix+test.expectedErr)
- }
-}
diff -Nru snapd-2.40/asserts/export_test.go snapd-2.42.1/asserts/export_test.go
--- snapd-2.40/asserts/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/asserts/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -191,3 +191,7 @@
func RuleFeature(rule featureExposer, flabel string) bool {
return rule.feature(flabel)
}
+
+func (b *Batch) DoPrecheck(db *Database) error {
+ return b.precheck(db)
+}
diff -Nru snapd-2.40/asserts/header_checks.go snapd-2.42.1/asserts/header_checks.go
--- snapd-2.40/asserts/header_checks.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/asserts/header_checks.go 2019-10-30 12:17:43.000000000 +0000
@@ -62,18 +62,22 @@
return s, nil
}
-func checkOptionalString(headers map[string]interface{}, name string) (string, error) {
+func checkOptionalStringWhat(headers map[string]interface{}, name, what string) (string, error) {
value, ok := headers[name]
if !ok {
return "", nil
}
s, ok := value.(string)
if !ok {
- return "", fmt.Errorf("%q header must be a string", name)
+ return "", fmt.Errorf("%q %s must be a string", name, what)
}
return s, nil
}
+func checkOptionalString(headers map[string]interface{}, name string) (string, error) {
+ return checkOptionalStringWhat(headers, name, "header")
+}
+
func checkPrimaryKey(headers map[string]interface{}, primKey string) (string, error) {
value, err := checkNotEmptyString(headers, primKey)
if err != nil {
diff -Nru snapd-2.40/asserts/model.go snapd-2.42.1/asserts/model.go
--- snapd-2.40/asserts/model.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/asserts/model.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,686 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package asserts
+
+import (
+ "fmt"
+ "regexp"
+ "strings"
+ "time"
+
+ "github.com/snapcore/snapd/snap/channel"
+ "github.com/snapcore/snapd/snap/naming"
+ "github.com/snapcore/snapd/strutil"
+)
+
+// TODO: for ModelSnap
+// * consider moving snap.Type out of snap and using it in ModelSnap
+// but remember assertions use "core" (never "os") for TypeOS
+// * consider having a first-class Presence type
+
+// ModelSnap holds the details about a snap specified by a model assertion.
+type ModelSnap struct {
+ Name string
+ SnapID string
+ // SnapType is one of: app|base|gadget|kernel|core, default is app
+ SnapType string
+ // Modes in which the snap must be made available
+ Modes []string
+ // DefaultChannel is the initial tracking channel, default is stable
+ DefaultChannel string
+ // Track is a locked track for the snap, if set DefaultChannel
+ // cannot be set at the same time
+ Track string
+ // Presence is one of: required|optional
+ Presence string
+}
+
+// SnapName implements naming.SnapRef.
+func (s *ModelSnap) SnapName() string {
+ return s.Name
+}
+
+// ID implements naming.SnapRef.
+func (s *ModelSnap) ID() string {
+ return s.SnapID
+}
+
+type modelSnaps struct {
+ base *ModelSnap
+ gadget *ModelSnap
+ kernel *ModelSnap
+ snapsNoEssential []*ModelSnap
+}
+
+func (ms *modelSnaps) list() (allSnaps []*ModelSnap, requiredWithEssentialSnaps []naming.SnapRef, numEssentialSnaps int) {
+ addSnap := func(snap *ModelSnap, essentialSnap int) {
+ if snap == nil {
+ return
+ }
+ numEssentialSnaps += essentialSnap
+ allSnaps = append(allSnaps, snap)
+ if snap.Presence == "required" {
+ requiredWithEssentialSnaps = append(requiredWithEssentialSnaps, snap)
+ }
+ }
+
+ addSnap(ms.base, 1)
+ addSnap(ms.gadget, 1)
+ addSnap(ms.kernel, 1)
+ for _, snap := range ms.snapsNoEssential {
+ addSnap(snap, 0)
+ }
+ return allSnaps, requiredWithEssentialSnaps, numEssentialSnaps
+}
+
+var (
+ essentialSnapModes = []string{"run", "ephemeral"}
+ defaultModes = []string{"run"}
+)
+
+func checkExtendedSnaps(extendedSnaps interface{}, base string) (*modelSnaps, error) {
+ const wrongHeaderType = `"snaps" header must be a list of maps`
+
+ entries, ok := extendedSnaps.([]interface{})
+ if !ok {
+ return nil, fmt.Errorf(wrongHeaderType)
+ }
+
+ var modelSnaps modelSnaps
+ seen := make(map[string]bool, len(entries))
+ seenIDs := make(map[string]string, len(entries))
+
+ for _, entry := range entries {
+ snap, ok := entry.(map[string]interface{})
+ if !ok {
+ return nil, fmt.Errorf(wrongHeaderType)
+ }
+ modelSnap, err := checkModelSnap(snap)
+ if err != nil {
+ return nil, err
+ }
+
+ if seen[modelSnap.Name] {
+ return nil, fmt.Errorf("cannot list the same snap %q multiple times", modelSnap.Name)
+ }
+ // at this time we do not support parallel installing
+ // from model/seed
+ if underName := seenIDs[modelSnap.SnapID]; underName != "" {
+ return nil, fmt.Errorf("cannot specify the same snap id %q multiple times, specified for snaps %q and %q", modelSnap.SnapID, underName, modelSnap.Name)
+ }
+ seen[modelSnap.Name] = true
+ seenIDs[modelSnap.SnapID] = modelSnap.Name
+
+ essential := false
+ switch {
+ case modelSnap.SnapType == "kernel":
+ essential = true
+ if modelSnaps.kernel != nil {
+ return nil, fmt.Errorf("cannot specify multiple kernel snaps: %q and %q", modelSnaps.kernel.Name, modelSnap.Name)
+ }
+ modelSnaps.kernel = modelSnap
+ case modelSnap.SnapType == "gadget":
+ essential = true
+ if modelSnaps.gadget != nil {
+ return nil, fmt.Errorf("cannot specify multiple gadget snaps: %q and %q", modelSnaps.gadget.Name, modelSnap.Name)
+ }
+ modelSnaps.gadget = modelSnap
+ case modelSnap.Name == base:
+ essential = true
+ if modelSnap.SnapType != "base" {
+ return nil, fmt.Errorf(`boot base %q must specify type "base", not %q`, base, modelSnap.SnapType)
+ }
+ modelSnaps.base = modelSnap
+ }
+
+ if essential {
+ if len(modelSnap.Modes) != 0 || modelSnap.Presence != "" {
+ return nil, fmt.Errorf("essential snaps are always available, cannot specify modes or presence for snap %q", modelSnap.Name)
+ }
+ modelSnap.Modes = essentialSnapModes
+ }
+
+ if len(modelSnap.Modes) == 0 {
+ modelSnap.Modes = defaultModes
+ }
+ if modelSnap.Presence == "" {
+ modelSnap.Presence = "required"
+ }
+
+ if !essential {
+ modelSnaps.snapsNoEssential = append(modelSnaps.snapsNoEssential, modelSnap)
+ }
+ }
+
+ return &modelSnaps, nil
+}
+
+var (
+ validSnapTypes = []string{"app", "base", "gadget", "kernel", "core"}
+ validSnapMode = regexp.MustCompile("^[a-z][-a-z]+$")
+ validSnapPresences = []string{"required", "optional"}
+)
+
+func checkModelSnap(snap map[string]interface{}) (*ModelSnap, error) {
+ name, err := checkNotEmptyStringWhat(snap, "name", "of snap")
+ if err != nil {
+ return nil, err
+ }
+ if err := naming.ValidateSnap(name); err != nil {
+ return nil, fmt.Errorf("invalid snap name %q", name)
+ }
+
+ what := fmt.Sprintf("of snap %q", name)
+
+ snapID, err := checkStringMatchesWhat(snap, "id", what, validSnapID)
+ if err != nil {
+ return nil, err
+ }
+
+ typ, err := checkOptionalStringWhat(snap, "type", what)
+ if err != nil {
+ return nil, err
+ }
+ if typ == "" {
+ typ = "app"
+ }
+ if !strutil.ListContains(validSnapTypes, typ) {
+ return nil, fmt.Errorf("type of snap %q must be one of app|base|gadget|kernel|core", name)
+ }
+
+ modes, err := checkStringListInMap(snap, "modes", fmt.Sprintf("%q %s", "modes", what), validSnapMode)
+ if err != nil {
+ return nil, err
+ }
+
+ defaultChannel, err := checkOptionalStringWhat(snap, "default-channel", what)
+ if err != nil {
+ return nil, err
+ }
+ // TODO: final name of this
+ track, err := checkOptionalStringWhat(snap, "track", what)
+ if err != nil {
+ return nil, err
+ }
+
+ if defaultChannel != "" && track != "" {
+ return nil, fmt.Errorf("snap %q cannot specify both default channel and locked track", name)
+ }
+ if track == "" && defaultChannel == "" {
+ defaultChannel = "stable"
+ }
+
+ if defaultChannel != "" {
+ _, err := channel.Parse(defaultChannel, "-")
+ if err != nil {
+ return nil, fmt.Errorf("invalid default channel for snap %q: %v", name, err)
+ }
+ } else {
+ trackCh, err := channel.ParseVerbatim(track, "-")
+ if err != nil || !trackCh.VerbatimTrackOnly() {
+ return nil, fmt.Errorf("invalid locked track for snap %q: %s", name, track)
+ }
+ }
+
+ presence, err := checkOptionalStringWhat(snap, "presence", what)
+ if err != nil {
+ return nil, err
+ }
+ if presence != "" && !strutil.ListContains(validSnapPresences, presence) {
+ return nil, fmt.Errorf("presence of snap %q must be one of required|optional", name)
+ }
+
+ return &ModelSnap{
+ Name: name,
+ SnapID: snapID,
+ SnapType: typ,
+ Modes: modes, // can be empty
+ DefaultChannel: defaultChannel,
+ Track: track,
+ Presence: presence, // can be empty
+ }, nil
+}
+
+// unextended case support
+
+func checkSnapWithTrack(headers map[string]interface{}, which string) (*ModelSnap, error) {
+ _, ok := headers[which]
+ if !ok {
+ return nil, nil
+ }
+ value, ok := headers[which].(string)
+ if !ok {
+ return nil, fmt.Errorf(`%q header must be a string`, which)
+ }
+ l := strings.SplitN(value, "=", 2)
+
+ name := l[0]
+ track := ""
+ if err := validateSnapName(name, which); err != nil {
+ return nil, err
+ }
+ if len(l) > 1 {
+ track = l[1]
+ if strings.Count(track, "/") != 0 {
+ return nil, fmt.Errorf(`%q channel selector must be a track name only`, which)
+ }
+ channelRisks := []string{"stable", "candidate", "beta", "edge"}
+ if strutil.ListContains(channelRisks, track) {
+ return nil, fmt.Errorf(`%q channel selector must be a track name`, which)
+ }
+ }
+
+ defaultChannel := ""
+ if track == "" {
+ defaultChannel = "stable"
+ }
+
+ return &ModelSnap{
+ Name: name,
+ SnapType: which,
+ Modes: defaultModes,
+ DefaultChannel: defaultChannel,
+ Track: track,
+ Presence: "required",
+ }, nil
+}
+
+func validateSnapName(name string, headerName string) error {
+ if err := naming.ValidateSnap(name); err != nil {
+ return fmt.Errorf("invalid snap name in %q header: %s", headerName, name)
+ }
+ return nil
+}
+
+func checkRequiredSnap(name string, headerName string, snapType string) (*ModelSnap, error) {
+ if err := validateSnapName(name, headerName); err != nil {
+ return nil, err
+ }
+
+ return &ModelSnap{
+ Name: name,
+ SnapType: snapType,
+ Modes: defaultModes,
+ DefaultChannel: "stable",
+ Presence: "required",
+ }, nil
+}
+
+// Model holds a model assertion, which is a statement by a brand
+// about the properties of a device model.
+type Model struct {
+ assertionBase
+ classic bool
+
+ baseSnap *ModelSnap
+ gadgetSnap *ModelSnap
+ kernelSnap *ModelSnap
+
+ allSnaps []*ModelSnap
+ // consumers of this info should care only about snap identity =>
+ // snapRef
+ requiredWithEssentialSnaps []naming.SnapRef
+ numEssentialSnaps int
+
+ sysUserAuthority []string
+ timestamp time.Time
+}
+
+// BrandID returns the brand identifier. Same as the authority id.
+func (mod *Model) BrandID() string {
+ return mod.HeaderString("brand-id")
+}
+
+// Model returns the model name identifier.
+func (mod *Model) Model() string {
+ return mod.HeaderString("model")
+}
+
+// DisplayName returns the human-friendly name of the model or
+// falls back to Model if this was not set.
+func (mod *Model) DisplayName() string {
+ display := mod.HeaderString("display-name")
+ if display == "" {
+ return mod.Model()
+ }
+ return display
+}
+
+// Series returns the series of the core software the model uses.
+func (mod *Model) Series() string {
+ return mod.HeaderString("series")
+}
+
+// Classic returns whether the model is a classic system.
+func (mod *Model) Classic() bool {
+ return mod.classic
+}
+
+// Architecture returns the archicteture the model is based on.
+func (mod *Model) Architecture() string {
+ return mod.HeaderString("architecture")
+}
+
+// GadgetSnap returns the details of the gadget snap the model uses.
+func (mod *Model) GadgetSnap() *ModelSnap {
+ return mod.gadgetSnap
+}
+
+// Gadget returns the gadget snap the model uses.
+func (mod *Model) Gadget() string {
+ if mod.gadgetSnap == nil {
+ return ""
+ }
+ return mod.gadgetSnap.Name
+}
+
+// GadgetTrack returns the gadget track the model uses.
+// XXX this should go away
+func (mod *Model) GadgetTrack() string {
+ if mod.gadgetSnap == nil {
+ return ""
+ }
+ return mod.gadgetSnap.Track
+}
+
+// KernelSnap returns the details of the kernel snap the model uses.
+func (mod *Model) KernelSnap() *ModelSnap {
+ return mod.kernelSnap
+}
+
+// Kernel returns the kernel snap the model uses.
+// XXX this should go away
+func (mod *Model) Kernel() string {
+ if mod.kernelSnap == nil {
+ return ""
+ }
+ return mod.kernelSnap.Name
+}
+
+// KernelTrack returns the kernel track the model uses.
+// XXX this should go away
+func (mod *Model) KernelTrack() string {
+ if mod.kernelSnap == nil {
+ return ""
+ }
+ return mod.kernelSnap.Track
+}
+
+// Base returns the base snap the model uses.
+func (mod *Model) Base() string {
+ return mod.HeaderString("base")
+}
+
+// BaseSnap returns the details of the base snap the model uses.
+func (mod *Model) BaseSnap() *ModelSnap {
+ return mod.baseSnap
+}
+
+// Store returns the snap store the model uses.
+func (mod *Model) Store() string {
+ return mod.HeaderString("store")
+}
+
+// RequiredNoEssentialSnaps returns the snaps that must be installed at all times and cannot be removed for this model, excluding the essential snaps (gadget, kernel, boot base).
+func (mod *Model) RequiredNoEssentialSnaps() []naming.SnapRef {
+ return mod.requiredWithEssentialSnaps[mod.numEssentialSnaps:]
+}
+
+// RequiredWithEssentialSnaps returns the snaps that must be installed at all times and cannot be removed for this model, including the essential snaps (gadget, kernel, boot base).
+func (mod *Model) RequiredWithEssentialSnaps() []naming.SnapRef {
+ return mod.requiredWithEssentialSnaps
+}
+
+// AllSnaps returns all the snap listed by the model.
+func (mod *Model) AllSnaps() []*ModelSnap {
+ return mod.allSnaps
+}
+
+// SystemUserAuthority returns the authority ids that are accepted as signers of system-user assertions for this model. Empty list means any.
+func (mod *Model) SystemUserAuthority() []string {
+ return mod.sysUserAuthority
+}
+
+// Timestamp returns the time when the model assertion was issued.
+func (mod *Model) Timestamp() time.Time {
+ return mod.timestamp
+}
+
+// Implement further consistency checks.
+func (mod *Model) checkConsistency(db RODatabase, acck *AccountKey) error {
+ // TODO: double check trust level of authority depending on class and possibly allowed-modes
+ return nil
+}
+
+// sanity
+var _ consistencyChecker = (*Model)(nil)
+
+// limit model to only lowercase for now
+var validModel = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$")
+
+func checkModel(headers map[string]interface{}) (string, error) {
+ s, err := checkStringMatches(headers, "model", validModel)
+ if err != nil {
+ return "", err
+ }
+
+ // TODO: support the concept of case insensitive/preserving string headers
+ if strings.ToLower(s) != s {
+ return "", fmt.Errorf(`"model" header cannot contain uppercase letters`)
+ }
+ return s, nil
+}
+
+func checkAuthorityMatchesBrand(a Assertion) error {
+ typeName := a.Type().Name
+ authorityID := a.AuthorityID()
+ brand := a.HeaderString("brand-id")
+ if brand != authorityID {
+ return fmt.Errorf("authority-id and brand-id must match, %s assertions are expected to be signed by the brand: %q != %q", typeName, authorityID, brand)
+ }
+ return nil
+}
+
+func checkOptionalSystemUserAuthority(headers map[string]interface{}, brandID string) ([]string, error) {
+ const name = "system-user-authority"
+ v, ok := headers[name]
+ if !ok {
+ return []string{brandID}, nil
+ }
+ switch x := v.(type) {
+ case string:
+ if x == "*" {
+ return nil, nil
+ }
+ case []interface{}:
+ lst, err := checkStringListMatches(headers, name, validAccountID)
+ if err == nil {
+ return lst, nil
+ }
+ }
+ return nil, fmt.Errorf("%q header must be '*' or a list of account ids", name)
+}
+
+var (
+ modelMandatory = []string{"architecture", "gadget", "kernel"}
+ extendedCoreMandatory = []string{"architecture", "base"}
+ extendedSnapsConflicting = []string{"gadget", "kernel", "required-snaps"}
+ classicModelOptional = []string{"architecture", "gadget"}
+)
+
+func assembleModel(assert assertionBase) (Assertion, error) {
+ err := checkAuthorityMatchesBrand(&assert)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkModel(assert.headers)
+ if err != nil {
+ return nil, err
+ }
+
+ classic, err := checkOptionalBool(assert.headers, "classic")
+ if err != nil {
+ return nil, err
+ }
+
+ // Core 20 extended snaps header
+ extendedSnaps, extended := assert.headers["snaps"]
+ if extended {
+ if classic {
+ return nil, fmt.Errorf("cannot use extended snaps header for a classic model (yet)")
+ }
+
+ for _, conflicting := range extendedSnapsConflicting {
+ if _, ok := assert.headers[conflicting]; ok {
+ return nil, fmt.Errorf("cannot specify separate %q header once using the extended snaps header", conflicting)
+ }
+ }
+
+ } else if classic {
+ if _, ok := assert.headers["kernel"]; ok {
+ return nil, fmt.Errorf("cannot specify a kernel with a classic model")
+ }
+ if _, ok := assert.headers["base"]; ok {
+ return nil, fmt.Errorf("cannot specify a base with a classic model")
+ }
+ }
+
+ checker := checkNotEmptyString
+ toCheck := modelMandatory
+ if extended {
+ toCheck = extendedCoreMandatory
+ } else if classic {
+ checker = checkOptionalString
+ toCheck = classicModelOptional
+ }
+
+ for _, h := range toCheck {
+ if _, err := checker(assert.headers, h); err != nil {
+ return nil, err
+ }
+ }
+
+ // base, if provided, must be a valid snap name too
+ var baseSnap *ModelSnap
+ base, err := checkOptionalString(assert.headers, "base")
+ if err != nil {
+ return nil, err
+ }
+ if base != "" {
+ baseSnap, err = checkRequiredSnap(base, "base", "base")
+ if err != nil {
+ return nil, err
+ }
+ }
+
+ // store is optional but must be a string, defaults to the ubuntu store
+ if _, err = checkOptionalString(assert.headers, "store"); err != nil {
+ return nil, err
+ }
+
+ // display-name is optional but must be a string
+ if _, err = checkOptionalString(assert.headers, "display-name"); err != nil {
+ return nil, err
+ }
+
+ var modSnaps *modelSnaps
+ if extended {
+ // TODO: support and consider grade!
+ modSnaps, err = checkExtendedSnaps(extendedSnaps, base)
+ if err != nil {
+ return nil, err
+ }
+ if modSnaps.gadget == nil {
+ return nil, fmt.Errorf(`one "snaps" header entry must specify the model gadget`)
+ }
+ if modSnaps.kernel == nil {
+ return nil, fmt.Errorf(`one "snaps" header entry must specify the model kernel`)
+ }
+
+ if modSnaps.base == nil {
+ // complete with defaults,
+ // the assumption is that base names are very stable
+ // essentially fixed
+ modSnaps.base = baseSnap
+ modSnaps.base.Modes = essentialSnapModes
+ }
+ } else {
+ modSnaps = &modelSnaps{
+ base: baseSnap,
+ }
+ // kernel/gadget must be valid snap names and can have (optional) tracks
+ // - validate those
+ modSnaps.kernel, err = checkSnapWithTrack(assert.headers, "kernel")
+ if err != nil {
+ return nil, err
+ }
+ modSnaps.gadget, err = checkSnapWithTrack(assert.headers, "gadget")
+ if err != nil {
+ return nil, err
+ }
+
+ // required snap must be valid snap names
+ reqSnaps, err := checkStringList(assert.headers, "required-snaps")
+ if err != nil {
+ return nil, err
+ }
+ for _, name := range reqSnaps {
+ reqSnap, err := checkRequiredSnap(name, "required-snaps", "")
+ if err != nil {
+ return nil, err
+ }
+ modSnaps.snapsNoEssential = append(modSnaps.snapsNoEssential, reqSnap)
+ }
+ }
+
+ sysUserAuthority, err := checkOptionalSystemUserAuthority(assert.headers, assert.HeaderString("brand-id"))
+ if err != nil {
+ return nil, err
+ }
+
+ timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
+ if err != nil {
+ return nil, err
+ }
+
+ allSnaps, requiredWithEssentialSnaps, numEssentialSnaps := modSnaps.list()
+
+ // NB:
+ // * core is not supported at this time, it defaults to ubuntu-core
+ // in prepare-image until rename and/or introduction of the header.
+ // * some form of allowed-modes, class are postponed,
+ //
+ // prepare-image takes care of not allowing them for now
+
+ // ignore extra headers and non-empty body for future compatibility
+ return &Model{
+ assertionBase: assert,
+ classic: classic,
+ baseSnap: modSnaps.base,
+ gadgetSnap: modSnaps.gadget,
+ kernelSnap: modSnaps.kernel,
+ allSnaps: allSnaps,
+ requiredWithEssentialSnaps: requiredWithEssentialSnaps,
+ numEssentialSnaps: numEssentialSnaps,
+ sysUserAuthority: sysUserAuthority,
+ timestamp: timestamp,
+ }, nil
+}
diff -Nru snapd-2.40/asserts/model_test.go snapd-2.42.1/asserts/model_test.go
--- snapd-2.40/asserts/model_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/asserts/model_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,731 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package asserts_test
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+)
+
+type modelSuite struct {
+ ts time.Time
+ tsLine string
+}
+
+var (
+ _ = Suite(&modelSuite{})
+)
+
+func (mods *modelSuite) SetUpSuite(c *C) {
+ mods.ts = time.Now().Truncate(time.Second).UTC()
+ mods.tsLine = "timestamp: " + mods.ts.Format(time.RFC3339) + "\n"
+}
+
+const (
+ reqSnaps = "required-snaps:\n - foo\n - bar\n"
+ sysUserAuths = "system-user-authority: *\n"
+)
+
+const (
+ modelExample = "type: model\n" +
+ "authority-id: brand-id1\n" +
+ "series: 16\n" +
+ "brand-id: brand-id1\n" +
+ "model: baz-3000\n" +
+ "display-name: Baz 3000\n" +
+ "architecture: amd64\n" +
+ "gadget: brand-gadget\n" +
+ "base: core18\n" +
+ "kernel: baz-linux\n" +
+ "store: brand-store\n" +
+ sysUserAuths +
+ reqSnaps +
+ "TSLINE" +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+
+ classicModelExample = "type: model\n" +
+ "authority-id: brand-id1\n" +
+ "series: 16\n" +
+ "brand-id: brand-id1\n" +
+ "model: baz-3000\n" +
+ "display-name: Baz 3000\n" +
+ "classic: true\n" +
+ "architecture: amd64\n" +
+ "gadget: brand-gadget\n" +
+ "store: brand-store\n" +
+ reqSnaps +
+ "TSLINE" +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+
+ core20ModelExample = `type: model
+authority-id: brand-id1
+series: 16
+brand-id: brand-id1
+model: baz-3000
+display-name: Baz 3000
+architecture: amd64
+system-user-authority: *
+base: core20
+store: brand-store
+snaps:
+ -
+ name: baz-linux
+ id: bazlinuxidididididididididididid
+ type: kernel
+ track: 20
+ -
+ name: brand-gadget
+ id: brandgadgetdidididididididididid
+ type: gadget
+ -
+ name: other-base
+ id: otherbasedididididididididididid
+ type: base
+ modes:
+ - run
+ presence: required
+ -
+ name: nm
+ id: nmididididididididididididididid
+ modes:
+ - ephemeral
+ - run
+ default-channel: 1.0
+ -
+ name: myapp
+ id: myappdididididididididididididid
+ type: app
+ default-channel: 2.0
+ -
+ name: myappopt
+ id: myappoptidididididididididididid
+ type: app
+ presence: optional
+OTHERgrade: stable
+` + "TSLINE" +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+)
+
+func (mods *modelSuite) TestDecodeOK(c *C) {
+ encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.ModelType)
+ model := a.(*asserts.Model)
+ c.Check(model.AuthorityID(), Equals, "brand-id1")
+ c.Check(model.Timestamp(), Equals, mods.ts)
+ c.Check(model.Series(), Equals, "16")
+ c.Check(model.BrandID(), Equals, "brand-id1")
+ c.Check(model.Model(), Equals, "baz-3000")
+ c.Check(model.DisplayName(), Equals, "Baz 3000")
+ c.Check(model.Architecture(), Equals, "amd64")
+ c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
+ Name: "brand-gadget",
+ SnapType: "gadget",
+ Modes: []string{"run"},
+ DefaultChannel: "stable",
+ Presence: "required",
+ })
+ c.Check(model.Gadget(), Equals, "brand-gadget")
+ c.Check(model.GadgetTrack(), Equals, "")
+ c.Check(model.KernelSnap(), DeepEquals, &asserts.ModelSnap{
+ Name: "baz-linux",
+ SnapType: "kernel",
+ Modes: []string{"run"},
+ DefaultChannel: "stable",
+ Presence: "required",
+ })
+ c.Check(model.Kernel(), Equals, "baz-linux")
+ c.Check(model.KernelTrack(), Equals, "")
+ c.Check(model.Base(), Equals, "core18")
+ c.Check(model.BaseSnap(), DeepEquals, &asserts.ModelSnap{
+ Name: "core18",
+ SnapType: "base",
+ Modes: []string{"run"},
+ DefaultChannel: "stable",
+ Presence: "required",
+ })
+ c.Check(model.Store(), Equals, "brand-store")
+ allSnaps := model.AllSnaps()
+ c.Check(allSnaps, DeepEquals, []*asserts.ModelSnap{
+ model.BaseSnap(),
+ model.GadgetSnap(),
+ model.KernelSnap(),
+ {
+ Name: "foo",
+ Modes: []string{"run"},
+ DefaultChannel: "stable",
+ Presence: "required",
+ },
+ {
+ Name: "bar",
+ Modes: []string{"run"},
+ DefaultChannel: "stable",
+ Presence: "required",
+ },
+ })
+ // essential snaps included
+ reqSnaps := model.RequiredWithEssentialSnaps()
+ c.Check(reqSnaps, HasLen, len(allSnaps))
+ for i, r := range reqSnaps {
+ c.Check(r.SnapName(), Equals, allSnaps[i].Name)
+ c.Check(r.ID(), Equals, "")
+ }
+ // essential snaps excluded
+ c.Check(model.RequiredNoEssentialSnaps(), DeepEquals, reqSnaps[3:])
+ c.Check(model.SystemUserAuthority(), HasLen, 0)
+}
+
+func (mods *modelSuite) TestDecodeStoreIsOptional(c *C) {
+ withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ encoded := strings.Replace(withTimestamp, "store: brand-store\n", "store: \n", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model := a.(*asserts.Model)
+ c.Check(model.Store(), Equals, "")
+
+ encoded = strings.Replace(withTimestamp, "store: brand-store\n", "", 1)
+ a, err = asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model = a.(*asserts.Model)
+ c.Check(model.Store(), Equals, "")
+}
+
+func (mods *modelSuite) TestDecodeBaseIsOptional(c *C) {
+ withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ encoded := strings.Replace(withTimestamp, "base: core18\n", "base: \n", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model := a.(*asserts.Model)
+ c.Check(model.Base(), Equals, "")
+
+ encoded = strings.Replace(withTimestamp, "base: core18\n", "", 1)
+ a, err = asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model = a.(*asserts.Model)
+ c.Check(model.Base(), Equals, "")
+ c.Check(model.BaseSnap(), IsNil)
+}
+
+func (mods *modelSuite) TestDecodeDisplayNameIsOptional(c *C) {
+ withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ encoded := strings.Replace(withTimestamp, "display-name: Baz 3000\n", "display-name: \n", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model := a.(*asserts.Model)
+ // optional but we fallback to Model
+ c.Check(model.DisplayName(), Equals, "baz-3000")
+
+ encoded = strings.Replace(withTimestamp, "display-name: Baz 3000\n", "", 1)
+ a, err = asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model = a.(*asserts.Model)
+ // optional but we fallback to Model
+ c.Check(model.DisplayName(), Equals, "baz-3000")
+}
+
+func (mods *modelSuite) TestDecodeRequiredSnapsAreOptional(c *C) {
+ withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ encoded := strings.Replace(withTimestamp, reqSnaps, "", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model := a.(*asserts.Model)
+ c.Check(model.RequiredNoEssentialSnaps(), HasLen, 0)
+}
+
+func (mods *modelSuite) TestDecodeValidatesSnapNames(c *C) {
+ withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ encoded := strings.Replace(withTimestamp, reqSnaps, "required-snaps:\n - foo_bar\n - bar\n", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(a, IsNil)
+ c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "required-snaps" header: foo_bar`)
+
+ encoded = strings.Replace(withTimestamp, reqSnaps, "required-snaps:\n - foo\n - bar-;;''\n", 1)
+ a, err = asserts.Decode([]byte(encoded))
+ c.Assert(a, IsNil)
+ c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "required-snaps" header: bar-;;''`)
+
+ encoded = strings.Replace(withTimestamp, "kernel: baz-linux\n", "kernel: baz-linux_instance\n", 1)
+ a, err = asserts.Decode([]byte(encoded))
+ c.Assert(a, IsNil)
+ c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "kernel" header: baz-linux_instance`)
+
+ encoded = strings.Replace(withTimestamp, "gadget: brand-gadget\n", "gadget: brand-gadget_instance\n", 1)
+ a, err = asserts.Decode([]byte(encoded))
+ c.Assert(a, IsNil)
+ c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "gadget" header: brand-gadget_instance`)
+
+ encoded = strings.Replace(withTimestamp, "base: core18\n", "base: core18_instance\n", 1)
+ a, err = asserts.Decode([]byte(encoded))
+ c.Assert(a, IsNil)
+ c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "base" header: core18_instance`)
+}
+
+func (mods modelSuite) TestDecodeValidSnapNames(c *C) {
+ // reuse test cases for snap.ValidateName()
+
+ withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+
+ validNames := []string{
+ "aa", "aaa", "aaaa",
+ "a-a", "aa-a", "a-aa", "a-b-c",
+ "a0", "a-0", "a-0a",
+ "01game", "1-or-2",
+ // a regexp stresser
+ "u-94903713687486543234157734673284536758",
+ }
+ for _, name := range validNames {
+ encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", fmt.Sprintf("kernel: %s\n", name), 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model := a.(*asserts.Model)
+ c.Check(model.Kernel(), Equals, name)
+ }
+ invalidNames := []string{
+ // name cannot be empty, never reaches snap name validation
+ "",
+ // too short (min 2 chars)
+ "a",
+ // names cannot be too long
+ "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx",
+ "xxxxxxxxxxxxxxxxxxxx-xxxxxxxxxxxxxxxxxxxx",
+ "1111111111111111111111111111111111111111x",
+ "x1111111111111111111111111111111111111111",
+ "x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x-x",
+ // a regexp stresser
+ "u-9490371368748654323415773467328453675-",
+ // dashes alone are not a name
+ "-", "--",
+ // double dashes in a name are not allowed
+ "a--a",
+ // name should not end with a dash
+ "a-",
+ // name cannot have any spaces in it
+ "a ", " a", "a a",
+ // a number alone is not a name
+ "0", "123",
+ // identifier must be plain ASCII
+ "日本語", "한글", "ру́сский язы́к",
+ // instance names are invalid too
+ "foo_bar", "x_1",
+ }
+ for _, name := range invalidNames {
+ encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", fmt.Sprintf("kernel: %s\n", name), 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(a, IsNil)
+ if name != "" {
+ c.Assert(err, ErrorMatches, `assertion model: invalid snap name in "kernel" header: .*`)
+ } else {
+ c.Assert(err, ErrorMatches, `assertion model: "kernel" header should not be empty`)
+ }
+ }
+}
+
+func (mods *modelSuite) TestDecodeSystemUserAuthorityIsOptional(c *C) {
+ withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ encoded := strings.Replace(withTimestamp, sysUserAuths, "", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model := a.(*asserts.Model)
+ // the default is just to accept the brand itself
+ c.Check(model.SystemUserAuthority(), DeepEquals, []string{"brand-id1"})
+
+ encoded = strings.Replace(withTimestamp, sysUserAuths, "system-user-authority:\n - foo\n - bar\n", 1)
+ a, err = asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model = a.(*asserts.Model)
+ c.Check(model.SystemUserAuthority(), DeepEquals, []string{"foo", "bar"})
+}
+
+func (mods *modelSuite) TestDecodeKernelTrack(c *C) {
+ withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ encoded := strings.Replace(withTimestamp, "kernel: baz-linux\n", "kernel: baz-linux=18\n", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model := a.(*asserts.Model)
+ c.Check(model.KernelSnap(), DeepEquals, &asserts.ModelSnap{
+ Name: "baz-linux",
+ SnapType: "kernel",
+ Modes: []string{"run"},
+ Track: "18",
+ Presence: "required",
+ })
+ c.Check(model.Kernel(), Equals, "baz-linux")
+ c.Check(model.KernelTrack(), Equals, "18")
+}
+
+func (mods *modelSuite) TestDecodeGadgetTrack(c *C) {
+ withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+ encoded := strings.Replace(withTimestamp, "gadget: brand-gadget\n", "gadget: brand-gadget=18\n", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ model := a.(*asserts.Model)
+ c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
+ Name: "brand-gadget",
+ SnapType: "gadget",
+ Modes: []string{"run"},
+ Track: "18",
+ Presence: "required",
+ })
+ c.Check(model.Gadget(), Equals, "brand-gadget")
+ c.Check(model.GadgetTrack(), Equals, "18")
+}
+
+const (
+ modelErrPrefix = "assertion model: "
+)
+
+func (mods *modelSuite) TestDecodeInvalid(c *C) {
+ encoded := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"series: 16\n", "", `"series" header is mandatory`},
+ {"series: 16\n", "series: \n", `"series" header should not be empty`},
+ {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`},
+ {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
+ {"brand-id: brand-id1\n", "brand-id: random\n", `authority-id and brand-id must match, model assertions are expected to be signed by the brand: "brand-id1" != "random"`},
+ {"model: baz-3000\n", "", `"model" header is mandatory`},
+ {"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
+ {"model: baz-3000\n", "model: baz/3000\n", `"model" primary key header cannot contain '/'`},
+ // lift this restriction at a later point
+ {"model: baz-3000\n", "model: BAZ-3000\n", `"model" header cannot contain uppercase letters`},
+ {"display-name: Baz 3000\n", "display-name:\n - xyz\n", `"display-name" header must be a string`},
+ {"architecture: amd64\n", "", `"architecture" header is mandatory`},
+ {"architecture: amd64\n", "architecture: \n", `"architecture" header should not be empty`},
+ {"gadget: brand-gadget\n", "", `"gadget" header is mandatory`},
+ {"gadget: brand-gadget\n", "gadget: \n", `"gadget" header should not be empty`},
+ {"gadget: brand-gadget\n", "gadget: brand-gadget=x/x/x\n", `"gadget" channel selector must be a track name only`},
+ {"gadget: brand-gadget\n", "gadget: brand-gadget=stable\n", `"gadget" channel selector must be a track name`},
+ {"gadget: brand-gadget\n", "gadget: brand-gadget=18/beta\n", `"gadget" channel selector must be a track name only`},
+ {"gadget: brand-gadget\n", "gadget:\n - xyz \n", `"gadget" header must be a string`},
+ {"kernel: baz-linux\n", "", `"kernel" header is mandatory`},
+ {"kernel: baz-linux\n", "kernel: \n", `"kernel" header should not be empty`},
+ {"kernel: baz-linux\n", "kernel: baz-linux=x/x/x\n", `"kernel" channel selector must be a track name only`},
+ {"kernel: baz-linux\n", "kernel: baz-linux=stable\n", `"kernel" channel selector must be a track name`},
+ {"kernel: baz-linux\n", "kernel: baz-linux=18/beta\n", `"kernel" channel selector must be a track name only`},
+ {"kernel: baz-linux\n", "kernel:\n - xyz \n", `"kernel" header must be a string`},
+ {"base: core18\n", "base:\n - xyz \n", `"base" header must be a string`},
+ {"store: brand-store\n", "store:\n - xyz\n", `"store" header must be a string`},
+ {mods.tsLine, "", `"timestamp" header is mandatory`},
+ {mods.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
+ {mods.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
+ {reqSnaps, "required-snaps: foo\n", `"required-snaps" header must be a list of strings`},
+ {reqSnaps, "required-snaps:\n -\n - nested\n", `"required-snaps" header must be a list of strings`},
+ {sysUserAuths, "system-user-authority:\n a: 1\n", `"system-user-authority" header must be '\*' or a list of account ids`},
+ {sysUserAuths, "system-user-authority:\n - 5_6\n", `"system-user-authority" header must be '\*' or a list of account ids`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr)
+ }
+}
+
+func (mods *modelSuite) TestModelCheck(c *C) {
+ ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)))
+ c.Assert(err, IsNil)
+
+ storeDB, db := makeStoreAndCheckDB(c)
+ brandDB := setup3rdPartySigning(c, "brand-id1", storeDB, db)
+
+ headers := ex.Headers()
+ headers["brand-id"] = brandDB.AuthorityID
+ headers["timestamp"] = time.Now().Format(time.RFC3339)
+ model, err := brandDB.Sign(asserts.ModelType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(model)
+ c.Assert(err, IsNil)
+}
+
+func (mods *modelSuite) TestModelCheckInconsistentTimestamp(c *C) {
+ ex, err := asserts.Decode([]byte(strings.Replace(modelExample, "TSLINE", mods.tsLine, 1)))
+ c.Assert(err, IsNil)
+
+ storeDB, db := makeStoreAndCheckDB(c)
+ brandDB := setup3rdPartySigning(c, "brand-id1", storeDB, db)
+
+ headers := ex.Headers()
+ headers["brand-id"] = brandDB.AuthorityID
+ headers["timestamp"] = "2011-01-01T14:00:00Z"
+ model, err := brandDB.Sign(asserts.ModelType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = db.Check(model)
+ c.Assert(err, ErrorMatches, `model assertion timestamp outside of signing key validity \(key valid since.*\)`)
+}
+
+func (mods *modelSuite) TestClassicDecodeOK(c *C) {
+ encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.ModelType)
+ model := a.(*asserts.Model)
+ c.Check(model.AuthorityID(), Equals, "brand-id1")
+ c.Check(model.Timestamp(), Equals, mods.ts)
+ c.Check(model.Series(), Equals, "16")
+ c.Check(model.BrandID(), Equals, "brand-id1")
+ c.Check(model.Model(), Equals, "baz-3000")
+ c.Check(model.DisplayName(), Equals, "Baz 3000")
+ c.Check(model.Classic(), Equals, true)
+ c.Check(model.Architecture(), Equals, "amd64")
+ c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
+ Name: "brand-gadget",
+ SnapType: "gadget",
+ Modes: []string{"run"},
+ DefaultChannel: "stable",
+ Presence: "required",
+ })
+ c.Check(model.Gadget(), Equals, "brand-gadget")
+ c.Check(model.KernelSnap(), IsNil)
+ c.Check(model.Kernel(), Equals, "")
+ c.Check(model.KernelTrack(), Equals, "")
+ c.Check(model.Base(), Equals, "")
+ c.Check(model.BaseSnap(), IsNil)
+ c.Check(model.Store(), Equals, "brand-store")
+ allSnaps := model.AllSnaps()
+ c.Check(allSnaps, DeepEquals, []*asserts.ModelSnap{
+ model.GadgetSnap(),
+ {
+ Name: "foo",
+ Modes: []string{"run"},
+ DefaultChannel: "stable",
+ Presence: "required",
+ },
+ {
+ Name: "bar",
+ Modes: []string{"run"},
+ DefaultChannel: "stable",
+ Presence: "required",
+ },
+ })
+ // gadget included
+ reqSnaps := model.RequiredWithEssentialSnaps()
+ c.Check(reqSnaps, HasLen, len(allSnaps))
+ for i, r := range reqSnaps {
+ c.Check(r.SnapName(), Equals, allSnaps[i].Name)
+ c.Check(r.ID(), Equals, "")
+ }
+ // gadget excluded
+ c.Check(model.RequiredNoEssentialSnaps(), DeepEquals, reqSnaps[1:])
+}
+
+func (mods *modelSuite) TestClassicDecodeInvalid(c *C) {
+ encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1)
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"classic: true\n", "classic: foo\n", `"classic" header must be 'true' or 'false'`},
+ {"architecture: amd64\n", "architecture:\n - foo\n", `"architecture" header must be a string`},
+ {"gadget: brand-gadget\n", "gadget:\n - foo\n", `"gadget" header must be a string`},
+ {"gadget: brand-gadget\n", "kernel: brand-kernel\n", `cannot specify a kernel with a classic model`},
+ {"gadget: brand-gadget\n", "base: some-base\n", `cannot specify a base with a classic model`},
+ {"gadget: brand-gadget\n", "gadget:\n - xyz\n", `"gadget" header must be a string`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr)
+ }
+}
+
+func (mods *modelSuite) TestClassicDecodeGadgetAndArchOptional(c *C) {
+ encoded := strings.Replace(classicModelExample, "TSLINE", mods.tsLine, 1)
+ encoded = strings.Replace(encoded, "gadget: brand-gadget\n", "", 1)
+ encoded = strings.Replace(encoded, "architecture: amd64\n", "", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.ModelType)
+ model := a.(*asserts.Model)
+ c.Check(model.Classic(), Equals, true)
+ c.Check(model.Architecture(), Equals, "")
+ c.Check(model.GadgetSnap(), IsNil)
+ c.Check(model.Gadget(), Equals, "")
+ c.Check(model.GadgetTrack(), Equals, "")
+}
+
+func (mods *modelSuite) TestCore20DecodeOK(c *C) {
+ encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
+ encoded = strings.Replace(encoded, "OTHER", "", 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.ModelType)
+ model := a.(*asserts.Model)
+ c.Check(model.AuthorityID(), Equals, "brand-id1")
+ c.Check(model.Timestamp(), Equals, mods.ts)
+ c.Check(model.Series(), Equals, "16")
+ c.Check(model.BrandID(), Equals, "brand-id1")
+ c.Check(model.Model(), Equals, "baz-3000")
+ c.Check(model.DisplayName(), Equals, "Baz 3000")
+ c.Check(model.Architecture(), Equals, "amd64")
+ c.Check(model.GadgetSnap(), DeepEquals, &asserts.ModelSnap{
+ Name: "brand-gadget",
+ SnapID: "brandgadgetdidididididididididid",
+ SnapType: "gadget",
+ Modes: []string{"run", "ephemeral"},
+ DefaultChannel: "stable",
+ Presence: "required",
+ })
+ c.Check(model.Gadget(), Equals, "brand-gadget")
+ c.Check(model.GadgetTrack(), Equals, "")
+ c.Check(model.KernelSnap(), DeepEquals, &asserts.ModelSnap{
+ Name: "baz-linux",
+ SnapID: "bazlinuxidididididididididididid",
+ SnapType: "kernel",
+ Modes: []string{"run", "ephemeral"},
+ Track: "20",
+ Presence: "required",
+ })
+ c.Check(model.Kernel(), Equals, "baz-linux")
+ c.Check(model.KernelTrack(), Equals, "20")
+ c.Check(model.Base(), Equals, "core20")
+ c.Check(model.BaseSnap(), DeepEquals, &asserts.ModelSnap{
+ Name: "core20",
+ SnapType: "base",
+ Modes: []string{"run", "ephemeral"},
+ DefaultChannel: "stable",
+ Presence: "required",
+ })
+ c.Check(model.Store(), Equals, "brand-store")
+ allSnaps := model.AllSnaps()
+ c.Check(allSnaps, DeepEquals, []*asserts.ModelSnap{
+ model.BaseSnap(),
+ model.GadgetSnap(),
+ model.KernelSnap(),
+ {
+ Name: "other-base",
+ SnapID: "otherbasedididididididididididid",
+ SnapType: "base",
+ Modes: []string{"run"},
+ DefaultChannel: "stable",
+ Presence: "required",
+ },
+ {
+ Name: "nm",
+ SnapID: "nmididididididididididididididid",
+ SnapType: "app",
+ Modes: []string{"ephemeral", "run"},
+ DefaultChannel: "1.0",
+ Presence: "required",
+ },
+ {
+ Name: "myapp",
+ SnapID: "myappdididididididididididididid",
+ SnapType: "app",
+ Modes: []string{"run"},
+ DefaultChannel: "2.0",
+ Presence: "required",
+ },
+ {
+ Name: "myappopt",
+ SnapID: "myappoptidididididididididididid",
+ SnapType: "app",
+ Modes: []string{"run"},
+ DefaultChannel: "stable",
+ Presence: "optional",
+ },
+ })
+ // essential snaps included
+ reqSnaps := model.RequiredWithEssentialSnaps()
+ c.Check(reqSnaps, HasLen, len(allSnaps)-1)
+ for i, r := range reqSnaps {
+ c.Check(r.SnapName(), Equals, allSnaps[i].Name)
+ c.Check(r.ID(), Equals, allSnaps[i].SnapID)
+ }
+ // essential snaps excluded
+ c.Check(model.RequiredNoEssentialSnaps(), DeepEquals, reqSnaps[3:])
+ c.Check(model.SystemUserAuthority(), HasLen, 0)
+}
+
+func (mods *modelSuite) TestCore20ExplictBootBase(c *C) {
+ encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
+ encoded = strings.Replace(encoded, "OTHER", ` -
+ name: core20
+ id: core20ididididididididididididid
+ type: base
+ default-channel: candidate
+`, 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.ModelType)
+ // model := a.(*asserts.Model)
+}
+
+func (mods *modelSuite) TestCore20DecodeInvalid(c *C) {
+ encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1)
+
+ snapsStanza := encoded[strings.Index(encoded, "snaps:"):strings.Index(encoded, "grade:")]
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"base: core20\n", "", `"base" header is mandatory`},
+ {"OTHER", "classic: true\n", `cannot use extended snaps header for a classic model \(yet\)`},
+ {snapsStanza, "snaps: snap\n", `"snaps" header must be a list of maps`},
+ {snapsStanza, "snaps:\n - snap\n", `"snaps" header must be a list of maps`},
+ {"name: myapp\n", "other: 1\n", `"name" of snap is mandatory`},
+ {"name: myapp\n", "name: myapp_2\n", `invalid snap name "myapp_2"`},
+ {"id: myappdididididididididididididid\n", "id: 2\n", `"id" of snap "myapp" contains invalid characters: "2"`},
+ {"type: gadget\n", "type:\n - g\n", `"type" of snap "brand-gadget" must be a string`},
+ {"type: app\n", "type: thing\n", `"type" of snap "myappopt" must be one of must be one of app|base|gadget|kernel|core`},
+ {"modes:\n - run\n", "modes: run\n", `"modes" of snap "other-base" must be a list of strings`},
+ {"track: 20\n", "track:\n - x\n", `"track" of snap "baz-linux" must be a string`},
+ {"track: 20\n", "track: 20/edge\n", `invalid locked track for snap \"baz-linux\": 20/edge`},
+ {"track: 20\n", "track: 20////\n", `invalid locked track for snap \"baz-linux\": 20////`},
+ {"default-channel: 2.0\n", "default-channel:\n - x\n", `"default-channel" of snap "myapp" must be a string`},
+ {"default-channel: 2.0\n", "default-channel: 2.0/xyz/z\n", `invalid default channel for snap "myapp": invalid risk in channel name: 2.0/xyz/z`},
+ {"track: 20\n", "track: 20\n default-channel: 20/foo\n", `snap "baz-linux" cannot specify both default channel and locked track`},
+ {"presence: optional\n", "presence:\n - opt\n", `"presence" of snap "myappopt" must be a string`},
+ {"presence: optional\n", "presence: no\n", `"presence" of snap "myappopt" must be one of must be one of required|optional`},
+ {"OTHER", " -\n name: myapp\n id: myappdididididididididididididid\n", `cannot list the same snap "myapp" multiple times`},
+ {"OTHER", " -\n name: myapp2\n id: myappdididididididididididididid\n", `cannot specify the same snap id "myappdididididididididididididid" multiple times, specified for snaps "myapp" and "myapp2"`},
+ {"OTHER", " -\n name: kernel2\n id: kernel2didididididididididididid\n type: kernel\n", `cannot specify multiple kernel snaps: "baz-linux" and "kernel2"`},
+ {"OTHER", " -\n name: gadget2\n id: gadget2didididididididididididid\n type: gadget\n", `cannot specify multiple gadget snaps: "brand-gadget" and "gadget2"`},
+ {"type: gadget\n", "type: gadget\n presence: required\n", `essential snaps are always available, cannot specify modes or presence for snap "brand-gadget"`},
+ {"type: gadget\n", "type: gadget\n modes:\n - run\n", `essential snaps are always available, cannot specify modes or presence for snap "brand-gadget"`},
+ {"type: kernel\n", "type: kernel\n presence: required\n", `essential snaps are always available, cannot specify modes or presence for snap "baz-linux"`},
+ {"OTHER", " -\n name: core20\n id: core20ididididididididididididid\n type: base\n presence: optional\n", `essential snaps are always available, cannot specify modes or presence for snap "core20"`},
+ {"type: gadget\n", "type: app\n", `one "snaps" header entry must specify the model gadget`},
+ {"type: kernel\n", "type: app\n", `one "snaps" header entry must specify the model kernel`},
+ {"OTHER", " -\n name: core20\n id: core20ididididididididididididid\n type: app\n", `boot base "core20" must specify type "base", not "app"`},
+ {"OTHER", "kernel: foo\n", `cannot specify separate "kernel" header once using the extended snaps header`},
+ {"OTHER", "gadget: foo\n", `cannot specify separate "gadget" header once using the extended snaps header`},
+ {"OTHER", "required-snaps:\n - foo\n", `cannot specify separate "required-snaps" header once using the extended snaps header`},
+ }
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ invalid = strings.Replace(invalid, "OTHER", "", 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, modelErrPrefix+test.expectedErr)
+ }
+}
diff -Nru snapd-2.40/asserts/serial_asserts.go snapd-2.42.1/asserts/serial_asserts.go
--- snapd-2.40/asserts/serial_asserts.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/asserts/serial_asserts.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,229 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package asserts
+
+import (
+ "fmt"
+ "time"
+)
+
+// Serial holds a serial assertion, which is a statement binding a
+// device identity with the device public key.
+type Serial struct {
+ assertionBase
+ timestamp time.Time
+ pubKey PublicKey
+}
+
+// BrandID returns the brand identifier of the device.
+func (ser *Serial) BrandID() string {
+ return ser.HeaderString("brand-id")
+}
+
+// Model returns the model name identifier of the device.
+func (ser *Serial) Model() string {
+ return ser.HeaderString("model")
+}
+
+// Serial returns the serial identifier of the device, together with
+// brand id and model they form the unique identifier of the device.
+func (ser *Serial) Serial() string {
+ return ser.HeaderString("serial")
+}
+
+// DeviceKey returns the public key of the device.
+func (ser *Serial) DeviceKey() PublicKey {
+ return ser.pubKey
+}
+
+// Timestamp returns the time when the serial assertion was issued.
+func (ser *Serial) Timestamp() time.Time {
+ return ser.timestamp
+}
+
+// TODO: implement further consistency checks for Serial but first review approach
+
+func assembleSerial(assert assertionBase) (Assertion, error) {
+ err := checkAuthorityMatchesBrand(&assert)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkModel(assert.headers)
+ if err != nil {
+ return nil, err
+ }
+
+ encodedKey, err := checkNotEmptyString(assert.headers, "device-key")
+ if err != nil {
+ return nil, err
+ }
+ pubKey, err := DecodePublicKey([]byte(encodedKey))
+ if err != nil {
+ return nil, err
+ }
+ keyID, err := checkNotEmptyString(assert.headers, "device-key-sha3-384")
+ if err != nil {
+ return nil, err
+ }
+ if keyID != pubKey.ID() {
+ return nil, fmt.Errorf("device key does not match provided key id")
+ }
+
+ timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
+ if err != nil {
+ return nil, err
+ }
+
+ // ignore extra headers and non-empty body for future compatibility
+ return &Serial{
+ assertionBase: assert,
+ timestamp: timestamp,
+ pubKey: pubKey,
+ }, nil
+}
+
+// SerialRequest holds a serial-request assertion, which is a self-signed request to obtain a full device identity bound to the device public key.
+type SerialRequest struct {
+ assertionBase
+ pubKey PublicKey
+}
+
+// BrandID returns the brand identifier of the device making the request.
+func (sreq *SerialRequest) BrandID() string {
+ return sreq.HeaderString("brand-id")
+}
+
+// Model returns the model name identifier of the device making the request.
+func (sreq *SerialRequest) Model() string {
+ return sreq.HeaderString("model")
+}
+
+// Serial returns the optional proposed serial identifier for the device, the service taking the request might use it or ignore it.
+func (sreq *SerialRequest) Serial() string {
+ return sreq.HeaderString("serial")
+}
+
+// RequestID returns the id for the request, obtained from and to be presented to the serial signing service.
+func (sreq *SerialRequest) RequestID() string {
+ return sreq.HeaderString("request-id")
+}
+
+// DeviceKey returns the public key of the device making the request.
+func (sreq *SerialRequest) DeviceKey() PublicKey {
+ return sreq.pubKey
+}
+
+func assembleSerialRequest(assert assertionBase) (Assertion, error) {
+ _, err := checkNotEmptyString(assert.headers, "brand-id")
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkModel(assert.headers)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkNotEmptyString(assert.headers, "request-id")
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkOptionalString(assert.headers, "serial")
+ if err != nil {
+ return nil, err
+ }
+
+ encodedKey, err := checkNotEmptyString(assert.headers, "device-key")
+ if err != nil {
+ return nil, err
+ }
+ pubKey, err := DecodePublicKey([]byte(encodedKey))
+ if err != nil {
+ return nil, err
+ }
+
+ if pubKey.ID() != assert.SignKeyID() {
+ return nil, fmt.Errorf("device key does not match included signing key id")
+ }
+
+ // ignore extra headers and non-empty body for future compatibility
+ return &SerialRequest{
+ assertionBase: assert,
+ pubKey: pubKey,
+ }, nil
+}
+
+// DeviceSessionRequest holds a device-session-request assertion, which is a request wrapping a store-provided nonce to start a session by a device signed with its key.
+type DeviceSessionRequest struct {
+ assertionBase
+ timestamp time.Time
+}
+
+// BrandID returns the brand identifier of the device making the request.
+func (req *DeviceSessionRequest) BrandID() string {
+ return req.HeaderString("brand-id")
+}
+
+// Model returns the model name identifier of the device making the request.
+func (req *DeviceSessionRequest) Model() string {
+ return req.HeaderString("model")
+}
+
+// Serial returns the serial identifier of the device making the request,
+// together with brand id and model it forms the unique identifier of
+// the device.
+func (req *DeviceSessionRequest) Serial() string {
+ return req.HeaderString("serial")
+}
+
+// Nonce returns the nonce obtained from store and to be presented when requesting a device session.
+func (req *DeviceSessionRequest) Nonce() string {
+ return req.HeaderString("nonce")
+}
+
+// Timestamp returns the time when the device-session-request was created.
+func (req *DeviceSessionRequest) Timestamp() time.Time {
+ return req.timestamp
+}
+
+func assembleDeviceSessionRequest(assert assertionBase) (Assertion, error) {
+ _, err := checkModel(assert.headers)
+ if err != nil {
+ return nil, err
+ }
+
+ _, err = checkNotEmptyString(assert.headers, "nonce")
+ if err != nil {
+ return nil, err
+ }
+
+ timestamp, err := checkRFC3339Date(assert.headers, "timestamp")
+ if err != nil {
+ return nil, err
+ }
+
+ // ignore extra headers and non-empty body for future compatibility
+ return &DeviceSessionRequest{
+ assertionBase: assert,
+ timestamp: timestamp,
+ }, nil
+}
diff -Nru snapd-2.40/asserts/serial_asserts_test.go snapd-2.42.1/asserts/serial_asserts_test.go
--- snapd-2.40/asserts/serial_asserts_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/asserts/serial_asserts_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,320 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package asserts_test
+
+import (
+ "strings"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+)
+
+var (
+ _ = Suite(&serialSuite{})
+)
+
+type serialSuite struct {
+ ts time.Time
+ tsLine string
+ deviceKey asserts.PrivateKey
+ encodedDevKey string
+}
+
+func (ss *serialSuite) SetUpSuite(c *C) {
+ ss.ts = time.Now().Truncate(time.Second).UTC()
+ ss.tsLine = "timestamp: " + ss.ts.Format(time.RFC3339) + "\n"
+
+ ss.deviceKey = testPrivKey2
+ encodedPubKey, err := asserts.EncodePublicKey(ss.deviceKey.PublicKey())
+ c.Assert(err, IsNil)
+ ss.encodedDevKey = string(encodedPubKey)
+}
+
+const serialExample = "type: serial\n" +
+ "authority-id: brand-id1\n" +
+ "brand-id: brand-id1\n" +
+ "model: baz-3000\n" +
+ "serial: 2700\n" +
+ "device-key:\n DEVICEKEY\n" +
+ "device-key-sha3-384: KEYID\n" +
+ "TSLINE" +
+ "body-length: 2\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
+ "HW" +
+ "\n\n" +
+ "AXNpZw=="
+
+func (ss *serialSuite) TestDecodeOK(c *C) {
+ encoded := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1)
+ encoded = strings.Replace(encoded, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
+ encoded = strings.Replace(encoded, "KEYID", ss.deviceKey.PublicKey().ID(), 1)
+ a, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.SerialType)
+ serial := a.(*asserts.Serial)
+ c.Check(serial.AuthorityID(), Equals, "brand-id1")
+ c.Check(serial.Timestamp(), Equals, ss.ts)
+ c.Check(serial.BrandID(), Equals, "brand-id1")
+ c.Check(serial.Model(), Equals, "baz-3000")
+ c.Check(serial.Serial(), Equals, "2700")
+ c.Check(serial.DeviceKey().ID(), Equals, ss.deviceKey.PublicKey().ID())
+}
+
+const (
+ deviceSessReqErrPrefix = "assertion device-session-request: "
+ serialErrPrefix = "assertion serial: "
+ serialReqErrPrefix = "assertion serial-request: "
+)
+
+func (ss *serialSuite) TestDecodeInvalid(c *C) {
+ encoded := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1)
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`},
+ {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
+ {"authority-id: brand-id1\n", "authority-id: random\n", `authority-id and brand-id must match, serial assertions are expected to be signed by the brand: "random" != "brand-id1"`},
+ {"model: baz-3000\n", "", `"model" header is mandatory`},
+ {"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
+ {"model: baz-3000\n", "model: _what\n", `"model" header contains invalid characters: "_what"`},
+ {"serial: 2700\n", "", `"serial" header is mandatory`},
+ {"serial: 2700\n", "serial: \n", `"serial" header should not be empty`},
+ {ss.tsLine, "", `"timestamp" header is mandatory`},
+ {ss.tsLine, "timestamp: \n", `"timestamp" header should not be empty`},
+ {ss.tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
+ {"device-key:\n DEVICEKEY\n", "", `"device-key" header is mandatory`},
+ {"device-key:\n DEVICEKEY\n", "device-key: \n", `"device-key" header should not be empty`},
+ {"device-key:\n DEVICEKEY\n", "device-key: $$$\n", `cannot decode public key: .*`},
+ {"device-key-sha3-384: KEYID\n", "", `"device-key-sha3-384" header is mandatory`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
+ invalid = strings.Replace(invalid, "KEYID", ss.deviceKey.PublicKey().ID(), 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, serialErrPrefix+test.expectedErr)
+ }
+}
+
+func (ss *serialSuite) TestDecodeKeyIDMismatch(c *C) {
+ invalid := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1)
+ invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
+ invalid = strings.Replace(invalid, "KEYID", "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", 1)
+
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, serialErrPrefix+"device key does not match provided key id")
+}
+
+func (ss *serialSuite) TestSerialCheck(c *C) {
+ encoded := strings.Replace(serialExample, "TSLINE", ss.tsLine, 1)
+ encoded = strings.Replace(encoded, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
+ encoded = strings.Replace(encoded, "KEYID", ss.deviceKey.PublicKey().ID(), 1)
+ ex, err := asserts.Decode([]byte(encoded))
+ c.Assert(err, IsNil)
+
+ storeDB, db := makeStoreAndCheckDB(c)
+ brandDB := setup3rdPartySigning(c, "brand1", storeDB, db)
+
+ tests := []struct {
+ signDB assertstest.SignerDB
+ brandID string
+ authID string
+ keyID string
+ }{
+ {brandDB, brandDB.AuthorityID, "", brandDB.KeyID},
+ }
+
+ for _, test := range tests {
+ headers := ex.Headers()
+ headers["brand-id"] = test.brandID
+ if test.authID != "" {
+ headers["authority-id"] = test.authID
+ } else {
+ headers["authority-id"] = test.brandID
+ }
+ headers["timestamp"] = time.Now().Format(time.RFC3339)
+ serial, err := test.signDB.Sign(asserts.SerialType, headers, nil, test.keyID)
+ c.Assert(err, IsNil)
+
+ err = db.Check(serial)
+ c.Check(err, IsNil)
+ }
+}
+
+func (ss *serialSuite) TestSerialRequestHappy(c *C) {
+ sreq, err := asserts.SignWithoutAuthority(asserts.SerialRequestType,
+ map[string]interface{}{
+ "brand-id": "brand-id1",
+ "model": "baz-3000",
+ "device-key": ss.encodedDevKey,
+ "request-id": "REQID",
+ }, []byte("HW-DETAILS"), ss.deviceKey)
+ c.Assert(err, IsNil)
+
+ // roundtrip
+ a, err := asserts.Decode(asserts.Encode(sreq))
+ c.Assert(err, IsNil)
+
+ sreq2, ok := a.(*asserts.SerialRequest)
+ c.Assert(ok, Equals, true)
+
+ // standalone signature check
+ err = asserts.SignatureCheck(sreq2, sreq2.DeviceKey())
+ c.Check(err, IsNil)
+
+ c.Check(sreq2.BrandID(), Equals, "brand-id1")
+ c.Check(sreq2.Model(), Equals, "baz-3000")
+ c.Check(sreq2.RequestID(), Equals, "REQID")
+
+ c.Check(sreq2.Serial(), Equals, "")
+}
+
+func (ss *serialSuite) TestSerialRequestHappyOptionalSerial(c *C) {
+ sreq, err := asserts.SignWithoutAuthority(asserts.SerialRequestType,
+ map[string]interface{}{
+ "brand-id": "brand-id1",
+ "model": "baz-3000",
+ "serial": "pserial",
+ "device-key": ss.encodedDevKey,
+ "request-id": "REQID",
+ }, []byte("HW-DETAILS"), ss.deviceKey)
+ c.Assert(err, IsNil)
+
+ // roundtrip
+ a, err := asserts.Decode(asserts.Encode(sreq))
+ c.Assert(err, IsNil)
+
+ sreq2, ok := a.(*asserts.SerialRequest)
+ c.Assert(ok, Equals, true)
+
+ c.Check(sreq2.Model(), Equals, "baz-3000")
+ c.Check(sreq2.Serial(), Equals, "pserial")
+}
+
+func (ss *serialSuite) TestSerialRequestDecodeInvalid(c *C) {
+ encoded := "type: serial-request\n" +
+ "brand-id: brand-id1\n" +
+ "model: baz-3000\n" +
+ "device-key:\n DEVICEKEY\n" +
+ "request-id: REQID\n" +
+ "serial: S\n" +
+ "body-length: 2\n" +
+ "sign-key-sha3-384: " + ss.deviceKey.PublicKey().ID() + "\n\n" +
+ "HW" +
+ "\n\n" +
+ "AXNpZw=="
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"brand-id: brand-id1\n", "", `"brand-id" header is mandatory`},
+ {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
+ {"model: baz-3000\n", "", `"model" header is mandatory`},
+ {"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
+ {"request-id: REQID\n", "", `"request-id" header is mandatory`},
+ {"request-id: REQID\n", "request-id: \n", `"request-id" header should not be empty`},
+ {"device-key:\n DEVICEKEY\n", "", `"device-key" header is mandatory`},
+ {"device-key:\n DEVICEKEY\n", "device-key: \n", `"device-key" header should not be empty`},
+ {"device-key:\n DEVICEKEY\n", "device-key: $$$\n", `cannot decode public key: .*`},
+ {"serial: S\n", "serial:\n - xyz\n", `"serial" header must be a string`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ invalid = strings.Replace(invalid, "DEVICEKEY", strings.Replace(ss.encodedDevKey, "\n", "\n ", -1), 1)
+
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, serialReqErrPrefix+test.expectedErr)
+ }
+}
+
+func (ss *serialSuite) TestSerialRequestDecodeKeyIDMismatch(c *C) {
+ invalid := "type: serial-request\n" +
+ "brand-id: brand-id1\n" +
+ "model: baz-3000\n" +
+ "device-key:\n " + strings.Replace(ss.encodedDevKey, "\n", "\n ", -1) + "\n" +
+ "request-id: REQID\n" +
+ "body-length: 2\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij\n\n" +
+ "HW" +
+ "\n\n" +
+ "AXNpZw=="
+
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, "assertion serial-request: device key does not match included signing key id")
+}
+
+func (ss *serialSuite) TestDeviceSessionRequest(c *C) {
+ ts := time.Now().UTC().Round(time.Second)
+ sessReq, err := asserts.SignWithoutAuthority(asserts.DeviceSessionRequestType,
+ map[string]interface{}{
+ "brand-id": "brand-id1",
+ "model": "baz-3000",
+ "serial": "99990",
+ "nonce": "NONCE",
+ "timestamp": ts.Format(time.RFC3339),
+ }, nil, ss.deviceKey)
+ c.Assert(err, IsNil)
+
+ // roundtrip
+ a, err := asserts.Decode(asserts.Encode(sessReq))
+ c.Assert(err, IsNil)
+
+ sessReq2, ok := a.(*asserts.DeviceSessionRequest)
+ c.Assert(ok, Equals, true)
+
+ // standalone signature check
+ err = asserts.SignatureCheck(sessReq2, ss.deviceKey.PublicKey())
+ c.Check(err, IsNil)
+
+ c.Check(sessReq2.BrandID(), Equals, "brand-id1")
+ c.Check(sessReq2.Model(), Equals, "baz-3000")
+ c.Check(sessReq2.Serial(), Equals, "99990")
+ c.Check(sessReq2.Nonce(), Equals, "NONCE")
+ c.Check(sessReq2.Timestamp().Equal(ts), Equals, true)
+}
+
+func (ss *serialSuite) TestDeviceSessionRequestDecodeInvalid(c *C) {
+ tsLine := "timestamp: " + time.Now().Format(time.RFC3339) + "\n"
+ encoded := "type: device-session-request\n" +
+ "brand-id: brand-id1\n" +
+ "model: baz-3000\n" +
+ "serial: 99990\n" +
+ "nonce: NONCE\n" +
+ tsLine +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: " + ss.deviceKey.PublicKey().ID() + "\n\n" +
+ "AXNpZw=="
+
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"brand-id: brand-id1\n", "brand-id: \n", `"brand-id" header should not be empty`},
+ {"model: baz-3000\n", "model: \n", `"model" header should not be empty`},
+ {"serial: 99990\n", "", `"serial" header is mandatory`},
+ {"nonce: NONCE\n", "nonce: \n", `"nonce" header should not be empty`},
+ {tsLine, "timestamp: 12:30\n", `"timestamp" header is not a RFC3339 date: .*`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(encoded, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, deviceSessReqErrPrefix+test.expectedErr)
+ }
+}
diff -Nru snapd-2.40/asserts/system_user.go snapd-2.42.1/asserts/system_user.go
--- snapd-2.40/asserts/system_user.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/asserts/system_user.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,281 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package asserts
+
+import (
+ "fmt"
+ "net/mail"
+ "regexp"
+ "strconv"
+ "strings"
+ "time"
+)
+
+var validSystemUserUsernames = regexp.MustCompile(`^[a-z0-9][-a-z0-9+.-_]*$`)
+
+// SystemUser holds a system-user assertion which allows creating local
+// system users.
+type SystemUser struct {
+ assertionBase
+ series []string
+ models []string
+ sshKeys []string
+ since time.Time
+ until time.Time
+
+ forcePasswordChange bool
+}
+
+// BrandID returns the brand identifier that signed this assertion.
+func (su *SystemUser) BrandID() string {
+ return su.HeaderString("brand-id")
+}
+
+// Email returns the email address that this assertion is valid for.
+func (su *SystemUser) Email() string {
+ return su.HeaderString("email")
+}
+
+// Series returns the series that this assertion is valid for.
+func (su *SystemUser) Series() []string {
+ return su.series
+}
+
+// Models returns the models that this assertion is valid for.
+func (su *SystemUser) Models() []string {
+ return su.models
+}
+
+// Name returns the full name of the user (e.g. Random Guy).
+func (su *SystemUser) Name() string {
+ return su.HeaderString("name")
+}
+
+// Username returns the system user name that should be created (e.g. "foo").
+func (su *SystemUser) Username() string {
+ return su.HeaderString("username")
+}
+
+// Password returns the crypt(3) compatible password for the user.
+// Note that only ID: $6$ or stronger is supported (sha512crypt).
+func (su *SystemUser) Password() string {
+ return su.HeaderString("password")
+}
+
+// ForcePasswordChange returns true if the user needs to change the password
+// after the first login.
+func (su *SystemUser) ForcePasswordChange() bool {
+ return su.forcePasswordChange
+}
+
+// SSHKeys returns the ssh keys for the user.
+func (su *SystemUser) SSHKeys() []string {
+ return su.sshKeys
+}
+
+// Since returns the time since the assertion is valid.
+func (su *SystemUser) Since() time.Time {
+ return su.since
+}
+
+// Until returns the time until the assertion is valid.
+func (su *SystemUser) Until() time.Time {
+ return su.until
+}
+
+// ValidAt returns whether the system-user is valid at 'when' time.
+func (su *SystemUser) ValidAt(when time.Time) bool {
+ valid := when.After(su.since) || when.Equal(su.since)
+ if valid {
+ valid = when.Before(su.until)
+ }
+ return valid
+}
+
+// Implement further consistency checks.
+func (su *SystemUser) checkConsistency(db RODatabase, acck *AccountKey) error {
+ // Do the cross-checks when this assertion is actually used,
+ // i.e. in the create-user code. See also Model.checkConsitency
+
+ return nil
+}
+
+// sanity
+var _ consistencyChecker = (*SystemUser)(nil)
+
+type shadow struct {
+ ID string
+ Rounds string
+ Salt string
+ Hash string
+}
+
+// crypt(3) compatible hashes have the forms:
+// - $id$salt$hash
+// - $id$rounds=N$salt$hash
+func parseShadowLine(line string) (*shadow, error) {
+ l := strings.SplitN(line, "$", 5)
+ if len(l) != 4 && len(l) != 5 {
+ return nil, fmt.Errorf(`hashed password must be of the form "$integer-id$salt$hash", see crypt(3)`)
+ }
+
+ // if rounds is the second field, the line must consist of 4
+ if strings.HasPrefix(l[2], "rounds=") && len(l) == 4 {
+ return nil, fmt.Errorf(`missing hash field`)
+ }
+
+ // shadow line without $rounds=N$
+ if len(l) == 4 {
+ return &shadow{
+ ID: l[1],
+ Salt: l[2],
+ Hash: l[3],
+ }, nil
+ }
+ // shadow line with rounds
+ return &shadow{
+ ID: l[1],
+ Rounds: l[2],
+ Salt: l[3],
+ Hash: l[4],
+ }, nil
+}
+
+// see crypt(3) for the legal chars
+var isValidSaltAndHash = regexp.MustCompile(`^[a-zA-Z0-9./]+$`).MatchString
+
+func checkHashedPassword(headers map[string]interface{}, name string) (string, error) {
+ pw, err := checkOptionalString(headers, name)
+ if err != nil {
+ return "", err
+ }
+ // the pw string is optional, so just return if its empty
+ if pw == "" {
+ return "", nil
+ }
+
+ // parse the shadow line
+ shd, err := parseShadowLine(pw)
+ if err != nil {
+ return "", fmt.Errorf(`%q header invalid: %s`, name, err)
+ }
+
+ // and verify it
+
+ // see crypt(3), ID 6 means SHA-512 (since glibc 2.7)
+ ID, err := strconv.Atoi(shd.ID)
+ if err != nil {
+ return "", fmt.Errorf(`%q header must start with "$integer-id$", got %q`, name, shd.ID)
+ }
+ // double check that we only allow modern hashes
+ if ID < 6 {
+ return "", fmt.Errorf("%q header only supports $id$ values of 6 (sha512crypt) or higher", name)
+ }
+
+ // the $rounds=N$ part is optional
+ if strings.HasPrefix(shd.Rounds, "rounds=") {
+ rounds, err := strconv.Atoi(strings.SplitN(shd.Rounds, "=", 2)[1])
+ if err != nil {
+ return "", fmt.Errorf("%q header has invalid number of rounds: %s", name, err)
+ }
+ if rounds < 5000 || rounds > 999999999 {
+ return "", fmt.Errorf("%q header rounds parameter out of bounds: %d", name, rounds)
+ }
+ }
+
+ if !isValidSaltAndHash(shd.Salt) {
+ return "", fmt.Errorf("%q header has invalid chars in salt %q", name, shd.Salt)
+ }
+ if !isValidSaltAndHash(shd.Hash) {
+ return "", fmt.Errorf("%q header has invalid chars in hash %q", name, shd.Hash)
+ }
+
+ return pw, nil
+}
+
+func assembleSystemUser(assert assertionBase) (Assertion, error) {
+ // brand-id here can be different from authority-id,
+ // the code using the assertion must use the policy set
+ // by the model assertion system-user-authority header
+ email, err := checkNotEmptyString(assert.headers, "email")
+ if err != nil {
+ return nil, err
+ }
+ if _, err := mail.ParseAddress(email); err != nil {
+ return nil, fmt.Errorf(`"email" header must be a RFC 5322 compliant email address: %s`, err)
+ }
+
+ series, err := checkStringList(assert.headers, "series")
+ if err != nil {
+ return nil, err
+ }
+ models, err := checkStringList(assert.headers, "models")
+ if err != nil {
+ return nil, err
+ }
+ if _, err := checkOptionalString(assert.headers, "name"); err != nil {
+ return nil, err
+ }
+ if _, err := checkStringMatches(assert.headers, "username", validSystemUserUsernames); err != nil {
+ return nil, err
+ }
+ password, err := checkHashedPassword(assert.headers, "password")
+ if err != nil {
+ return nil, err
+ }
+ forcePasswordChange, err := checkOptionalBool(assert.headers, "force-password-change")
+ if err != nil {
+ return nil, err
+ }
+ if forcePasswordChange && password == "" {
+ return nil, fmt.Errorf(`cannot use "force-password-change" with an empty "password"`)
+ }
+
+ sshKeys, err := checkStringList(assert.headers, "ssh-keys")
+ if err != nil {
+ return nil, err
+ }
+ since, err := checkRFC3339Date(assert.headers, "since")
+ if err != nil {
+ return nil, err
+ }
+ until, err := checkRFC3339Date(assert.headers, "until")
+ if err != nil {
+ return nil, err
+ }
+ if until.Before(since) {
+ return nil, fmt.Errorf("'until' time cannot be before 'since' time")
+ }
+
+ // "global" system-user assertion can only be valid for 1y
+ if len(models) == 0 && until.After(since.AddDate(1, 0, 0)) {
+ return nil, fmt.Errorf("'until' time cannot be more than 365 days in the future when no models are specified")
+ }
+
+ return &SystemUser{
+ assertionBase: assert,
+ series: series,
+ models: models,
+ sshKeys: sshKeys,
+ since: since,
+ until: until,
+ forcePasswordChange: forcePasswordChange,
+ }, nil
+}
diff -Nru snapd-2.40/asserts/system_user_test.go snapd-2.42.1/asserts/system_user_test.go
--- snapd-2.40/asserts/system_user_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/asserts/system_user_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,214 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package asserts_test
+
+import (
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/snapcore/snapd/asserts"
+ . "gopkg.in/check.v1"
+)
+
+var (
+ _ = Suite(&systemUserSuite{})
+)
+
+type systemUserSuite struct {
+ until time.Time
+ untilLine string
+ since time.Time
+ sinceLine string
+
+ modelsLine string
+
+ systemUserStr string
+}
+
+const systemUserExample = "type: system-user\n" +
+ "authority-id: canonical\n" +
+ "brand-id: canonical\n" +
+ "email: foo@example.com\n" +
+ "series:\n" +
+ " - 16\n" +
+ "MODELSLINE\n" +
+ "name: Nice Guy\n" +
+ "username: guy\n" +
+ "password: $6$salt$hash\n" +
+ "ssh-keys:\n" +
+ " - ssh-rsa AAAABcdefg\n" +
+ "SINCELINE\n" +
+ "UNTILLINE\n" +
+ "body-length: 0\n" +
+ "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
+ "\n\n" +
+ "AXNpZw=="
+
+func (s *systemUserSuite) SetUpTest(c *C) {
+ s.since = time.Now().Truncate(time.Second)
+ s.sinceLine = fmt.Sprintf("since: %s\n", s.since.Format(time.RFC3339))
+ s.until = time.Now().AddDate(0, 1, 0).Truncate(time.Second)
+ s.untilLine = fmt.Sprintf("until: %s\n", s.until.Format(time.RFC3339))
+ s.modelsLine = "models:\n - frobinator\n"
+ s.systemUserStr = strings.Replace(systemUserExample, "UNTILLINE\n", s.untilLine, 1)
+ s.systemUserStr = strings.Replace(s.systemUserStr, "SINCELINE\n", s.sinceLine, 1)
+ s.systemUserStr = strings.Replace(s.systemUserStr, "MODELSLINE\n", s.modelsLine, 1)
+}
+
+func (s *systemUserSuite) TestDecodeOK(c *C) {
+ a, err := asserts.Decode([]byte(s.systemUserStr))
+ c.Assert(err, IsNil)
+ c.Check(a.Type(), Equals, asserts.SystemUserType)
+ systemUser := a.(*asserts.SystemUser)
+ c.Check(systemUser.BrandID(), Equals, "canonical")
+ c.Check(systemUser.Email(), Equals, "foo@example.com")
+ c.Check(systemUser.Series(), DeepEquals, []string{"16"})
+ c.Check(systemUser.Models(), DeepEquals, []string{"frobinator"})
+ c.Check(systemUser.Name(), Equals, "Nice Guy")
+ c.Check(systemUser.Username(), Equals, "guy")
+ c.Check(systemUser.Password(), Equals, "$6$salt$hash")
+ c.Check(systemUser.SSHKeys(), DeepEquals, []string{"ssh-rsa AAAABcdefg"})
+ c.Check(systemUser.Since().Equal(s.since), Equals, true)
+ c.Check(systemUser.Until().Equal(s.until), Equals, true)
+}
+
+func (s *systemUserSuite) TestDecodePasswd(c *C) {
+ validTests := []struct{ original, valid string }{
+ {"password: $6$salt$hash\n", "password: $6$rounds=9999$salt$hash\n"},
+ {"password: $6$salt$hash\n", ""},
+ }
+ for _, test := range validTests {
+ valid := strings.Replace(s.systemUserStr, test.original, test.valid, 1)
+ _, err := asserts.Decode([]byte(valid))
+ c.Check(err, IsNil)
+ }
+}
+
+func (s *systemUserSuite) TestDecodeForcePasswdChange(c *C) {
+
+ old := "password: $6$salt$hash\n"
+ new := "password: $6$salt$hash\nforce-password-change: true\n"
+
+ valid := strings.Replace(s.systemUserStr, old, new, 1)
+ a, err := asserts.Decode([]byte(valid))
+ c.Check(err, IsNil)
+ systemUser := a.(*asserts.SystemUser)
+ c.Check(systemUser.ForcePasswordChange(), Equals, true)
+}
+
+func (s *systemUserSuite) TestValidAt(c *C) {
+ a, err := asserts.Decode([]byte(s.systemUserStr))
+ c.Assert(err, IsNil)
+ su := a.(*asserts.SystemUser)
+
+ c.Check(su.ValidAt(su.Since()), Equals, true)
+ c.Check(su.ValidAt(su.Since().AddDate(0, 0, -1)), Equals, false)
+ c.Check(su.ValidAt(su.Since().AddDate(0, 0, 1)), Equals, true)
+
+ c.Check(su.ValidAt(su.Until()), Equals, false)
+ c.Check(su.ValidAt(su.Until().AddDate(0, -1, 0)), Equals, true)
+ c.Check(su.ValidAt(su.Until().AddDate(0, 1, 0)), Equals, false)
+}
+
+func (s *systemUserSuite) TestValidAtRevoked(c *C) {
+ // With since == until, i.e. system-user has been revoked.
+ revoked := strings.Replace(s.systemUserStr, s.sinceLine, fmt.Sprintf("since: %s\n", s.until.Format(time.RFC3339)), 1)
+ a, err := asserts.Decode([]byte(revoked))
+ c.Assert(err, IsNil)
+ su := a.(*asserts.SystemUser)
+
+ c.Check(su.ValidAt(su.Since()), Equals, false)
+ c.Check(su.ValidAt(su.Since().AddDate(0, 0, -1)), Equals, false)
+ c.Check(su.ValidAt(su.Since().AddDate(0, 0, 1)), Equals, false)
+
+ c.Check(su.ValidAt(su.Until()), Equals, false)
+ c.Check(su.ValidAt(su.Until().AddDate(0, -1, 0)), Equals, false)
+ c.Check(su.ValidAt(su.Until().AddDate(0, 1, 0)), Equals, false)
+}
+
+const (
+ systemUserErrPrefix = "assertion system-user: "
+)
+
+func (s *systemUserSuite) TestDecodeInvalid(c *C) {
+ invalidTests := []struct{ original, invalid, expectedErr string }{
+ {"brand-id: canonical\n", "", `"brand-id" header is mandatory`},
+ {"brand-id: canonical\n", "brand-id: \n", `"brand-id" header should not be empty`},
+ {"email: foo@example.com\n", "", `"email" header is mandatory`},
+ {"email: foo@example.com\n", "email: \n", `"email" header should not be empty`},
+ {"email: foo@example.com\n", "email: \n", `"email" header must be a RFC 5322 compliant email address: mail: missing @ in addr-spec`},
+ {"email: foo@example.com\n", "email: no-mail\n", `"email" header must be a RFC 5322 compliant email address:.*`},
+ {"series:\n - 16\n", "series: \n", `"series" header must be a list of strings`},
+ {"series:\n - 16\n", "series: something\n", `"series" header must be a list of strings`},
+ {"models:\n - frobinator\n", "models: \n", `"models" header must be a list of strings`},
+ {"models:\n - frobinator\n", "models: something\n", `"models" header must be a list of strings`},
+ {"ssh-keys:\n - ssh-rsa AAAABcdefg\n", "ssh-keys: \n", `"ssh-keys" header must be a list of strings`},
+ {"ssh-keys:\n - ssh-rsa AAAABcdefg\n", "ssh-keys: something\n", `"ssh-keys" header must be a list of strings`},
+ {"name: Nice Guy\n", "name:\n - foo\n", `"name" header must be a string`},
+ {"username: guy\n", "username:\n - foo\n", `"username" header must be a string`},
+ {"username: guy\n", "username: bäää\n", `"username" header contains invalid characters: "bäää"`},
+ {"username: guy\n", "", `"username" header is mandatory`},
+ {"password: $6$salt$hash\n", "password:\n - foo\n", `"password" header must be a string`},
+ {"password: $6$salt$hash\n", "password: cleartext\n", `"password" header invalid: hashed password must be of the form "\$integer-id\$salt\$hash", see crypt\(3\)`},
+ {"password: $6$salt$hash\n", "password: $ni!$salt$hash\n", `"password" header must start with "\$integer-id\$", got "ni!"`},
+ {"password: $6$salt$hash\n", "password: $3$salt$hash\n", `"password" header only supports \$id\$ values of 6 \(sha512crypt\) or higher`},
+ {"password: $6$salt$hash\n", "password: $7$invalid-salt$hash\n", `"password" header has invalid chars in salt "invalid-salt"`},
+ {"password: $6$salt$hash\n", "password: $8$salt$invalid-hash\n", `"password" header has invalid chars in hash "invalid-hash"`},
+ {"password: $6$salt$hash\n", "password: $8$rounds=9999$hash\n", `"password" header invalid: missing hash field`},
+ {"password: $6$salt$hash\n", "password: $8$rounds=xxx$salt$hash\n", `"password" header has invalid number of rounds:.*`},
+ {"password: $6$salt$hash\n", "password: $8$rounds=1$salt$hash\n", `"password" header rounds parameter out of bounds: 1`},
+ {"password: $6$salt$hash\n", "password: $8$rounds=1999999999$salt$hash\n", `"password" header rounds parameter out of bounds: 1999999999`},
+ {"password: $6$salt$hash\n", "force-password-change: true\n", `cannot use "force-password-change" with an empty "password"`},
+ {"password: $6$salt$hash\n", "password: $6$salt$hash\nforce-password-change: xxx\n", `"force-password-change" header must be 'true' or 'false'`},
+ {s.sinceLine, "since: \n", `"since" header should not be empty`},
+ {s.sinceLine, "since: 12:30\n", `"since" header is not a RFC3339 date: .*`},
+ {s.untilLine, "until: \n", `"until" header should not be empty`},
+ {s.untilLine, "until: 12:30\n", `"until" header is not a RFC3339 date: .*`},
+ {s.untilLine, "until: 1002-11-01T22:08:41+00:00\n", `'until' time cannot be before 'since' time`},
+ }
+
+ for _, test := range invalidTests {
+ invalid := strings.Replace(s.systemUserStr, test.original, test.invalid, 1)
+ _, err := asserts.Decode([]byte(invalid))
+ c.Check(err, ErrorMatches, systemUserErrPrefix+test.expectedErr)
+ }
+}
+
+func (s *systemUserSuite) TestUntilNoModels(c *C) {
+ // no models is good for <1y
+ su := strings.Replace(s.systemUserStr, s.modelsLine, "", -1)
+ _, err := asserts.Decode([]byte(su))
+ c.Check(err, IsNil)
+
+ // but invalid for more than one year
+ oneYearPlusOne := time.Now().AddDate(1, 0, 1).Truncate(time.Second)
+ su = strings.Replace(su, s.untilLine, fmt.Sprintf("until: %s\n", oneYearPlusOne.Format(time.RFC3339)), -1)
+ _, err = asserts.Decode([]byte(su))
+ c.Check(err, ErrorMatches, systemUserErrPrefix+"'until' time cannot be more than 365 days in the future when no models are specified")
+}
+
+func (s *systemUserSuite) TestUntilWithModels(c *C) {
+ // with models it can be valid forever
+ oneYearPlusOne := time.Now().AddDate(10, 0, 1).Truncate(time.Second)
+ su := strings.Replace(s.systemUserStr, s.untilLine, fmt.Sprintf("until: %s\n", oneYearPlusOne.Format(time.RFC3339)), -1)
+ _, err := asserts.Decode([]byte(su))
+ c.Check(err, IsNil)
+}
diff -Nru snapd-2.40/asserts/user.go snapd-2.42.1/asserts/user.go
--- snapd-2.40/asserts/user.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/asserts/user.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,281 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2016 Canonical Ltd
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package asserts
-
-import (
- "fmt"
- "net/mail"
- "regexp"
- "strconv"
- "strings"
- "time"
-)
-
-var validSystemUserUsernames = regexp.MustCompile(`^[a-z0-9][-a-z0-9+.-_]*$`)
-
-// SystemUser holds a system-user assertion which allows creating local
-// system users.
-type SystemUser struct {
- assertionBase
- series []string
- models []string
- sshKeys []string
- since time.Time
- until time.Time
-
- forcePasswordChange bool
-}
-
-// BrandID returns the brand identifier that signed this assertion.
-func (su *SystemUser) BrandID() string {
- return su.HeaderString("brand-id")
-}
-
-// Email returns the email address that this assertion is valid for.
-func (su *SystemUser) Email() string {
- return su.HeaderString("email")
-}
-
-// Series returns the series that this assertion is valid for.
-func (su *SystemUser) Series() []string {
- return su.series
-}
-
-// Models returns the models that this assertion is valid for.
-func (su *SystemUser) Models() []string {
- return su.models
-}
-
-// Name returns the full name of the user (e.g. Random Guy).
-func (su *SystemUser) Name() string {
- return su.HeaderString("name")
-}
-
-// Username returns the system user name that should be created (e.g. "foo").
-func (su *SystemUser) Username() string {
- return su.HeaderString("username")
-}
-
-// Password returns the crypt(3) compatible password for the user.
-// Note that only ID: $6$ or stronger is supported (sha512crypt).
-func (su *SystemUser) Password() string {
- return su.HeaderString("password")
-}
-
-// ForcePasswordChange returns true if the user needs to change the password
-// after the first login.
-func (su *SystemUser) ForcePasswordChange() bool {
- return su.forcePasswordChange
-}
-
-// SSHKeys returns the ssh keys for the user.
-func (su *SystemUser) SSHKeys() []string {
- return su.sshKeys
-}
-
-// Since returns the time since the assertion is valid.
-func (su *SystemUser) Since() time.Time {
- return su.since
-}
-
-// Until returns the time until the assertion is valid.
-func (su *SystemUser) Until() time.Time {
- return su.until
-}
-
-// ValidAt returns whether the system-user is valid at 'when' time.
-func (su *SystemUser) ValidAt(when time.Time) bool {
- valid := when.After(su.since) || when.Equal(su.since)
- if valid {
- valid = when.Before(su.until)
- }
- return valid
-}
-
-// Implement further consistency checks.
-func (su *SystemUser) checkConsistency(db RODatabase, acck *AccountKey) error {
- // Do the cross-checks when this assertion is actually used,
- // i.e. in the create-user code. See also Model.checkConsitency
-
- return nil
-}
-
-// sanity
-var _ consistencyChecker = (*SystemUser)(nil)
-
-type shadow struct {
- ID string
- Rounds string
- Salt string
- Hash string
-}
-
-// crypt(3) compatible hashes have the forms:
-// - $id$salt$hash
-// - $id$rounds=N$salt$hash
-func parseShadowLine(line string) (*shadow, error) {
- l := strings.SplitN(line, "$", 5)
- if len(l) != 4 && len(l) != 5 {
- return nil, fmt.Errorf(`hashed password must be of the form "$integer-id$salt$hash", see crypt(3)`)
- }
-
- // if rounds is the second field, the line must consist of 4
- if strings.HasPrefix(l[2], "rounds=") && len(l) == 4 {
- return nil, fmt.Errorf(`missing hash field`)
- }
-
- // shadow line without $rounds=N$
- if len(l) == 4 {
- return &shadow{
- ID: l[1],
- Salt: l[2],
- Hash: l[3],
- }, nil
- }
- // shadow line with rounds
- return &shadow{
- ID: l[1],
- Rounds: l[2],
- Salt: l[3],
- Hash: l[4],
- }, nil
-}
-
-// see crypt(3) for the legal chars
-var isValidSaltAndHash = regexp.MustCompile(`^[a-zA-Z0-9./]+$`).MatchString
-
-func checkHashedPassword(headers map[string]interface{}, name string) (string, error) {
- pw, err := checkOptionalString(headers, name)
- if err != nil {
- return "", err
- }
- // the pw string is optional, so just return if its empty
- if pw == "" {
- return "", nil
- }
-
- // parse the shadow line
- shd, err := parseShadowLine(pw)
- if err != nil {
- return "", fmt.Errorf(`%q header invalid: %s`, name, err)
- }
-
- // and verify it
-
- // see crypt(3), ID 6 means SHA-512 (since glibc 2.7)
- ID, err := strconv.Atoi(shd.ID)
- if err != nil {
- return "", fmt.Errorf(`%q header must start with "$integer-id$", got %q`, name, shd.ID)
- }
- // double check that we only allow modern hashes
- if ID < 6 {
- return "", fmt.Errorf("%q header only supports $id$ values of 6 (sha512crypt) or higher", name)
- }
-
- // the $rounds=N$ part is optional
- if strings.HasPrefix(shd.Rounds, "rounds=") {
- rounds, err := strconv.Atoi(strings.SplitN(shd.Rounds, "=", 2)[1])
- if err != nil {
- return "", fmt.Errorf("%q header has invalid number of rounds: %s", name, err)
- }
- if rounds < 5000 || rounds > 999999999 {
- return "", fmt.Errorf("%q header rounds parameter out of bounds: %d", name, rounds)
- }
- }
-
- if !isValidSaltAndHash(shd.Salt) {
- return "", fmt.Errorf("%q header has invalid chars in salt %q", name, shd.Salt)
- }
- if !isValidSaltAndHash(shd.Hash) {
- return "", fmt.Errorf("%q header has invalid chars in hash %q", name, shd.Hash)
- }
-
- return pw, nil
-}
-
-func assembleSystemUser(assert assertionBase) (Assertion, error) {
- // brand-id here can be different from authority-id,
- // the code using the assertion must use the policy set
- // by the model assertion system-user-authority header
- email, err := checkNotEmptyString(assert.headers, "email")
- if err != nil {
- return nil, err
- }
- if _, err := mail.ParseAddress(email); err != nil {
- return nil, fmt.Errorf(`"email" header must be a RFC 5322 compliant email address: %s`, err)
- }
-
- series, err := checkStringList(assert.headers, "series")
- if err != nil {
- return nil, err
- }
- models, err := checkStringList(assert.headers, "models")
- if err != nil {
- return nil, err
- }
- if _, err := checkOptionalString(assert.headers, "name"); err != nil {
- return nil, err
- }
- if _, err := checkStringMatches(assert.headers, "username", validSystemUserUsernames); err != nil {
- return nil, err
- }
- password, err := checkHashedPassword(assert.headers, "password")
- if err != nil {
- return nil, err
- }
- forcePasswordChange, err := checkOptionalBool(assert.headers, "force-password-change")
- if err != nil {
- return nil, err
- }
- if forcePasswordChange && password == "" {
- return nil, fmt.Errorf(`cannot use "force-password-change" with an empty "password"`)
- }
-
- sshKeys, err := checkStringList(assert.headers, "ssh-keys")
- if err != nil {
- return nil, err
- }
- since, err := checkRFC3339Date(assert.headers, "since")
- if err != nil {
- return nil, err
- }
- until, err := checkRFC3339Date(assert.headers, "until")
- if err != nil {
- return nil, err
- }
- if until.Before(since) {
- return nil, fmt.Errorf("'until' time cannot be before 'since' time")
- }
-
- // "global" system-user assertion can only be valid for 1y
- if len(models) == 0 && until.After(since.AddDate(1, 0, 0)) {
- return nil, fmt.Errorf("'until' time cannot be more than 365 days in the future when no models are specified")
- }
-
- return &SystemUser{
- assertionBase: assert,
- series: series,
- models: models,
- sshKeys: sshKeys,
- since: since,
- until: until,
- forcePasswordChange: forcePasswordChange,
- }, nil
-}
diff -Nru snapd-2.40/asserts/user_test.go snapd-2.42.1/asserts/user_test.go
--- snapd-2.40/asserts/user_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/asserts/user_test.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,214 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2016 Canonical Ltd
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package asserts_test
-
-import (
- "fmt"
- "strings"
- "time"
-
- "github.com/snapcore/snapd/asserts"
- . "gopkg.in/check.v1"
-)
-
-var (
- _ = Suite(&systemUserSuite{})
-)
-
-type systemUserSuite struct {
- until time.Time
- untilLine string
- since time.Time
- sinceLine string
-
- modelsLine string
-
- systemUserStr string
-}
-
-const systemUserExample = "type: system-user\n" +
- "authority-id: canonical\n" +
- "brand-id: canonical\n" +
- "email: foo@example.com\n" +
- "series:\n" +
- " - 16\n" +
- "MODELSLINE\n" +
- "name: Nice Guy\n" +
- "username: guy\n" +
- "password: $6$salt$hash\n" +
- "ssh-keys:\n" +
- " - ssh-rsa AAAABcdefg\n" +
- "SINCELINE\n" +
- "UNTILLINE\n" +
- "body-length: 0\n" +
- "sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij" +
- "\n\n" +
- "AXNpZw=="
-
-func (s *systemUserSuite) SetUpTest(c *C) {
- s.since = time.Now().Truncate(time.Second)
- s.sinceLine = fmt.Sprintf("since: %s\n", s.since.Format(time.RFC3339))
- s.until = time.Now().AddDate(0, 1, 0).Truncate(time.Second)
- s.untilLine = fmt.Sprintf("until: %s\n", s.until.Format(time.RFC3339))
- s.modelsLine = "models:\n - frobinator\n"
- s.systemUserStr = strings.Replace(systemUserExample, "UNTILLINE\n", s.untilLine, 1)
- s.systemUserStr = strings.Replace(s.systemUserStr, "SINCELINE\n", s.sinceLine, 1)
- s.systemUserStr = strings.Replace(s.systemUserStr, "MODELSLINE\n", s.modelsLine, 1)
-}
-
-func (s *systemUserSuite) TestDecodeOK(c *C) {
- a, err := asserts.Decode([]byte(s.systemUserStr))
- c.Assert(err, IsNil)
- c.Check(a.Type(), Equals, asserts.SystemUserType)
- systemUser := a.(*asserts.SystemUser)
- c.Check(systemUser.BrandID(), Equals, "canonical")
- c.Check(systemUser.Email(), Equals, "foo@example.com")
- c.Check(systemUser.Series(), DeepEquals, []string{"16"})
- c.Check(systemUser.Models(), DeepEquals, []string{"frobinator"})
- c.Check(systemUser.Name(), Equals, "Nice Guy")
- c.Check(systemUser.Username(), Equals, "guy")
- c.Check(systemUser.Password(), Equals, "$6$salt$hash")
- c.Check(systemUser.SSHKeys(), DeepEquals, []string{"ssh-rsa AAAABcdefg"})
- c.Check(systemUser.Since().Equal(s.since), Equals, true)
- c.Check(systemUser.Until().Equal(s.until), Equals, true)
-}
-
-func (s *systemUserSuite) TestDecodePasswd(c *C) {
- validTests := []struct{ original, valid string }{
- {"password: $6$salt$hash\n", "password: $6$rounds=9999$salt$hash\n"},
- {"password: $6$salt$hash\n", ""},
- }
- for _, test := range validTests {
- valid := strings.Replace(s.systemUserStr, test.original, test.valid, 1)
- _, err := asserts.Decode([]byte(valid))
- c.Check(err, IsNil)
- }
-}
-
-func (s *systemUserSuite) TestDecodeForcePasswdChange(c *C) {
-
- old := "password: $6$salt$hash\n"
- new := "password: $6$salt$hash\nforce-password-change: true\n"
-
- valid := strings.Replace(s.systemUserStr, old, new, 1)
- a, err := asserts.Decode([]byte(valid))
- c.Check(err, IsNil)
- systemUser := a.(*asserts.SystemUser)
- c.Check(systemUser.ForcePasswordChange(), Equals, true)
-}
-
-func (s *systemUserSuite) TestValidAt(c *C) {
- a, err := asserts.Decode([]byte(s.systemUserStr))
- c.Assert(err, IsNil)
- su := a.(*asserts.SystemUser)
-
- c.Check(su.ValidAt(su.Since()), Equals, true)
- c.Check(su.ValidAt(su.Since().AddDate(0, 0, -1)), Equals, false)
- c.Check(su.ValidAt(su.Since().AddDate(0, 0, 1)), Equals, true)
-
- c.Check(su.ValidAt(su.Until()), Equals, false)
- c.Check(su.ValidAt(su.Until().AddDate(0, -1, 0)), Equals, true)
- c.Check(su.ValidAt(su.Until().AddDate(0, 1, 0)), Equals, false)
-}
-
-func (s *systemUserSuite) TestValidAtRevoked(c *C) {
- // With since == until, i.e. system-user has been revoked.
- revoked := strings.Replace(s.systemUserStr, s.sinceLine, fmt.Sprintf("since: %s\n", s.until.Format(time.RFC3339)), 1)
- a, err := asserts.Decode([]byte(revoked))
- c.Assert(err, IsNil)
- su := a.(*asserts.SystemUser)
-
- c.Check(su.ValidAt(su.Since()), Equals, false)
- c.Check(su.ValidAt(su.Since().AddDate(0, 0, -1)), Equals, false)
- c.Check(su.ValidAt(su.Since().AddDate(0, 0, 1)), Equals, false)
-
- c.Check(su.ValidAt(su.Until()), Equals, false)
- c.Check(su.ValidAt(su.Until().AddDate(0, -1, 0)), Equals, false)
- c.Check(su.ValidAt(su.Until().AddDate(0, 1, 0)), Equals, false)
-}
-
-const (
- systemUserErrPrefix = "assertion system-user: "
-)
-
-func (s *systemUserSuite) TestDecodeInvalid(c *C) {
- invalidTests := []struct{ original, invalid, expectedErr string }{
- {"brand-id: canonical\n", "", `"brand-id" header is mandatory`},
- {"brand-id: canonical\n", "brand-id: \n", `"brand-id" header should not be empty`},
- {"email: foo@example.com\n", "", `"email" header is mandatory`},
- {"email: foo@example.com\n", "email: \n", `"email" header should not be empty`},
- {"email: foo@example.com\n", "email: \n", `"email" header must be a RFC 5322 compliant email address: mail: missing @ in addr-spec`},
- {"email: foo@example.com\n", "email: no-mail\n", `"email" header must be a RFC 5322 compliant email address:.*`},
- {"series:\n - 16\n", "series: \n", `"series" header must be a list of strings`},
- {"series:\n - 16\n", "series: something\n", `"series" header must be a list of strings`},
- {"models:\n - frobinator\n", "models: \n", `"models" header must be a list of strings`},
- {"models:\n - frobinator\n", "models: something\n", `"models" header must be a list of strings`},
- {"ssh-keys:\n - ssh-rsa AAAABcdefg\n", "ssh-keys: \n", `"ssh-keys" header must be a list of strings`},
- {"ssh-keys:\n - ssh-rsa AAAABcdefg\n", "ssh-keys: something\n", `"ssh-keys" header must be a list of strings`},
- {"name: Nice Guy\n", "name:\n - foo\n", `"name" header must be a string`},
- {"username: guy\n", "username:\n - foo\n", `"username" header must be a string`},
- {"username: guy\n", "username: bäää\n", `"username" header contains invalid characters: "bäää"`},
- {"username: guy\n", "", `"username" header is mandatory`},
- {"password: $6$salt$hash\n", "password:\n - foo\n", `"password" header must be a string`},
- {"password: $6$salt$hash\n", "password: cleartext\n", `"password" header invalid: hashed password must be of the form "\$integer-id\$salt\$hash", see crypt\(3\)`},
- {"password: $6$salt$hash\n", "password: $ni!$salt$hash\n", `"password" header must start with "\$integer-id\$", got "ni!"`},
- {"password: $6$salt$hash\n", "password: $3$salt$hash\n", `"password" header only supports \$id\$ values of 6 \(sha512crypt\) or higher`},
- {"password: $6$salt$hash\n", "password: $7$invalid-salt$hash\n", `"password" header has invalid chars in salt "invalid-salt"`},
- {"password: $6$salt$hash\n", "password: $8$salt$invalid-hash\n", `"password" header has invalid chars in hash "invalid-hash"`},
- {"password: $6$salt$hash\n", "password: $8$rounds=9999$hash\n", `"password" header invalid: missing hash field`},
- {"password: $6$salt$hash\n", "password: $8$rounds=xxx$salt$hash\n", `"password" header has invalid number of rounds:.*`},
- {"password: $6$salt$hash\n", "password: $8$rounds=1$salt$hash\n", `"password" header rounds parameter out of bounds: 1`},
- {"password: $6$salt$hash\n", "password: $8$rounds=1999999999$salt$hash\n", `"password" header rounds parameter out of bounds: 1999999999`},
- {"password: $6$salt$hash\n", "force-password-change: true\n", `cannot use "force-password-change" with an empty "password"`},
- {"password: $6$salt$hash\n", "password: $6$salt$hash\nforce-password-change: xxx\n", `"force-password-change" header must be 'true' or 'false'`},
- {s.sinceLine, "since: \n", `"since" header should not be empty`},
- {s.sinceLine, "since: 12:30\n", `"since" header is not a RFC3339 date: .*`},
- {s.untilLine, "until: \n", `"until" header should not be empty`},
- {s.untilLine, "until: 12:30\n", `"until" header is not a RFC3339 date: .*`},
- {s.untilLine, "until: 1002-11-01T22:08:41+00:00\n", `'until' time cannot be before 'since' time`},
- }
-
- for _, test := range invalidTests {
- invalid := strings.Replace(s.systemUserStr, test.original, test.invalid, 1)
- _, err := asserts.Decode([]byte(invalid))
- c.Check(err, ErrorMatches, systemUserErrPrefix+test.expectedErr)
- }
-}
-
-func (s *systemUserSuite) TestUntilNoModels(c *C) {
- // no models is good for <1y
- su := strings.Replace(s.systemUserStr, s.modelsLine, "", -1)
- _, err := asserts.Decode([]byte(su))
- c.Check(err, IsNil)
-
- // but invalid for more than one year
- oneYearPlusOne := time.Now().AddDate(1, 0, 1).Truncate(time.Second)
- su = strings.Replace(su, s.untilLine, fmt.Sprintf("until: %s\n", oneYearPlusOne.Format(time.RFC3339)), -1)
- _, err = asserts.Decode([]byte(su))
- c.Check(err, ErrorMatches, systemUserErrPrefix+"'until' time cannot be more than 365 days in the future when no models are specified")
-}
-
-func (s *systemUserSuite) TestUntilWithModels(c *C) {
- // with models it can be valid forever
- oneYearPlusOne := time.Now().AddDate(10, 0, 1).Truncate(time.Second)
- su := strings.Replace(s.systemUserStr, s.untilLine, fmt.Sprintf("until: %s\n", oneYearPlusOne.Format(time.RFC3339)), -1)
- _, err := asserts.Decode([]byte(su))
- c.Check(err, IsNil)
-}
diff -Nru snapd-2.40/boot/boot.go snapd-2.42.1/boot/boot.go
--- snapd-2.40/boot/boot.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/boot/boot.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,367 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package boot
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/snap"
+)
+
+// A BootParticipant handles the boot process details for a snap involved in it.
+type BootParticipant interface {
+ // SetNextBoot will schedule the snap to be used in the next boot. For
+ // base snaps it is up to the caller to select the right bootable base
+ // (from the model assertion).
+ SetNextBoot() error
+ // ChangeRequiresReboot returns whether a reboot is required to switch
+ // to the snap.
+ ChangeRequiresReboot() bool
+ // Is this a trivial implementation of the interface?
+ IsTrivial() bool
+}
+
+// A BootKernel handles the bootloader setup of a kernel.
+type BootKernel interface {
+ // RemoveKernelAssets removes the unpacked kernel/initrd for the given
+ // kernel snap.
+ RemoveKernelAssets() error
+ // ExtractKernelAssets extracts kernel/initrd/dtb data from the given
+ // kernel snap, if required, to a versioned bootloader directory so
+ // that the bootloader can use it.
+ ExtractKernelAssets(snap.Container) error
+ // Is this a trivial implementation of the interface?
+ IsTrivial() bool
+}
+
+type trivial struct{}
+
+func (trivial) SetNextBoot() error { return nil }
+func (trivial) ChangeRequiresReboot() bool { return false }
+func (trivial) IsTrivial() bool { return true }
+func (trivial) RemoveKernelAssets() error { return nil }
+func (trivial) ExtractKernelAssets(snap.Container) error { return nil }
+
+// ensure trivial is a BootParticipant
+var _ BootParticipant = trivial{}
+
+// ensure trivial is a Kernel
+var _ BootKernel = trivial{}
+
+// Model carries information about the model that is relevant to boot.
+// Note *asserts.Model implements this, and that's the expected use case.
+type Model interface {
+ Kernel() string
+ Base() string
+ Classic() bool
+}
+
+// Participant figures out what the BootParticipant is for the given
+// arguments, and returns it. If the snap does _not_ participate in
+// the boot process, the returned object will be a NOP, so it's safe
+// to call anything on it always.
+//
+// Currently, on classic, nothing is a boot participant (returned will
+// always be NOP).
+func Participant(s snap.PlaceInfo, t snap.Type, model Model, onClassic bool) BootParticipant {
+ if applicable(s, t, model, onClassic) {
+ return &coreBootParticipant{s: s, t: t}
+ }
+ return trivial{}
+}
+
+// Kernel checks that the given arguments refer to a kernel snap
+// that participates in the boot process, and returns the associated
+// BootKernel, or a trivial implementation otherwise.
+func Kernel(s snap.PlaceInfo, t snap.Type, model Model, onClassic bool) BootKernel {
+ if t == snap.TypeKernel && applicable(s, t, model, onClassic) {
+ return &coreKernel{s: s}
+ }
+ return trivial{}
+}
+
+func applicable(s snap.PlaceInfo, t snap.Type, model Model, onClassic bool) bool {
+ if onClassic {
+ return false
+ }
+ if t != snap.TypeOS && t != snap.TypeKernel && t != snap.TypeBase {
+ // note we don't currently have anything useful to do with gadgets
+ return false
+ }
+
+ if model != nil {
+ switch t {
+ case snap.TypeKernel:
+ if s.InstanceName() != model.Kernel() {
+ // a remodel might leave you in this state
+ return false
+ }
+ case snap.TypeBase, snap.TypeOS:
+ base := model.Base()
+ if base == "" {
+ base = "core"
+ }
+ if s.InstanceName() != base {
+ return false
+ }
+ }
+ }
+
+ return true
+}
+
+// InUse checks if the given name/revision is used in the
+// boot environment
+func InUse(name string, rev snap.Revision) bool {
+ bootloader, err := bootloader.Find("", nil)
+ if err != nil {
+ logger.Noticef("cannot get boot settings: %s", err)
+ return false
+ }
+
+ bootVars, err := bootloader.GetBootVars("snap_kernel", "snap_try_kernel", "snap_core", "snap_try_core")
+ if err != nil {
+ logger.Noticef("cannot get boot vars: %s", err)
+ return false
+ }
+
+ snapFile := filepath.Base(snap.MountFile(name, rev))
+ for _, bootVar := range bootVars {
+ if bootVar == snapFile {
+ return true
+ }
+ }
+
+ return false
+}
+
+var (
+ ErrBootNameAndRevisionNotReady = errors.New("boot revision not yet established")
+)
+
+type NameAndRevision struct {
+ Name string
+ Revision snap.Revision
+}
+
+// GetCurrentBoot returns the currently set name and revision for boot for the given
+// type of snap, which can be snap.TypeBase (or snap.TypeOS), or snap.TypeKernel.
+// Returns ErrBootNameAndRevisionNotReady if the values are temporarily not established.
+func GetCurrentBoot(t snap.Type) (*NameAndRevision, error) {
+ var bootVar, errName string
+ switch t {
+ case snap.TypeKernel:
+ bootVar = "snap_kernel"
+ errName = "kernel"
+ case snap.TypeOS, snap.TypeBase:
+ bootVar = "snap_core"
+ errName = "base"
+ default:
+ return nil, fmt.Errorf("internal error: cannot find boot revision for snap type %q", t)
+ }
+
+ bloader, err := bootloader.Find("", nil)
+ if err != nil {
+ return nil, fmt.Errorf("cannot get boot settings: %s", err)
+ }
+
+ m, err := bloader.GetBootVars(bootVar, "snap_mode")
+ if err != nil {
+ return nil, fmt.Errorf("cannot get boot variables: %s", err)
+ }
+
+ if m["snap_mode"] == "trying" {
+ return nil, ErrBootNameAndRevisionNotReady
+ }
+
+ nameAndRevno, err := nameAndRevnoFromSnap(m[bootVar])
+ if err != nil {
+ return nil, fmt.Errorf("cannot get name and revision of boot %s: %v", errName, err)
+ }
+
+ return nameAndRevno, nil
+}
+
+// nameAndRevnoFromSnap grabs the snap name and revision from the
+// value of a boot variable. E.g., foo_2.snap -> name "foo", revno 2
+func nameAndRevnoFromSnap(sn string) (*NameAndRevision, error) {
+ if sn == "" {
+ return nil, fmt.Errorf("boot variable unset")
+ }
+ idx := strings.IndexByte(sn, '_')
+ if idx < 1 {
+ return nil, fmt.Errorf("input %q has invalid format (not enough '_')", sn)
+ }
+ name := sn[:idx]
+ revnoNSuffix := sn[idx+1:]
+ rev, err := snap.ParseRevision(strings.TrimSuffix(revnoNSuffix, ".snap"))
+ if err != nil {
+ return nil, err
+ }
+ return &NameAndRevision{Name: name, Revision: rev}, nil
+}
+
+// MarkBootSuccessful marks the current boot as successful. This means
+// that snappy will consider this combination of kernel/os a valid
+// target for rollback.
+//
+// The states that a boot goes through are the following:
+// - By default snap_mode is "" in which case the bootloader loads
+// two squashfs'es denoted by variables snap_core and snap_kernel.
+// - On a refresh of core/kernel snapd will set snap_mode=try and
+// will also set snap_try_{core,kernel} to the core/kernel that
+// will be tried next.
+// - On reboot the bootloader will inspect the snap_mode and if the
+// mode is set to "try" it will set "snap_mode=trying" and then
+// try to boot the snap_try_{core,kernel}".
+// - On a successful boot snapd resets snap_mode to "" and copies
+// snap_try_{core,kernel} to snap_{core,kernel}. The snap_try_*
+// values are cleared afterwards.
+// - On a failing boot the bootloader will see snap_mode=trying which
+// means snapd did not start successfully. In this case the bootloader
+// will set snap_mode="" and the system will boot with the known good
+// values from snap_{core,kernel}
+func MarkBootSuccessful() error {
+ bl, err := bootloader.Find("", nil)
+ if err != nil {
+ return fmt.Errorf("cannot mark boot successful: %s", err)
+ }
+ m, err := bl.GetBootVars("snap_mode", "snap_try_core", "snap_try_kernel")
+ if err != nil {
+ return err
+ }
+
+ // snap_mode goes from "" -> "try" -> "trying" -> ""
+ // so if we are not in "trying" mode, nothing to do here
+ if m["snap_mode"] != "trying" {
+ return nil
+ }
+
+ // update the boot vars
+ for _, k := range []string{"kernel", "core"} {
+ tryBootVar := fmt.Sprintf("snap_try_%s", k)
+ bootVar := fmt.Sprintf("snap_%s", k)
+ // update the boot vars
+ if m[tryBootVar] != "" {
+ m[bootVar] = m[tryBootVar]
+ m[tryBootVar] = ""
+ }
+ }
+ m["snap_mode"] = ""
+
+ return bl.SetBootVars(m)
+}
+
+// MakeBootable sets up the image filesystem with the given rootdir
+// such that it can be booted. bootWith is a map from the paths in the
+// image seed for kernel and boot base to their snap info. This
+// entails:
+// - installing the bootloader configuration from the gadget
+// - creating symlinks for boot snaps from seed to the runtime blob dir
+// - setting boot env vars pointing to the revisions of the boot snaps to use
+// - extracting kernel assets as needed by the bootloader
+func MakeBootable(model *asserts.Model, rootdir string, bootWith map[string]*snap.Info, unpackedGadgetDir string) error {
+ if len(bootWith) != 2 {
+ return fmt.Errorf("internal error: MakeBootable can only be called with exactly one kernel and exactly one core/base boot info: %v", bootWith)
+ }
+
+ // install the bootloader configuration from the gadget
+ if err := bootloader.InstallBootConfig(unpackedGadgetDir, rootdir); err != nil {
+ return err
+ }
+
+ // setup symlinks for kernel and boot base from the blob directory
+ // to the seed snaps
+
+ snapBlobDir := dirs.SnapBlobDirUnder(rootdir)
+ if err := os.MkdirAll(snapBlobDir, 0755); err != nil {
+ return err
+ }
+
+ for fn := range bootWith {
+ dst := filepath.Join(snapBlobDir, filepath.Base(fn))
+ // construct a relative symlink from the blob dir
+ // to the seed snap file
+ relSymlink, err := filepath.Rel(snapBlobDir, fn)
+ if err != nil {
+ return fmt.Errorf("cannot build symlink for boot snap: %v", err)
+ }
+ if err := os.Symlink(relSymlink, dst); err != nil {
+ return err
+ }
+ }
+
+ // Set bootvars for kernel/core snaps so the system boots and
+ // does the first-time initialization. There is also no
+ // mounted kernel/core/base snap, but just the blobs.
+ bl, err := bootloader.Find(rootdir, &bootloader.Options{
+ PrepareImageTime: true,
+ })
+ if err != nil {
+ return fmt.Errorf("cannot set kernel/core boot variables: %s", err)
+ }
+
+ m := map[string]string{
+ "snap_mode": "",
+ "snap_try_core": "",
+ "snap_try_kernel": "",
+ }
+ if model.DisplayName() != "" {
+ m["snap_menuentry"] = model.DisplayName()
+ }
+
+ for fn, info := range bootWith {
+ bootvar := ""
+
+ switch info.GetType() {
+ case snap.TypeOS, snap.TypeBase:
+ bootvar = "snap_core"
+ case snap.TypeKernel:
+ snapf, err := snap.Open(fn)
+ if err != nil {
+ return err
+ }
+
+ if err := bl.ExtractKernelAssets(info, snapf); err != nil {
+ return err
+ }
+ bootvar = "snap_kernel"
+ }
+
+ if bootvar != "" {
+ name := filepath.Base(fn)
+ m[bootvar] = name
+ }
+ }
+ if err := bl.SetBootVars(m); err != nil {
+ return err
+ }
+
+ return nil
+
+}
diff -Nru snapd-2.40/boot/boottest/mockbootloader.go snapd-2.42.1/boot/boottest/mockbootloader.go
--- snapd-2.40/boot/boottest/mockbootloader.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/boot/boottest/mockbootloader.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,86 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2014-2016 Canonical Ltd
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package boottest
-
-import (
- "github.com/snapcore/snapd/snap"
- "path/filepath"
-)
-
-// MockBootloader mocks the bootloader interface and records all
-// set/get calls.
-type MockBootloader struct {
- BootVars map[string]string
- SetErr error
- GetErr error
-
- name string
- bootdir string
-
- ExtractKernelAssetsCalls []*snap.Info
- RemoveKernelAssetsCalls []snap.PlaceInfo
-}
-
-func NewMockBootloader(name, bootdir string) *MockBootloader {
- return &MockBootloader{
- name: name,
- bootdir: bootdir,
-
- BootVars: make(map[string]string),
- }
-}
-
-func (b *MockBootloader) SetBootVars(values map[string]string) error {
- for k, v := range values {
- b.BootVars[k] = v
- }
- return b.SetErr
-}
-
-func (b *MockBootloader) GetBootVars(keys ...string) (map[string]string, error) {
- out := map[string]string{}
- for _, k := range keys {
- out[k] = b.BootVars[k]
- }
-
- return out, b.GetErr
-}
-
-func (b *MockBootloader) Dir() string {
- return b.bootdir
-}
-
-func (b *MockBootloader) Name() string {
- return b.name
-}
-
-func (b *MockBootloader) ConfigFile() string {
- return filepath.Join(b.bootdir, "mockboot/mockboot.cfg")
-}
-
-func (b *MockBootloader) ExtractKernelAssets(s *snap.Info, snapf snap.Container) error {
- b.ExtractKernelAssetsCalls = append(b.ExtractKernelAssetsCalls, s)
- return nil
-}
-
-func (b *MockBootloader) RemoveKernelAssets(s snap.PlaceInfo) error {
- b.RemoveKernelAssetsCalls = append(b.RemoveKernelAssetsCalls, s)
- return nil
-}
diff -Nru snapd-2.40/boot/boot_test.go snapd-2.42.1/boot/boot_test.go
--- snapd-2.40/boot/boot_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/boot/boot_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,456 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package boot_test
+
+import (
+ "errors"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/boot"
+ "github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/bootloader/bootloadertest"
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+// set up gocheck
+func TestBoot(t *testing.T) { TestingT(t) }
+
+// baseBootSuite is used to setup the common test environment
+type baseBootSetSuite struct {
+ testutil.BaseTest
+
+ bootdir string
+}
+
+func (s *baseBootSetSuite) SetUpTest(c *C) {
+ s.BaseTest.SetUpTest(c)
+
+ dirs.SetRootDir(c.MkDir())
+ s.AddCleanup(func() { dirs.SetRootDir("") })
+ restore := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})
+ s.AddCleanup(restore)
+
+ s.bootdir = filepath.Join(dirs.GlobalRootDir, "boot")
+}
+
+// bootSetSuite tests the abstract BootSet interface, and tools that
+// don't depend on a specific BootSet implementation
+type bootSetSuite struct {
+ baseBootSetSuite
+
+ bootloader *bootloadertest.MockBootloader
+}
+
+var _ = Suite(&bootSetSuite{})
+
+func (s *bootSetSuite) SetUpTest(c *C) {
+ s.baseBootSetSuite.SetUpTest(c)
+
+ s.bootloader = bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(s.bootloader)
+ s.AddCleanup(func() { bootloader.Force(nil) })
+}
+
+func (s *bootSetSuite) TestNameAndRevnoFromSnapValid(c *C) {
+ info, err := boot.NameAndRevnoFromSnap("foo_2.snap")
+ c.Assert(err, IsNil)
+ c.Assert(info.Name, Equals, "foo")
+ c.Assert(info.Revision, Equals, snap.R(2))
+}
+
+func (s *bootSetSuite) TestNameAndRevnoFromSnapInvalidFormat(c *C) {
+ _, err := boot.NameAndRevnoFromSnap("invalid")
+ c.Assert(err, ErrorMatches, `input "invalid" has invalid format \(not enough '_'\)`)
+ _, err = boot.NameAndRevnoFromSnap("invalid_xxx.snap")
+ c.Assert(err, ErrorMatches, `invalid snap revision: "xxx"`)
+}
+
+func BenchmarkNameAndRevno(b *testing.B) {
+ for n := 0; n < b.N; n++ {
+ for _, sn := range []string{
+ "core_21.snap",
+ "kernel_41.snap",
+ "some-long-kernel-name-kernel_82.snap",
+ "what-is-this-core_111.snap",
+ } {
+ boot.NameAndRevnoFromSnap(sn)
+ }
+ }
+}
+
+func (s *bootSetSuite) TestInUse(c *C) {
+ for _, t := range []struct {
+ bootVarKey string
+ bootVarValue string
+
+ snapName string
+ snapRev snap.Revision
+
+ inUse bool
+ }{
+ // in use
+ {"snap_kernel", "kernel_41.snap", "kernel", snap.R(41), true},
+ {"snap_try_kernel", "kernel_82.snap", "kernel", snap.R(82), true},
+ {"snap_core", "core_21.snap", "core", snap.R(21), true},
+ {"snap_try_core", "core_42.snap", "core", snap.R(42), true},
+ // not in use
+ {"snap_core", "core_111.snap", "core", snap.R(21), false},
+ {"snap_try_core", "core_111.snap", "core", snap.R(21), false},
+ {"snap_kernel", "kernel_111.snap", "kernel", snap.R(1), false},
+ {"snap_try_kernel", "kernel_111.snap", "kernel", snap.R(1), false},
+ } {
+ s.bootloader.BootVars[t.bootVarKey] = t.bootVarValue
+ c.Assert(boot.InUse(t.snapName, t.snapRev), Equals, t.inUse, Commentf("unexpected result: %s %s %v", t.snapName, t.snapRev, t.inUse))
+ }
+}
+
+func (s *bootSetSuite) TestInUseUnhapy(c *C) {
+ logbuf, restore := logger.MockLogger()
+ defer restore()
+ s.bootloader.BootVars["snap_kernel"] = "kernel_41.snap"
+
+ // sanity check
+ c.Check(boot.InUse("kernel", snap.R(41)), Equals, true)
+
+ // make GetVars fail
+ s.bootloader.GetErr = errors.New("zap")
+ c.Check(boot.InUse("kernel", snap.R(41)), Equals, false)
+ c.Check(logbuf.String(), testutil.Contains, "cannot get boot vars: zap")
+ s.bootloader.GetErr = nil
+
+ // make bootloader.Find fail
+ bootloader.ForceError(errors.New("broken bootloader"))
+ c.Check(boot.InUse("kernel", snap.R(41)), Equals, false)
+ c.Check(logbuf.String(), testutil.Contains, "cannot get boot settings: broken bootloader")
+}
+
+func (s *bootSetSuite) TestCurrentBootNameAndRevision(c *C) {
+ s.bootloader.BootVars["snap_core"] = "core_2.snap"
+ s.bootloader.BootVars["snap_kernel"] = "canonical-pc-linux_2.snap"
+
+ current, err := boot.GetCurrentBoot(snap.TypeOS)
+ c.Check(err, IsNil)
+ c.Check(current.Name, Equals, "core")
+ c.Check(current.Revision, Equals, snap.R(2))
+
+ current, err = boot.GetCurrentBoot(snap.TypeKernel)
+ c.Check(err, IsNil)
+ c.Check(current.Name, Equals, "canonical-pc-linux")
+ c.Check(current.Revision, Equals, snap.R(2))
+
+ s.bootloader.BootVars["snap_mode"] = "trying"
+ _, err = boot.GetCurrentBoot(snap.TypeKernel)
+ c.Check(err, Equals, boot.ErrBootNameAndRevisionNotReady)
+}
+
+func (s *bootSetSuite) TestCurrentBootNameAndRevisionUnhappy(c *C) {
+ _, err := boot.GetCurrentBoot(snap.TypeKernel)
+ c.Check(err, ErrorMatches, "cannot get name and revision of boot kernel: boot variable unset")
+
+ _, err = boot.GetCurrentBoot(snap.TypeOS)
+ c.Check(err, ErrorMatches, "cannot get name and revision of boot base: boot variable unset")
+
+ _, err = boot.GetCurrentBoot(snap.TypeBase)
+ c.Check(err, ErrorMatches, "cannot get name and revision of boot base: boot variable unset")
+
+ _, err = boot.GetCurrentBoot(snap.TypeApp)
+ c.Check(err, ErrorMatches, "internal error: cannot find boot revision for snap type \"app\"")
+
+ // sanity check
+ s.bootloader.BootVars["snap_kernel"] = "kernel_41.snap"
+ current, err := boot.GetCurrentBoot(snap.TypeKernel)
+ c.Check(err, IsNil)
+ c.Check(current.Name, Equals, "kernel")
+ c.Check(current.Revision, Equals, snap.R(41))
+
+ // make GetVars fail
+ s.bootloader.GetErr = errors.New("zap")
+ _, err = boot.GetCurrentBoot(snap.TypeKernel)
+ c.Check(err, ErrorMatches, "cannot get boot variables: zap")
+ s.bootloader.GetErr = nil
+
+ // make bootloader.Find fail
+ bootloader.ForceError(errors.New("broken bootloader"))
+ _, err = boot.GetCurrentBoot(snap.TypeKernel)
+ c.Check(err, ErrorMatches, "cannot get boot settings: broken bootloader")
+}
+
+func (s *bootSetSuite) TestParticipant(c *C) {
+ info := &snap.Info{}
+ info.RealName = "some-snap"
+
+ bp := boot.Participant(info, snap.TypeApp, nil, false)
+ c.Check(bp.IsTrivial(), Equals, true)
+
+ for _, typ := range []snap.Type{
+ snap.TypeKernel,
+ snap.TypeOS,
+ snap.TypeBase,
+ } {
+ bp = boot.Participant(info, typ, nil, true)
+ c.Check(bp.IsTrivial(), Equals, true)
+
+ bp = boot.Participant(info, typ, nil, false)
+ c.Check(bp.IsTrivial(), Equals, false)
+
+ c.Check(bp, DeepEquals, boot.NewCoreBootParticipant(info, typ))
+ }
+}
+
+type mockModel string
+
+func (s mockModel) Kernel() string { return string(s) }
+func (s mockModel) Base() string { return string(s) }
+func (s mockModel) Classic() bool { return s == "" }
+
+func (s *bootSetSuite) TestParticipantBaseWithModel(c *C) {
+ core := &snap.Info{SideInfo: snap.SideInfo{RealName: "core"}, SnapType: snap.TypeOS}
+ core18 := &snap.Info{SideInfo: snap.SideInfo{RealName: "core18"}, SnapType: snap.TypeBase}
+
+ type tableT struct {
+ with *snap.Info
+ model mockModel
+ nop bool
+ }
+
+ table := []tableT{
+ {
+ with: core,
+ model: "",
+ nop: false,
+ }, {
+ with: core,
+ model: "core",
+ nop: false,
+ }, {
+ with: core,
+ model: "core18",
+ nop: true,
+ },
+ {
+ with: core18,
+ model: "",
+ nop: true,
+ },
+ {
+ with: core18,
+ model: "core",
+ nop: true,
+ },
+ {
+ with: core18,
+ model: "core18",
+ nop: false,
+ },
+ }
+
+ for i, t := range table {
+ bp := boot.Participant(t.with, t.with.GetType(), t.model, true)
+ c.Check(bp.IsTrivial(), Equals, true)
+
+ bp = boot.Participant(t.with, t.with.GetType(), t.model, false)
+ c.Check(bp.IsTrivial(), Equals, t.nop, Commentf("%d", i))
+ if !t.nop {
+ c.Check(bp, DeepEquals, boot.NewCoreBootParticipant(t.with, t.with.GetType()))
+ }
+ }
+}
+
+func (s *bootSetSuite) TestKernelWithModel(c *C) {
+ info := &snap.Info{}
+ info.RealName = "kernel"
+ expected := boot.NewCoreKernel(info)
+
+ type tableT struct {
+ model mockModel
+ nop bool
+ krn boot.BootKernel
+ }
+
+ table := []tableT{
+ {
+ model: "other-kernel",
+ nop: true,
+ krn: boot.Trivial{},
+ }, {
+ model: "kernel",
+ nop: false,
+ krn: expected,
+ }, {
+ model: "",
+ nop: true,
+ krn: boot.Trivial{},
+ },
+ }
+
+ for _, t := range table {
+ krn := boot.Kernel(info, snap.TypeKernel, t.model, true)
+ c.Check(krn.IsTrivial(), Equals, true)
+
+ krn = boot.Kernel(info, snap.TypeKernel, t.model, false)
+ c.Check(krn.IsTrivial(), Equals, t.nop)
+ c.Check(krn, DeepEquals, t.krn)
+ }
+}
+
+func (s *bootSetSuite) TestMarkBootSuccessfulAllSnap(c *C) {
+ s.bootloader.BootVars["snap_mode"] = "trying"
+ s.bootloader.BootVars["snap_try_core"] = "os1"
+ s.bootloader.BootVars["snap_try_kernel"] = "k1"
+ err := boot.MarkBootSuccessful()
+ c.Assert(err, IsNil)
+
+ expected := map[string]string{
+ // cleared
+ "snap_mode": "",
+ "snap_try_kernel": "",
+ "snap_try_core": "",
+ // updated
+ "snap_kernel": "k1",
+ "snap_core": "os1",
+ }
+ c.Assert(s.bootloader.BootVars, DeepEquals, expected)
+
+ // do it again, verify its still valid
+ err = boot.MarkBootSuccessful()
+ c.Assert(err, IsNil)
+ c.Assert(s.bootloader.BootVars, DeepEquals, expected)
+}
+
+func (s *bootSetSuite) TestMarkBootSuccessfulKKernelUpdate(c *C) {
+ s.bootloader.BootVars["snap_mode"] = "trying"
+ s.bootloader.BootVars["snap_core"] = "os1"
+ s.bootloader.BootVars["snap_kernel"] = "k1"
+ s.bootloader.BootVars["snap_try_core"] = ""
+ s.bootloader.BootVars["snap_try_kernel"] = "k2"
+ err := boot.MarkBootSuccessful()
+ c.Assert(err, IsNil)
+ c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{
+ // cleared
+ "snap_mode": "",
+ "snap_try_kernel": "",
+ "snap_try_core": "",
+ // unchanged
+ "snap_core": "os1",
+ // updated
+ "snap_kernel": "k2",
+ })
+}
+
+func (s *bootSetSuite) makeSnap(c *C, name, yaml string, revno snap.Revision) (fn string, info *snap.Info) {
+ si := &snap.SideInfo{
+ RealName: name,
+ Revision: revno,
+ }
+ fn = snaptest.MakeTestSnapWithFiles(c, yaml, nil)
+ snapf, err := snap.Open(fn)
+ c.Assert(err, IsNil)
+ info, err = snap.ReadInfoFromSnapFile(snapf, si)
+ c.Assert(err, IsNil)
+ return fn, info
+}
+
+func (s *bootSetSuite) TestMakeBootable(c *C) {
+ dirs.SetRootDir("")
+
+ headers := map[string]interface{}{
+ "type": "model",
+ "authority-id": "my-brand",
+ "series": "16",
+ "brand-id": "my-brand",
+ "model": "my-model",
+ "display-name": "My Model",
+ "architecture": "amd64",
+ "base": "core18",
+ "gadget": "pc=18",
+ "kernel": "pc-kernel=18",
+ "timestamp": "2018-01-01T08:00:00+00:00",
+ }
+ model := assertstest.FakeAssertion(headers).(*asserts.Model)
+
+ grubCfg := []byte("#grub cfg")
+ unpackedGadgetDir := c.MkDir()
+ err := ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub.conf"), grubCfg, 0644)
+ c.Assert(err, IsNil)
+
+ rootdir := c.MkDir()
+
+ seedSnapsDirs := filepath.Join(rootdir, "/var/lib/snapd/seed", "snaps")
+ err = os.MkdirAll(seedSnapsDirs, 0755)
+ c.Assert(err, IsNil)
+
+ baseFn, baseInfo := s.makeSnap(c, "core18", `name: core18
+type: base
+version: 4.0
+`, snap.R(3))
+ baseInSeed := filepath.Join(seedSnapsDirs, filepath.Base(baseInfo.MountFile()))
+ err = os.Rename(baseFn, baseInSeed)
+ c.Assert(err, IsNil)
+ kernelFn, kernelInfo := s.makeSnap(c, "pc-kernel", `name: pc-kernel
+type: kernel
+version: 4.0
+`, snap.R(5))
+ kernelInSeed := filepath.Join(seedSnapsDirs, filepath.Base(kernelInfo.MountFile()))
+ err = os.Rename(kernelFn, kernelInSeed)
+ c.Assert(err, IsNil)
+
+ err = boot.MakeBootable(model, rootdir, map[string]*snap.Info{
+ kernelInSeed: kernelInfo,
+ baseInSeed: baseInfo,
+ }, unpackedGadgetDir)
+ c.Assert(err, IsNil)
+
+ // check the bootloader config
+ m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core", "snap_menuentry")
+ c.Assert(err, IsNil)
+ c.Check(m["snap_kernel"], Equals, "pc-kernel_5.snap")
+ c.Check(m["snap_core"], Equals, "core18_3.snap")
+ c.Check(m["snap_menuentry"], Equals, "My Model")
+
+ // kernel was extracted as needed
+ c.Check(s.bootloader.ExtractKernelAssetsCalls, DeepEquals, []snap.PlaceInfo{kernelInfo})
+
+ // check symlinks from snap blob dir
+ kernelBlob := filepath.Join(dirs.SnapBlobDirUnder(rootdir), filepath.Base(kernelInfo.MountFile()))
+ dst, err := os.Readlink(filepath.Join(dirs.SnapBlobDirUnder(rootdir), filepath.Base(kernelInfo.MountFile())))
+ c.Assert(err, IsNil)
+ c.Check(dst, Equals, "../seed/snaps/pc-kernel_5.snap")
+ c.Check(kernelBlob, testutil.FilePresent)
+
+ baseBlob := filepath.Join(dirs.SnapBlobDirUnder(rootdir), filepath.Base(baseInfo.MountFile()))
+ dst, err = os.Readlink(filepath.Join(dirs.SnapBlobDirUnder(rootdir), filepath.Base(baseInfo.MountFile())))
+ c.Assert(err, IsNil)
+ c.Check(dst, Equals, "../seed/snaps/core18_3.snap")
+ c.Check(baseBlob, testutil.FilePresent)
+
+ // check that the bootloader (grub here) configuration was copied
+ c.Check(filepath.Join(rootdir, "boot", "grub/grub.cfg"), testutil.FileEquals, grubCfg)
+}
diff -Nru snapd-2.40/boot/export_test.go snapd-2.42.1/boot/export_test.go
--- snapd-2.40/boot/export_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/boot/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,38 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package boot
+
+import (
+ "github.com/snapcore/snapd/snap"
+)
+
+var (
+ NameAndRevnoFromSnap = nameAndRevnoFromSnap
+)
+
+func NewCoreBootParticipant(s snap.PlaceInfo, t snap.Type) *coreBootParticipant {
+ return &coreBootParticipant{s: s, t: t}
+}
+
+func NewCoreKernel(s snap.PlaceInfo) *coreKernel {
+ return &coreKernel{s}
+}
+
+type Trivial = trivial
diff -Nru snapd-2.40/boot/kernel_os.go snapd-2.42.1/boot/kernel_os.go
--- snapd-2.40/boot/kernel_os.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/boot/kernel_os.go 2019-10-30 12:17:43.000000000 +0000
@@ -25,58 +25,27 @@
"github.com/snapcore/snapd/bootloader"
"github.com/snapcore/snapd/logger"
- "github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
)
-// RemoveKernelAssets removes the unpacked kernel/initrd for the given
-// kernel snap.
-func RemoveKernelAssets(s snap.PlaceInfo) error {
- bootloader, err := bootloader.Find()
- if err != nil {
- return fmt.Errorf("no not remove kernel assets: %s", err)
- }
-
- // ask bootloader to remove the kernel assets if needed
- return bootloader.RemoveKernelAssets(s)
-}
-
-// ExtractKernelAssets extracts kernel/initrd/dtb data from the given
-// kernel snap, if required, to a versioned bootloader directory so
-// that the bootloader can use it.
-func ExtractKernelAssets(s *snap.Info, snapf snap.Container) error {
- if s.GetType() != snap.TypeKernel {
- return fmt.Errorf("cannot extract kernel assets from snap type %q", s.GetType())
- }
-
- bootloader, err := bootloader.Find()
- if err != nil {
- return fmt.Errorf("cannot extract kernel assets: %s", err)
- }
-
- // ask bootloader to extract the kernel assets if needed
- return bootloader.ExtractKernelAssets(s, snapf)
+type coreBootParticipant struct {
+ s snap.PlaceInfo
+ t snap.Type
}
-// SetNextBoot will schedule the given OS or base or kernel snap to be
-// used in the next boot. For base snaps it up to the caller to select
-// the right bootable base (from the model assertion).
-func SetNextBoot(s *snap.Info) error {
- if release.OnClassic {
- return fmt.Errorf("cannot set next boot on classic systems")
- }
+// ensure coreBootParticipant is a BootParticipant
+var _ BootParticipant = (*coreBootParticipant)(nil)
- if s.GetType() != snap.TypeOS && s.GetType() != snap.TypeKernel && s.GetType() != snap.TypeBase {
- return fmt.Errorf("cannot set next boot to snap %q with type %q", s.SnapName(), s.GetType())
- }
+func (*coreBootParticipant) IsTrivial() bool { return false }
- bootloader, err := bootloader.Find()
+func (bs *coreBootParticipant) SetNextBoot() error {
+ bootloader, err := bootloader.Find("", nil)
if err != nil {
return fmt.Errorf("cannot set next boot: %s", err)
}
var nextBoot, goodBoot string
- switch s.GetType() {
+ switch bs.t {
case snap.TypeOS, snap.TypeBase:
nextBoot = "snap_try_core"
goodBoot = "snap_core"
@@ -84,14 +53,14 @@
nextBoot = "snap_try_kernel"
goodBoot = "snap_kernel"
}
- blobName := filepath.Base(s.MountFile())
+ blobName := filepath.Base(bs.s.MountFile())
// check if we actually need to do anything, i.e. the exact same
// kernel/core revision got installed again (e.g. firstboot)
// and we are not in any special boot mode
m, err := bootloader.GetBootVars("snap_mode", goodBoot)
if err != nil {
- return err
+ return fmt.Errorf("cannot set next boot: %s", err)
}
if m[goodBoot] == blobName {
// If we were in anything but default ("") mode before
@@ -113,21 +82,15 @@
})
}
-// ChangeRequiresReboot returns whether a reboot is required to switch
-// to the given OS, base or kernel snap.
-func ChangeRequiresReboot(s *snap.Info) bool {
- if s.GetType() != snap.TypeKernel && s.GetType() != snap.TypeOS && s.GetType() != snap.TypeBase {
- return false
- }
-
- bootloader, err := bootloader.Find()
+func (bs *coreBootParticipant) ChangeRequiresReboot() bool {
+ bootloader, err := bootloader.Find("", nil)
if err != nil {
logger.Noticef("cannot get boot settings: %s", err)
return false
}
var nextBoot, goodBoot string
- switch s.GetType() {
+ switch bs.t {
case snap.TypeKernel:
nextBoot = "snap_try_kernel"
goodBoot = "snap_kernel"
@@ -142,7 +105,7 @@
return false
}
- squashfsName := filepath.Base(s.MountFile())
+ squashfsName := filepath.Base(bs.s.MountFile())
if m[nextBoot] == squashfsName && m[goodBoot] != m[nextBoot] {
return true
}
@@ -150,27 +113,32 @@
return false
}
-// InUse checks if the given name/revision is used in the
-// boot environment
-func InUse(name string, rev snap.Revision) bool {
- bootloader, err := bootloader.Find()
- if err != nil {
- logger.Noticef("cannot get boot settings: %s", err)
- return false
- }
+type coreKernel struct {
+ s snap.PlaceInfo
+}
+
+// ensure coreKernel is a Kernel
+var _ BootKernel = (*coreKernel)(nil)
- bootVars, err := bootloader.GetBootVars("snap_kernel", "snap_try_kernel", "snap_core", "snap_try_core")
+func (*coreKernel) IsTrivial() bool { return false }
+
+func (k *coreKernel) RemoveKernelAssets() error {
+ // XXX: shouldn't we check the snap type?
+ bootloader, err := bootloader.Find("", nil)
if err != nil {
- logger.Noticef("cannot get boot vars: %s", err)
- return false
+ return fmt.Errorf("cannot remove kernel assets: %s", err)
}
- snapFile := filepath.Base(snap.MountFile(name, rev))
- for _, bootVar := range bootVars {
- if bootVar == snapFile {
- return true
- }
+ // ask bootloader to remove the kernel assets if needed
+ return bootloader.RemoveKernelAssets(k.s)
+}
+
+func (k *coreKernel) ExtractKernelAssets(snapf snap.Container) error {
+ bootloader, err := bootloader.Find("", nil)
+ if err != nil {
+ return fmt.Errorf("cannot extract kernel assets: %s", err)
}
- return false
+ // ask bootloader to extract the kernel assets if needed
+ return bootloader.ExtractKernelAssets(k.s, snapf)
}
diff -Nru snapd-2.40/boot/kernel_os_test.go snapd-2.42.1/boot/kernel_os_test.go
--- snapd-2.40/boot/kernel_os_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/boot/kernel_os_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2014-2016 Canonical Ltd
+ * Copyright (C) 2014-2019 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
@@ -20,25 +20,23 @@
package boot_test
import (
+ "errors"
"io/ioutil"
"path/filepath"
- "testing"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/boot"
- "github.com/snapcore/snapd/boot/boottest"
"github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/bootloader/bootloadertest"
"github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
- "github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/testutil"
)
-func TestBoot(t *testing.T) { TestingT(t) }
-
const packageKernel = `
name: ubuntu-kernel
version: 4.0-1
@@ -46,109 +44,114 @@
vendor: Someone
`
-// baseKernelOSSuite is used to setup the common test environment
-type baseKernelOSSuite struct {
- testutil.BaseTest
-}
-
-func (s *baseKernelOSSuite) SetUpTest(c *C) {
- s.BaseTest.SetUpTest(c)
-
- dirs.SetRootDir(c.MkDir())
- s.AddCleanup(func() { dirs.SetRootDir("") })
- restore := snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})
- s.AddCleanup(restore)
- restore = release.MockOnClassic(false)
- s.AddCleanup(restore)
-}
-
-// kernelOSSuite tests the abstract bootloader behaviour including
-// bootenv setting, error handling etc
-type kernelOSSuite struct {
- baseKernelOSSuite
+// coreBootSetSuite tests the abstract bootloader behaviour including
+// bootenv setting, error handling etc., for a core BootSet.
+type coreBootSetSuite struct {
+ baseBootSetSuite
- loader *boottest.MockBootloader
+ bootloader *bootloadertest.MockBootloader
}
-var _ = Suite(&kernelOSSuite{})
+var _ = Suite(&coreBootSetSuite{})
-func (s *kernelOSSuite) SetUpTest(c *C) {
- s.baseKernelOSSuite.SetUpTest(c)
+func (s *coreBootSetSuite) SetUpTest(c *C) {
+ s.baseBootSetSuite.SetUpTest(c)
- s.loader = boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(s.loader)
+ s.bootloader = bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(s.bootloader)
s.AddCleanup(func() { bootloader.Force(nil) })
}
-func (s *kernelOSSuite) TestExtractKernelAssetsError(c *C) {
- info := &snap.Info{}
- info.SnapType = snap.TypeApp
+func (s *coreBootSetSuite) TestExtractKernelAssetsError(c *C) {
+ bootloader.ForceError(errors.New("brkn"))
+ err := boot.NewCoreKernel(&snap.Info{}).ExtractKernelAssets(nil)
+ c.Check(err, ErrorMatches, `cannot extract kernel assets: brkn`)
+}
- err := boot.ExtractKernelAssets(info, nil)
- c.Assert(err, ErrorMatches, `cannot extract kernel assets from snap type "app"`)
+func (s *coreBootSetSuite) TestRemoveKernelAssetsError(c *C) {
+ bootloader.ForceError(errors.New("brkn"))
+ err := boot.NewCoreKernel(&snap.Info{}).RemoveKernelAssets()
+ c.Check(err, ErrorMatches, `cannot remove kernel assets: brkn`)
}
-// SetNextBoot should do nothing on classic LP: #1580403
-func (s *kernelOSSuite) TestSetNextBootOnClassic(c *C) {
- restore := release.MockOnClassic(true)
+func (s *coreBootSetSuite) TestChangeRequiresRebootError(c *C) {
+ logbuf, restore := logger.MockLogger()
defer restore()
+ bp := boot.NewCoreBootParticipant(&snap.Info{}, snap.TypeBase)
- // Create a fake OS snap that we try to update
- snapInfo := snaptest.MockSnap(c, "name: os\ntype: os", &snap.SideInfo{Revision: snap.R(42)})
- err := boot.SetNextBoot(snapInfo)
- c.Assert(err, ErrorMatches, "cannot set next boot on classic systems")
+ s.bootloader.GetErr = errors.New("zap")
- c.Assert(s.loader.BootVars, HasLen, 0)
+ c.Check(bp.ChangeRequiresReboot(), Equals, false)
+ c.Check(logbuf.String(), testutil.Contains, `cannot get boot variables: zap`)
+ s.bootloader.GetErr = nil
+ logbuf.Reset()
+
+ bootloader.ForceError(errors.New("brkn"))
+ c.Check(bp.ChangeRequiresReboot(), Equals, false)
+ c.Check(logbuf.String(), testutil.Contains, `cannot get boot settings: brkn`)
+}
+
+func (s *coreBootSetSuite) TestSetNextBootError(c *C) {
+ s.bootloader.GetErr = errors.New("zap")
+ err := boot.NewCoreBootParticipant(&snap.Info{}, snap.TypeApp).SetNextBoot()
+ c.Check(err, ErrorMatches, `cannot set next boot: zap`)
+
+ bootloader.ForceError(errors.New("brkn"))
+ err = boot.NewCoreBootParticipant(&snap.Info{}, snap.TypeApp).SetNextBoot()
+ c.Check(err, ErrorMatches, `cannot set next boot: brkn`)
}
-func (s *kernelOSSuite) TestSetNextBootForCore(c *C) {
+func (s *coreBootSetSuite) TestSetNextBootForCore(c *C) {
info := &snap.Info{}
info.SnapType = snap.TypeOS
info.RealName = "core"
info.Revision = snap.R(100)
- err := boot.SetNextBoot(info)
+ bs := boot.NewCoreBootParticipant(info, info.GetType())
+ err := bs.SetNextBoot()
c.Assert(err, IsNil)
- v, err := s.loader.GetBootVars("snap_try_core", "snap_mode")
+ v, err := s.bootloader.GetBootVars("snap_try_core", "snap_mode")
c.Assert(err, IsNil)
c.Assert(v, DeepEquals, map[string]string{
"snap_try_core": "core_100.snap",
"snap_mode": "try",
})
- c.Check(boot.ChangeRequiresReboot(info), Equals, true)
+ c.Check(bs.ChangeRequiresReboot(), Equals, true)
}
-func (s *kernelOSSuite) TestSetNextBootWithBaseForCore(c *C) {
+func (s *coreBootSetSuite) TestSetNextBootWithBaseForCore(c *C) {
info := &snap.Info{}
info.SnapType = snap.TypeBase
info.RealName = "core18"
info.Revision = snap.R(1818)
- err := boot.SetNextBoot(info)
+ bs := boot.NewCoreBootParticipant(info, info.GetType())
+ err := bs.SetNextBoot()
c.Assert(err, IsNil)
- v, err := s.loader.GetBootVars("snap_try_core", "snap_mode")
+ v, err := s.bootloader.GetBootVars("snap_try_core", "snap_mode")
c.Assert(err, IsNil)
c.Assert(v, DeepEquals, map[string]string{
"snap_try_core": "core18_1818.snap",
"snap_mode": "try",
})
- c.Check(boot.ChangeRequiresReboot(info), Equals, true)
+ c.Check(bs.ChangeRequiresReboot(), Equals, true)
}
-func (s *kernelOSSuite) TestSetNextBootForKernel(c *C) {
+func (s *coreBootSetSuite) TestSetNextBootForKernel(c *C) {
info := &snap.Info{}
info.SnapType = snap.TypeKernel
info.RealName = "krnl"
info.Revision = snap.R(42)
- err := boot.SetNextBoot(info)
+ bp := boot.NewCoreBootParticipant(info, snap.TypeKernel)
+ err := bp.SetNextBoot()
c.Assert(err, IsNil)
- v, err := s.loader.GetBootVars("snap_try_kernel", "snap_mode")
+ v, err := s.bootloader.GetBootVars("snap_try_kernel", "snap_mode")
c.Assert(err, IsNil)
c.Assert(v, DeepEquals, map[string]string{
"snap_try_kernel": "krnl_42.snap",
@@ -158,35 +161,35 @@
bootVars := map[string]string{
"snap_kernel": "krnl_40.snap",
"snap_try_kernel": "krnl_42.snap"}
- s.loader.SetBootVars(bootVars)
- c.Check(boot.ChangeRequiresReboot(info), Equals, true)
+ s.bootloader.SetBootVars(bootVars)
+ c.Check(bp.ChangeRequiresReboot(), Equals, true)
// simulate good boot
bootVars = map[string]string{"snap_kernel": "krnl_42.snap"}
- s.loader.SetBootVars(bootVars)
- c.Check(boot.ChangeRequiresReboot(info), Equals, false)
+ s.bootloader.SetBootVars(bootVars)
+ c.Check(bp.ChangeRequiresReboot(), Equals, false)
}
-func (s *kernelOSSuite) TestSetNextBootForKernelForTheSameKernel(c *C) {
+func (s *coreBootSetSuite) TestSetNextBootForKernelForTheSameKernel(c *C) {
info := &snap.Info{}
info.SnapType = snap.TypeKernel
info.RealName = "krnl"
info.Revision = snap.R(40)
bootVars := map[string]string{"snap_kernel": "krnl_40.snap"}
- s.loader.SetBootVars(bootVars)
+ s.bootloader.SetBootVars(bootVars)
- err := boot.SetNextBoot(info)
+ err := boot.NewCoreBootParticipant(info, snap.TypeKernel).SetNextBoot()
c.Assert(err, IsNil)
- v, err := s.loader.GetBootVars("snap_kernel")
+ v, err := s.bootloader.GetBootVars("snap_kernel")
c.Assert(err, IsNil)
c.Assert(v, DeepEquals, map[string]string{
"snap_kernel": "krnl_40.snap",
})
}
-func (s *kernelOSSuite) TestSetNextBootForKernelForTheSameKernelTryMode(c *C) {
+func (s *coreBootSetSuite) TestSetNextBootForKernelForTheSameKernelTryMode(c *C) {
info := &snap.Info{}
info.SnapType = snap.TypeKernel
info.RealName = "krnl"
@@ -196,12 +199,12 @@
"snap_kernel": "krnl_40.snap",
"snap_try_kernel": "krnl_99.snap",
"snap_mode": "try"}
- s.loader.SetBootVars(bootVars)
+ s.bootloader.SetBootVars(bootVars)
- err := boot.SetNextBoot(info)
+ err := boot.NewCoreBootParticipant(info, snap.TypeKernel).SetNextBoot()
c.Assert(err, IsNil)
- v, err := s.loader.GetBootVars("snap_kernel", "snap_try_kernel", "snap_mode")
+ v, err := s.bootloader.GetBootVars("snap_kernel", "snap_try_kernel", "snap_mode")
c.Assert(err, IsNil)
c.Assert(v, DeepEquals, map[string]string{
"snap_kernel": "krnl_40.snap",
@@ -210,60 +213,34 @@
})
}
-func (s *kernelOSSuite) TestInUse(c *C) {
- for _, t := range []struct {
- bootVarKey string
- bootVarValue string
-
- snapName string
- snapRev snap.Revision
-
- inUse bool
- }{
- // in use
- {"snap_kernel", "kernel_41.snap", "kernel", snap.R(41), true},
- {"snap_try_kernel", "kernel_82.snap", "kernel", snap.R(82), true},
- {"snap_core", "core_21.snap", "core", snap.R(21), true},
- {"snap_try_core", "core_42.snap", "core", snap.R(42), true},
- // not in use
- {"snap_core", "core_111.snap", "core", snap.R(21), false},
- {"snap_try_core", "core_111.snap", "core", snap.R(21), false},
- {"snap_kernel", "kernel_111.snap", "kernel", snap.R(1), false},
- {"snap_try_kernel", "kernel_111.snap", "kernel", snap.R(1), false},
- } {
- s.loader.BootVars[t.bootVarKey] = t.bootVarValue
- c.Assert(boot.InUse(t.snapName, t.snapRev), Equals, t.inUse, Commentf("unexpected result: %s %s %v", t.snapName, t.snapRev, t.inUse))
- }
-}
-
-// ubootKernelOSSuite tests the uboot specific code in the bootloader handling
-type ubootKernelOSSuite struct {
- baseKernelOSSuite
+// ubootBootSetSuite tests the uboot specific code in the bootloader handling
+type ubootBootSetSuite struct {
+ baseBootSetSuite
}
-var _ = Suite(&ubootKernelOSSuite{})
+var _ = Suite(&ubootBootSetSuite{})
-func (s *ubootKernelOSSuite) forceUbootBootloader(c *C) bootloader.Bootloader {
+func (s *ubootBootSetSuite) forceUbootBootloader(c *C) bootloader.Bootloader {
mockGadgetDir := c.MkDir()
err := ioutil.WriteFile(filepath.Join(mockGadgetDir, "uboot.conf"), nil, 0644)
c.Assert(err, IsNil)
- err = bootloader.InstallBootConfig(mockGadgetDir)
+ err = bootloader.InstallBootConfig(mockGadgetDir, dirs.GlobalRootDir)
c.Assert(err, IsNil)
- loader, err := bootloader.Find()
+ bloader, err := bootloader.Find("", nil)
c.Assert(err, IsNil)
- c.Check(loader, NotNil)
- bootloader.Force(loader)
+ c.Check(bloader, NotNil)
+ bootloader.Force(bloader)
s.AddCleanup(func() { bootloader.Force(nil) })
- fn := filepath.Join(dirs.GlobalRootDir, "/boot/uboot/uboot.env")
+ fn := filepath.Join(s.bootdir, "/uboot/uboot.env")
c.Assert(osutil.FileExists(fn), Equals, true)
- return loader
+ return bloader
}
-func (s *ubootKernelOSSuite) TestExtractKernelAssetsAndRemoveOnUboot(c *C) {
- loader := s.forceUbootBootloader(c)
- c.Assert(loader, NotNil)
+func (s *ubootBootSetSuite) TestExtractKernelAssetsAndRemoveOnUboot(c *C) {
+ bloader := s.forceUbootBootloader(c)
+ c.Assert(bloader, NotNil)
files := [][]string{
{"kernel.img", "I'm a kernel"},
@@ -285,12 +262,12 @@
info, err := snap.ReadInfoFromSnapFile(snapf, si)
c.Assert(err, IsNil)
- err = boot.ExtractKernelAssets(info, snapf)
+ bp := boot.NewCoreKernel(info)
+ err = bp.ExtractKernelAssets(snapf)
c.Assert(err, IsNil)
// this is where the kernel/initrd is unpacked
- bootdir := loader.Dir()
- kernelAssetsDir := filepath.Join(bootdir, "ubuntu-kernel_42.snap")
+ kernelAssetsDir := filepath.Join(s.bootdir, "/uboot/ubuntu-kernel_42.snap")
for _, def := range files {
if def[0] == "meta/kernel.yaml" {
break
@@ -301,51 +278,51 @@
}
// it's idempotent
- err = boot.ExtractKernelAssets(info, snapf)
+ err = bp.ExtractKernelAssets(snapf)
c.Assert(err, IsNil)
// remove
- err = boot.RemoveKernelAssets(info)
+ err = bp.RemoveKernelAssets()
c.Assert(err, IsNil)
c.Check(osutil.FileExists(kernelAssetsDir), Equals, false)
// it's idempotent
- err = boot.RemoveKernelAssets(info)
+ err = bp.RemoveKernelAssets()
c.Assert(err, IsNil)
}
-// grubKernelOSSuite tests the GRUB specific code in the bootloader handling
-type grubKernelOSSuite struct {
- baseKernelOSSuite
+// grubBootSetSuite tests the GRUB specific code in the bootloader handling
+type grubBootSetSuite struct {
+ baseBootSetSuite
}
-var _ = Suite(&grubKernelOSSuite{})
+var _ = Suite(&grubBootSetSuite{})
-func (s *grubKernelOSSuite) forceGrubBootloader(c *C) bootloader.Bootloader {
+func (s *grubBootSetSuite) forceGrubBootloader(c *C) bootloader.Bootloader {
// make mock grub bootenv dir
mockGadgetDir := c.MkDir()
err := ioutil.WriteFile(filepath.Join(mockGadgetDir, "grub.conf"), nil, 0644)
c.Assert(err, IsNil)
- err = bootloader.InstallBootConfig(mockGadgetDir)
+ err = bootloader.InstallBootConfig(mockGadgetDir, dirs.GlobalRootDir)
c.Assert(err, IsNil)
- loader, err := bootloader.Find()
+ bloader, err := bootloader.Find("", nil)
c.Assert(err, IsNil)
- c.Check(loader, NotNil)
- loader.SetBootVars(map[string]string{
+ c.Check(bloader, NotNil)
+ bloader.SetBootVars(map[string]string{
"snap_kernel": "kernel_41.snap",
"snap_core": "core_21.snap",
})
- bootloader.Force(loader)
+ bootloader.Force(bloader)
s.AddCleanup(func() { bootloader.Force(nil) })
- fn := filepath.Join(dirs.GlobalRootDir, "/boot/grub/grub.cfg")
+ fn := filepath.Join(s.bootdir, "/grub/grub.cfg")
c.Assert(osutil.FileExists(fn), Equals, true)
- return loader
+ return bloader
}
-func (s *grubKernelOSSuite) TestExtractKernelAssetsNoUnpacksKernelForGrub(c *C) {
- loader := s.forceGrubBootloader(c)
+func (s *grubBootSetSuite) TestExtractKernelAssetsNoUnpacksKernelForGrub(c *C) {
+ s.forceGrubBootloader(c)
files := [][]string{
{"kernel.img", "I'm a kernel"},
@@ -363,20 +340,21 @@
info, err := snap.ReadInfoFromSnapFile(snapf, si)
c.Assert(err, IsNil)
- err = boot.ExtractKernelAssets(info, snapf)
+ bp := boot.NewCoreKernel(info)
+ err = bp.ExtractKernelAssets(snapf)
c.Assert(err, IsNil)
// kernel is *not* here
- kernimg := filepath.Join(loader.Dir(), "ubuntu-kernel_42.snap", "kernel.img")
+ kernimg := filepath.Join(s.bootdir, "grub", "ubuntu-kernel_42.snap", "kernel.img")
c.Assert(osutil.FileExists(kernimg), Equals, false)
// it's idempotent
- err = boot.ExtractKernelAssets(info, snapf)
+ err = bp.ExtractKernelAssets(snapf)
c.Assert(err, IsNil)
}
-func (s *grubKernelOSSuite) TestExtractKernelForceWorks(c *C) {
- loader := s.forceGrubBootloader(c)
+func (s *grubBootSetSuite) TestExtractKernelForceWorks(c *C) {
+ s.forceGrubBootloader(c)
files := [][]string{
{"kernel.img", "I'm a kernel"},
@@ -395,28 +373,29 @@
info, err := snap.ReadInfoFromSnapFile(snapf, si)
c.Assert(err, IsNil)
- err = boot.ExtractKernelAssets(info, snapf)
+ bp := boot.NewCoreKernel(info)
+ err = bp.ExtractKernelAssets(snapf)
c.Assert(err, IsNil)
// kernel is extracted
- kernimg := filepath.Join(loader.Dir(), "ubuntu-kernel_42.snap", "kernel.img")
+ kernimg := filepath.Join(s.bootdir, "/grub/ubuntu-kernel_42.snap/kernel.img")
c.Assert(osutil.FileExists(kernimg), Equals, true)
// initrd
- initrdimg := filepath.Join(loader.Dir(), "ubuntu-kernel_42.snap", "initrd.img")
+ initrdimg := filepath.Join(s.bootdir, "/grub/ubuntu-kernel_42.snap/initrd.img")
c.Assert(osutil.FileExists(initrdimg), Equals, true)
// it's idempotent
- err = boot.ExtractKernelAssets(info, snapf)
+ err = bp.ExtractKernelAssets(snapf)
c.Assert(err, IsNil)
// ensure that removal of assets also works
- err = boot.RemoveKernelAssets(info)
+ err = bp.RemoveKernelAssets()
c.Assert(err, IsNil)
exists, _, err := osutil.DirExists(filepath.Dir(kernimg))
c.Assert(err, IsNil)
c.Check(exists, Equals, false)
// it's idempotent
- err = boot.RemoveKernelAssets(info)
+ err = bp.RemoveKernelAssets()
c.Assert(err, IsNil)
}
diff -Nru snapd-2.40/bootloader/androidboot.go snapd-2.42.1/bootloader/androidboot.go
--- snapd-2.40/bootloader/androidboot.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/bootloader/androidboot.go 2019-10-30 12:17:43.000000000 +0000
@@ -24,16 +24,17 @@
"path/filepath"
"github.com/snapcore/snapd/bootloader/androidbootenv"
- "github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
)
-type androidboot struct{}
+type androidboot struct {
+ rootdir string
+}
// newAndroidboot creates a new Androidboot bootloader object
-func newAndroidBoot() Bootloader {
- a := &androidboot{}
+func newAndroidBoot(rootdir string) Bootloader {
+ a := &androidboot{rootdir: rootdir}
if !osutil.FileExists(a.ConfigFile()) {
return nil
}
@@ -44,12 +45,19 @@
return "androidboot"
}
-func (a *androidboot) Dir() string {
- return filepath.Join(dirs.GlobalRootDir, "/boot/androidboot")
+func (a *androidboot) setRootDir(rootdir string) {
+ a.rootdir = rootdir
+}
+
+func (a *androidboot) dir() string {
+ if a.rootdir == "" {
+ panic("internal error: unset rootdir")
+ }
+ return filepath.Join(a.rootdir, "/boot/androidboot")
}
func (a *androidboot) ConfigFile() string {
- return filepath.Join(a.Dir(), "androidboot.env")
+ return filepath.Join(a.dir(), "androidboot.env")
}
func (a *androidboot) GetBootVars(names ...string) (map[string]string, error) {
@@ -77,7 +85,7 @@
return env.Save()
}
-func (a *androidboot) ExtractKernelAssets(s *snap.Info, snapf snap.Container) error {
+func (a *androidboot) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error {
return nil
}
diff -Nru snapd-2.40/bootloader/androidboot_test.go snapd-2.42.1/bootloader/androidboot_test.go
--- snapd-2.40/bootloader/androidboot_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/bootloader/androidboot_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -25,7 +25,6 @@
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/bootloader"
- "github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
@@ -41,22 +40,21 @@
s.baseBootenvTestSuite.SetUpTest(c)
// the file needs to exist for androidboot object to be created
- bootloader.MockAndroidBootFile(c, 0644)
+ bootloader.MockAndroidBootFile(c, s.rootdir, 0644)
}
func (s *androidBootTestSuite) TestNewAndroidbootNoAndroidbootReturnsNil(c *C) {
- dirs.GlobalRootDir = "/something/not/there"
- a := bootloader.NewAndroidBoot()
+ a := bootloader.NewAndroidBoot("/something/not/there")
c.Assert(a, IsNil)
}
func (s *androidBootTestSuite) TestNewAndroidboot(c *C) {
- a := bootloader.NewAndroidBoot()
+ a := bootloader.NewAndroidBoot(s.rootdir)
c.Assert(a, NotNil)
}
func (s *androidBootTestSuite) TestSetGetBootVar(c *C) {
- a := bootloader.NewAndroidBoot()
+ a := bootloader.NewAndroidBoot(s.rootdir)
bootVars := map[string]string{"snap_mode": "try"}
a.SetBootVars(bootVars)
@@ -67,7 +65,7 @@
}
func (s *androidBootTestSuite) TestExtractKernelAssetsNoUnpacksKernel(c *C) {
- a := bootloader.NewAndroidBoot()
+ a := bootloader.NewAndroidBoot(s.rootdir)
c.Assert(a, NotNil)
@@ -91,6 +89,6 @@
c.Assert(err, IsNil)
// kernel is *not* here
- kernimg := filepath.Join(a.Dir(), "ubuntu-kernel_42.snap", "kernel.img")
+ kernimg := filepath.Join(s.rootdir, "boot", "androidboot", "ubuntu-kernel_42.snap", "kernel.img")
c.Assert(osutil.FileExists(kernimg), Equals, false)
}
diff -Nru snapd-2.40/bootloader/bootloader.go snapd-2.42.1/bootloader/bootloader.go
--- snapd-2.40/bootloader/bootloader.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/bootloader/bootloader.go 2019-10-30 12:17:43.000000000 +0000
@@ -25,22 +25,11 @@
"os"
"path/filepath"
+ "github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
)
-const (
- // bootloader variable used to determine if boot was
- // successful. Set to value of either modeTry (when
- // attempting to boot a new rootfs) or modeSuccess (to denote
- // that the boot of the new rootfs was successful).
- bootmodeVar = "snap_mode"
-
- // Initial and final values
- modeTry = "try"
- modeSuccess = ""
-)
-
var (
// ErrBootloader is returned if the bootloader can not be determined
ErrBootloader = errors.New("cannot determine bootloader")
@@ -55,9 +44,6 @@
// Set the value of the specified bootloader variable
SetBootVars(values map[string]string) error
- // Dir returns the bootloader directory
- Dir() string
-
// Name returns the bootloader name
Name() string
@@ -65,22 +51,29 @@
ConfigFile() string
// ExtractKernelAssets extracts kernel assets from the given kernel snap
- ExtractKernelAssets(s *snap.Info, snapf snap.Container) error
+ ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error
// RemoveKernelAssets removes the assets for the given kernel snap.
RemoveKernelAssets(s snap.PlaceInfo) error
}
+type installableBootloader interface {
+ Bootloader
+ setRootDir(string)
+}
+
// InstallBootConfig installs the bootloader config from the gadget
// snap dir into the right place.
-func InstallBootConfig(gadgetDir string) error {
- for _, bl := range []Bootloader{&grub{}, &uboot{}, &androidboot{}} {
+func InstallBootConfig(gadgetDir, rootDir string) error {
+ for _, bl := range []installableBootloader{&grub{}, &uboot{}, &androidboot{}, &lk{}} {
// the bootloader config file has to be root of the gadget snap
gadgetFile := filepath.Join(gadgetDir, bl.Name()+".conf")
if !osutil.FileExists(gadgetFile) {
continue
}
+ bl.setRootDir(rootDir)
+
systemFile := bl.ConfigFile()
if err := os.MkdirAll(filepath.Dir(systemFile), 0755); err != nil {
return err
@@ -91,88 +84,72 @@
return fmt.Errorf("cannot find boot config in %q", gadgetDir)
}
-var forcedBootloader Bootloader
+var (
+ forcedBootloader Bootloader
+ forcedError error
+)
+
+// Options carries bootloader options.
+type Options struct {
+ // PrepareImageTime indicates whether the booloader is being
+ // used at prepare-image time, that means not on a runtime
+ // system.
+ PrepareImageTime bool
+}
+
+// Find returns the bootloader for the system
+// or an error if no bootloader is found.
+func Find(rootdir string, opts *Options) (Bootloader, error) {
+ if forcedBootloader != nil || forcedError != nil {
+ return forcedBootloader, forcedError
+ }
-// Find returns the bootloader for the given system
-// or an error if no bootloader is found
-func Find() (Bootloader, error) {
- if forcedBootloader != nil {
- return forcedBootloader, nil
+ if rootdir == "" {
+ rootdir = dirs.GlobalRootDir
+ }
+ if opts == nil {
+ opts = &Options{}
}
// try uboot
- if uboot := newUboot(); uboot != nil {
+ if uboot := newUboot(rootdir); uboot != nil {
return uboot, nil
}
// no, try grub
- if grub := newGrub(); grub != nil {
+ if grub := newGrub(rootdir); grub != nil {
return grub, nil
}
// no, try androidboot
- if androidboot := newAndroidBoot(); androidboot != nil {
+ if androidboot := newAndroidBoot(rootdir); androidboot != nil {
return androidboot, nil
}
+ // no, try lk
+ if lk := newLk(rootdir, opts); lk != nil {
+ return lk, nil
+ }
+
// no, weeeee
return nil, ErrBootloader
}
// Force can be used to force setting a booloader to that Find will not use the
-// usual lookup process, use nil to reset to normal lookup.
+// usual lookup process; use nil to reset to normal lookup.
func Force(booloader Bootloader) {
forcedBootloader = booloader
+ forcedError = nil
}
-// MarkBootSuccessful marks the current boot as successful. This means
-// that snappy will consider this combination of kernel/os a valid
-// target for rollback.
-//
-// The states that a boot goes through are the following:
-// - By default snap_mode is "" in which case the bootloader loads
-// two squashfs'es denoted by variables snap_core and snap_kernel.
-// - On a refresh of core/kernel snapd will set snap_mode=try and
-// will also set snap_try_{core,kernel} to the core/kernel that
-// will be tried next.
-// - On reboot the bootloader will inspect the snap_mode and if the
-// mode is set to "try" it will set "snap_mode=trying" and then
-// try to boot the snap_try_{core,kernel}".
-// - On a successful boot snapd resets snap_mode to "" and copies
-// snap_try_{core,kernel} to snap_{core,kernel}. The snap_try_*
-// values are cleared afterwards.
-// - On a failing boot the bootloader will see snap_mode=trying which
-// means snapd did not start successfully. In this case the bootloader
-// will set snap_mode="" and the system will boot with the known good
-// values from snap_{core,kernel}
-func MarkBootSuccessful(bootloader Bootloader) error {
- m, err := bootloader.GetBootVars("snap_mode", "snap_try_core", "snap_try_kernel")
- if err != nil {
- return err
- }
-
- // snap_mode goes from "" -> "try" -> "trying" -> ""
- // so if we are not in "trying" mode, nothing to do here
- if m["snap_mode"] != "trying" {
- return nil
- }
-
- // update the boot vars
- for _, k := range []string{"kernel", "core"} {
- tryBootVar := fmt.Sprintf("snap_try_%s", k)
- bootVar := fmt.Sprintf("snap_%s", k)
- // update the boot vars
- if m[tryBootVar] != "" {
- m[bootVar] = m[tryBootVar]
- m[tryBootVar] = ""
- }
- }
- m["snap_mode"] = modeSuccess
-
- return bootloader.SetBootVars(m)
+// Force can be used to force Find to return an error; use nil to
+// reset to normal lookup.
+func ForceError(err error) {
+ forcedBootloader = nil
+ forcedError = err
}
-func extractKernelAssetsToBootDir(bootDir string, s *snap.Info, snapf snap.Container) error {
+func extractKernelAssetsToBootDir(bootDir string, s snap.PlaceInfo, snapf snap.Container) error {
// now do the kernel specific bits
blobName := filepath.Base(s.MountFile())
dstDir := filepath.Join(bootDir, blobName)
diff -Nru snapd-2.40/bootloader/bootloadertest/bootloadertest.go snapd-2.42.1/bootloader/bootloadertest/bootloadertest.go
--- snapd-2.40/bootloader/bootloadertest/bootloadertest.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/bootloader/bootloadertest/bootloadertest.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,99 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package bootloadertest
+
+import (
+ "path/filepath"
+
+ "github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/snap"
+)
+
+// MockBootloader mocks the bootloader interface and records all
+// set/get calls.
+type MockBootloader struct {
+ BootVars map[string]string
+ SetErr error
+ GetErr error
+
+ name string
+ bootdir string
+
+ ExtractKernelAssetsCalls []snap.PlaceInfo
+ RemoveKernelAssetsCalls []snap.PlaceInfo
+}
+
+// ensure MockBootloader implements the Bootloader interface
+var _ bootloader.Bootloader = (*MockBootloader)(nil)
+
+func Mock(name, bootdir string) *MockBootloader {
+ return &MockBootloader{
+ name: name,
+ bootdir: bootdir,
+
+ BootVars: make(map[string]string),
+ }
+}
+
+func (b *MockBootloader) SetBootVars(values map[string]string) error {
+ for k, v := range values {
+ b.BootVars[k] = v
+ }
+ return b.SetErr
+}
+
+func (b *MockBootloader) GetBootVars(keys ...string) (map[string]string, error) {
+ out := map[string]string{}
+ for _, k := range keys {
+ out[k] = b.BootVars[k]
+ }
+
+ return out, b.GetErr
+}
+
+func (b *MockBootloader) Name() string {
+ return b.name
+}
+
+func (b *MockBootloader) ConfigFile() string {
+ return filepath.Join(b.bootdir, "mockboot/mockboot.cfg")
+}
+
+func (b *MockBootloader) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error {
+ b.ExtractKernelAssetsCalls = append(b.ExtractKernelAssetsCalls, s)
+ return nil
+}
+
+func (b *MockBootloader) RemoveKernelAssets(s snap.PlaceInfo) error {
+ b.RemoveKernelAssetsCalls = append(b.RemoveKernelAssetsCalls, s)
+ return nil
+}
+
+// SetBootKernel sets the current boot kernel string. Should be
+// something like "pc-kernel_1234.snap".
+func (b *MockBootloader) SetBootKernel(kernel string) {
+ b.SetBootVars(map[string]string{"snap_kernel": kernel})
+}
+
+// SetBootBase sets the current boot base string. Should be something
+// like "core_1234.snap".
+func (b *MockBootloader) SetBootBase(base string) {
+ b.SetBootVars(map[string]string{"snap_core": base})
+}
diff -Nru snapd-2.40/bootloader/bootloader_test.go snapd-2.42.1/bootloader/bootloader_test.go
--- snapd-2.40/bootloader/bootloader_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/bootloader/bootloader_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,15 +20,16 @@
package bootloader_test
import (
+ "errors"
"io/ioutil"
"path/filepath"
"testing"
. "gopkg.in/check.v1"
- "github.com/snapcore/snapd/boot/boottest"
"github.com/snapcore/snapd/bootloader"
- "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/bootloader/bootloadertest"
+ //"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/testutil"
@@ -46,19 +47,20 @@
type baseBootenvTestSuite struct {
testutil.BaseTest
+
+ rootdir string
}
func (s *baseBootenvTestSuite) SetUpTest(c *C) {
s.BaseTest.SetUpTest(c)
s.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
- dirs.SetRootDir(c.MkDir())
- s.AddCleanup(func() { dirs.SetRootDir("") })
+ s.rootdir = c.MkDir()
}
type bootenvTestSuite struct {
baseBootenvTestSuite
- b *boottest.MockBootloader
+ b *bootloadertest.MockBootloader
}
var _ = Suite(&bootenvTestSuite{})
@@ -66,64 +68,30 @@
func (s *bootenvTestSuite) SetUpTest(c *C) {
s.baseBootenvTestSuite.SetUpTest(c)
- s.b = boottest.NewMockBootloader("mocky", c.MkDir())
+ s.b = bootloadertest.Mock("mocky", c.MkDir())
}
func (s *bootenvTestSuite) TestForceBootloader(c *C) {
bootloader.Force(s.b)
defer bootloader.Force(nil)
- got, err := bootloader.Find()
+ got, err := bootloader.Find("", nil)
c.Assert(err, IsNil)
c.Check(got, Equals, s.b)
}
-func (s *bootenvTestSuite) TestMarkBootSuccessfulAllSnap(c *C) {
- s.b.BootVars["snap_mode"] = "trying"
- s.b.BootVars["snap_try_core"] = "os1"
- s.b.BootVars["snap_try_kernel"] = "k1"
- err := bootloader.MarkBootSuccessful(s.b)
- c.Assert(err, IsNil)
-
- expected := map[string]string{
- // cleared
- "snap_mode": "",
- "snap_try_kernel": "",
- "snap_try_core": "",
- // updated
- "snap_kernel": "k1",
- "snap_core": "os1",
- }
- c.Assert(s.b.BootVars, DeepEquals, expected)
-
- // do it again, verify its still valid
- err = bootloader.MarkBootSuccessful(s.b)
- c.Assert(err, IsNil)
- c.Assert(s.b.BootVars, DeepEquals, expected)
-}
-
-func (s *bootenvTestSuite) TestMarkBootSuccessfulKKernelUpdate(c *C) {
- s.b.BootVars["snap_mode"] = "trying"
- s.b.BootVars["snap_core"] = "os1"
- s.b.BootVars["snap_kernel"] = "k1"
- s.b.BootVars["snap_try_core"] = ""
- s.b.BootVars["snap_try_kernel"] = "k2"
- err := bootloader.MarkBootSuccessful(s.b)
- c.Assert(err, IsNil)
- c.Assert(s.b.BootVars, DeepEquals, map[string]string{
- // cleared
- "snap_mode": "",
- "snap_try_kernel": "",
- "snap_try_core": "",
- // unchanged
- "snap_core": "os1",
- // updated
- "snap_kernel": "k2",
- })
+func (s *bootenvTestSuite) TestForceBootloaderError(c *C) {
+ myErr := errors.New("zap")
+ bootloader.ForceError(myErr)
+ defer bootloader.ForceError(nil)
+
+ got, err := bootloader.Find("", nil)
+ c.Assert(err, Equals, myErr)
+ c.Check(got, IsNil)
}
func (s *bootenvTestSuite) TestInstallBootloaderConfigNoConfig(c *C) {
- err := bootloader.InstallBootConfig(c.MkDir())
+ err := bootloader.InstallBootConfig(c.MkDir(), s.rootdir)
c.Assert(err, ErrorMatches, `cannot find boot config in.*`)
}
@@ -132,13 +100,14 @@
{"grub.conf", "/boot/grub/grub.cfg"},
{"uboot.conf", "/boot/uboot/uboot.env"},
{"androidboot.conf", "/boot/androidboot/androidboot.env"},
+ {"lk.conf", "/boot/lk/snapbootsel.bin"},
} {
mockGadgetDir := c.MkDir()
err := ioutil.WriteFile(filepath.Join(mockGadgetDir, t.gadgetFile), nil, 0644)
c.Assert(err, IsNil)
- err = bootloader.InstallBootConfig(mockGadgetDir)
+ err = bootloader.InstallBootConfig(mockGadgetDir, s.rootdir)
c.Assert(err, IsNil)
- fn := filepath.Join(dirs.GlobalRootDir, t.systemFile)
+ fn := filepath.Join(s.rootdir, t.systemFile)
c.Assert(osutil.FileExists(fn), Equals, true)
}
}
diff -Nru snapd-2.40/bootloader/export_test.go snapd-2.42.1/bootloader/export_test.go
--- snapd-2.40/bootloader/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/bootloader/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -25,29 +25,30 @@
. "gopkg.in/check.v1"
+ "github.com/snapcore/snapd/bootloader/lkenv"
"github.com/snapcore/snapd/bootloader/ubootenv"
)
// creates a new Androidboot bootloader object
-func NewAndroidBoot() Bootloader {
- return newAndroidBoot()
+func NewAndroidBoot(rootdir string) Bootloader {
+ return newAndroidBoot(rootdir)
}
-func MockAndroidBootFile(c *C, mode os.FileMode) {
- f := &androidboot{}
- err := os.MkdirAll(f.Dir(), 0755)
+func MockAndroidBootFile(c *C, rootdir string, mode os.FileMode) {
+ f := &androidboot{rootdir: rootdir}
+ err := os.MkdirAll(f.dir(), 0755)
c.Assert(err, IsNil)
err = ioutil.WriteFile(f.ConfigFile(), nil, mode)
c.Assert(err, IsNil)
}
-func NewUboot() Bootloader {
- return newUboot()
+func NewUboot(rootdir string) Bootloader {
+ return newUboot(rootdir)
}
-func MockUbootFiles(c *C) {
- u := &uboot{}
- err := os.MkdirAll(u.Dir(), 0755)
+func MockUbootFiles(c *C, rootdir string) {
+ u := &uboot{rootdir: rootdir}
+ err := os.MkdirAll(u.dir(), 0755)
c.Assert(err, IsNil)
// ensure that we have a valid uboot.env too
@@ -57,14 +58,45 @@
c.Assert(err, IsNil)
}
-func NewGrub() Bootloader {
- return newGrub()
+func NewGrub(rootdir string) Bootloader {
+ return newGrub(rootdir)
}
-func MockGrubFiles(c *C) {
- g := &grub{}
- err := os.MkdirAll(g.Dir(), 0755)
+func MockGrubFiles(c *C, rootdir string) {
+ g := &grub{rootdir: rootdir}
+ err := os.MkdirAll(g.dir(), 0755)
c.Assert(err, IsNil)
err = ioutil.WriteFile(g.ConfigFile(), nil, 0644)
c.Assert(err, IsNil)
}
+
+func NewLk(rootdir string, opts *Options) Bootloader {
+ if opts == nil {
+ opts = &Options{}
+ }
+ return newLk(rootdir, opts)
+}
+
+func MockLkFiles(c *C, rootdir string, opts *Options) {
+ if opts == nil {
+ opts = &Options{}
+ }
+ l := &lk{rootdir: rootdir, inRuntimeMode: !opts.PrepareImageTime}
+ err := os.MkdirAll(l.dir(), 0755)
+ c.Assert(err, IsNil)
+
+ // first create empty env file
+ buf := make([]byte, 4096)
+ err = ioutil.WriteFile(l.envFile(), buf, 0660)
+ c.Assert(err, IsNil)
+ // now write env in it with correct crc
+ env := lkenv.NewEnv(l.envFile())
+ env.ConfigureBootPartitions("boot_a", "boot_b")
+ err = env.Save()
+ c.Assert(err, IsNil)
+}
+
+func LkRuntimeMode(b Bootloader) bool {
+ lk := b.(*lk)
+ return lk.inRuntimeMode
+}
diff -Nru snapd-2.40/bootloader/grub.go snapd-2.42.1/bootloader/grub.go
--- snapd-2.40/bootloader/grub.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/bootloader/grub.go 2019-10-30 12:17:43.000000000 +0000
@@ -24,16 +24,17 @@
"path/filepath"
"github.com/snapcore/snapd/bootloader/grubenv"
- "github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
)
-type grub struct{}
+type grub struct {
+ rootdir string
+}
// newGrub create a new Grub bootloader object
-func newGrub() Bootloader {
- g := &grub{}
+func newGrub(rootdir string) Bootloader {
+ g := &grub{rootdir: rootdir}
if !osutil.FileExists(g.ConfigFile()) {
return nil
}
@@ -45,16 +46,23 @@
return "grub"
}
-func (g *grub) Dir() string {
- return filepath.Join(dirs.GlobalRootDir, "/boot/grub")
+func (g *grub) setRootDir(rootdir string) {
+ g.rootdir = rootdir
+}
+
+func (g *grub) dir() string {
+ if g.rootdir == "" {
+ panic("internal error: unset rootdir")
+ }
+ return filepath.Join(g.rootdir, "/boot/grub")
}
func (g *grub) ConfigFile() string {
- return filepath.Join(g.Dir(), "grub.cfg")
+ return filepath.Join(g.dir(), "grub.cfg")
}
func (g *grub) envFile() string {
- return filepath.Join(g.Dir(), "grubenv")
+ return filepath.Join(g.dir(), "grubenv")
}
func (g *grub) GetBootVars(names ...string) (map[string]string, error) {
@@ -83,14 +91,14 @@
return env.Save()
}
-func (g *grub) ExtractKernelAssets(s *snap.Info, snapf snap.Container) error {
+func (g *grub) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error {
// XXX: should we use "kernel.yaml" for this?
if _, err := snapf.ReadFile("meta/force-kernel-extraction"); err == nil {
- return extractKernelAssetsToBootDir(g.Dir(), s, snapf)
+ return extractKernelAssetsToBootDir(g.dir(), s, snapf)
}
return nil
}
func (g *grub) RemoveKernelAssets(s snap.PlaceInfo) error {
- return removeKernelAssetsFromBootDir(g.Dir(), s)
+ return removeKernelAssetsFromBootDir(g.dir(), s)
}
diff -Nru snapd-2.40/bootloader/grub_test.go snapd-2.42.1/bootloader/grub_test.go
--- snapd-2.40/bootloader/grub_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/bootloader/grub_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -36,13 +36,17 @@
type grubTestSuite struct {
baseBootenvTestSuite
+
+ bootdir string
}
var _ = Suite(&grubTestSuite{})
func (s *grubTestSuite) SetUpTest(c *C) {
s.baseBootenvTestSuite.SetUpTest(c)
- bootloader.MockGrubFiles(c)
+ bootloader.MockGrubFiles(c, s.rootdir)
+
+ s.bootdir = filepath.Join(s.rootdir, "boot")
}
// grubEditenvCmd finds the right grub{,2}-editenv command
@@ -55,25 +59,25 @@
return ""
}
-func grubEnvPath() string {
- return filepath.Join(dirs.GlobalRootDir, "boot/grub/grubenv")
+func grubEnvPath(rootdir string) string {
+ return filepath.Join(rootdir, "boot/grub/grubenv")
}
-func grubEditenvSet(c *C, key, value string) {
+func (s *grubTestSuite) grubEditenvSet(c *C, key, value string) {
if grubEditenvCmd() == "" {
c.Skip("grub{,2}-editenv is not available")
}
- err := exec.Command(grubEditenvCmd(), grubEnvPath(), "set", fmt.Sprintf("%s=%s", key, value)).Run()
+ err := exec.Command(grubEditenvCmd(), grubEnvPath(s.rootdir), "set", fmt.Sprintf("%s=%s", key, value)).Run()
c.Assert(err, IsNil)
}
-func grubEditenvGet(c *C, key string) string {
+func (s *grubTestSuite) grubEditenvGet(c *C, key string) string {
if grubEditenvCmd() == "" {
c.Skip("grub{,2}-editenv is not available")
}
- output, err := exec.Command(grubEditenvCmd(), grubEnvPath(), "list").CombinedOutput()
+ output, err := exec.Command(grubEditenvCmd(), grubEnvPath(s.rootdir), "list").CombinedOutput()
c.Assert(err, IsNil)
cfg := goconfigparser.New()
cfg.AllowNoSectionHeader = true
@@ -85,20 +89,18 @@
}
func (s *grubTestSuite) makeFakeGrubEnv(c *C) {
- grubEditenvSet(c, "k", "v")
+ s.grubEditenvSet(c, "k", "v")
}
func (s *grubTestSuite) TestNewGrubNoGrubReturnsNil(c *C) {
- dirs.GlobalRootDir = "/something/not/there"
-
- g := bootloader.NewGrub()
+ g := bootloader.NewGrub("/something/not/there")
c.Assert(g, IsNil)
}
func (s *grubTestSuite) TestNewGrub(c *C) {
s.makeFakeGrubEnv(c)
- g := bootloader.NewGrub()
+ g := bootloader.NewGrub(s.rootdir)
c.Assert(g, NotNil)
c.Assert(g.Name(), Equals, "grub")
}
@@ -106,16 +108,27 @@
func (s *grubTestSuite) TestGetBootloaderWithGrub(c *C) {
s.makeFakeGrubEnv(c)
- bootloader, err := bootloader.Find()
+ bootloader, err := bootloader.Find(s.rootdir, nil)
+ c.Assert(err, IsNil)
+ c.Assert(bootloader.Name(), Equals, "grub")
+}
+
+func (s *grubTestSuite) TestGetBootloaderWithGrubWithDefaultRoot(c *C) {
+ s.makeFakeGrubEnv(c)
+
+ dirs.SetRootDir(s.rootdir)
+ defer func() { dirs.SetRootDir("") }()
+
+ bootloader, err := bootloader.Find("", nil)
c.Assert(err, IsNil)
c.Assert(bootloader.Name(), Equals, "grub")
}
func (s *grubTestSuite) TestGetBootVer(c *C) {
s.makeFakeGrubEnv(c)
- grubEditenvSet(c, "snap_mode", "regular")
+ s.grubEditenvSet(c, "snap_mode", "regular")
- g := bootloader.NewGrub()
+ g := bootloader.NewGrub(s.rootdir)
v, err := g.GetBootVars("snap_mode")
c.Assert(err, IsNil)
c.Check(v, HasLen, 1)
@@ -125,21 +138,21 @@
func (s *grubTestSuite) TestSetBootVer(c *C) {
s.makeFakeGrubEnv(c)
- g := bootloader.NewGrub()
+ g := bootloader.NewGrub(s.rootdir)
err := g.SetBootVars(map[string]string{
"k1": "v1",
"k2": "v2",
})
c.Assert(err, IsNil)
- c.Check(grubEditenvGet(c, "k1"), Equals, "v1")
- c.Check(grubEditenvGet(c, "k2"), Equals, "v2")
+ c.Check(s.grubEditenvGet(c, "k1"), Equals, "v1")
+ c.Check(s.grubEditenvGet(c, "k2"), Equals, "v2")
}
func (s *grubTestSuite) TestExtractKernelAssetsNoUnpacksKernelForGrub(c *C) {
s.makeFakeGrubEnv(c)
- g := bootloader.NewGrub()
+ g := bootloader.NewGrub(s.rootdir)
files := [][]string{
{"kernel.img", "I'm a kernel"},
@@ -161,14 +174,14 @@
c.Assert(err, IsNil)
// kernel is *not* here
- kernimg := filepath.Join(g.Dir(), "ubuntu-kernel_42.snap", "kernel.img")
+ kernimg := filepath.Join(s.bootdir, "grub", "ubuntu-kernel_42.snap", "kernel.img")
c.Assert(osutil.FileExists(kernimg), Equals, false)
}
func (s *grubTestSuite) TestExtractKernelForceWorks(c *C) {
s.makeFakeGrubEnv(c)
- g := bootloader.NewGrub()
+ g := bootloader.NewGrub(s.rootdir)
c.Assert(g, NotNil)
files := [][]string{
@@ -192,10 +205,10 @@
c.Assert(err, IsNil)
// kernel is extracted
- kernimg := filepath.Join(g.Dir(), "ubuntu-kernel_42.snap", "kernel.img")
+ kernimg := filepath.Join(s.bootdir, "grub", "ubuntu-kernel_42.snap", "kernel.img")
c.Assert(osutil.FileExists(kernimg), Equals, true)
// initrd
- initrdimg := filepath.Join(g.Dir(), "ubuntu-kernel_42.snap", "initrd.img")
+ initrdimg := filepath.Join(s.bootdir, "grub", "ubuntu-kernel_42.snap", "initrd.img")
c.Assert(osutil.FileExists(initrdimg), Equals, true)
// ensure that removal of assets also works
diff -Nru snapd-2.40/bootloader/lkenv/export_test.go snapd-2.42.1/bootloader/lkenv/export_test.go
--- snapd-2.40/bootloader/lkenv/export_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/bootloader/lkenv/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,25 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package lkenv
+
+var (
+ CopyString = copyString
+ CToGoString = cToGoString
+)
diff -Nru snapd-2.40/bootloader/lkenv/lkenv.go snapd-2.42.1/bootloader/lkenv/lkenv.go
--- snapd-2.40/bootloader/lkenv/lkenv.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/bootloader/lkenv/lkenv.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,428 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package lkenv
+
+import (
+ "bytes"
+ "encoding/binary"
+ "fmt"
+ "hash/crc32"
+ "os"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+)
+
+const SNAP_BOOTSELECT_VERSION = 0x00010001
+
+// const SNAP_BOOTSELECT_SIGNATURE ('S' | ('B' << 8) | ('s' << 16) | ('e' << 24))
+const SNAP_BOOTSELECT_SIGNATURE = 0x53 | 0x42<<8 | 0x73<<16 | 0x65<<24
+const SNAP_NAME_MAX_LEN = 256
+
+/* number of available boot partitions */
+const SNAP_BOOTIMG_PART_NUM = 2
+
+/* Default boot image file name to be used from kernel snap */
+const BOOTIMG_DEFAULT_NAME = "boot.img"
+
+// for accessing the Bootimg_matrix
+const (
+ MATRIX_ROW_PARTITION = 0
+ MATRIX_ROW_KERNEL = 1
+)
+
+/**
+ * Following structure has to be kept in sync with c structure defined by
+ * include/snappy-boot_v1.h
+ * c headerfile is used by bootloader, this ensures sync of the environment
+ * between snapd and bootloader
+
+ * when this structure needs to be updated,
+ * new version should be introduced instead together with c header file,
+ * which is to be adopted by bootloader
+ *
+ * !!! Support for old version has to be maintained, as it is not guaranteed
+ * all existing bootloader would adopt new version!
+ */
+type SnapBootSelect_v1 struct {
+ /* Contains value BOOTSELECT_SIGNATURE defined above */
+ Signature uint32
+ /* snappy boot select version */
+ Version uint32
+
+ /* snap_mode, one of: 'empty', "try", "trying" */
+ Snap_mode [SNAP_NAME_MAX_LEN]byte
+ /* current core snap revision */
+ Snap_core [SNAP_NAME_MAX_LEN]byte
+ /* try core snap revision */
+ Snap_try_core [SNAP_NAME_MAX_LEN]byte
+ /* current kernel snap revision */
+ Snap_kernel [SNAP_NAME_MAX_LEN]byte
+ /* current kernel snap revision */
+ Snap_try_kernel [SNAP_NAME_MAX_LEN]byte
+
+ /* gadget_mode, one of: 'empty', "try", "trying" */
+ Gadget_mode [SNAP_NAME_MAX_LEN]byte
+ /* GADGET assets: current gadget assets revision */
+ Snap_gadget [SNAP_NAME_MAX_LEN]byte
+ /* GADGET assets: try gadget assets revision */
+ Snap_try_gadget [SNAP_NAME_MAX_LEN]byte
+
+ /**
+ * Reboot reason
+ * optional parameter to signal bootloader alternative reboot reasons
+ * e.g. recovery/factory-reset/boot asset update
+ */
+ Reboot_reason [SNAP_NAME_MAX_LEN]byte
+
+ /**
+ * Matrix for mapping of boot img partion to installed kernel snap revision
+ *
+ * First column represents boot image partition label (e.g. boot_a,boot_b )
+ * value are static and should be populated at gadget built time
+ * or latest at image build time. Values are not further altered at run time.
+ * Second column represents name currently installed kernel snap
+ * e.g. pi2-kernel_123.snap
+ * initial value representing initial kernel snap revision
+ * is pupulated at image build time by snapd
+ *
+ * There are two rows in the matrix, representing current and previous kernel revision
+ * following describes how this matrix should be modified at different stages:
+ * - at image build time:
+ * - extracted kernel snap revision name should be filled
+ * into free slow (first row, second row)
+ * - snapd:
+ * - when new kernel snap revision is being installed, snapd cycles through
+ * matrix to find unused 'boot slot' to be used for new kernel snap revision
+ * from free slot, first column represents partition label to which kernel
+ * snap boot image should be extracted. Second column is then populated with
+ * kernel snap revision name.
+ * - snap_mode, snap_try_kernel, snap_try_core behaves same way as with u-boot
+ * - bootloader:
+ * - bootloader reads snap_mode to determine if snap_kernel or snap_kernel is used
+ * to get kernel snap revision name
+ * kernel snap revision is then used to search matrix to determine
+ * partition label to be used for current boot
+ * - bootloader NEVER alters this matrix values
+ *
+ * [ ] [ ]
+ * [ ] [ ]
+ */
+ Bootimg_matrix [SNAP_BOOTIMG_PART_NUM][2][SNAP_NAME_MAX_LEN]byte
+
+ /**
+ * name of the boot image from kernel snap to be used for extraction
+ * when not defined or empty, default boot.img will be used
+ */
+ Bootimg_file_name [SNAP_NAME_MAX_LEN]byte
+
+ /**
+ * gadget assets: Matrix for mapping of gadget asset partions
+ * Optional boot asset tracking, based on bootloader support
+ * Some boot chains support A/B boot assets for increased robustness
+ * example being A/B TrustExecutionEnvironment
+ * This matrix can be used to track current and try boot assets for
+ * robust updates
+ * Use of Gadget_asset_matrix matches use of Bootimg_matrix
+ *
+ * [ ] [ ]
+ * [ ] [ ]
+ */
+ Gadget_asset_matrix [SNAP_BOOTIMG_PART_NUM][2][SNAP_NAME_MAX_LEN]byte
+
+ /* unused placeholders for additional parameters in the future */
+ Unused_key_01 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_02 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_03 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_04 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_05 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_06 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_07 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_08 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_09 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_10 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_11 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_12 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_13 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_14 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_15 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_16 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_17 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_18 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_19 [SNAP_NAME_MAX_LEN]byte
+ Unused_key_20 [SNAP_NAME_MAX_LEN]byte
+
+ /* unused array of 10 key value pairs */
+ Kye_value_pairs [10][2][SNAP_NAME_MAX_LEN]byte
+
+ /* crc32 value for structure */
+ Crc32 uint32
+}
+
+// Env contains the data of the uboot environment
+// path can be file or partition device node
+type Env struct {
+ path string
+ pathbak string
+ env SnapBootSelect_v1
+}
+
+// cToGoString convert string in passed byte array into string type
+// if string in byte array is not terminated, empty string is returned
+func cToGoString(c []byte) string {
+ if end := bytes.IndexByte(c, 0); end >= 0 {
+ return string(c[:end])
+ }
+ // no trailing \0 - return ""
+ return ""
+}
+
+// copyString copy passed string into byte array
+// make sure string is terminated
+// if string does not fit into byte array, it will be concatenated
+func copyString(b []byte, s string) {
+ sl := len(s)
+ bs := len(b)
+ if bs > sl {
+ copy(b[:], s)
+ b[sl] = 0
+ } else {
+ copy(b[:bs-1], s)
+ b[bs-1] = 0
+ }
+}
+
+func NewEnv(path string) *Env {
+ // osutil.FileExists(path + "bak")
+ return &Env{
+ path: path,
+ pathbak: path + "bak",
+ env: SnapBootSelect_v1{
+ Signature: SNAP_BOOTSELECT_SIGNATURE,
+ Version: SNAP_BOOTSELECT_VERSION,
+ },
+ }
+}
+
+func (l *Env) Get(key string) string {
+ switch key {
+ case "snap_mode":
+ return cToGoString(l.env.Snap_mode[:])
+ case "snap_kernel":
+ return cToGoString(l.env.Snap_kernel[:])
+ case "snap_try_kernel":
+ return cToGoString(l.env.Snap_try_kernel[:])
+ case "snap_core":
+ return cToGoString(l.env.Snap_core[:])
+ case "snap_try_core":
+ return cToGoString(l.env.Snap_try_core[:])
+ case "snap_gadget":
+ return cToGoString(l.env.Snap_gadget[:])
+ case "snap_try_gadget":
+ return cToGoString(l.env.Snap_try_gadget[:])
+ case "reboot_reason":
+ return cToGoString(l.env.Reboot_reason[:])
+ case "bootimg_file_name":
+ return cToGoString(l.env.Bootimg_file_name[:])
+ }
+ return ""
+}
+
+func (l *Env) Set(key, value string) {
+ switch key {
+ case "snap_mode":
+ copyString(l.env.Snap_mode[:], value)
+ case "snap_kernel":
+ copyString(l.env.Snap_kernel[:], value)
+ case "snap_try_kernel":
+ copyString(l.env.Snap_try_kernel[:], value)
+ case "snap_core":
+ copyString(l.env.Snap_core[:], value)
+ case "snap_try_core":
+ copyString(l.env.Snap_try_core[:], value)
+ case "snap_gadget":
+ copyString(l.env.Snap_gadget[:], value)
+ case "snap_try_gadget":
+ copyString(l.env.Snap_try_gadget[:], value)
+ case "reboot_reason":
+ copyString(l.env.Reboot_reason[:], value)
+ case "bootimg_file_name":
+ copyString(l.env.Bootimg_file_name[:], value)
+ }
+}
+
+// ConfigureBootPartitions set boot partitions label names
+// this function should not be used at run time!
+// it should be used only at image build time,
+// if partition labels are not pre-filled by gadget built
+func (l *Env) ConfigureBootPartitions(boot_1, boot_2 string) {
+ copyString(l.env.Bootimg_matrix[0][MATRIX_ROW_PARTITION][:], boot_1)
+ copyString(l.env.Bootimg_matrix[1][MATRIX_ROW_PARTITION][:], boot_2)
+}
+
+// ConfigureBootimgName set boot image file name
+// boot image file name is used at kernel extraction time
+// this function should not be used at run time!
+// it should be used only at image build time
+// if default boot.img is not set by gadget built
+func (l *Env) ConfigureBootimgName(bootimgName string) {
+ copyString(l.env.Bootimg_file_name[:], bootimgName)
+}
+
+func (l *Env) Load() error {
+ err := l.LoadEnv(l.path)
+ if err != nil {
+ return l.LoadEnv(l.pathbak)
+ }
+ return nil
+}
+
+func (l *Env) LoadEnv(path string) error {
+ f, err := os.Open(path)
+ if err != nil {
+ return fmt.Errorf("cannot open LK env file: %v", err)
+ }
+
+ defer f.Close()
+ if err := binary.Read(f, binary.LittleEndian, &l.env); err != nil {
+ return fmt.Errorf("cannot read LK env from file: %v", err)
+ }
+
+ // calculate crc32 to validate structure
+ w := bytes.NewBuffer(nil)
+ ss := binary.Size(l.env)
+ w.Grow(ss)
+ if err := binary.Write(w, binary.LittleEndian, &l.env); err != nil {
+ return fmt.Errorf("cannot write LK env to buffer for validation: %v", err)
+ }
+ if l.env.Version != SNAP_BOOTSELECT_VERSION || l.env.Signature != SNAP_BOOTSELECT_SIGNATURE {
+ return fmt.Errorf("cannot validate version/signature for %s, got 0x%X expected 0x%X, got 0x%X expected 0x%X\n", path, l.env.Version, SNAP_BOOTSELECT_VERSION, l.env.Signature, SNAP_BOOTSELECT_SIGNATURE)
+ }
+
+ crc := crc32.ChecksumIEEE(w.Bytes()[:ss-4]) // size of crc32 itself at the end of the structure
+ if crc != l.env.Crc32 {
+ return fmt.Errorf("cannot validate environment checksum %s, got 0x%X expected 0x%X\n", path, crc, l.env.Crc32)
+ }
+ logger.Debugf("Load: validated crc32 (0x%X)", l.env.Crc32)
+ return nil
+}
+
+func (l *Env) Save() error {
+ logger.Debugf("Save")
+ w := bytes.NewBuffer(nil)
+ ss := binary.Size(l.env)
+ w.Grow(ss)
+ if err := binary.Write(w, binary.LittleEndian, &l.env); err != nil {
+ return fmt.Errorf("cannot write LK env to buffer for saving: %v", err)
+ }
+ // calculate crc32
+ l.env.Crc32 = crc32.ChecksumIEEE(w.Bytes()[:ss-4])
+ logger.Debugf("Save: calculated crc32 (0x%X)", l.env.Crc32)
+ w.Truncate(ss - 4)
+ binary.Write(w, binary.LittleEndian, &l.env.Crc32)
+
+ err := l.SaveEnv(l.path, w)
+ if err != nil {
+ logger.Debugf("Save: failed to save main environment")
+ }
+ // if there is backup environment file save to it as well
+ if osutil.FileExists(l.pathbak) {
+ if err := l.SaveEnv(l.pathbak, w); err != nil {
+ logger.Debugf("Save: failed to save backup environment %v", err)
+ }
+ }
+ return err
+}
+
+func (l *Env) SaveEnv(path string, buf *bytes.Buffer) error {
+ f, err := os.OpenFile(path, os.O_WRONLY, 0660)
+ if err != nil {
+ return fmt.Errorf("cannot open LK env file for env storing: %v", err)
+ }
+ defer f.Close()
+
+ if _, err := f.Write(buf.Bytes()); err != nil {
+ return fmt.Errorf("cannot write LK env buf to LK env file: %v", err)
+ }
+ if err := f.Sync(); err != nil {
+ return fmt.Errorf("cannot sync LK env file: %v", err)
+ }
+ return nil
+}
+
+// FindFreeBootPartition find free boot partition to be used for new kernel revision
+// - consider kernel snap blob name, if kernel name matches
+// already installed revision, return coresponding partition name
+// - protect partition used by kernel_snap, consider other as free
+// - consider only boot partitions with defined partition name
+func (l *Env) FindFreeBootPartition(kernel string) (string, error) {
+ for x := range l.env.Bootimg_matrix {
+ bp := cToGoString(l.env.Bootimg_matrix[x][MATRIX_ROW_PARTITION][:])
+ if bp != "" {
+ k := cToGoString(l.env.Bootimg_matrix[x][MATRIX_ROW_KERNEL][:])
+ if k != cToGoString(l.env.Snap_kernel[:]) || k == kernel || k == "" {
+ return cToGoString(l.env.Bootimg_matrix[x][MATRIX_ROW_PARTITION][:]), nil
+ }
+ }
+ }
+ return "", fmt.Errorf("cannot find free partition for boot image")
+}
+
+// SetBootPartition set kernel revision name to passed boot partition
+func (l *Env) SetBootPartition(bootpart, kernel string) error {
+ for x := range l.env.Bootimg_matrix {
+ if bootpart == cToGoString(l.env.Bootimg_matrix[x][MATRIX_ROW_PARTITION][:]) {
+ copyString(l.env.Bootimg_matrix[x][MATRIX_ROW_KERNEL][:], kernel)
+ return nil
+ }
+ }
+ return fmt.Errorf("cannot find defined [%s] boot image partition", bootpart)
+}
+
+func (l *Env) GetBootPartition(kernel string) (string, error) {
+ for x := range l.env.Bootimg_matrix {
+ if kernel == cToGoString(l.env.Bootimg_matrix[x][MATRIX_ROW_KERNEL][:]) {
+ return cToGoString(l.env.Bootimg_matrix[x][MATRIX_ROW_PARTITION][:]), nil
+ }
+ }
+ return "", fmt.Errorf("cannot find kernel %q in boot image partitions", kernel)
+}
+
+// FreeBootPartition free passed kernel revision from any boot partition
+// ignore if there is no boot partition with given kernel revision
+func (l *Env) FreeBootPartition(kernel string) (bool, error) {
+ for x := range l.env.Bootimg_matrix {
+ if "" != cToGoString(l.env.Bootimg_matrix[x][MATRIX_ROW_PARTITION][:]) {
+ if kernel == cToGoString(l.env.Bootimg_matrix[x][MATRIX_ROW_KERNEL][:]) {
+ l.env.Bootimg_matrix[x][1][MATRIX_ROW_PARTITION] = 0
+ return true, nil
+ }
+ }
+ }
+ return false, fmt.Errorf("cannot find defined [%s] boot image partition", kernel)
+}
+
+// GetBootImageName return expected boot image file name in kernel snap
+func (l *Env) GetBootImageName() string {
+ if "" != cToGoString(l.env.Bootimg_file_name[:]) {
+ return cToGoString(l.env.Bootimg_file_name[:])
+ }
+ return BOOTIMG_DEFAULT_NAME
+}
diff -Nru snapd-2.40/bootloader/lkenv/lkenv_test.go snapd-2.42.1/bootloader/lkenv/lkenv_test.go
--- snapd-2.40/bootloader/lkenv/lkenv_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/bootloader/lkenv/lkenv_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,336 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package lkenv_test
+
+import (
+ "bytes"
+ "compress/gzip"
+ . "gopkg.in/check.v1"
+ "io"
+ "io/ioutil"
+ "path/filepath"
+ "testing"
+
+ "github.com/snapcore/snapd/bootloader/lkenv"
+)
+
+// Hook up check.v1 into the "go test" runner
+func Test(t *testing.T) { TestingT(t) }
+
+type lkenvTestSuite struct {
+ envPath string
+ envPathbak string
+}
+
+var _ = Suite(&lkenvTestSuite{})
+
+func (l *lkenvTestSuite) SetUpTest(c *C) {
+ l.envPath = filepath.Join(c.MkDir(), "snapbootsel.bin")
+ l.envPathbak = l.envPath + "bak"
+}
+
+// unpack test data packed with gzip
+func unpackTestData(data []byte) (resData []byte, err error) {
+ b := bytes.NewBuffer(data)
+ var r io.Reader
+ r, err = gzip.NewReader(b)
+ if err != nil {
+ return
+ }
+ var env bytes.Buffer
+ _, err = env.ReadFrom(r)
+ if err != nil {
+ return
+ }
+ return env.Bytes(), nil
+}
+
+func (l *lkenvTestSuite) TestSet(c *C) {
+ env := lkenv.NewEnv(l.envPath)
+ c.Check(env, NotNil)
+
+ env.Set("snap_mode", "try")
+ c.Check(env.Get("snap_mode"), Equals, "try")
+}
+
+func (l *lkenvTestSuite) TestSave(c *C) {
+ buf := make([]byte, 4096)
+ err := ioutil.WriteFile(l.envPathbak, buf, 0644)
+ c.Assert(err, IsNil)
+ l.TestSaveNoBak(c)
+}
+
+func (l *lkenvTestSuite) TestCtoGoString(c *C) {
+ for _, t := range []struct {
+ input []byte
+ expected string
+ }{
+ {[]byte{0, 0, 0, 0, 0}, ""},
+ {[]byte{'a', 0, 0, 0, 0}, "a"},
+ {[]byte{'a', 'b', 0, 0, 0}, "ab"},
+ {[]byte{'a', 'b', 'c', 0, 0}, "abc"},
+ {[]byte{'a', 'b', 'c', 'd', 0}, "abcd"},
+ // no trailing \0 - assume corrupted "" ?
+ {[]byte{'a', 'b', 'c', 'd', 'e'}, ""},
+ // first \0 is the cutof
+ {[]byte{'a', 'b', 0, 'z', 0}, "ab"},
+ } {
+ c.Check(lkenv.CToGoString(t.input), Equals, t.expected)
+ }
+
+}
+
+func (l *lkenvTestSuite) TestCopyStringHappy(c *C) {
+ for _, t := range []struct {
+ input string
+ expected []byte
+ }{
+ // input up to the size of the buffer works
+ {"", []byte{0, 0, 0, 0, 0}},
+ {"a", []byte{'a', 0, 0, 0, 0}},
+ {"ab", []byte{'a', 'b', 0, 0, 0}},
+ {"abc", []byte{'a', 'b', 'c', 0, 0}},
+ {"abcd", []byte{'a', 'b', 'c', 'd', 0}},
+ // only what fit is copied
+ {"abcde", []byte{'a', 'b', 'c', 'd', 0}},
+ {"abcdef", []byte{'a', 'b', 'c', 'd', 0}},
+ // strange embedded stuff works
+ {"ab\000z", []byte{'a', 'b', 0, 'z', 0}},
+ } {
+ b := make([]byte, 5)
+ lkenv.CopyString(b, t.input)
+ c.Check(b, DeepEquals, t.expected)
+ }
+}
+
+func (l *lkenvTestSuite) TestCopyStringNoPanic(c *C) {
+ // too long, string should get concatenate
+ b := make([]byte, 5)
+ defer lkenv.CopyString(b, "12345")
+ c.Assert(recover(), IsNil)
+ defer lkenv.CopyString(b, "123456")
+ c.Assert(recover(), IsNil)
+}
+
+func (l *lkenvTestSuite) TestSaveNoBak(c *C) {
+ buf := make([]byte, 4096)
+ err := ioutil.WriteFile(l.envPath, buf, 0644)
+ c.Assert(err, IsNil)
+
+ env := lkenv.NewEnv(l.envPath)
+ c.Check(env, NotNil)
+
+ env.Set("snap_mode", "trying")
+ env.Set("snap_kernel", "kernel-1")
+ env.Set("snap_try_kernel", "kernel-2")
+ env.Set("snap_core", "core-1")
+ env.Set("snap_try_core", "core-2")
+ env.Set("snap_gadget", "gadget-1")
+ env.Set("snap_try_gadget", "gadget-2")
+ env.Set("bootimg_file_name", "boot.img")
+
+ err = env.Save()
+ c.Assert(err, IsNil)
+
+ env2 := lkenv.NewEnv(l.envPath)
+ err = env2.Load()
+ c.Assert(err, IsNil)
+ c.Check(env2.Get("snap_mode"), Equals, "trying")
+ c.Check(env2.Get("snap_kernel"), Equals, "kernel-1")
+ c.Check(env2.Get("snap_try_kernel"), Equals, "kernel-2")
+ c.Check(env2.Get("snap_core"), Equals, "core-1")
+ c.Check(env2.Get("snap_try_core"), Equals, "core-2")
+ c.Check(env2.Get("snap_gadget"), Equals, "gadget-1")
+ c.Check(env2.Get("snap_try_gadget"), Equals, "gadget-2")
+ c.Check(env2.Get("bootimg_file_name"), Equals, "boot.img")
+}
+
+func (l *lkenvTestSuite) TestFailedCRC(c *C) {
+ buf := make([]byte, 4096)
+ err := ioutil.WriteFile(l.envPathbak, buf, 0644)
+ c.Assert(err, IsNil)
+ l.TestFailedCRCNoBak(c)
+}
+
+func (l *lkenvTestSuite) TestFailedCRCNoBak(c *C) {
+ buf := make([]byte, 4096)
+ err := ioutil.WriteFile(l.envPath, buf, 0644)
+ c.Assert(err, IsNil)
+
+ env := lkenv.NewEnv(l.envPath)
+ c.Check(env, NotNil)
+
+ err = env.Load()
+ c.Assert(err, NotNil)
+}
+
+func (l *lkenvTestSuite) TestFailedCRCFallBack(c *C) {
+ buf := make([]byte, 4096)
+ err := ioutil.WriteFile(l.envPath, buf, 0644)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(l.envPathbak, buf, 0644)
+ c.Assert(err, IsNil)
+
+ env := lkenv.NewEnv(l.envPath)
+ c.Check(env, NotNil)
+
+ env.Set("snap_mode", "trying")
+ env.Set("snap_kernel", "kernel-1")
+ env.Set("snap_try_kernel", "kernel-2")
+ err = env.Save()
+ c.Assert(err, IsNil)
+
+ // break main env file
+ err = ioutil.WriteFile(l.envPath, buf, 0644)
+ c.Assert(err, IsNil)
+
+ env2 := lkenv.NewEnv(l.envPath)
+ err = env2.Load()
+ c.Assert(err, IsNil)
+ c.Check(env2.Get("snap_mode"), Equals, "trying")
+ c.Check(env2.Get("snap_kernel"), Equals, "kernel-1")
+ c.Check(env2.Get("snap_try_kernel"), Equals, "kernel-2")
+}
+
+func (l *lkenvTestSuite) TestGetBootPartition(c *C) {
+ buf := make([]byte, 4096)
+ err := ioutil.WriteFile(l.envPath, buf, 0644)
+ c.Assert(err, IsNil)
+
+ env := lkenv.NewEnv(l.envPath)
+ c.Assert(err, IsNil)
+ env.ConfigureBootPartitions("boot_a", "boot_b")
+ // test no boot partition used
+ p, err := env.FindFreeBootPartition("kernel-1")
+ c.Check(p, Equals, "boot_a")
+ c.Assert(err, IsNil)
+ // set kernel-2 to boot_a partition
+ err = env.SetBootPartition("boot_a", "kernel-1")
+ c.Assert(err, IsNil)
+ // set kernel-2 to boot_a partition
+ err = env.SetBootPartition("boot_b", "kernel-2")
+ c.Assert(err, IsNil)
+
+ // 'boot_a' has 'kernel-1' revision
+ p, err = env.GetBootPartition("kernel-1")
+ c.Check(p, Equals, "boot_a")
+ c.Assert(err, IsNil)
+ // 'boot_b' has 'kernel-2' revision
+ p, err = env.GetBootPartition("kernel-2")
+ c.Check(p, Equals, "boot_b")
+ c.Assert(err, IsNil)
+}
+
+func (l *lkenvTestSuite) TestFindFree_Set_Free_BootPartition(c *C) {
+ buf := make([]byte, 4096)
+ err := ioutil.WriteFile(l.envPath, buf, 0644)
+ c.Assert(err, IsNil)
+
+ env := lkenv.NewEnv(l.envPath)
+ c.Assert(err, IsNil)
+ env.ConfigureBootPartitions("boot_a", "boot_b")
+ // test no boot partition used
+ p, err := env.FindFreeBootPartition("kernel-1")
+ c.Check(p, Equals, "boot_a")
+ c.Assert(err, IsNil)
+ // set kernel-2 to boot_a partition
+ err = env.SetBootPartition("boot_a", "kernel-2")
+ c.Assert(err, IsNil)
+
+ env.Set("snap_kernel", "kernel-2")
+ // kernel-2 should now return first part, as it's already there
+ p, err = env.FindFreeBootPartition("kernel-2")
+ c.Check(p, Equals, "boot_a")
+ c.Assert(err, IsNil)
+ // test kernel-1 snapd, it should now offer second partition
+ p, err = env.FindFreeBootPartition("kernel-1")
+ c.Check(p, Equals, "boot_b")
+ c.Assert(err, IsNil)
+ err = env.SetBootPartition("boot_b", "kernel-1")
+ c.Assert(err, IsNil)
+ // set boot kernel-1
+ env.Set("snap_kernel", "kernel-1")
+ // now kernel-2 should not be protected and boot_a shoild be offered
+ p, err = env.FindFreeBootPartition("kernel-3")
+ c.Check(p, Equals, "boot_a")
+ c.Assert(err, IsNil)
+ err = env.SetBootPartition("boot_a", "kernel-3")
+ c.Assert(err, IsNil)
+ // remove kernel
+ used, err := env.FreeBootPartition("kernel-3")
+ c.Assert(err, IsNil)
+ c.Check(used, Equals, true)
+ // repeated use should return false and error
+ used, err = env.FreeBootPartition("kernel-3")
+ c.Assert(err, NotNil)
+ c.Check(used, Equals, false)
+}
+
+func (l *lkenvTestSuite) TestZippedDataSample(c *C) {
+ // test data is generated with gadget build helper tool:
+ // $ parts/snap-boot-sel-env/build/lk-boot-env -w test.bin
+ // --snap-mode="trying" --snap-kernel="kernel-1" --snap-try-kernel="kernel-2"
+ // --snap-core="core-1" --snap-try-core="core-2" --reboot-reason=""
+ // --boot-0-part="boot_a" --boot-1-part="boot_b" --boot-0-snap="kernel-1"
+ // --boot-1-snap="kernel-3" --bootimg-file="boot.img"
+ // $ cat test.bin | gzip | xxd -i
+ gzipedData := []byte{
+ 0x1f, 0x8b, 0x08, 0x00, 0x95, 0x88, 0x77, 0x5d, 0x00, 0x03, 0xed, 0xd7,
+ 0xc1, 0x09, 0xc2, 0x40, 0x10, 0x05, 0xd0, 0xa4, 0x20, 0x05, 0x63, 0x07,
+ 0x96, 0xa0, 0x05, 0x88, 0x91, 0x25, 0x04, 0x35, 0x0b, 0x6b, 0x2e, 0x1e,
+ 0xac, 0xcb, 0xf6, 0xc4, 0x90, 0x1e, 0x06, 0xd9, 0xf7, 0x2a, 0xf8, 0xc3,
+ 0x1f, 0x18, 0xe6, 0x74, 0x78, 0xa6, 0xb6, 0x69, 0x9b, 0xb9, 0xbc, 0xc6,
+ 0x69, 0x68, 0xaa, 0x75, 0xcd, 0x25, 0x6d, 0x76, 0xd1, 0x29, 0xe2, 0x2c,
+ 0xf3, 0x77, 0xd1, 0x29, 0xe2, 0xdc, 0x52, 0x99, 0xd2, 0xbd, 0xde, 0x0d,
+ 0x58, 0xe7, 0xaf, 0x78, 0x03, 0x80, 0x5a, 0xf5, 0x39, 0xcf, 0xe7, 0x4b,
+ 0x74, 0x8a, 0x38, 0xb5, 0xdf, 0xbf, 0xa5, 0xff, 0x3e, 0x3a, 0x45, 0x9c,
+ 0xb5, 0xff, 0x7d, 0x74, 0x8e, 0x28, 0xbf, 0xfe, 0xb7, 0xe3, 0xa3, 0xe2,
+ 0x0f, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
+ 0xf8, 0x17, 0xc7, 0xf7, 0xa7, 0xfb, 0x02, 0x1c, 0xdf, 0x44, 0x21, 0x0c,
+ 0x3a, 0x00, 0x00}
+
+ // uncompress test data to sample env file
+ rawData, err := unpackTestData(gzipedData)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(l.envPath, rawData, 0644)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(l.envPathbak, rawData, 0644)
+ c.Assert(err, IsNil)
+
+ env := lkenv.NewEnv(l.envPath)
+ c.Check(env, NotNil)
+ err = env.Load()
+ c.Assert(err, IsNil)
+ c.Check(env.Get("snap_mode"), Equals, "trying")
+ c.Check(env.Get("snap_kernel"), Equals, "kernel-1")
+ c.Check(env.Get("snap_try_kernel"), Equals, "kernel-2")
+ c.Check(env.Get("snap_core"), Equals, "core-1")
+ c.Check(env.Get("snap_try_core"), Equals, "core-2")
+ c.Check(env.Get("bootimg_file_name"), Equals, "boot.img")
+ c.Check(env.Get("reboot_reason"), Equals, "")
+ // first partition should be with label 'boot_a' and 'kernel-1' revision
+ p, err := env.GetBootPartition("kernel-1")
+ c.Check(p, Equals, "boot_a")
+ c.Assert(err, IsNil)
+ // test second boot partition is free with label "boot_b"
+ p, err = env.FindFreeBootPartition("kernel-2")
+ c.Check(p, Equals, "boot_b")
+ c.Assert(err, IsNil)
+}
diff -Nru snapd-2.40/bootloader/lk.go snapd-2.42.1/bootloader/lk.go
--- snapd-2.40/bootloader/lk.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/bootloader/lk.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,212 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package bootloader
+
+import (
+ "fmt"
+ "io"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/bootloader/lkenv"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+)
+
+type lk struct {
+ rootdir string
+ inRuntimeMode bool
+}
+
+// newLk create a new lk bootloader object
+func newLk(rootdir string, opts *Options) Bootloader {
+ l := &lk{rootdir: rootdir}
+
+ // XXX: in the long run we want this to go away, we probably add
+ // something like "boot.PrepareImage()" and add an (optional)
+ // method "PrepareImage" to the bootloader interface that is
+ // used to setup a bootloader from prepare-image if things
+ // are very different from runtime vs image-building mode.
+ //
+ // determine mode we are in, runtime or image build
+ l.inRuntimeMode = !opts.PrepareImageTime
+
+ if !osutil.FileExists(l.envFile()) {
+ return nil
+ }
+
+ return l
+}
+
+func (l *lk) setRootDir(rootdir string) {
+ l.rootdir = rootdir
+}
+
+func (l *lk) Name() string {
+ return "lk"
+}
+
+func (l *lk) dir() string {
+ // we have two scenarios, image building and runtime
+ // during image building we store environment into file
+ // at runtime environment is written directly into dedicated partition
+ if l.inRuntimeMode {
+ return filepath.Join(l.rootdir, "/dev/disk/by-partlabel/")
+ } else {
+ return filepath.Join(l.rootdir, "/boot/lk/")
+ }
+}
+
+func (l *lk) ConfigFile() string {
+ return l.envFile()
+}
+
+func (l *lk) envFile() string {
+ // as for dir, we have two scenarios, image building and runtime
+ if l.inRuntimeMode {
+ // TO-DO: this should be eventually fetched from gadget.yaml
+ return filepath.Join(l.dir(), "snapbootsel")
+ } else {
+ return filepath.Join(l.dir(), "snapbootsel.bin")
+ }
+}
+
+func (l *lk) GetBootVars(names ...string) (map[string]string, error) {
+ out := make(map[string]string)
+
+ env := lkenv.NewEnv(l.envFile())
+ if err := env.Load(); err != nil {
+ return nil, err
+ }
+
+ for _, name := range names {
+ out[name] = env.Get(name)
+ }
+
+ return out, nil
+}
+
+func (l *lk) SetBootVars(values map[string]string) error {
+ env := lkenv.NewEnv(l.envFile())
+ if err := env.Load(); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+
+ // update environment only if something change
+ dirty := false
+ for k, v := range values {
+ // already set to the right value, nothing to do
+ if env.Get(k) == v {
+ continue
+ }
+ env.Set(k, v)
+ dirty = true
+ }
+
+ if dirty {
+ return env.Save()
+ }
+
+ return nil
+}
+
+// ExtractKernelAssets extract kernel assets per bootloader specifics
+// lk bootloader requires boot partition to hold valid boot image
+// there are two boot partition available, one holding current bootimage
+// kernel assets are extracted to other (free) boot partition
+// in case this function is called as part of image creation,
+// boot image is extracted to the file
+func (l *lk) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error {
+ blobName := filepath.Base(s.MountFile())
+
+ logger.Debugf("ExtractKernelAssets (%s)", blobName)
+
+ env := lkenv.NewEnv(l.envFile())
+ if err := env.Load(); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+
+ bootPartition, err := env.FindFreeBootPartition(blobName)
+ if err != nil {
+ return err
+ }
+
+ if l.inRuntimeMode {
+ logger.Debugf("ExtractKernelAssets handling run time usecase")
+ // this is live system, extracted bootimg needs to be flashed to
+ // free bootimg partition and env has to be updated with
+ // new kernel snap to bootimg partition mapping
+ tmpdir, err := ioutil.TempDir("", "bootimg")
+ if err != nil {
+ return fmt.Errorf("cannot create temp directory: %v", err)
+ }
+ defer os.RemoveAll(tmpdir)
+
+ bootImg := env.GetBootImageName()
+ if err := snapf.Unpack(bootImg, tmpdir); err != nil {
+ return fmt.Errorf("cannot unpack %s: %v", bootImg, err)
+ }
+ // write boot.img to free boot partition
+ bootimgName := filepath.Join(tmpdir, bootImg)
+ bif, err := os.Open(bootimgName)
+ if err != nil {
+ return fmt.Errorf("cannot open unpacked %s: %v", bootImg, err)
+ }
+ defer bif.Close()
+ bpart := filepath.Join(l.dir(), bootPartition)
+
+ bpf, err := os.OpenFile(bpart, os.O_WRONLY, 0660)
+ if err != nil {
+ return fmt.Errorf("cannot open boot partition [%s]: %v", bpart, err)
+ }
+ defer bpf.Close()
+
+ if _, err := io.Copy(bpf, bif); err != nil {
+ return err
+ }
+ } else {
+ // we are preparing image, just extract boot image to bootloader directory
+ logger.Debugf("ExtractKernelAssets handling image prepare")
+ if err := snapf.Unpack(env.GetBootImageName(), l.dir()); err != nil {
+ return fmt.Errorf("cannot open unpacked %s: %v", env.GetBootImageName(), err)
+ }
+ }
+ if err := env.SetBootPartition(bootPartition, blobName); err != nil {
+ return err
+ }
+
+ return env.Save()
+}
+
+func (l *lk) RemoveKernelAssets(s snap.PlaceInfo) error {
+ blobName := filepath.Base(s.MountFile())
+ logger.Debugf("RemoveKernelAssets (%s)", blobName)
+ env := lkenv.NewEnv(l.envFile())
+ if err := env.Load(); err != nil && !os.IsNotExist(err) {
+ return err
+ }
+ dirty, _ := env.FreeBootPartition(blobName)
+ if dirty {
+ return env.Save()
+ }
+ return nil
+}
diff -Nru snapd-2.40/bootloader/lk_test.go snapd-2.42.1/bootloader/lk_test.go
--- snapd-2.40/bootloader/lk_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/bootloader/lk_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,234 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package bootloader_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "sort"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/bootloader/lkenv"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+type lkTestSuite struct {
+ baseBootenvTestSuite
+}
+
+var _ = Suite(&lkTestSuite{})
+
+func (s *lkTestSuite) TestNewLkNolkReturnsNil(c *C) {
+ l := bootloader.NewLk("/does/not/exist", nil)
+ c.Assert(l, IsNil)
+}
+
+func (s *lkTestSuite) TestNewLk(c *C) {
+ bootloader.MockLkFiles(c, s.rootdir, nil)
+ l := bootloader.NewLk(s.rootdir, nil)
+ c.Assert(l, NotNil)
+ c.Check(bootloader.LkRuntimeMode(l), Equals, true)
+ c.Check(l.ConfigFile(), Equals, filepath.Join(s.rootdir, "/dev/disk/by-partlabel", "snapbootsel"))
+}
+
+func (s *lkTestSuite) TestNewLkImageBuildingTime(c *C) {
+ opts := &bootloader.Options{
+ PrepareImageTime: true,
+ }
+ bootloader.MockLkFiles(c, s.rootdir, opts)
+ l := bootloader.NewLk(s.rootdir, opts)
+ c.Assert(l, NotNil)
+ c.Check(bootloader.LkRuntimeMode(l), Equals, false)
+ c.Check(l.ConfigFile(), Equals, filepath.Join(s.rootdir, "/boot/lk", "snapbootsel.bin"))
+}
+
+func (s *lkTestSuite) TestSetGetBootVar(c *C) {
+ bootloader.MockLkFiles(c, s.rootdir, nil)
+ l := bootloader.NewLk(s.rootdir, nil)
+ bootVars := map[string]string{"snap_mode": "try"}
+ l.SetBootVars(bootVars)
+
+ v, err := l.GetBootVars("snap_mode")
+ c.Assert(err, IsNil)
+ c.Check(v, HasLen, 1)
+ c.Check(v["snap_mode"], Equals, "try")
+}
+
+func (s *lkTestSuite) TestExtractKernelAssetsUnpacksBootimgImageBuilding(c *C) {
+ opts := &bootloader.Options{
+ PrepareImageTime: true,
+ }
+ bootloader.MockLkFiles(c, s.rootdir, opts)
+ l := bootloader.NewLk(s.rootdir, opts)
+
+ c.Assert(l, NotNil)
+
+ files := [][]string{
+ {"kernel.img", "I'm a kernel"},
+ {"initrd.img", "...and I'm an initrd"},
+ {"boot.img", "...and I'm an boot image"},
+ {"dtbs/foo.dtb", "g'day, I'm foo.dtb"},
+ {"dtbs/bar.dtb", "hello, I'm bar.dtb"},
+ // must be last
+ {"meta/kernel.yaml", "version: 4.2"},
+ }
+ si := &snap.SideInfo{
+ RealName: "ubuntu-kernel",
+ Revision: snap.R(42),
+ }
+ fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files)
+ snapf, err := snap.Open(fn)
+ c.Assert(err, IsNil)
+
+ info, err := snap.ReadInfoFromSnapFile(snapf, si)
+ c.Assert(err, IsNil)
+
+ err = l.ExtractKernelAssets(info, snapf)
+ c.Assert(err, IsNil)
+
+ // just boot.img and snapbootsel.bin are there, no kernel.img
+ infos, err := ioutil.ReadDir(filepath.Join(s.rootdir, "boot", "lk", ""))
+ c.Assert(err, IsNil)
+ var fnames []string
+ for _, info := range infos {
+ fnames = append(fnames, info.Name())
+ }
+ sort.Strings(fnames)
+ c.Assert(fnames, HasLen, 2)
+ c.Assert(fnames, DeepEquals, []string{"boot.img", "snapbootsel.bin"})
+}
+
+func (s *lkTestSuite) TestExtractKernelAssetsUnpacksCustomBootimgImageBuilding(c *C) {
+ opts := &bootloader.Options{
+ PrepareImageTime: true,
+ }
+ bootloader.MockLkFiles(c, s.rootdir, opts)
+ l := bootloader.NewLk(s.rootdir, opts)
+
+ c.Assert(l, NotNil)
+
+ // first configure custom boot image file name
+ env := lkenv.NewEnv(l.ConfigFile())
+ env.Load()
+ env.ConfigureBootimgName("boot-2.img")
+ err := env.Save()
+ c.Assert(err, IsNil)
+
+ files := [][]string{
+ {"kernel.img", "I'm a kernel"},
+ {"initrd.img", "...and I'm an initrd"},
+ {"boot-2.img", "...and I'm an boot image"},
+ {"dtbs/foo.dtb", "g'day, I'm foo.dtb"},
+ {"dtbs/bar.dtb", "hello, I'm bar.dtb"},
+ // must be last
+ {"meta/kernel.yaml", "version: 4.2"},
+ }
+ si := &snap.SideInfo{
+ RealName: "ubuntu-kernel",
+ Revision: snap.R(42),
+ }
+ fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files)
+ snapf, err := snap.Open(fn)
+ c.Assert(err, IsNil)
+
+ info, err := snap.ReadInfoFromSnapFile(snapf, si)
+ c.Assert(err, IsNil)
+
+ err = l.ExtractKernelAssets(info, snapf)
+ c.Assert(err, IsNil)
+
+ // boot-2.img is there
+ bootimg := filepath.Join(s.rootdir, "boot", "lk", "boot-2.img")
+ c.Assert(osutil.FileExists(bootimg), Equals, true)
+}
+
+func (s *lkTestSuite) TestExtractKernelAssetsUnpacksAndRemoveInRuntimeMode(c *C) {
+ bootloader.MockLkFiles(c, s.rootdir, nil)
+ lk := bootloader.NewLk(s.rootdir, nil)
+ c.Assert(lk, NotNil)
+
+ // create mock bootsel, boot_a, boot_b partitions
+ for _, partName := range []string{"snapbootsel", "boot_a", "boot_b"} {
+ mockPart := filepath.Join(s.rootdir, "/dev/disk/by-partlabel/", partName)
+ err := os.MkdirAll(filepath.Dir(mockPart), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(mockPart, nil, 0600)
+ c.Assert(err, IsNil)
+ }
+ // ensure we have a valid boot env
+ bootselPartition := filepath.Join(s.rootdir, "/dev/disk/by-partlabel/snapbootsel")
+ lkenv := lkenv.NewEnv(bootselPartition)
+ lkenv.ConfigureBootPartitions("boot_a", "boot_b")
+ err := lkenv.Save()
+ c.Assert(err, IsNil)
+
+ // mock a kernel snap that has a boot.img
+ files := [][]string{
+ {"boot.img", "I'm the default boot image name"},
+ }
+ si := &snap.SideInfo{
+ RealName: "ubuntu-kernel",
+ Revision: snap.R(42),
+ }
+ fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files)
+ snapf, err := snap.Open(fn)
+ c.Assert(err, IsNil)
+
+ info, err := snap.ReadInfoFromSnapFile(snapf, si)
+ c.Assert(err, IsNil)
+
+ // now extract
+ err = lk.ExtractKernelAssets(info, snapf)
+ c.Assert(err, IsNil)
+
+ // and validate it went to the "boot_a" partition
+ bootA := filepath.Join(s.rootdir, "/dev/disk/by-partlabel/boot_a")
+ content, err := ioutil.ReadFile(bootA)
+ c.Assert(err, IsNil)
+ c.Assert(string(content), Equals, "I'm the default boot image name")
+
+ // also validate that bootB is empty
+ bootB := filepath.Join(s.rootdir, "/dev/disk/by-partlabel/boot_b")
+ content, err = ioutil.ReadFile(bootB)
+ c.Assert(err, IsNil)
+ c.Assert(content, HasLen, 0)
+
+ // test that boot partition got set
+ err = lkenv.Load()
+ c.Assert(err, IsNil)
+ bootPart, err := lkenv.GetBootPartition("ubuntu-kernel_42.snap")
+ c.Assert(err, IsNil)
+ c.Assert(bootPart, Equals, "boot_a")
+
+ // now remove the kernel
+ err = lk.RemoveKernelAssets(info)
+ c.Assert(err, IsNil)
+ // and ensure its no longer available in the boot partions
+ err = lkenv.Load()
+ c.Assert(err, IsNil)
+ bootPart, err = lkenv.GetBootPartition("ubuntu-kernel_42.snap")
+ c.Assert(err, ErrorMatches, "cannot find kernel .* in boot image partitions")
+ c.Assert(bootPart, Equals, "")
+}
diff -Nru snapd-2.40/bootloader/uboot.go snapd-2.42.1/bootloader/uboot.go
--- snapd-2.40/bootloader/uboot.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/bootloader/uboot.go 2019-10-30 12:17:43.000000000 +0000
@@ -23,16 +23,17 @@
"path/filepath"
"github.com/snapcore/snapd/bootloader/ubootenv"
- "github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
)
-type uboot struct{}
+type uboot struct {
+ rootdir string
+}
// newUboot create a new Uboot bootloader object
-func newUboot() Bootloader {
- u := &uboot{}
+func newUboot(rootdir string) Bootloader {
+ u := &uboot{rootdir: rootdir}
if !osutil.FileExists(u.envFile()) {
return nil
}
@@ -44,8 +45,15 @@
return "uboot"
}
-func (u *uboot) Dir() string {
- return filepath.Join(dirs.GlobalRootDir, "/boot/uboot")
+func (u *uboot) setRootDir(rootdir string) {
+ u.rootdir = rootdir
+}
+
+func (u *uboot) dir() string {
+ if u.rootdir == "" {
+ panic("internal error: unset rootdir")
+ }
+ return filepath.Join(u.rootdir, "/boot/uboot")
}
func (u *uboot) ConfigFile() string {
@@ -53,7 +61,7 @@
}
func (u *uboot) envFile() string {
- return filepath.Join(u.Dir(), "uboot.env")
+ return filepath.Join(u.dir(), "uboot.env")
}
func (u *uboot) SetBootVars(values map[string]string) error {
@@ -94,10 +102,10 @@
return out, nil
}
-func (u *uboot) ExtractKernelAssets(s *snap.Info, snapf snap.Container) error {
- return extractKernelAssetsToBootDir(u.Dir(), s, snapf)
+func (u *uboot) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error {
+ return extractKernelAssetsToBootDir(u.dir(), s, snapf)
}
func (u *uboot) RemoveKernelAssets(s snap.PlaceInfo) error {
- return removeKernelAssetsFromBootDir(u.Dir(), s)
+ return removeKernelAssetsFromBootDir(u.dir(), s)
}
diff -Nru snapd-2.40/bootloader/uboot_test.go snapd-2.42.1/bootloader/uboot_test.go
--- snapd-2.40/bootloader/uboot_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/bootloader/uboot_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -41,20 +41,20 @@
var _ = Suite(&ubootTestSuite{})
func (s *ubootTestSuite) TestNewUbootNoUbootReturnsNil(c *C) {
- u := bootloader.NewUboot()
+ u := bootloader.NewUboot(s.rootdir)
c.Assert(u, IsNil)
}
func (s *ubootTestSuite) TestNewUboot(c *C) {
- bootloader.MockUbootFiles(c)
- u := bootloader.NewUboot()
+ bootloader.MockUbootFiles(c, s.rootdir)
+ u := bootloader.NewUboot(s.rootdir)
c.Assert(u, NotNil)
c.Assert(u.Name(), Equals, "uboot")
}
func (s *ubootTestSuite) TestUbootGetEnvVar(c *C) {
- bootloader.MockUbootFiles(c)
- u := bootloader.NewUboot()
+ bootloader.MockUbootFiles(c, s.rootdir)
+ u := bootloader.NewUboot(s.rootdir)
c.Assert(u, NotNil)
err := u.SetBootVars(map[string]string{
"snap_mode": "",
@@ -71,16 +71,16 @@
}
func (s *ubootTestSuite) TestGetBootloaderWithUboot(c *C) {
- bootloader.MockUbootFiles(c)
+ bootloader.MockUbootFiles(c, s.rootdir)
- bootloader, err := bootloader.Find()
+ bootloader, err := bootloader.Find(s.rootdir, nil)
c.Assert(err, IsNil)
c.Assert(bootloader.Name(), Equals, "uboot")
}
func (s *ubootTestSuite) TestUbootSetEnvNoUselessWrites(c *C) {
- bootloader.MockUbootFiles(c)
- u := bootloader.NewUboot()
+ bootloader.MockUbootFiles(c, s.rootdir)
+ u := bootloader.NewUboot(s.rootdir)
c.Assert(u, NotNil)
envFile := u.ConfigFile()
@@ -109,8 +109,8 @@
}
func (s *ubootTestSuite) TestUbootSetBootVarFwEnv(c *C) {
- bootloader.MockUbootFiles(c)
- u := bootloader.NewUboot()
+ bootloader.MockUbootFiles(c, s.rootdir)
+ u := bootloader.NewUboot(s.rootdir)
err := u.SetBootVars(map[string]string{"key": "value"})
c.Assert(err, IsNil)
@@ -121,8 +121,8 @@
}
func (s *ubootTestSuite) TestUbootGetBootVarFwEnv(c *C) {
- bootloader.MockUbootFiles(c)
- u := bootloader.NewUboot()
+ bootloader.MockUbootFiles(c, s.rootdir)
+ u := bootloader.NewUboot(s.rootdir)
err := u.SetBootVars(map[string]string{"key2": "value2"})
c.Assert(err, IsNil)
@@ -133,8 +133,8 @@
}
func (s *ubootTestSuite) TestExtractKernelAssetsAndRemove(c *C) {
- bootloader.MockUbootFiles(c)
- u := bootloader.NewUboot()
+ bootloader.MockUbootFiles(c, s.rootdir)
+ u := bootloader.NewUboot(s.rootdir)
files := [][]string{
{"kernel.img", "I'm a kernel"},
@@ -159,9 +159,7 @@
c.Assert(err, IsNil)
// this is where the kernel/initrd is unpacked
- bootdir := u.Dir()
-
- kernelAssetsDir := filepath.Join(bootdir, "ubuntu-kernel_42.snap")
+ kernelAssetsDir := filepath.Join(s.rootdir, "boot", "uboot", "ubuntu-kernel_42.snap")
for _, def := range files {
if def[0] == "meta/kernel.yaml" {
diff -Nru snapd-2.40/check-pr-title.py snapd-2.42.1/check-pr-title.py
--- snapd-2.40/check-pr-title.py 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/check-pr-title.py 2019-10-30 12:17:43.000000000 +0000
@@ -51,7 +51,7 @@
# package, otherpackage/subpackage: this is a title
# tests/regression/lp-12341234: foo
# [RFC] foo: bar
- if not re.match(r'[a-zA-Z0-9_\-/,. \[\]]+: .*', title):
+ if not re.match(r'[a-zA-Z0-9_\-/,. \[\]{}]+: .*', title):
raise InvalidPRTitle(title)
diff -Nru snapd-2.40/client/aliases_test.go snapd-2.42.1/client/aliases_test.go
--- snapd-2.40/client/aliases_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/aliases_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -34,6 +34,7 @@
}
func (cs *clientSuite) TestClientAlias(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
@@ -62,6 +63,7 @@
}
func (cs *clientSuite) TestClientUnalias(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
@@ -89,6 +91,7 @@
}
func (cs *clientSuite) TestClientDisableAllAliases(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
@@ -115,6 +118,7 @@
}
func (cs *clientSuite) TestClientRemoveManualAlias(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
@@ -141,6 +145,7 @@
}
func (cs *clientSuite) TestClientPrefer(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
diff -Nru snapd-2.40/client/apps.go snapd-2.42.1/client/apps.go
--- snapd-2.40/client/apps.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/apps.go 2019-10-30 12:17:43.000000000 +0000
@@ -130,7 +130,7 @@
if err := decodeInto(rsp.Body, &r); err != nil {
return nil, err
}
- return nil, r.err(client)
+ return nil, r.err(client, rsp.StatusCode)
}
ch := make(chan Log, 20)
diff -Nru snapd-2.40/client/apps_test.go snapd-2.42.1/client/apps_test.go
--- snapd-2.40/client/apps_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/apps_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -213,6 +213,7 @@
}
func (cs *clientSuite) TestClientServiceStart(c *check.C) {
+ cs.status = 202
cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}`
type scenario struct {
@@ -274,6 +275,7 @@
}
func (cs *clientSuite) TestClientServiceStop(c *check.C) {
+ cs.status = 202
cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}`
type tT struct {
@@ -335,6 +337,7 @@
}
func (cs *clientSuite) TestClientServiceRestart(c *check.C) {
+ cs.status = 202
cs.rsp = `{"type": "async", "status-code": 202, "change": "24"}`
type tT struct {
diff -Nru snapd-2.40/client/asserts.go snapd-2.42.1/client/asserts.go
--- snapd-2.40/client/asserts.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/asserts.go 2019-10-30 12:17:43.000000000 +0000
@@ -27,6 +27,7 @@
"strconv"
"github.com/snapcore/snapd/asserts" // for parsing
+ "github.com/snapcore/snapd/snap"
)
// Ack tries to add an assertion to the system assertion
@@ -102,3 +103,31 @@
return asserts, nil
}
+
+// StoreAccount returns the full store account info for the specified accountID
+func (client *Client) StoreAccount(accountID string) (*snap.StoreAccount, error) {
+ assertions, err := client.Known("account", map[string]string{"account-id": accountID})
+ if err != nil {
+ return nil, err
+ }
+ switch len(assertions) {
+ case 1:
+ // happy case, break out of the switch
+ case 0:
+ return nil, fmt.Errorf("no assertion found for account-id %s", accountID)
+ default:
+ // unknown how this could happen...
+ return nil, fmt.Errorf("multiple assertions for account-id %s", accountID)
+ }
+
+ acct, ok := assertions[0].(*asserts.Account)
+ if !ok {
+ return nil, fmt.Errorf("incorrect type of account assertion returned")
+ }
+ return &snap.StoreAccount{
+ ID: acct.AccountID(),
+ Username: acct.Username(),
+ DisplayName: acct.DisplayName(),
+ Validation: acct.Validation(),
+ }, nil
+}
diff -Nru snapd-2.40/client/asserts_test.go snapd-2.42.1/client/asserts_test.go
--- snapd-2.40/client/asserts_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/asserts_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -28,6 +28,7 @@
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/snap"
)
func (cs *clientSuite) TestClientAssert(c *C) {
@@ -157,3 +158,58 @@
_, err := cs.cli.Known("snap-build", nil)
c.Assert(err, ErrorMatches, "response did not have the expected number of assertions")
}
+
+func (cs *clientSuite) TestStoreAccount(c *C) {
+ cs.header = http.Header{}
+ cs.header.Add("X-Ubuntu-Assertions-Count", "1")
+ cs.rsp = `type: account
+authority-id: canonical
+account-id: canonicalID
+display-name: canonicalDisplay
+timestamp: 2016-04-01T00:00:00.0Z
+username: canonicalUser
+validation: certified
+sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk
+
+AcLDXAQAAQoABgUCV7UYzwAKCRDUpVvql9g3IK7uH/4udqNOurx5WYVknzXdwekp0ovHCQJ0iBPw
+TSFxEVr9faZSzb7eqJ1WicHsShf97PYS3ClRYAiluFsjRA8Y03kkSVJHjC+sIwGFubsnkmgflt6D
+WEmYIl0UBmeaEDS8uY4Xvp9NsLTzNEj2kvzy/52gKaTc1ZSl5RDL9ppMav+0V9iBYpiDPBWH2rJ+
+aDSD8Rkyygm0UscfAKyDKH4lrvZ0WkYyi1YVNPrjQ/AtBySh6Q4iJ3LifzKa9woIyAuJET/4/FPY
+oirqHAfuvNod36yNQIyNqEc20AvTvZNH0PSsg4rq3DLjIPzv5KbJO9lhsasNJK1OdL6x8Yqrdsbk
+ldZp4qkzfjV7VOMQKaadfcZPRaVVeJWOBnBiaukzkhoNlQi1sdCdkBB/AJHZF8QXw6c7vPDcfnCV
+1lW7ddQ2p8IsJbT6LzpJu3GW/P4xhNgCjtCJ1AJm9a9RqLwQYgdLZwwDa9iCRtqTbRXBlfy3apps
+1VjbQ3h5iCd0hNfwDBnGVm1rhLKHCD1DUdNE43oN2ZlE7XGyh0HFV6vKlpqoW3eoXCIxWu+HBY96
++LSl/jQgCkb0nxYyzEYK4Reb31D0mYw1Nji5W+MIF5E09+DYZoOT0UvR05YMwMEOeSdI/hLWg/5P
+k+GDK+/KopMmpd4D1+jjtF7ZvqDpmAV98jJGB2F88RyVb4gcjmFFyTi4Kv6vzz/oLpbm0qrizC0W
+HLGDN/ymGA5sHzEgEx7U540vz/q9VX60FKqL2YZr/DcyY9GKX5kCG4sNqIIHbcJneZ4frM99oVDu
+7Jv+DIx/Di6D1ULXol2XjxbbJLKHFtHksR97ceaFvcZwTogC61IYUBJCvvMoqdXAWMhEXCr0QfQ5
+Xbi31XW2d4/lF/zWlAkRnGTzufIXFni7+nEuOK0SQEzO3/WaRedK1SGOOtTDjB8/3OJeW96AUYK5
+oTIynkYkEyHWMNCXALg+WQW6L4/YO7aUjZ97zOWIugd7Xy63aT3r/EHafqaY2nacOhLfkeKZ830b
+o/ezjoZQAxbh6ce7JnXRgE9ELxjdAhBTpGjmmmN2sYrJ7zP9bOgly0BnEPXGSQfFA+NNNw1FADx1
+MUY8q9DBjmVtgqY+1KGTV5X8KvQCBMODZIf/XJPHdCRAHxMd8COypcwgL2vDIIXpOFbi1J/B0GF+
+eklxk9wzBA8AecBMCwCzIRHDNpD1oa2we38bVFrOug6e/VId1k1jYFJjiLyLCDmV8IMYwEllHSXp
+LQAdm3xZ7t4WnxYC8YSCk9mXf3CZg59SpmnV5Q5Z6A5Pl7Nc3sj7hcsMBZEsOMPzNC9dPsBnZvjs
+WpPUffJzEdhHBFhvYMuD4Vqj6ejUv9l3oTrjQWVC
+`
+
+ account, err := cs.cli.StoreAccount("canonicalID")
+ c.Assert(err, IsNil)
+ c.Check(cs.req.Method, Equals, "GET")
+ c.Check(cs.req.URL.Query(), HasLen, 1)
+ c.Check(cs.req.URL.Query().Get("account-id"), Equals, "canonicalID")
+ c.Assert(account, DeepEquals, &snap.StoreAccount{
+ ID: "canonicalID",
+ Username: "canonicalUser",
+ DisplayName: "canonicalDisplay",
+ Validation: "verified",
+ })
+}
+
+func (cs *clientSuite) TestStoreAccountNoAssertionFound(c *C) {
+ cs.header = http.Header{}
+ cs.header.Add("X-Ubuntu-Assertions-Count", "0")
+ cs.rsp = ""
+
+ _, err := cs.cli.StoreAccount("canonicalID")
+ c.Assert(err, ErrorMatches, "no assertion found for account-id canonicalID")
+}
diff -Nru snapd-2.40/client/client.go snapd-2.42.1/client/client.go
--- snapd-2.40/client/client.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/client.go 2019-10-30 12:17:43.000000000 +0000
@@ -262,12 +262,11 @@
// do performs a request and decodes the resulting json into the given
// value. It's low-level, for testing/experimenting only; you should
// usually use a higher level interface that builds on this.
-func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) error {
+func (client *Client) do(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (statusCode int, err error) {
retry := time.NewTicker(doRetry)
defer retry.Stop()
timeout := time.After(doTimeout)
var rsp *http.Response
- var err error
for {
rsp, err = client.raw(method, path, query, headers, body)
if err == nil || method != "GET" {
@@ -281,17 +280,17 @@
break
}
if err != nil {
- return err
+ return 0, err
}
defer rsp.Body.Close()
if v != nil {
if err := decodeInto(rsp.Body, v); err != nil {
- return err
+ return rsp.StatusCode, err
}
}
- return nil
+ return rsp.StatusCode, nil
}
func decodeInto(reader io.Reader, v interface{}) error {
@@ -313,10 +312,11 @@
// which produces json.Numbers instead of float64 types for numbers.
func (client *Client) doSync(method, path string, query url.Values, headers map[string]string, body io.Reader, v interface{}) (*ResultInfo, error) {
var rsp response
- if err := client.do(method, path, query, headers, body, &rsp); err != nil {
+ statusCode, err := client.do(method, path, query, headers, body, &rsp)
+ if err != nil {
return nil, err
}
- if err := rsp.err(client); err != nil {
+ if err := rsp.err(client, statusCode); err != nil {
return nil, err
}
if rsp.Type != "sync" {
@@ -342,17 +342,17 @@
func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader) (result json.RawMessage, changeID string, err error) {
var rsp response
-
- if err := client.do(method, path, query, headers, body, &rsp); err != nil {
+ statusCode, err := client.do(method, path, query, headers, body, &rsp)
+ if err != nil {
return nil, "", err
}
- if err := rsp.err(client); err != nil {
+ if err := rsp.err(client, statusCode); err != nil {
return nil, "", err
}
if rsp.Type != "async" {
return nil, "", fmt.Errorf("expected async response for %q on %q, got %q", method, path, rsp.Type)
}
- if rsp.StatusCode != 202 {
+ if statusCode != 202 {
return nil, "", fmt.Errorf("operation not accepted")
}
if rsp.Change == "" {
@@ -392,11 +392,9 @@
// A response produced by the REST API will usually fit in this
// (exceptions are the icons/ endpoints obvs)
type response struct {
- Result json.RawMessage `json:"result"`
- Status string `json:"status"`
- StatusCode int `json:"status-code"`
- Type string `json:"type"`
- Change string `json:"change"`
+ Result json.RawMessage `json:"result"`
+ Type string `json:"type"`
+ Change string `json:"change"`
WarningCount int `json:"warning-count"`
WarningTimestamp time.Time `json:"warning-timestamp"`
@@ -458,6 +456,8 @@
ErrorKindSystemRestart = "system-restart"
ErrorKindDaemonRestart = "daemon-restart"
+
+ ErrorKindAssertionNotFound = "assertion-not-found"
)
// IsRetryable returns true if the given error is an error
@@ -491,6 +491,17 @@
return e.Kind == ErrorKindInterfacesUnchanged
}
+// IsAssertionNotFoundError returns whether the given error means that the
+// assertion wasn't found and thus the device isn't ready/seeded.
+func IsAssertionNotFoundError(err error) bool {
+ e, ok := err.(*Error)
+ if !ok || e == nil {
+ return false
+ }
+
+ return e.Kind == ErrorKindAssertionNotFound
+}
+
// OSRelease contains information about the system extracted from /etc/os-release.
type OSRelease struct {
ID string `json:"id"`
@@ -524,7 +535,7 @@
SandboxFeatures map[string][]string `json:"sandbox-features,omitempty"`
}
-func (rsp *response) err(cli *Client) error {
+func (rsp *response) err(cli *Client, statusCode int) error {
if cli != nil {
maintErr := rsp.Maintenance
// avoid setting to (*client.Error)(nil)
@@ -540,9 +551,9 @@
var resultErr Error
err := json.Unmarshal(rsp.Result, &resultErr)
if err != nil || resultErr.Message == "" {
- return fmt.Errorf("server error: %q", rsp.Status)
+ return fmt.Errorf("server error: %q", http.StatusText(statusCode))
}
- resultErr.StatusCode = rsp.StatusCode
+ resultErr.StatusCode = statusCode
return &resultErr
}
@@ -558,7 +569,7 @@
return fmt.Errorf("cannot unmarshal error: %v", err)
}
- err := rsp.err(nil)
+ err := rsp.err(nil, r.StatusCode)
if err == nil {
return fmt.Errorf("server error: %q", r.Status)
}
diff -Nru snapd-2.40/client/client_test.go snapd-2.42.1/client/client_test.go
--- snapd-2.40/client/client_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/client_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -105,7 +105,7 @@
func (cs *clientSuite) TestClientDoReportsErrors(c *C) {
cs.err = errors.New("ouchie")
- err := cs.cli.Do("GET", "/", nil, nil, nil)
+ _, err := cs.cli.Do("GET", "/", nil, nil, nil)
c.Check(err, ErrorMatches, "cannot communicate with server: ouchie")
if cs.doCalls < 2 {
c.Fatalf("do did not retry")
@@ -116,8 +116,25 @@
var v []int
cs.rsp = `[1,2]`
reqBody := ioutil.NopCloser(strings.NewReader(""))
- err := cs.cli.Do("GET", "/this", nil, reqBody, &v)
+ statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v)
c.Check(err, IsNil)
+ c.Check(statusCode, Equals, 200)
+ c.Check(v, DeepEquals, []int{1, 2})
+ c.Assert(cs.req, NotNil)
+ c.Assert(cs.req.URL, NotNil)
+ c.Check(cs.req.Method, Equals, "GET")
+ c.Check(cs.req.Body, Equals, reqBody)
+ c.Check(cs.req.URL.Path, Equals, "/this")
+}
+
+func (cs *clientSuite) TestClientUnderstandsStatusCode(c *C) {
+ var v []int
+ cs.status = 202
+ cs.rsp = `[1,2]`
+ reqBody := ioutil.NopCloser(strings.NewReader(""))
+ statusCode, err := cs.cli.Do("GET", "/this", nil, reqBody, &v)
+ c.Check(err, IsNil)
+ c.Check(statusCode, Equals, 202)
c.Check(v, DeepEquals, []int{1, 2})
c.Assert(cs.req, NotNil)
c.Assert(cs.req.URL, NotNil)
@@ -131,7 +148,7 @@
defer os.Unsetenv(client.TestAuthFileEnvKey)
var v string
- _ = cs.cli.Do("GET", "/this", nil, nil, &v)
+ _, _ = cs.cli.Do("GET", "/this", nil, nil, &v)
c.Assert(cs.req, NotNil)
authorization := cs.req.Header.Get("Authorization")
c.Check(authorization, Equals, "")
@@ -149,7 +166,7 @@
c.Assert(err, IsNil)
var v string
- _ = cs.cli.Do("GET", "/this", nil, nil, &v)
+ _, _ = cs.cli.Do("GET", "/this", nil, nil, &v)
authorization := cs.req.Header.Get("Authorization")
c.Check(authorization, Equals, `Macaroon root="macaroon", discharge="discharge"`)
}
@@ -168,7 +185,7 @@
var v string
cli := client.New(&client.Config{DisableAuth: true})
cli.SetDoer(cs)
- _ = cli.Do("GET", "/this", nil, nil, &v)
+ _, _ = cli.Do("GET", "/this", nil, nil, &v)
authorization := cs.req.Header.Get("Authorization")
c.Check(authorization, Equals, "")
}
@@ -177,13 +194,13 @@
var v string
cli := client.New(&client.Config{Interactive: false})
cli.SetDoer(cs)
- _ = cli.Do("GET", "/this", nil, nil, &v)
+ _, _ = cli.Do("GET", "/this", nil, nil, &v)
interactive := cs.req.Header.Get(client.AllowInteractionHeader)
c.Check(interactive, Equals, "")
cli = client.New(&client.Config{Interactive: true})
cli.SetDoer(cs)
- _ = cli.Do("GET", "/this", nil, nil, &v)
+ _, _ = cli.Do("GET", "/this", nil, nil, &v)
interactive = cs.req.Header.Get(client.AllowInteractionHeader)
c.Check(interactive, Equals, "true")
}
@@ -318,12 +335,14 @@
}
func (cs *clientSuite) TestClientReportsOpError(c *C) {
- cs.rsp = `{"type": "error", "status": "potatoes"}`
+ cs.status = 500
+ cs.rsp = `{"type": "error"}`
_, err := cs.cli.SysInfo()
- c.Check(err, ErrorMatches, `.*server error: "potatoes"`)
+ c.Check(err, ErrorMatches, `.*server error: "Internal Server Error"`)
}
func (cs *clientSuite) TestClientReportsOpErrorStr(c *C) {
+ cs.status = 400
cs.rsp = `{
"result": {},
"status": "Bad Request",
@@ -368,6 +387,7 @@
}
func (cs *clientSuite) TestClientAsyncOpMaintenance(c *C) {
+ cs.status = 202
cs.rsp = `{"type":"async", "status-code": 202, "change": "42", "maintenance": {"kind": "system-restart", "message": "system is restarting"}}`
_, err := cs.cli.Install("foo", nil)
c.Assert(err, IsNil)
@@ -463,7 +483,7 @@
cli.SetDoer(cs)
var v string
- _ = cli.Do("GET", "/", nil, nil, &v)
+ _, _ = cli.Do("GET", "/", nil, nil, &v)
c.Assert(cs.req, NotNil)
c.Check(cs.req.Header.Get("User-Agent"), Equals, "some-agent/9.87")
}
diff -Nru snapd-2.40/client/conf_test.go snapd-2.42.1/client/conf_test.go
--- snapd-2.40/client/conf_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/conf_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -46,6 +46,7 @@
}
func (cs *clientSuite) TestClientSetConf(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
diff -Nru snapd-2.40/client/export_test.go snapd-2.42.1/client/export_test.go
--- snapd-2.40/client/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -31,7 +31,7 @@
}
// Do does do.
-func (client *Client) Do(method, path string, query url.Values, body io.Reader, v interface{}) error {
+func (client *Client) Do(method, path string, query url.Values, body io.Reader, v interface{}) (statusCode int, err error) {
return client.do(method, path, query, nil, body, v)
}
diff -Nru snapd-2.40/client/interfaces_test.go snapd-2.42.1/client/interfaces_test.go
--- snapd-2.40/client/interfaces_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/interfaces_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -163,6 +163,7 @@
}
func (cs *clientSuite) TestClientConnect(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
@@ -200,6 +201,7 @@
}
func (cs *clientSuite) TestClientDisconnect(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
diff -Nru snapd-2.40/client/model.go snapd-2.42.1/client/model.go
--- snapd-2.40/client/model.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/model.go 2019-10-30 12:17:43.000000000 +0000
@@ -23,6 +23,9 @@
"bytes"
"encoding/json"
"fmt"
+ "net/url"
+
+ "github.com/snapcore/snapd/asserts"
)
type remodelData struct {
@@ -43,3 +46,54 @@
return client.doAsync("POST", "/v2/model", nil, headers, bytes.NewReader(data))
}
+
+// CurrentModelAssertion returns the current model assertion
+func (client *Client) CurrentModelAssertion() (*asserts.Model, error) {
+ assert, err := currentAssertion(client, "/v2/model")
+ if err != nil {
+ return nil, err
+ }
+ modelAssert, ok := assert.(*asserts.Model)
+ if !ok {
+ return nil, fmt.Errorf("unexpected assertion type (%s) returned", assert.Type().Name)
+ }
+ return modelAssert, nil
+}
+
+// CurrentSerialAssertion returns the current serial assertion
+func (client *Client) CurrentSerialAssertion() (*asserts.Serial, error) {
+ assert, err := currentAssertion(client, "/v2/model/serial")
+ if err != nil {
+ return nil, err
+ }
+ serialAssert, ok := assert.(*asserts.Serial)
+ if !ok {
+ return nil, fmt.Errorf("unexpected assertion type (%s) returned", assert.Type().Name)
+ }
+ return serialAssert, nil
+}
+
+// helper function for getting assertions from the daemon via a REST path
+func currentAssertion(client *Client, path string) (asserts.Assertion, error) {
+ q := url.Values{}
+
+ response, err := client.raw("GET", path, q, nil, nil)
+ if err != nil {
+ return nil, fmt.Errorf("failed to query current assertion: %v", err)
+ }
+ defer response.Body.Close()
+ if response.StatusCode != 200 {
+ return nil, parseError(response)
+ }
+
+ dec := asserts.NewDecoder(response.Body)
+
+ // only decode a single assertion - we can't ever get more than a single
+ // assertion through these endpoints by design
+ assert, err := dec.Decode()
+ if err != nil {
+ return nil, fmt.Errorf("failed to decode assertions: %v", err)
+ }
+
+ return assert, nil
+}
diff -Nru snapd-2.40/client/model_test.go snapd-2.42.1/client/model_test.go
--- snapd-2.40/client/model_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/model_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -22,10 +22,82 @@
import (
"encoding/json"
"io/ioutil"
+ "net/http"
+
+ "github.com/snapcore/snapd/asserts"
. "gopkg.in/check.v1"
)
+const happyModelAssertionResponse = `type: model
+authority-id: mememe
+series: 16
+brand-id: mememe
+model: test-model
+architecture: amd64
+base: core18
+gadget: pc=18
+kernel: pc-kernel=18
+required-snaps:
+ - core
+ - hello-world
+timestamp: 2017-07-27T00:00:00.0Z
+sign-key-sha3-384: 8B3Wmemeu3H6i4dEV4Q85Q4gIUCHIBCNMHq49e085QeLGHi7v27l3Cqmemer4__t
+
+AcLBcwQAAQoAHRYhBMbX+t6MbKGH5C3nnLZW7+q0g6ELBQJdTdwTAAoJELZW7+q0g6ELEvgQAI3j
+jXTqR6kKOqvw94pArwdMDUaZ++tebASAZgso8ejrW2DQGWSc0Q7SQICIR8bvHxqS1GtupQswOzwS
+U8hjDTv7WEchH1jylyTj/1W1GernmitTKycecRlEkSOE+EpuqBFgTtj6PdA1Fj3CiCRi1rLMhgF2
+luCOitBLaP+E8P3fuATsLqqDLYzt1VY4Y14MU75hMn+CxAQdnOZTI+NzGMasPsldmOYCPNaN/b3N
+6/fDLU47RtNlMJ3K0Tz8kj0bqRbegKlD0RdNbAgo9iZwNmrr5E9WCu9f/0rUor/NIxO77H2ExIll
+zhmsZ7E6qlxvAgBmzKgAXrn68gGrBkIb0eXKiCaKy/i2ApvjVZ9HkOzA6Ldd+SwNJv/iA8rdiMsq
+p2BfKV5f3ju5b6+WktHxAakJ8iqQmj9Yh7piHjsOAUf1PEJd2s2nqQ+pEEn1F0B23gVCY/Fa9YRQ
+iKtWVeL3rBw4dSAaK9rpTMqlNcr+yrdXfTK5YzkCC6RU4yzc5MW0hKeseeSiEDSaRYxvftjFfVNa
+ZaVXKg8Lu+cHtCJDeYXEkPIDQzXswdBO1M8Mb9D0mYxQwHxwvsWv1DByB+Otq08EYgPh4kyHo7ag
+85yK2e/NQ/fxSwQJMhBF74jM1z9arq6RMiE/KOleFAOraKn2hcROKnEeinABW+sOn6vNuMVv
+`
+
+// note: this serial assertion was generated by adding print statements to the
+// test in api_model_test.go that generate a fake serial assertion
+const happySerialAssertionResponse = `type: serial
+authority-id: my-brand
+brand-id: my-brand
+model: my-old-model
+serial: serialserial
+device-key:
+ AcZrBFaFwYABAvCgEOrrLA6FKcreHxCcOoTgBUZ+IRG7Nb8tzmEAklaQPGpv7skapUjwD1luE2go
+ mTcoTssVHrfLpBoSDV1aBs44rg3NK40ZKPJP7d2zkds1GxUo1Ea5vfet3SJ4h3aRABEBAAE=
+device-key-sha3-384: iqLo9doLzK8De9925UrdUyuvPbBad72OTWVE9YJXqd6nz9dKvwJ_lHP5bVxrl3VO
+timestamp: 2019-08-26T16:34:21-05:00
+sign-key-sha3-384: anCEGC2NYq7DzDEi6y7OafQCVeVLS90XlLt9PNjrRl9sim5rmRHDDNFNO7ODcWQW
+
+AcJwBAABCgAGBQJdZFBdAADCLALwR6Sy24wm9PffwbvUhOEXneyY3BnxKC0+NgdHu1gU8go9vEP1
+i+Flh5uoS70+MBIO+nmF8T+9JWIx2QWFDDxvcuFosnIhvUajCEQohauys5FMz/H/WvB0vrbTBpvK
+eg==`
+
+const noModelAssertionYetResponse = `
+{
+ "type": "error",
+ "status-code": 404,
+ "status": "Not Found",
+ "result": {
+ "message": "no model assertion yet",
+ "kind": "assertion-not-found",
+ "value": "model"
+ }
+}`
+
+const noSerialAssertionYetResponse = `
+{
+ "type": "error",
+ "status-code": 404,
+ "status": "Not Found",
+ "result": {
+ "message": "no serial assertion yet",
+ "kind": "assertion-not-found",
+ "value": "serial"
+ }
+}`
+
func (cs *clientSuite) TestClientRemodelEndpoint(c *C) {
cs.cli.Remodel([]byte(`{"new-model": "some-model"}`))
c.Check(cs.req.Method, Equals, "POST")
@@ -33,6 +105,7 @@
}
func (cs *clientSuite) TestClientRemodel(c *C) {
+ cs.status = 202
cs.rsp = `{
"type": "async",
"status-code": 202,
@@ -53,3 +126,41 @@
c.Check(jsonBody, HasLen, 1)
c.Check(jsonBody["new-model"], Equals, string(remodelJsonData))
}
+
+func (cs *clientSuite) TestClientGetModelHappy(c *C) {
+ cs.status = 200
+ cs.rsp = happyModelAssertionResponse
+ modelAssertion, err := cs.cli.CurrentModelAssertion()
+ c.Assert(err, IsNil)
+ expectedAssert, err := asserts.Decode([]byte(happyModelAssertionResponse))
+ c.Assert(err, IsNil)
+ c.Assert(modelAssertion, DeepEquals, expectedAssert)
+}
+
+func (cs *clientSuite) TestClientGetModelNoModel(c *C) {
+ cs.status = 404
+ cs.rsp = noModelAssertionYetResponse
+ cs.header = http.Header{}
+ cs.header.Add("Content-Type", "application/json")
+ _, err := cs.cli.CurrentModelAssertion()
+ c.Assert(err, ErrorMatches, "no model assertion yet")
+}
+
+func (cs *clientSuite) TestClientGetModelNoSerial(c *C) {
+ cs.status = 404
+ cs.rsp = noSerialAssertionYetResponse
+ cs.header = http.Header{}
+ cs.header.Add("Content-Type", "application/json")
+ _, err := cs.cli.CurrentSerialAssertion()
+ c.Assert(err, ErrorMatches, "no serial assertion yet")
+}
+
+func (cs *clientSuite) TestClientGetSerialHappy(c *C) {
+ cs.status = 200
+ cs.rsp = happySerialAssertionResponse
+ serialAssertion, err := cs.cli.CurrentSerialAssertion()
+ c.Assert(err, IsNil)
+ expectedAssert, err := asserts.Decode([]byte(happySerialAssertionResponse))
+ c.Assert(err, IsNil)
+ c.Assert(serialAssertion, DeepEquals, expectedAssert)
+}
diff -Nru snapd-2.40/client/packages.go snapd-2.42.1/client/packages.go
--- snapd-2.40/client/packages.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/packages.go 2019-10-30 12:17:43.000000000 +0000
@@ -74,6 +74,16 @@
// The ordered list of tracks that contains channels
Tracks []string `json:"tracks,omitempty"`
+
+ Health *SnapHealth `json:"health,omitempty"`
+}
+
+type SnapHealth struct {
+ Revision snap.Revision `json:"revision"`
+ Timestamp time.Time `json:"timestamp"`
+ Status string `json:"status"`
+ Message string `json:"message,omitempty"`
+ Code string `json:"code,omitempty"`
}
func (s *Snap) MarshalJSON() ([]byte, error) {
diff -Nru snapd-2.40/client/packages_test.go snapd-2.42.1/client/packages_test.go
--- snapd-2.40/client/packages_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/packages_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -115,6 +115,10 @@
}
func (cs *clientSuite) TestClientSnaps(c *check.C) {
+ healthTimestamp, err := time.Parse(time.RFC3339Nano, "2019-05-13T16:27:01.475851677+01:00")
+ c.Assert(err, check.IsNil)
+
+ // TODO: update this JSON as it's ancient
cs.rsp = `{
"type": "sync",
"result": [{
@@ -123,6 +127,11 @@
"summary": "salutation snap",
"description": "hello-world",
"download-size": 22212,
+ "health": {
+ "revision": "29",
+ "timestamp": "2019-05-13T16:27:01.475851677+01:00",
+ "status": "okay"
+ },
"icon": "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
"installed-size": -1,
"license": "GPL-3.0",
@@ -147,12 +156,17 @@
applications, err := cs.cli.List(nil, nil)
c.Check(err, check.IsNil)
c.Check(applications, check.DeepEquals, []*client.Snap{{
- ID: "funky-snap-id",
- Title: "Title",
- Summary: "salutation snap",
- Description: "hello-world",
- DownloadSize: 22212,
- Icon: "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
+ ID: "funky-snap-id",
+ Title: "Title",
+ Summary: "salutation snap",
+ Description: "hello-world",
+ DownloadSize: 22212,
+ Icon: "https://myapps.developer.ubuntu.com/site_media/appmedia/2015/03/hello.svg_NZLfWbh.png",
+ Health: &client.SnapHealth{
+ Revision: snap.R(29),
+ Timestamp: healthTimestamp,
+ Status: "okay",
+ },
InstalledSize: -1,
License: "GPL-3.0",
Name: "hello-world",
diff -Nru snapd-2.40/client/snap_op_test.go snapd-2.42.1/client/snap_op_test.go
--- snapd-2.40/client/snap_op_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/snap_op_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -77,21 +77,23 @@
}
func (cs *clientSuite) TestClientOpSnapResponseError(c *check.C) {
- cs.rsp = `{"type": "error", "status": "potatoes"}`
+ cs.status = 400
+ cs.rsp = `{"type": "error"}`
for _, s := range ops {
_, err := s.op(cs.cli, pkgName, nil)
- c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`, check.Commentf(s.action))
+ c.Check(err, check.ErrorMatches, `.*server error: "Bad Request"`, check.Commentf(s.action))
}
}
func (cs *clientSuite) TestClientMultiOpSnapResponseError(c *check.C) {
- cs.rsp = `{"type": "error", "status": "potatoes"}`
+ cs.status = 500
+ cs.rsp = `{"type": "error"}`
for _, s := range multiOps {
_, err := s.op(cs.cli, nil, nil)
- c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`, check.Commentf(s.action))
+ c.Check(err, check.ErrorMatches, `.*server error: "Internal Server Error"`, check.Commentf(s.action))
}
_, _, err := cs.cli.SnapshotMany(nil, nil)
- c.Check(err, check.ErrorMatches, `.*server error: "potatoes"`)
+ c.Check(err, check.ErrorMatches, `.*server error: "Internal Server Error"`)
}
func (cs *clientSuite) TestClientOpSnapBadType(c *check.C) {
@@ -114,6 +116,7 @@
}
func (cs *clientSuite) TestClientOpSnapNoChange(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"status-code": 202,
"type": "async"
@@ -125,6 +128,7 @@
}
func (cs *clientSuite) TestClientOpSnap(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"change": "d728",
"status-code": 202,
@@ -151,6 +155,7 @@
}
func (cs *clientSuite) TestClientMultiOpSnap(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"change": "d728",
"status-code": 202,
@@ -179,6 +184,7 @@
func (cs *clientSuite) TestClientMultiSnapshot(c *check.C) {
// Note body is essentially the same as TestClientMultiOpSnap; keep in sync
+ cs.status = 202
cs.rsp = `{
"result": {"set-id": 42},
"change": "d728",
@@ -203,6 +209,7 @@
}
func (cs *clientSuite) TestClientOpInstallPath(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"change": "66b3",
"status-code": 202,
@@ -230,6 +237,7 @@
}
func (cs *clientSuite) TestClientOpInstallPathInstance(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"change": "66b3",
"status-code": 202,
@@ -258,6 +266,7 @@
}
func (cs *clientSuite) TestClientOpInstallDangerous(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"change": "66b3",
"status-code": 202,
@@ -294,6 +303,7 @@
}
func (cs *clientSuite) TestClientOpInstallUnaliased(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"change": "66b3",
"status-code": 202,
@@ -344,6 +354,7 @@
}
func (cs *clientSuite) TestClientOpTryMode(c *check.C) {
+ cs.status = 202
cs.rsp = `{
"change": "66b3",
"status-code": 202,
diff -Nru snapd-2.40/client/snapshot_test.go snapd-2.42.1/client/snapshot_test.go
--- snapd-2.40/client/snapshot_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/client/snapshot_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -94,6 +94,7 @@
}
func (cs *clientSuite) testClientSnapshotActionFull(c *check.C, action string, users []string, f func() (string, error)) {
+ cs.status = 202
cs.rsp = `{
"status-code": 202,
"type": "async",
diff -Nru snapd-2.40/cmd/cmd_linux.go snapd-2.42.1/cmd/cmd_linux.go
--- snapd-2.40/cmd/cmd_linux.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/cmd_linux.go 2019-10-30 12:17:43.000000000 +0000
@@ -73,12 +73,12 @@
return true
}
-// coreSupportsReExec returns true if the given core snap should be used as re-exec target.
+// coreSupportsReExec returns true if the given core/snapd snap should be used as re-exec target.
//
// Ensure we do not use older version of snapd, look for info file and ignore
// version of core that do not yet have it.
-func coreSupportsReExec(corePath string) bool {
- fullInfo := filepath.Join(corePath, filepath.Join(dirs.CoreLibExecDir, "info"))
+func coreSupportsReExec(coreOrSnapdPath string) bool {
+ fullInfo := filepath.Join(coreOrSnapdPath, filepath.Join(dirs.CoreLibExecDir, "info"))
content, err := ioutil.ReadFile(fullInfo)
if err != nil {
if !os.IsNotExist(err) {
@@ -108,7 +108,7 @@
return false
}
if res > 0 {
- logger.Debugf("core snap (at %q) is older (%q) than distribution package (%q)", corePath, ver, Version)
+ logger.Debugf("snap (at %q) is older (%q) than distribution package (%q)", coreOrSnapdPath, ver, Version)
return false
}
return true
@@ -206,10 +206,10 @@
}
// Is this executable in the core snap too?
- corePath := snapdSnap
+ coreOrSnapdPath := snapdSnap
full := filepath.Join(snapdSnap, exe)
if !osutil.FileExists(full) {
- corePath = coreSnap
+ coreOrSnapdPath = coreSnap
full = filepath.Join(coreSnap, exe)
if !osutil.FileExists(full) {
return
@@ -217,7 +217,7 @@
}
// If the core snap doesn't support re-exec or run-from-core then don't do it.
- if !coreSupportsReExec(corePath) {
+ if !coreSupportsReExec(coreOrSnapdPath) {
return
}
diff -Nru snapd-2.40/cmd/libsnap-confine-private/cgroup-support.c snapd-2.42.1/cmd/libsnap-confine-private/cgroup-support.c
--- snapd-2.40/cmd/libsnap-confine-private/cgroup-support.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/cgroup-support.c 2019-10-30 12:17:43.000000000 +0000
@@ -25,6 +25,7 @@
#include
#include
#include
+#include
#include
#include "cleanup-funcs.h"
@@ -66,3 +67,33 @@
}
debug("moved process %ld to cgroup hierarchy %s/%s", (long)pid, parent, name);
}
+
+static const char *cgroup_dir = "/sys/fs/cgroup";
+
+// from statfs(2)
+#ifndef CGRUOP2_SUPER_MAGIC
+#define CGROUP2_SUPER_MAGIC 0x63677270
+#endif
+
+// Detect if we are running in cgroup v2 unified mode (as opposed to
+// hybrid or legacy) The algorithm is described in
+// https://systemd.io/CGROUP_DELEGATION.html
+bool sc_cgroup_is_v2() {
+ static bool did_warn = false;
+ struct statfs buf;
+
+ if (statfs(cgroup_dir, &buf) != 0) {
+ if (errno == ENOENT) {
+ return false;
+ }
+ die("cannot statfs %s", cgroup_dir);
+ }
+ if (buf.f_type == CGROUP2_SUPER_MAGIC) {
+ if (!did_warn) {
+ fprintf(stderr, "WARNING: cgroup v2 is not fully supported yet, proceeding with partial confinement\n");
+ did_warn = true;
+ }
+ return true;
+ }
+ return false;
+}
diff -Nru snapd-2.40/cmd/libsnap-confine-private/cgroup-support.h snapd-2.42.1/cmd/libsnap-confine-private/cgroup-support.h
--- snapd-2.40/cmd/libsnap-confine-private/cgroup-support.h 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/cgroup-support.h 2019-10-30 12:17:43.000000000 +0000
@@ -19,6 +19,7 @@
#define SC_CGROUP_SUPPORT_H
#include
+#include
/**
* sc_cgroup_create_and_join joins, perhaps creating, a cgroup hierarchy.
@@ -30,4 +31,10 @@
**/
void sc_cgroup_create_and_join(const char *parent, const char *name, pid_t pid);
+/**
+ * sc_cgroup_is_v2() returns true if running on cgroups v2
+ *
+ **/
+bool sc_cgroup_is_v2(void);
+
#endif
diff -Nru snapd-2.40/cmd/libsnap-confine-private/error.c snapd-2.42.1/cmd/libsnap-confine-private/error.c
--- snapd-2.40/cmd/libsnap-confine-private/error.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/error.c 2019-10-30 12:17:43.000000000 +0000
@@ -59,6 +59,26 @@
return err;
}
+sc_error *sc_error_init_simple(const char *msgfmt, ...)
+{
+ va_list ap;
+ va_start(ap, msgfmt);
+ sc_error *err = sc_error_initv(SC_LIBSNAP_DOMAIN,
+ SC_UNSPECIFIED_ERROR, msgfmt, ap);
+ va_end(ap);
+ return err;
+}
+
+sc_error *sc_error_init_api_misuse(const char *msgfmt, ...)
+{
+ va_list ap;
+ va_start(ap, msgfmt);
+ sc_error *err = sc_error_initv(SC_LIBSNAP_DOMAIN,
+ SC_API_MISUSE, msgfmt, ap);
+ va_end(ap);
+ return err;
+}
+
const char *sc_error_domain(sc_error * err)
{
if (err == NULL) {
@@ -102,13 +122,12 @@
{
if (error != NULL) {
if (strcmp(sc_error_domain(error), SC_ERRNO_DOMAIN) == 0) {
- // Set errno just before the call to die() as it is used internally
- errno = sc_error_code(error);
- die("%s", sc_error_msg(error));
+ fprintf(stderr, "%s: %s\n", sc_error_msg(error), strerror(sc_error_code(error)));
} else {
- errno = 0;
- die("%s", sc_error_msg(error));
+ fprintf(stderr, "%s\n", sc_error_msg(error));
}
+ sc_error_free(error);
+ exit(1);
}
}
diff -Nru snapd-2.40/cmd/libsnap-confine-private/error.h snapd-2.42.1/cmd/libsnap-confine-private/error.h
--- snapd-2.40/cmd/libsnap-confine-private/error.h 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/error.h 2019-10-30 12:17:43.000000000 +0000
@@ -63,7 +63,7 @@
/**
* Error domain for errors in the libsnap-confine-private library.
**/
-#define SC_LIBSNAP_ERROR "libsnap-confine-private"
+#define SC_LIBSNAP_DOMAIN "libsnap-confine-private"
/** sc_libsnap_error represents distinct error codes used by libsnap-confine-private library. */
typedef enum sc_libsnap_error {
@@ -90,6 +90,26 @@
sc_error *sc_error_init(const char *domain, int code, const char *msgfmt, ...);
/**
+ * Initialize an unspecified error with formatted message.
+ *
+ * This is just syntactic sugar for sc_error_init(SC_LIBSNAP_ERROR,
+ * SC_UNSPECIFIED_ERROR, msgfmt, ...) which is repeated often.
+ **/
+__attribute__((warn_unused_result,
+ format(printf, 1, 2) SC_APPEND_RETURNS_NONNULL))
+sc_error *sc_error_init_simple(const char *msgfmt, ...);
+
+/**
+ * Initialize an API misuse error with formatted message.
+ *
+ * This is just syntactic sugar for sc_error_init(SC_LIBSNAP_DOMAIN,
+ * SC_API_MISUSE, msgfmt, ...) which is repeated often.
+ **/
+__attribute__((warn_unused_result,
+ format(printf, 1, 2) SC_APPEND_RETURNS_NONNULL))
+sc_error *sc_error_init_api_misuse(const char *msgfmt, ...);
+
+/**
* Initialize an errno-based error.
*
* The error carries a copy of errno and a custom error message as designed by
diff -Nru snapd-2.40/cmd/libsnap-confine-private/error-test.c snapd-2.42.1/cmd/libsnap-confine-private/error-test.c
--- snapd-2.40/cmd/libsnap-confine-private/error-test.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/error-test.c 2019-10-30 12:17:43.000000000 +0000
@@ -49,6 +49,34 @@
g_assert_cmpstr(sc_error_msg(err), ==, "printer is on fire");
}
+static void test_sc_error_init_simple(void)
+{
+ struct sc_error *err;
+ // Create an error
+ err = sc_error_init_simple("hello %s", "errors");
+ g_assert_nonnull(err);
+ g_test_queue_destroy((GDestroyNotify) sc_error_free, err);
+
+ // Inspect the exposed attributes
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
+ g_assert_cmpint(sc_error_code(err), ==, 0);
+ g_assert_cmpstr(sc_error_msg(err), ==, "hello errors");
+}
+
+static void test_sc_error_init_api_misuse(void)
+{
+ struct sc_error *err;
+ // Create an error
+ err = sc_error_init_api_misuse("foo cannot be %d", 42);
+ g_assert_nonnull(err);
+ g_test_queue_destroy((GDestroyNotify) sc_error_free, err);
+
+ // Inspect the exposed attributes
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
+ g_assert_cmpint(sc_error_code(err), ==, SC_API_MISUSE);
+ g_assert_cmpstr(sc_error_msg(err), ==, "foo cannot be 42");
+}
+
static void test_sc_error_cleanup(void)
{
// Check that sc_error_cleanup() is safe to use.
@@ -236,6 +264,10 @@
g_test_add_func("/error/sc_error_init", test_sc_error_init);
g_test_add_func("/error/sc_error_init_from_errno",
test_sc_error_init_from_errno);
+ g_test_add_func("/error/sc_error_init_simple",
+ test_sc_error_init_simple);
+ g_test_add_func("/error/sc_error_init_api_misue",
+ test_sc_error_init_api_misuse);
g_test_add_func("/error/sc_error_cleanup", test_sc_error_cleanup);
g_test_add_func("/error/sc_error_domain/NULL",
test_sc_error_domain__NULL);
diff -Nru snapd-2.40/cmd/libsnap-confine-private/feature.c snapd-2.42.1/cmd/libsnap-confine-private/feature.c
--- snapd-2.40/cmd/libsnap-confine-private/feature.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/feature.c 2019-10-30 12:17:43.000000000 +0000
@@ -40,6 +40,9 @@
case SC_FEATURE_REFRESH_APP_AWARENESS:
file_name = "refresh-app-awareness";
break;
+ case SC_FEATURE_PARALLEL_INSTANCES:
+ file_name = "parallel-instances";
+ break;
default:
die("unknown feature flag code %d", flag);
}
diff -Nru snapd-2.40/cmd/libsnap-confine-private/feature.h snapd-2.42.1/cmd/libsnap-confine-private/feature.h
--- snapd-2.40/cmd/libsnap-confine-private/feature.h 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/feature.h 2019-10-30 12:17:43.000000000 +0000
@@ -21,8 +21,9 @@
#include
typedef enum sc_feature_flag {
- SC_FEATURE_PER_USER_MOUNT_NAMESPACE,
- SC_FEATURE_REFRESH_APP_AWARENESS,
+ SC_FEATURE_PER_USER_MOUNT_NAMESPACE = 1 << 0,
+ SC_FEATURE_REFRESH_APP_AWARENESS = 1 << 1,
+ SC_FEATURE_PARALLEL_INSTANCES = 1 << 2,
} sc_feature_flag;
/**
diff -Nru snapd-2.40/cmd/libsnap-confine-private/feature-test.c snapd-2.42.1/cmd/libsnap-confine-private/feature-test.c
--- snapd-2.40/cmd/libsnap-confine-private/feature-test.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/feature-test.c 2019-10-30 12:17:43.000000000 +0000
@@ -75,6 +75,20 @@
g_assert(sc_feature_enabled(SC_FEATURE_PER_USER_MOUNT_NAMESPACE));
}
+static void test_feature_parallel_instances(void)
+{
+ const char *d = sc_testdir();
+ sc_mock_feature_flag_dir(d);
+
+ g_assert(!sc_feature_enabled(SC_FEATURE_PARALLEL_INSTANCES));
+
+ char pname[PATH_MAX];
+ sc_must_snprintf(pname, sizeof pname, "%s/parallel-instances", d);
+ g_file_set_contents(pname, "", -1, NULL);
+
+ g_assert(sc_feature_enabled(SC_FEATURE_PARALLEL_INSTANCES));
+}
+
static void __attribute__((constructor)) init(void)
{
g_test_add_func("/feature/missing_dir",
@@ -83,4 +97,6 @@
test_feature_enabled__missing_file);
g_test_add_func("/feature/present_file",
test_feature_enabled__present_file);
+ g_test_add_func("/feature/parallel_instances",
+ test_feature_parallel_instances);
}
diff -Nru snapd-2.40/cmd/libsnap-confine-private/infofile.c snapd-2.42.1/cmd/libsnap-confine-private/infofile.c
--- snapd-2.40/cmd/libsnap-confine-private/infofile.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/infofile.c 2019-10-30 12:17:43.000000000 +0000
@@ -33,15 +33,15 @@
char *line_buf SC_CLEANUP(sc_cleanup_string) = NULL;
if (stream == NULL) {
- err = sc_error_init(SC_LIBSNAP_ERROR, SC_API_MISUSE, "stream cannot be NULL");
+ err = sc_error_init_api_misuse("stream cannot be NULL");
goto out;
}
if (key == NULL) {
- err = sc_error_init(SC_LIBSNAP_ERROR, SC_API_MISUSE, "key cannot be NULL");
+ err = sc_error_init_api_misuse("key cannot be NULL");
goto out;
}
if (value == NULL) {
- err = sc_error_init(SC_LIBSNAP_ERROR, SC_API_MISUSE, "value cannot be NULL");
+ err = sc_error_init_api_misuse("value cannot be NULL");
goto out;
}
@@ -65,13 +65,13 @@
/* Guard against malformed input that may contain NUL bytes that
* would confuse the code below. */
if (memchr(line_buf, '\0', nread) != NULL) {
- err = sc_error_init(SC_LIBSNAP_ERROR, 0, "line %d contains NUL byte", lineno);
+ err = sc_error_init_simple("line %d contains NUL byte", lineno);
goto out;
}
/* Guard against non-strictly formatted input that doesn't contain
* trailing newline. */
if (line_buf[nread - 1] != '\n') {
- err = sc_error_init(SC_LIBSNAP_ERROR, 0, "line %d does not end with a newline", lineno);
+ err = sc_error_init(SC_LIBSNAP_DOMAIN, 0, "line %d does not end with a newline", lineno);
goto out;
}
/* Replace the trailing newline character with the NUL byte. */
@@ -79,12 +79,12 @@
/* Guard against malformed input that does not contain '=' byte */
char *eq_ptr = memchr(line_buf, '=', nread);
if (eq_ptr == NULL) {
- err = sc_error_init(SC_LIBSNAP_ERROR, 0, "line %d is not a key=value assignment", lineno);
+ err = sc_error_init_simple("line %d is not a key=value assignment", lineno);
goto out;
}
/* Guard against malformed input with empty key. */
if (eq_ptr == line_buf) {
- err = sc_error_init(SC_LIBSNAP_ERROR, 0, "line %d contains empty key", lineno);
+ err = sc_error_init_simple("line %d contains empty key", lineno);
goto out;
}
/* Replace the first '=' with string terminator byte. */
diff -Nru snapd-2.40/cmd/libsnap-confine-private/infofile-test.c snapd-2.42.1/cmd/libsnap-confine-private/infofile-test.c
--- snapd-2.40/cmd/libsnap-confine-private/infofile-test.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/infofile-test.c 2019-10-30 12:17:43.000000000 +0000
@@ -39,7 +39,7 @@
rc = sc_infofile_get_key(NULL, "key", &value, &err);
g_assert_cmpint(rc, ==, -1);
g_assert_nonnull(err);
- g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
g_assert_cmpint(sc_error_code(err), ==, SC_API_MISUSE);
g_assert_cmpstr(sc_error_msg(err), ==, "stream cannot be NULL");
sc_error_free(err);
@@ -48,7 +48,7 @@
rc = sc_infofile_get_key(stream, NULL, &value, &err);
g_assert_cmpint(rc, ==, -1);
g_assert_nonnull(err);
- g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
g_assert_cmpint(sc_error_code(err), ==, SC_API_MISUSE);
g_assert_cmpstr(sc_error_msg(err), ==, "key cannot be NULL");
sc_error_free(err);
@@ -57,7 +57,7 @@
rc = sc_infofile_get_key(stream, "key", NULL, &err);
g_assert_cmpint(rc, ==, -1);
g_assert_nonnull(err);
- g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
g_assert_cmpint(sc_error_code(err), ==, SC_API_MISUSE);
g_assert_cmpstr(sc_error_msg(err), ==, "value cannot be NULL");
sc_error_free(err);
@@ -100,7 +100,7 @@
rc = sc_infofile_get_key(stream, "key", &tricky_value, &err);
g_assert_cmpint(rc, ==, -1);
g_assert_nonnull(err);
- g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
g_assert_cmpint(sc_error_code(err), ==, 0);
g_assert_cmpstr(sc_error_msg(err), ==, "line 1 is not a key=value assignment");
g_assert_null(tricky_value);
@@ -114,7 +114,7 @@
rc = sc_infofile_get_key(stream, "key", &tricky_value, &err);
g_assert_cmpint(rc, ==, -1);
g_assert_nonnull(err);
- g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
g_assert_cmpint(sc_error_code(err), ==, 0);
g_assert_cmpstr(sc_error_msg(err), ==, "line 1 contains NUL byte");
g_assert_null(tricky_value);
@@ -128,7 +128,7 @@
rc = sc_infofile_get_key(stream, "key", &tricky_value, &err);
g_assert_cmpint(rc, ==, -1);
g_assert_nonnull(err);
- g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
g_assert_cmpint(sc_error_code(err), ==, 0);
g_assert_cmpstr(sc_error_msg(err), ==, "line 1 does not end with a newline");
g_assert_null(tricky_value);
@@ -154,7 +154,7 @@
rc = sc_infofile_get_key(stream, "key", &tricky_value, &err);
g_assert_cmpint(rc, ==, -1);
g_assert_nonnull(err);
- g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_ERROR);
+ g_assert_cmpstr(sc_error_domain(err), ==, SC_LIBSNAP_DOMAIN);
g_assert_cmpint(sc_error_code(err), ==, 0);
g_assert_cmpstr(sc_error_msg(err), ==, "line 1 contains empty key");
g_assert_null(tricky_value);
diff -Nru snapd-2.40/cmd/libsnap-confine-private/panic.c snapd-2.42.1/cmd/libsnap-confine-private/panic.c
--- snapd-2.40/cmd/libsnap-confine-private/panic.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/panic.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,67 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#include "panic.h"
+
+#include
+#include
+#include
+#include
+#include
+#include
+
+static sc_panic_exit_fn panic_exit_fn = NULL;
+static sc_panic_msg_fn panic_msg_fn = NULL;
+
+void sc_panic(const char *fmt, ...) {
+ va_list ap;
+ va_start(ap, fmt);
+ sc_panicv(fmt, ap);
+ va_end(ap);
+}
+
+void sc_panicv(const char *fmt, va_list ap) {
+ int errno_copy = errno;
+
+ if (panic_msg_fn != NULL) {
+ panic_msg_fn(fmt, ap, errno_copy);
+ } else {
+ vfprintf(stderr, fmt, ap);
+ if (errno != 0) {
+ fprintf(stderr, ": %s\n", strerror(errno_copy));
+ } else {
+ fprintf(stderr, "\n");
+ }
+ }
+
+ if (panic_exit_fn != NULL) {
+ panic_exit_fn();
+ }
+ exit(1);
+}
+
+sc_panic_exit_fn sc_set_panic_exit_fn(sc_panic_exit_fn fn) {
+ sc_panic_exit_fn old = panic_exit_fn;
+ panic_exit_fn = fn;
+ return old;
+}
+
+sc_panic_msg_fn sc_set_panic_msg_fn(sc_panic_msg_fn fn) {
+ sc_panic_msg_fn old = panic_msg_fn;
+ panic_msg_fn = fn;
+ return old;
+}
diff -Nru snapd-2.40/cmd/libsnap-confine-private/panic.h snapd-2.42.1/cmd/libsnap-confine-private/panic.h
--- snapd-2.40/cmd/libsnap-confine-private/panic.h 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/panic.h 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#ifndef SC_PANIC_H
+#define SC_PANIC_H
+
+#include
+
+/**
+ * sc_panic is an exit-with-message utility function.
+ *
+ * The function takes a printf-like format string that is formatted and printed
+ * somehow. The function then terminates the process by calling exit. Both
+ * aspects can be customized.
+ *
+ * The particular nature of the exit can be customized by calling
+ * sc_set_panic_action. The panic action is a function that is called before
+ * attempting to exit.
+ *
+ * The way the error message is formatted and printed can be customized by
+ * calling sc_set_panic_format_fn(). By default the error is printed to
+ * standard error. If the error is related to a system call failure then errno
+ * can be set to a non-zero value just prior to calling sc_panic. The value
+ * will then be used when crafting the error message.
+ **/
+__attribute__((noreturn, format(printf, 1, 2))) void sc_panic(const char *fmt, ...);
+
+/**
+ * sc_panicv is a variant of sc_panic with an argument list.
+ **/
+__attribute__((noreturn)) void sc_panicv(const char *fmt, va_list ap);
+
+/**
+ * sc_panic_exit_fn is the type of the exit function used by sc_panic().
+ **/
+typedef void (*sc_panic_exit_fn)(void);
+
+/**
+ * sc_set_panic_exit_fn sets the panic exit function.
+ *
+ * When sc_panic is called it will eventually exit the running process. Just
+ * prior to that, it will call the panic exit function, if one has been set.
+ *
+ * If exiting the process is undesired, for example while running in intrd as
+ * pid 1, during the system shutdown phase, then a process can set the panic
+ * exit function. Note that if the specified function returns then panic will
+ * proceed to call exit(3) anyway.
+ *
+ * The old exit function, if any, is returned.
+ **/
+sc_panic_exit_fn sc_set_panic_exit_fn(sc_panic_exit_fn fn);
+
+/**
+ * sc_panic_msg_fn is the type of the format function used by sc_panic().
+ **/
+typedef void (*sc_panic_msg_fn)(const char *fmt, va_list ap, int errno_copy);
+
+/**
+ * sc_set_panic_msg_fn sets the panic message function.
+ *
+ * When sc_panic is called it will attempt to print an error message to
+ * standard error. The message includes information provided by the caller: the
+ * format string, the argument vector for a printf-like function as well as a
+ * copy of the system errno value, which may be zero if the error is not
+ * originated by a system call error.
+ *
+ * If custom formatting of the error message is desired, for example while
+ * running in initrd as pid 1, during the system shutdown phase, then a process
+ * can set the panic message function. Once set the function takes over the
+ * responsibility of printing an error message (in whatever form is
+ * appropriate).
+ *
+ * The old message function, if any, is returned.
+ **/
+sc_panic_msg_fn sc_set_panic_msg_fn(sc_panic_msg_fn fn);
+
+#endif
diff -Nru snapd-2.40/cmd/libsnap-confine-private/panic-test.c snapd-2.42.1/cmd/libsnap-confine-private/panic-test.c
--- snapd-2.40/cmd/libsnap-confine-private/panic-test.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/panic-test.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,88 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#include "panic.h"
+#include "panic.c"
+
+#include
+
+static void test_panic(void)
+{
+ if (g_test_subprocess()) {
+ errno = 0;
+ sc_panic("death message");
+ g_test_message("expected die not to return");
+ g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("death message\n");
+}
+
+static void test_panic_with_errno(void)
+{
+ if (g_test_subprocess()) {
+ errno = EPERM;
+ sc_panic("death message");
+ g_test_message("expected die not to return");
+ g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("death message: Operation not permitted\n");
+}
+
+static void custom_panic_msg(const char *fmt, va_list ap, int errno_copy)
+{
+ fprintf(stderr, "PANIC: ");
+ vfprintf(stderr, fmt, ap);
+ fprintf(stderr, " (errno: %d)", errno_copy);
+ fprintf(stderr, "\n");
+}
+
+static void custom_panic_exit(void)
+{
+ fprintf(stderr, "EXITING\n");
+ exit(2);
+}
+
+static void test_panic_customization(void)
+{
+ if (g_test_subprocess()) {
+ sc_set_panic_msg_fn(custom_panic_msg);
+ sc_set_panic_exit_fn(custom_panic_exit);
+ errno = 123;
+ sc_panic("death message");
+ g_test_message("expected die not to return");
+ g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("PANIC: death message (errno: 123)\n"
+ "EXITING\n");
+ // NOTE: g_test doesn't offer facilities to observe the exit code.
+}
+
+static void __attribute__((constructor)) init(void)
+{
+ g_test_add_func("/panic/panic", test_panic);
+ g_test_add_func("/panic/panic_with_errno", test_panic_with_errno);
+ g_test_add_func("/panic/panic_customization", test_panic_customization);
+}
diff -Nru snapd-2.40/cmd/libsnap-confine-private/snap.c snapd-2.42.1/cmd/libsnap-confine-private/snap.c
--- snapd-2.40/cmd/libsnap-confine-private/snap.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/snap.c 2019-10-30 12:17:43.000000000 +0000
@@ -31,7 +31,7 @@
bool verify_security_tag(const char *security_tag, const char *snap_name)
{
const char *whitelist_re =
- "^snap\\.([a-z0-9](-?[a-z0-9])*(_[a-z0-9]{1,10})?)\\.([a-zA-Z0-9](-?[a-zA-Z0-9])*|hook\\.[a-z](-?[a-z])*)$";
+ "^snap\\.([a-z0-9](-?[a-z0-9])*(_[a-z0-9]{1,10})?)\\.([a-zA-Z0-9](-?[a-zA-Z0-9])*|hook\\.[a-z](-?[a-z0-9])*)$";
regex_t re;
if (regcomp(&re, whitelist_re, REG_EXTENDED) != 0)
die("can not compile regex %s", whitelist_re);
@@ -111,9 +111,8 @@
"snap instance name cannot be NULL");
goto out;
}
- // 40 char snap_name + '_' + 10 char instance_key + 1 extra overflow + 1
- // NULL
- char s[53] = { 0 };
+ // instance name length + 1 extra overflow + 1 NULL
+ char s[SNAP_INSTANCE_LEN + 1 + 1] = { 0 };
strncpy(s, instance_name, sizeof(s) - 1);
char *t = s;
@@ -175,7 +174,7 @@
err =
sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY,
"instance key must contain at least one letter or digit");
- } else if (i > 10) {
+ } else if (i > SNAP_INSTANCE_KEY_LEN) {
err =
sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_INSTANCE_KEY,
"instance key must be shorter than 10 characters");
@@ -253,7 +252,7 @@
"snap name must be longer than 1 character");
goto out;
}
- if (n > 40) {
+ if (n > SNAP_NAME_LEN) {
err = sc_error_init(SC_SNAP_DOMAIN, SC_SNAP_INVALID_NAME,
"snap name must be shorter than 40 characters");
goto out;
diff -Nru snapd-2.40/cmd/libsnap-confine-private/snap.h snapd-2.42.1/cmd/libsnap-confine-private/snap.h
--- snapd-2.40/cmd/libsnap-confine-private/snap.h 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/snap.h 2019-10-30 12:17:43.000000000 +0000
@@ -37,6 +37,17 @@
SC_SNAP_INVALID_INSTANCE_NAME = 3,
};
+/* SNAP_NAME_LEN is the maximum length of a snap name, enforced by snapd and the
+ * store. */
+#define SNAP_NAME_LEN 40
+/* SNAP_INSTANCE_KEY_LEN is the maximum length of instance key, enforced locally
+ * by snapd. */
+#define SNAP_INSTANCE_KEY_LEN 10
+/* SNAP_INSTANCE_LEN is the maximum length of snap instance name, composed of
+ * the snap name, separator '_' and the instance key, enforced locally by
+ * snapd. */
+#define SNAP_INSTANCE_LEN (SNAP_NAME_LEN + 1 + SNAP_INSTANCE_KEY_LEN)
+
/**
* Validate the given snap name.
*
diff -Nru snapd-2.40/cmd/libsnap-confine-private/snap-test.c snapd-2.42.1/cmd/libsnap-confine-private/snap-test.c
--- snapd-2.40/cmd/libsnap-confine-private/snap-test.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/snap-test.c 2019-10-30 12:17:43.000000000 +0000
@@ -90,6 +90,10 @@
g_assert_true(verify_security_tag("snap.123test.123test", "123test"));
g_assert_true(verify_security_tag
("snap.123test.hook.configure", "123test"));
+
+ // regression test snap.eon-edg-shb-pulseaudio.hook.connect-plug-i2c
+ g_assert_true(verify_security_tag
+ ("snap.foo.hook.connect-plug-i2c", "foo"));
}
static void test_sc_is_hook_security_tag(void)
@@ -359,7 +363,6 @@
{
if (g_test_subprocess()) {
sc_snap_drop_instance_key("foo_bar", NULL, 0);
- g_test_fail();
return;
}
g_test_trap_subprocess(NULL, 0, 0);
@@ -373,7 +376,6 @@
char dest[10] = { 0 };
sc_snap_drop_instance_key("foo-foo-foo-foo-foo_bar", dest,
sizeof dest);
- g_test_fail();
return;
}
g_test_trap_subprocess(NULL, 0, 0);
@@ -385,7 +387,6 @@
if (g_test_subprocess()) {
char dest[3] = { 0 }; // "foo" sans the nil byte
sc_snap_drop_instance_key("foo", dest, sizeof dest);
- g_test_fail();
return;
}
g_test_trap_subprocess(NULL, 0, 0);
@@ -397,7 +398,20 @@
if (g_test_subprocess()) {
char dest[10] = { 0 };
sc_snap_drop_instance_key(NULL, dest, sizeof dest);
- g_test_fail();
+ return;
+ }
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+}
+
+static void test_sc_snap_drop_instance_key_short_dest_max(void)
+{
+ if (g_test_subprocess()) {
+ char dest[SNAP_NAME_LEN + 1] = { 0 };
+ /* 40 chars (max valid length), pretend dest is the same length, no space for terminator */
+ sc_snap_drop_instance_key
+ ("01234567890123456789012345678901234567890", dest,
+ sizeof dest - 1);
return;
}
g_test_trap_subprocess(NULL, 0, 0);
@@ -406,7 +420,7 @@
static void test_sc_snap_drop_instance_key_basic(void)
{
- char name[41] = { 0xff };
+ char name[SNAP_NAME_LEN + 1] = { 0xff };
sc_snap_drop_instance_key("foo_bar", name, sizeof name);
g_assert_cmpstr(name, ==, "foo");
@@ -426,6 +440,12 @@
memset(name, 0xff, sizeof name);
sc_snap_drop_instance_key("foo", name, sizeof name);
g_assert_cmpstr(name, ==, "foo");
+
+ memset(name, 0xff, sizeof name);
+ /* 40 chars - snap name length */
+ sc_snap_drop_instance_key("0123456789012345678901234567890123456789",
+ name, sizeof name);
+ g_assert_cmpstr(name, ==, "0123456789012345678901234567890123456789");
}
static void test_sc_snap_split_instance_name_trailing_nil(void)
@@ -434,7 +454,6 @@
char dest[3] = { 0 };
// pretend there is no place for trailing \0
sc_snap_split_instance_name("_", NULL, 0, dest, 0);
- g_test_fail();
return;
}
g_test_trap_subprocess(NULL, 0, 0);
@@ -447,7 +466,6 @@
char dest[10] = { 0 };
sc_snap_split_instance_name("foo_barbarbarbar", NULL, 0,
dest, sizeof dest);
- g_test_fail();
return;
}
g_test_trap_subprocess(NULL, 0, 0);
@@ -456,7 +474,7 @@
static void test_sc_snap_split_instance_name_basic(void)
{
- char name[41] = { 0xff };
+ char name[SNAP_NAME_LEN + 1] = { 0xff };
char instance[20] = { 0xff };
sc_snap_split_instance_name("foo_bar", name, sizeof name, instance,
@@ -558,6 +576,8 @@
test_sc_snap_drop_instance_key_short_dest);
g_test_add_func("/snap/sc_snap_drop_instance_key/short_dest2",
test_sc_snap_drop_instance_key_short_dest2);
+ g_test_add_func("/snap/sc_snap_drop_instance_key/short_dest_max",
+ test_sc_snap_drop_instance_key_short_dest_max);
g_test_add_func("/snap/sc_snap_split_instance_name/basic",
test_sc_snap_split_instance_name_basic);
diff -Nru snapd-2.40/cmd/libsnap-confine-private/test-utils.c snapd-2.42.1/cmd/libsnap-confine-private/test-utils.c
--- snapd-2.40/cmd/libsnap-confine-private/test-utils.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/test-utils.c 2019-10-30 12:17:43.000000000 +0000
@@ -16,6 +16,7 @@
*/
#include "test-utils.h"
+#include "string-utils.h"
#include "error.h"
#include "utils.h"
@@ -71,3 +72,37 @@
g_free(argv[3]);
g_free(argv);
}
+
+void
+ __attribute__((sentinel)) test_argc_argv(int *argcp, char ***argvp, ...)
+{
+ int argc = 0;
+ char **argv = NULL;
+ va_list ap;
+
+ /* find out how many elements there are */
+ va_start(ap, argvp);
+ while (NULL != va_arg(ap, const char *)) {
+ argc += 1;
+ }
+ va_end(ap);
+
+ /* argc + terminating NULL entry */
+ argv = calloc(argc + 1, sizeof argv[0]);
+ g_assert_nonnull(argv);
+
+ va_start(ap, argvp);
+ for (int i = 0; i < argc; i++) {
+ const char *arg = va_arg(ap, const char *);
+ char *arg_copy = sc_strdup(arg);
+ g_test_queue_free(arg_copy);
+ argv[i] = arg_copy;
+ }
+ va_end(ap);
+
+ /* free argv last, so that entries do not leak */
+ g_test_queue_free(argv);
+
+ *argcp = argc;
+ *argvp = argv;
+}
diff -Nru snapd-2.40/cmd/libsnap-confine-private/test-utils.h snapd-2.42.1/cmd/libsnap-confine-private/test-utils.h
--- snapd-2.40/cmd/libsnap-confine-private/test-utils.h 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/test-utils.h 2019-10-30 12:17:43.000000000 +0000
@@ -23,4 +23,10 @@
*/
void rm_rf_tmp(const char *dir);
+/**
+ * Create an argc + argv pair out of a NULL terminated argument list.
+ **/
+void
+ __attribute__((sentinel)) test_argc_argv(int *argcp, char ***argvp, ...);
+
#endif
diff -Nru snapd-2.40/cmd/libsnap-confine-private/test-utils-test.c snapd-2.42.1/cmd/libsnap-confine-private/test-utils-test.c
--- snapd-2.40/cmd/libsnap-confine-private/test-utils-test.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/test-utils-test.c 2019-10-30 12:17:43.000000000 +0000
@@ -39,7 +39,31 @@
g_test_trap_assert_failed();
}
+static void test_test_argc_argv(void)
+{
+ // Check that test_argc_argv() correctly stores data
+ int argc = 0;
+ char **argv = NULL;
+
+ test_argc_argv(&argc, &argv, NULL);
+ g_assert_cmpint(argc, ==, 0);
+ g_assert_nonnull(argv);
+ g_assert_null(argv[0]);
+
+ argc = 0;
+ argv = NULL;
+
+ test_argc_argv(&argc, &argv, "zero", "one", "two", NULL);
+ g_assert_cmpint(argc, ==, 3);
+ g_assert_nonnull(argv);
+ g_assert_cmpstr(argv[0], ==, "zero");
+ g_assert_cmpstr(argv[1], ==, "one");
+ g_assert_cmpstr(argv[2], ==, "two");
+ g_assert_null(argv[3]);
+}
+
static void __attribute__((constructor)) init(void)
{
g_test_add_func("/test-utils/rm_rf_tmp", test_rm_rf_tmp);
+ g_test_add_func("/test-utils/test_argc_argv", test_test_argc_argv);
}
diff -Nru snapd-2.40/cmd/libsnap-confine-private/utils.c snapd-2.42.1/cmd/libsnap-confine-private/utils.c
--- snapd-2.40/cmd/libsnap-confine-private/utils.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/libsnap-confine-private/utils.c 2019-10-30 12:17:43.000000000 +0000
@@ -23,23 +23,16 @@
#include
#include
-#include "utils.h"
#include "cleanup-funcs.h"
+#include "panic.h"
+#include "utils.h"
void die(const char *msg, ...)
{
- int saved_errno = errno;
- va_list va;
- va_start(va, msg);
- vfprintf(stderr, msg, va);
- va_end(va);
-
- if (errno != 0) {
- fprintf(stderr, ": %s\n", strerror(saved_errno));
- } else {
- fprintf(stderr, "\n");
- }
- exit(1);
+ va_list ap;
+ va_start(ap, msg);
+ sc_panicv(msg, ap);
+ va_end(ap);
}
struct sc_bool_name {
diff -Nru snapd-2.40/cmd/Makefile.am snapd-2.42.1/cmd/Makefile.am
--- snapd-2.40/cmd/Makefile.am 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/Makefile.am 2019-10-30 12:17:43.000000000 +0000
@@ -62,10 +62,14 @@
libsnap-confine-private/infofile-test.c \
libsnap-confine-private/infofile.c \
libsnap-confine-private/infofile.h \
+ libsnap-confine-private/panic-test.h \
+ libsnap-confine-private/panic.c \
+ libsnap-confine-private/panic.h \
snap-confine/seccomp-support-ext.c \
snap-confine/seccomp-support-ext.h \
snap-confine/selinux-support.c \
snap-confine/selinux-support.h \
+ snap-confine/snap-confine-invocation-test.c \
snap-confine/snap-confine-invocation.c \
snap-confine/snap-confine-invocation.h \
snap-discard-ns/snap-discard-ns.c
@@ -93,9 +97,9 @@
# for the hack target also:
snap-update-ns/snap-update-ns: snap-update-ns/*.go snap-update-ns/*.[ch]
- cd snap-update-ns && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -i -v
+ cd snap-update-ns && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -v
snap-seccomp/snap-seccomp: snap-seccomp/*.go
- cd snap-seccomp && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -i -v
+ cd snap-seccomp && GOPATH=$(or $(GOPATH),$(realpath $(srcdir)/../../../../..)) go build -v
##
## libsnap-confine-private.a
@@ -129,6 +133,8 @@
libsnap-confine-private/mount-opt.h \
libsnap-confine-private/mountinfo.c \
libsnap-confine-private/mountinfo.h \
+ libsnap-confine-private/panic.c \
+ libsnap-confine-private/panic.h \
libsnap-confine-private/privs.c \
libsnap-confine-private/privs.h \
libsnap-confine-private/secure-getenv.c \
@@ -159,6 +165,7 @@
libsnap-confine-private/locking-test.c \
libsnap-confine-private/mount-opt-test.c \
libsnap-confine-private/mountinfo-test.c \
+ libsnap-confine-private/panic-test.c \
libsnap-confine-private/privs-test.c \
libsnap-confine-private/secure-getenv-test.c \
libsnap-confine-private/snap-test.c \
@@ -330,8 +337,7 @@
snap-confine/mount-support-test.c \
snap-confine/ns-support-test.c \
snap-confine/snap-confine-args-test.c \
- snap-confine/snap-confine-invocation.c \
- snap-confine/snap-confine-invocation.h \
+ snap-confine/snap-confine-invocation-test.c \
snap-confine/snap-device-helper-test.c
snap_confine_unit_tests_CFLAGS = $(snap_confine_snap_confine_CFLAGS) $(GLIB_CFLAGS)
snap_confine_unit_tests_LDADD = $(snap_confine_snap_confine_LDADD) $(GLIB_LIBS)
diff -Nru snapd-2.40/cmd/snap/cmd_alias_test.go snapd-2.42.1/cmd/snap/cmd_alias_test.go
--- snapd-2.40/cmd/snap/cmd_alias_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_alias_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -55,6 +55,7 @@
"app": "cmd1",
"alias": "alias1",
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
diff -Nru snapd-2.40/cmd/snap/cmd_connect_test.go snapd-2.42.1/cmd/snap/cmd_connect_test.go
--- snapd-2.40/cmd/snap/cmd_connect_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_connect_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -80,6 +80,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
@@ -113,6 +114,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
@@ -146,6 +148,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
@@ -179,6 +182,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
diff -Nru snapd-2.40/cmd/snap/cmd_debug_state.go snapd-2.42.1/cmd/snap/cmd_debug_state.go
--- snapd-2.40/cmd/snap/cmd_debug_state.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_debug_state.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,311 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package main
+
+import (
+ "fmt"
+ "os"
+ "sort"
+ "strconv"
+ "strings"
+ "text/tabwriter"
+
+ "github.com/jessevdk/go-flags"
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+type cmdDebugState struct {
+ timeMixin
+
+ st *state.State
+
+ Changes bool `long:"changes"`
+ TaskID string `long:"task"`
+ ChangeID string `long:"change"`
+
+ // flags for --change=N output
+ DotOutput bool `long:"dot"` // XXX: mildly useful (too crowded in many cases), but let's have it just in case
+ // When inspecting errors/undone tasks, those in Hold state are usually irrelevant, make it possible to ignore them
+ NoHoldState bool `long:"no-hold"`
+
+ Positional struct {
+ StateFilePath string `positional-args:"yes" positional-arg-name:""`
+ } `positional-args:"yes"`
+}
+
+var cmdDebugStateShortHelp = i18n.G("Inspect a snapd state file.")
+var cmdDebugStateLongHelp = i18n.G("Inspect a snapd state file, bypassing snapd API.")
+
+type byChangeID []*state.Change
+
+func (c byChangeID) Len() int { return len(c) }
+func (c byChangeID) Swap(i, j int) { c[i], c[j] = c[j], c[i] }
+func (c byChangeID) Less(i, j int) bool { return c[i].ID() < c[j].ID() }
+
+func loadState(path string) (*state.State, error) {
+ if path == "" {
+ path = "state.json"
+ }
+ r, err := os.Open(path)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read the state file: %s", err)
+ }
+ defer r.Close()
+
+ return state.ReadState(nil, r)
+}
+
+func init() {
+ addDebugCommand("state", cmdDebugStateShortHelp, cmdDebugStateLongHelp, func() flags.Commander {
+ return &cmdDebugState{}
+ }, timeDescs.also(map[string]string{
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "change": i18n.G("ID of the change to inspect"),
+ "task": i18n.G("ID of the task to inspect"),
+ "dot": i18n.G("Dot (graphviz) output"),
+ "no-hold": i18n.G("Omit tasks in 'Hold' state in the change output"),
+ "changes": i18n.G("List all changes"),
+ }), nil)
+}
+
+type byLaneAndWaitTaskChain []*state.Task
+
+func (t byLaneAndWaitTaskChain) Len() int { return len(t) }
+func (t byLaneAndWaitTaskChain) Swap(i, j int) { t[i], t[j] = t[j], t[i] }
+func (t byLaneAndWaitTaskChain) Less(i, j int) bool {
+ // cover the typical case (just one lane), and order by first lane
+ if t[i].Lanes()[0] == t[j].Lanes()[0] {
+ return waitChainSearch(t[i], t[j])
+ }
+ return t[i].Lanes()[0] < t[j].Lanes()[0]
+}
+
+func waitChainSearch(startT, searchT *state.Task) bool {
+ for _, cand := range startT.HaltTasks() {
+ if cand == searchT {
+ return true
+ }
+ if waitChainSearch(cand, searchT) {
+ return true
+ }
+ }
+
+ return false
+}
+
+func (c *cmdDebugState) writeDotOutput(st *state.State, changeID string) error {
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.Change(changeID)
+ if chg == nil {
+ return fmt.Errorf("no such change: %s", changeID)
+ }
+
+ fmt.Fprintf(Stdout, "digraph D{\n")
+ tasks := chg.Tasks()
+ for _, t := range tasks {
+ if c.NoHoldState && t.Status() == state.HoldStatus {
+ continue
+ }
+ fmt.Fprintf(Stdout, " %s [label=%q];\n", t.ID(), t.Kind())
+ for _, wt := range t.WaitTasks() {
+ if c.NoHoldState && wt.Status() == state.HoldStatus {
+ continue
+ }
+ fmt.Fprintf(Stdout, " %s -> %s;\n", t.ID(), wt.ID())
+ }
+ }
+ fmt.Fprintf(Stdout, "}\n")
+
+ return nil
+}
+
+func (c *cmdDebugState) showTasks(st *state.State, changeID string) error {
+ st.Lock()
+ defer st.Unlock()
+
+ chg := st.Change(changeID)
+ if chg == nil {
+ return fmt.Errorf("no such change: %s", changeID)
+ }
+
+ tasks := chg.Tasks()
+ sort.Sort(byLaneAndWaitTaskChain(tasks))
+
+ w := tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0)
+ fmt.Fprintf(w, "Lanes\tID\tStatus\tSpawn\tReady\tKind\tSummary\n")
+ for _, t := range tasks {
+ if c.NoHoldState && t.Status() == state.HoldStatus {
+ continue
+ }
+ var lanes []string
+ for _, lane := range t.Lanes() {
+ lanes = append(lanes, fmt.Sprintf("%d", lane))
+ }
+ fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\t%s\n",
+ strings.Join(lanes, ","),
+ t.ID(),
+ t.Status().String(),
+ c.fmtTime(t.SpawnTime()),
+ c.fmtTime(t.ReadyTime()),
+ t.Kind(),
+ t.Summary())
+ }
+
+ w.Flush()
+
+ for _, t := range tasks {
+ logs := t.Log()
+ if len(logs) > 0 {
+ fmt.Fprintf(Stdout, "---\n")
+ fmt.Fprintf(Stdout, "%s %s\n", t.ID(), t.Summary())
+ for _, log := range logs {
+ fmt.Fprintf(Stdout, " %s\n", log)
+ }
+ }
+ }
+
+ return nil
+}
+
+func (c *cmdDebugState) showChanges(st *state.State) error {
+ st.Lock()
+ defer st.Unlock()
+
+ changes := st.Changes()
+ sort.Sort(byChangeID(changes))
+
+ w := tabwriter.NewWriter(Stdout, 5, 3, 2, ' ', 0)
+ fmt.Fprintf(w, "ID\tStatus\tSpawn\tReady\tLabel\tSummary\n")
+ for _, chg := range changes {
+ fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n",
+ chg.ID(),
+ chg.Status().String(),
+ c.fmtTime(chg.SpawnTime()),
+ c.fmtTime(chg.ReadyTime()),
+ chg.Kind(),
+ chg.Summary())
+ }
+ w.Flush()
+
+ return nil
+}
+
+func (c *cmdDebugState) showTask(st *state.State, taskID string) error {
+ st.Lock()
+ defer st.Unlock()
+
+ task := st.Task(taskID)
+ if task == nil {
+ return fmt.Errorf("no such task: %s", taskID)
+ }
+
+ termWidth, _ := termSize()
+ termWidth -= 3
+ if termWidth > 100 {
+ // any wider than this and it gets hard to read
+ termWidth = 100
+ }
+
+ // the output of 'debug task' is yaml'ish
+ fmt.Fprintf(Stdout, "id: %s\nkind: %s\nsummary: %s\nstatus: %s\n",
+ taskID, task.Kind(),
+ task.Summary(),
+ task.Status().String())
+ log := task.Log()
+ if len(log) > 0 {
+ fmt.Fprintf(Stdout, "log: |\n")
+ for _, msg := range log {
+ if err := wrapLine(Stdout, []rune(msg), " ", termWidth); err != nil {
+ break
+ }
+ }
+ fmt.Fprintln(Stdout)
+ }
+
+ fmt.Fprintf(Stdout, "halt-tasks:")
+ if len(task.HaltTasks()) == 0 {
+ fmt.Fprintln(Stdout, " []")
+ } else {
+ fmt.Fprintln(Stdout)
+ for _, ht := range task.HaltTasks() {
+ fmt.Fprintf(Stdout, " - %s (%s)\n", ht.Kind(), ht.ID())
+ }
+ }
+
+ return nil
+}
+
+func (c *cmdDebugState) Execute(args []string) error {
+ st, err := loadState(c.Positional.StateFilePath)
+ if err != nil {
+ return err
+ }
+
+ // check valid combinations of args
+ var cmds []string
+ if c.Changes {
+ cmds = append(cmds, "--changes")
+ }
+ if c.ChangeID != "" {
+ cmds = append(cmds, "--change=")
+ }
+ if c.TaskID != "" {
+ cmds = append(cmds, "--task=")
+ }
+ if len(cmds) > 1 {
+ return fmt.Errorf("cannot use %s and %s together", cmds[0], cmds[1])
+ }
+
+ if c.DotOutput && c.ChangeID == "" {
+ return fmt.Errorf("--dot can only be used with --change=")
+ }
+ if c.NoHoldState && c.ChangeID == "" {
+ return fmt.Errorf("--no-hold can only be used with --change=")
+ }
+
+ if c.Changes {
+ return c.showChanges(st)
+ }
+
+ if c.ChangeID != "" {
+ _, err := strconv.ParseInt(c.ChangeID, 0, 64)
+ if err != nil {
+ return fmt.Errorf("invalid change: %s", c.ChangeID)
+ }
+ if c.DotOutput {
+ return c.writeDotOutput(st, c.ChangeID)
+ }
+ return c.showTasks(st, c.ChangeID)
+ }
+
+ if c.TaskID != "" {
+ _, err := strconv.ParseInt(c.TaskID, 0, 64)
+ if err != nil {
+ return fmt.Errorf("invalid task: %s", c.TaskID)
+ }
+ return c.showTask(st, c.TaskID)
+ }
+
+ // show changes by default
+ return c.showChanges(st)
+}
diff -Nru snapd-2.40/cmd/snap/cmd_debug_state_test.go snapd-2.42.1/cmd/snap/cmd_debug_state_test.go
--- snapd-2.40/cmd/snap/cmd_debug_state_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_debug_state_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,204 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package main_test
+
+import (
+ "io/ioutil"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/cmd/snap"
+)
+
+var stateJSON = []byte(`
+{
+ "last-task-id": 31,
+ "last-change-id": 2,
+
+ "data": {
+ "snaps": {}
+ },
+ "changes": {
+ "1": {
+ "id": "1",
+ "kind": "install-snap",
+ "summary": "install a snap",
+ "status": 0,
+ "data": {"snap-names": ["a"]},
+ "task-ids": ["11","12"]
+ },
+ "2": {
+ "id": "2",
+ "kind": "revert-snap",
+ "summary": "revert c snap",
+ "status": 0,
+ "data": {"snap-names": ["c"]},
+ "task-ids": ["21","31"]
+ }
+ },
+ "tasks": {
+ "11": {
+ "id": "11",
+ "change": "1",
+ "kind": "download-snap",
+ "summary": "Download snap a from channel edge",
+ "status": 4,
+ "data": {"snap-setup": {
+ "channel": "edge",
+ "flags": 1
+ }},
+ "halt-tasks": ["12"]
+ },
+ "12": {"id": "12", "change": "1", "kind": "some-other-task"},
+ "21": {
+ "id": "21",
+ "change": "2",
+ "kind": "download-snap",
+ "summary": "Download snap b from channel beta",
+ "status": 4,
+ "data": {"snap-setup": {
+ "channel": "beta",
+ "flags": 2
+ }},
+ "halt-tasks": ["12"]
+ },
+ "31": {
+ "id": "31",
+ "change": "2",
+ "kind": "prepare-snap",
+ "summary": "Prepare snap c",
+ "status": 4,
+ "data": {"snap-setup": {
+ "channel": "stable",
+ "flags": 1073741828
+ }},
+ "halt-tasks": ["12"],
+ "log": ["logline1", "logline2"]
+ }
+ }
+}
+`)
+
+func (s *SnapSuite) TestDebugChanges(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil)
+
+ rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--changes", stateFile})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Matches,
+ "ID Status Spawn Ready Label Summary\n"+
+ "1 Do 0001-01-01 0001-01-01 install-snap install a snap\n"+
+ "2 Done 0001-01-01 0001-01-01 revert-snap revert c snap\n")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestDebugChangesMissingState(c *C) {
+ _, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--changes", "/missing-state.json"})
+ c.Check(err, ErrorMatches, "cannot read the state file: open /missing-state.json: no such file or directory")
+}
+
+func (s *SnapSuite) TestDebugTask(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil)
+
+ rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--task=31", stateFile})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Equals, "id: 31\n"+
+ "kind: prepare-snap\n"+
+ "summary: Prepare snap c\n"+
+ "status: Done\n"+
+ "log: |\n"+
+ " logline1\n"+
+ " logline2\n"+
+ "\n"+
+ "halt-tasks:\n"+
+ " - some-other-task (12)\n")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestDebugTaskEmptyLists(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil)
+
+ rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--task=12", stateFile})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Equals, "id: 12\n"+
+ "kind: some-other-task\n"+
+ "summary: \n"+
+ "status: Do\n"+
+ "halt-tasks: []\n")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestDebugTaskMissingState(c *C) {
+ _, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--task=1", "/missing-state.json"})
+ c.Check(err, ErrorMatches, "cannot read the state file: open /missing-state.json: no such file or directory")
+}
+
+func (s *SnapSuite) TestDebugTaskNoSuchTaskError(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil)
+
+ _, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--task=99", stateFile})
+ c.Check(err, ErrorMatches, "no such task: 99")
+}
+
+func (s *SnapSuite) TestDebugTaskMutuallyExclusiveCommands(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil)
+
+ _, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--task=99", "--changes", stateFile})
+ c.Check(err, ErrorMatches, "cannot use --changes and --task= together")
+
+ _, err = main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--changes", "--change=1", stateFile})
+ c.Check(err, ErrorMatches, "cannot use --changes and --change= together")
+
+ _, err = main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--change=1", "--task=1", stateFile})
+ c.Check(err, ErrorMatches, "cannot use --change= and --task= together")
+}
+
+func (s *SnapSuite) TestDebugTasks(c *C) {
+ dir := c.MkDir()
+ stateFile := filepath.Join(dir, "test-state.json")
+ c.Assert(ioutil.WriteFile(stateFile, stateJSON, 0644), IsNil)
+
+ rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--change=1", stateFile})
+ c.Assert(err, IsNil)
+ c.Assert(rest, DeepEquals, []string{})
+ c.Check(s.Stdout(), Matches,
+ "Lanes ID Status Spawn Ready Kind Summary\n"+
+ "0 11 Done 0001-01-01 0001-01-01 download-snap Download snap a from channel edge\n"+
+ "0 12 Do 0001-01-01 0001-01-01 some-other-task \n")
+ c.Check(s.Stderr(), Equals, "")
+}
+
+func (s *SnapSuite) TestDebugTasksMissingState(c *C) {
+ _, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--change=1", "/missing-state.json"})
+ c.Check(err, ErrorMatches, "cannot read the state file: open /missing-state.json: no such file or directory")
+}
diff -Nru snapd-2.40/cmd/snap/cmd_debug_validate_seed.go snapd-2.42.1/cmd/snap/cmd_debug_validate_seed.go
--- snapd-2.40/cmd/snap/cmd_debug_validate_seed.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_debug_validate_seed.go 2019-10-30 12:17:43.000000000 +0000
@@ -22,13 +22,13 @@
import (
"github.com/jessevdk/go-flags"
- "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/seed"
)
type cmdValidateSeed struct {
Positionals struct {
- SeedYamlPath string `positional-arg-name:""`
- } `positional-args:"true"`
+ SeedYamlPath flags.Filename `positional-arg-name:""`
+ } `positional-args:"true" required:"true"`
}
func init() {
@@ -46,8 +46,5 @@
return ErrExtraArgs
}
- if _, err := snap.ReadSeedYaml(x.Positionals.SeedYamlPath); err != nil {
- return err
- }
- return nil
+ return seed.ValidateFromYaml(string(x.Positionals.SeedYamlPath))
}
diff -Nru snapd-2.40/cmd/snap/cmd_debug_validate_seed_test.go snapd-2.42.1/cmd/snap/cmd_debug_validate_seed_test.go
--- snapd-2.40/cmd/snap/cmd_debug_validate_seed_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_debug_validate_seed_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -28,7 +28,7 @@
snap "github.com/snapcore/snapd/cmd/snap"
)
-func (s *SnapSuite) TestDebugValidateSeedHappy(c *C) {
+func (s *SnapSuite) TestDebugValidateSeedRegressionLp1825437(c *C) {
tmpf := filepath.Join(c.MkDir(), "seed.yaml")
err := ioutil.WriteFile(tmpf, []byte(`
snaps:
@@ -37,32 +37,28 @@
channel: stable
file: core_6673.snap
-
- name: gtk-common-themes
+ -
+ name: gnome-foo
channel: stable/ubuntu-19.04
file: gtk-common-themes_1198.snap
`), 0644)
c.Assert(err, IsNil)
_, err = snap.Parser(snap.Client()).ParseArgs([]string{"debug", "validate-seed", tmpf})
- c.Assert(err, IsNil)
+ c.Assert(err, ErrorMatches, "cannot read seed yaml: empty element in seed")
}
-func (s *SnapSuite) TestDebugValidateSeedRegressionLp1825437(c *C) {
+func (s *SnapSuite) TestDebugValidateSeedDuplicatedSnap(c *C) {
tmpf := filepath.Join(c.MkDir(), "seed.yaml")
err := ioutil.WriteFile(tmpf, []byte(`
snaps:
- -
- name: core
- channel: stable
- file: core_6673.snap
- -
- -
- name: gnome-foo
- channel: stable/ubuntu-19.04
- file: gtk-common-themes_1198.snap
+ - name: foo
+ file: foo.snap
+ - name: foo
+ file: bar.snap
`), 0644)
c.Assert(err, IsNil)
_, err = snap.Parser(snap.Client()).ParseArgs([]string{"debug", "validate-seed", tmpf})
- c.Assert(err, ErrorMatches, "cannot read seed yaml: empty element in seed")
+ c.Assert(err, ErrorMatches, `cannot read seed yaml: snap name "foo" must be unique`)
}
diff -Nru snapd-2.40/cmd/snap/cmd_disconnect_test.go snapd-2.42.1/cmd/snap/cmd_disconnect_test.go
--- snapd-2.40/cmd/snap/cmd_disconnect_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_disconnect_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -73,6 +73,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
@@ -108,6 +109,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
@@ -143,6 +145,7 @@
},
},
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
diff -Nru snapd-2.40/cmd/snap/cmd_download.go snapd-2.42.1/cmd/snap/cmd_download.go
--- snapd-2.40/cmd/snap/cmd_download.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_download.go 2019-10-30 12:17:43.000000000 +0000
@@ -140,6 +140,8 @@
Channel: x.Channel,
CohortKey: x.CohortKey,
Revision: revision,
+ // if something goes wrong, don't force it to start over again
+ LeavePartialOnError: true,
}
snapPath, snapInfo, err := tsto.DownloadSnap(snapName, dlOpts)
if err != nil {
diff -Nru snapd-2.40/cmd/snap/cmd_handle_link.go snapd-2.42.1/cmd/snap/cmd_handle_link.go
--- snapd-2.40/cmd/snap/cmd_handle_link.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_handle_link.go 2019-10-30 12:17:43.000000000 +0000
@@ -28,7 +28,7 @@
"github.com/jessevdk/go-flags"
"github.com/snapcore/snapd/i18n"
- "github.com/snapcore/snapd/userd/ui"
+ "github.com/snapcore/snapd/usersession/userd/ui"
)
type cmdHandleLink struct {
diff -Nru snapd-2.40/cmd/snap/cmd_help.go snapd-2.42.1/cmd/snap/cmd_help.go
--- snapd-2.40/cmd/snap/cmd_help.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_help.go 2019-10-30 12:17:43.000000000 +0000
@@ -200,7 +200,7 @@
}, {
Label: i18n.G("Configuration"),
Description: i18n.G("system administration and configuration"),
- Commands: []string{"get", "set", "wait"},
+ Commands: []string{"get", "set", "unset", "wait"},
}, {
Label: i18n.G("Account"),
Description: i18n.G("authentication to snapd and the snap store"),
@@ -216,7 +216,7 @@
}, {
Label: i18n.G("Other"),
Description: i18n.G("miscellanea"),
- Commands: []string{"version", "warnings", "okay", "ack", "known", "create-cohort"},
+ Commands: []string{"version", "warnings", "okay", "ack", "known", "model", "create-cohort"},
}, {
Label: i18n.G("Development"),
Description: i18n.G("developer-oriented features"),
diff -Nru snapd-2.40/cmd/snap/cmd_info.go snapd-2.42.1/cmd/snap/cmd_info.go
--- snapd-2.40/cmd/snap/cmd_info.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_info.go 2019-10-30 12:17:43.000000000 +0000
@@ -76,6 +76,41 @@
}), nil)
}
+func (iw *infoWriter) maybePrintHealth() {
+ if iw.localSnap == nil {
+ return
+ }
+ health := iw.localSnap.Health
+ if health == nil {
+ if !iw.verbose {
+ return
+ }
+ health = &client.SnapHealth{
+ Status: "unknown",
+ Message: "health has not been set",
+ }
+ }
+ if health.Status == "okay" && !iw.verbose {
+ return
+ }
+
+ fmt.Fprintln(iw, "health:")
+ fmt.Fprintf(iw, " status:\t%s\n", health.Status)
+ if health.Message != "" {
+ wrapGeneric(iw, quotedIfNeeded(health.Message), " message:\t", " ", iw.termWidth)
+ }
+ if health.Code != "" {
+ fmt.Fprintf(iw, " code:\t%s\n", health.Code)
+ }
+ if !health.Timestamp.IsZero() {
+ fmt.Fprintf(iw, " checked:\t%s\n", iw.fmtTime(health.Timestamp))
+ }
+ if !health.Revision.Unset() {
+ fmt.Fprintf(iw, " revision:\t%s\n", health.Revision)
+ }
+ iw.Flush()
+}
+
func clientSnapFromPath(path string) (*client.Snap, error) {
snapf, err := snap.Open(path)
if err != nil {
@@ -198,6 +233,20 @@
return err
}
+func quotedIfNeeded(raw string) []rune {
+ // simplest way of checking to see if it needs quoting is to try
+ raw = strings.TrimSpace(raw)
+ type T struct {
+ S string
+ }
+ if len(raw) == 0 {
+ raw = `""`
+ } else if err := yaml.UnmarshalStrict([]byte("s: "+raw), &T{}); err != nil {
+ raw = strconv.Quote(raw)
+ }
+ return []rune(raw)
+}
+
// printDescr formats a given string (typically a snap description)
// in a user friendly way.
//
@@ -349,18 +398,7 @@
}
func (iw *infoWriter) printSummary() {
- // simplest way of checking to see if it needs quoting is to try
- raw := strings.TrimSpace(iw.theSnap.Summary)
- type T struct {
- S string
- }
- if len(raw) == 0 {
- raw = `""`
- } else if err := yaml.UnmarshalStrict([]byte("s: "+raw), &T{}); err != nil {
- raw = strconv.Quote(raw)
- }
-
- wrapFlow(iw, []rune(raw), "summary:\t", iw.termWidth)
+ wrapFlow(iw, quotedIfNeeded(iw.theSnap.Summary), "summary:\t", iw.termWidth)
}
func (iw *infoWriter) maybePrintPublisher() {
@@ -645,7 +683,7 @@
noneOK := true
for i, snapName := range x.Positional.Snaps {
- snapName := norm(string(snapName))
+ snapName := string(snapName)
if i > 0 {
fmt.Fprintln(w, "---")
}
@@ -655,9 +693,9 @@
}
if diskSnap, err := clientSnapFromPath(snapName); err == nil {
- iw.setupDiskSnap(snapName, diskSnap)
+ iw.setupDiskSnap(norm(snapName), diskSnap)
} else {
- remoteSnap, resInfo, _ := x.client.FindOne(snapName)
+ remoteSnap, resInfo, _ := x.client.FindOne(snap.InstanceSnap(snapName))
localSnap, _, _ := x.client.Snap(snapName)
iw.setupSnap(localSnap, remoteSnap, resInfo)
}
@@ -677,6 +715,7 @@
iw.maybePrintPath()
iw.printName()
iw.printSummary()
+ iw.maybePrintHealth()
iw.maybePrintPublisher()
iw.maybePrintStandaloneVersion()
iw.maybePrintBuildDate()
diff -Nru snapd-2.40/cmd/snap/cmd_info_test.go snapd-2.42.1/cmd/snap/cmd_info_test.go
--- snapd-2.40/cmd/snap/cmd_info_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_info_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -475,6 +475,7 @@
c.Check(s.Stderr(), check.Equals, "")
}
+// only used for results on /v2/find
const mockInfoJSON = `
{
"type": "sync",
@@ -594,6 +595,7 @@
c.Check(s.Stderr(), check.Equals, "")
}
+// only used for /v2/snaps/hello
const mockInfoJSONOtherLicense = `
{
"type": "sync",
@@ -610,6 +612,7 @@
"display-name": "Canonical",
"validation": "verified"
},
+ "health": {"revision": "1", "status": "blocked", "message": "please configure the grawflit", "timestamp": "2019-05-13T16:27:01.475851677+01:00"},
"id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6",
"install-date": "2006-01-02T22:04:07.123456789Z",
"installed-size": 1024,
@@ -680,8 +683,14 @@
rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "--abs-time", "hello"})
c.Assert(err, check.IsNil)
c.Assert(rest, check.DeepEquals, []string{})
- c.Check(s.Stdout(), check.Equals, `name: hello
-summary: The GNU Hello snap
+ c.Check(s.Stdout(), check.Equals, `
+name: hello
+summary: The GNU Hello snap
+health:
+ status: blocked
+ message: please configure the grawflit
+ checked: 2019-05-13T16:27:01+01:00
+ revision: 1
publisher: Canonical*
license: BSD-3
description: |
@@ -690,8 +699,8 @@
snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6
tracking: beta
refresh-date: 2006-01-02T22:04:07Z
-installed: 2.10 (1) 1kB disabled
-`)
+installed: 2.10 (1) 1kB disabled,blocked
+`[1:])
c.Check(s.Stderr(), check.Equals, "")
}
@@ -956,6 +965,69 @@
}
}
+func (infoSuite) TestMaybePrintHealth(c *check.C) {
+ type T struct {
+ snap *client.Snap
+ verbose bool
+ expected string
+ }
+
+ goodHealth := &client.SnapHealth{Status: "okay"}
+ t0 := time.Date(1970, 1, 1, 10, 24, 0, 0, time.UTC)
+ badHealth := &client.SnapHealth{
+ Status: "waiting",
+ Message: "godot should be here any moment now",
+ Code: "godot-is-a-lie",
+ Revision: snaplib.R("42"),
+ Timestamp: t0,
+ }
+
+ tests := []T{
+ {snap: nil, verbose: false, expected: ""},
+ {snap: nil, verbose: true, expected: ""},
+ {snap: &client.Snap{}, verbose: false, expected: ""},
+ {snap: &client.Snap{}, verbose: true, expected: `health:
+ status: unknown
+ message: health
+ has not been set
+`},
+ {snap: &client.Snap{Health: goodHealth}, verbose: false, expected: ``},
+ {snap: &client.Snap{Health: goodHealth}, verbose: true, expected: `health:
+ status: okay
+`},
+ {snap: &client.Snap{Health: badHealth}, verbose: false, expected: `health:
+ status: waiting
+ message: godot
+ should be here
+ any moment now
+ code: godot-is-a-lie
+ checked: 10:24AM
+ revision: 42
+`},
+ {snap: &client.Snap{Health: badHealth}, verbose: true, expected: `health:
+ status: waiting
+ message: godot
+ should be here
+ any moment now
+ code: godot-is-a-lie
+ checked: 10:24AM
+ revision: 42
+`},
+ }
+
+ var buf flushBuffer
+ iw := snap.NewInfoWriter(&buf)
+ defer snap.MockIsStdoutTTY(false)()
+
+ for i, t := range tests {
+ buf.Reset()
+ snap.SetupSnap(iw, t.snap, nil, nil)
+ snap.SetVerbose(iw, t.verbose)
+ snap.MaybePrintHealth(iw)
+ c.Check(buf.String(), check.Equals, t.expected, check.Commentf("%d", i))
+ }
+}
+
func (infoSuite) TestWrapCornerCase(c *check.C) {
// this particular corner case isn't currently reachable from
// printDescr nor printSummary, but best to have it covered
@@ -987,3 +1059,80 @@
indented.
`)
}
+
+const mockInfoJSONParallelInstance = `
+{
+ "type": "sync",
+ "status-code": 200,
+ "status": "OK",
+ "result": {
+ "channel": "stable",
+ "confinement": "strict",
+ "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/",
+ "developer": "canonical",
+ "publisher": {
+ "id": "canonical",
+ "username": "canonical",
+ "display-name": "Canonical",
+ "validation": "verified"
+ },
+ "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6",
+ "install-date": "2006-01-02T22:04:07.123456789Z",
+ "installed-size": 1024,
+ "name": "hello_foo",
+ "private": false,
+ "revision": "100",
+ "status": "available",
+ "summary": "The GNU Hello snap",
+ "type": "app",
+ "version": "2.10",
+ "license": "",
+ "tracking-channel": "beta"
+ }
+}
+`
+
+func (s *infoSuite) TestInfoParllelInstance(c *check.C) {
+ n := 0
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ switch n {
+ case 0:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/find")
+ q := r.URL.Query()
+ // asks for the instance snap
+ c.Check(q.Get("name"), check.Equals, "hello")
+ fmt.Fprintln(w, mockInfoJSONWithChannels)
+ case 1:
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello_foo")
+ fmt.Fprintln(w, mockInfoJSONParallelInstance)
+ default:
+ c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r)
+ }
+
+ n++
+ })
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello_foo"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ // make sure local and remote info is combined in the output
+ c.Check(s.Stdout(), check.Equals, `name: hello_foo
+summary: The GNU Hello snap
+publisher: Canonical*
+license: unset
+description: |
+ GNU hello prints a friendly greeting. This is part of the snapcraft tour at
+ https://snapcraft.io/
+snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6
+tracking: beta
+refresh-date: 2006-01-02
+channels:
+ 1/stable: 2.10 2018-12-18 (1) 65kB -
+ 1/candidate: ^
+ 1/beta: ^
+ 1/edge: ^
+installed: 2.10 (100) 1kB disabled
+`)
+ c.Check(s.Stderr(), check.Equals, "")
+}
diff -Nru snapd-2.40/cmd/snap/cmd_list_test.go snapd-2.42.1/cmd/snap/cmd_list_test.go
--- snapd-2.40/cmd/snap/cmd_list_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_list_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -55,7 +55,17 @@
c.Check(r.Method, check.Equals, "GET")
c.Check(r.URL.Path, check.Equals, "/v2/snaps")
c.Check(r.URL.RawQuery, check.Equals, "")
- fmt.Fprintln(w, `{"type": "sync", "result": [{"name": "foo", "status": "active", "version": "4.2", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":17, "tracking-channel": "potatoes"}]}`)
+ fmt.Fprintln(w, `{"type": "sync", "result": [
+{
+ "name": "foo",
+ "status": "active",
+ "version": "4.2",
+ "developer": "bar",
+ "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"},
+ "health": {"status": "blocked"},
+ "revision": 17,
+ "tracking-channel": "potatoes"
+}]}`)
default:
c.Fatalf("expected to get 1 requests, now on %d", n+1)
}
@@ -65,9 +75,10 @@
rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"list"})
c.Assert(err, check.IsNil)
c.Assert(rest, check.DeepEquals, []string{})
- c.Check(s.Stdout(), check.Matches, `Name +Version +Rev +Tracking +Publisher +Notes
-foo +4.2 +17 +potatoes +bar +-
-`)
+ c.Check(s.Stdout(), check.Equals, `
+Name Version Rev Tracking Publisher Notes
+foo 4.2 17 potatoes bar blocked
+`[1:])
c.Check(s.Stderr(), check.Equals, "")
}
diff -Nru snapd-2.40/cmd/snap/cmd_model.go snapd-2.42.1/cmd/snap/cmd_model.go
--- snapd-2.40/cmd/snap/cmd_model.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_model.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,330 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package main
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/client"
+ "github.com/snapcore/snapd/i18n"
+)
+
+var (
+ shortModelHelp = i18n.G("Get the active model for this device")
+ longModelHelp = i18n.G(`
+The model command returns the active model assertion information for this
+device.
+
+By default, only the essential model identification information is
+included in the output, but this can be expanded to include all of an
+assertion's non-meta headers.
+
+The verbose output is presented in a structured, yaml-like format.
+
+Similarly, the active serial assertion can be used for the output instead of the
+model assertion.
+`)
+
+ invalidTypeMessage = i18n.G("invalid type for %q header")
+ errNoMainAssertion = errors.New(i18n.G("device not ready yet (no assertions found)"))
+ errNoSerial = errors.New(i18n.G("device not registered yet (no serial assertion found)"))
+ errNoVerboseAssertion = errors.New(i18n.G("cannot use --verbose with --assertion"))
+
+ // this list is a "nice" "human" "readable" "ordering" of headers to print
+ // off, sorted in lexographical order with meta headers and primary key
+ // headers removed, and big nasty keys such as device-key-sha3-384 and
+ // device-key at the bottom
+ // it also contains both serial and model assertion headers, but we
+ // follow the same code path for both assertion types and some of the
+ // headers are shared between the two, so it still works out correctly
+ niceOrdering = [...]string{
+ "architecture",
+ "base",
+ "classic",
+ "display-name",
+ "gadget",
+ "kernel",
+ "revision",
+ "timestamp",
+ "required-snaps",
+ "device-key-sha3-384",
+ "device-key",
+ }
+)
+
+type cmdModel struct {
+ waitMixin
+ timeMixin
+ colorMixin
+
+ Serial bool `long:"serial"`
+ Verbose bool `long:"verbose"`
+ Assertion bool `long:"assertion"`
+}
+
+func init() {
+ addCommand("model",
+ shortModelHelp,
+ longModelHelp,
+ func() flags.Commander {
+ return &cmdModel{}
+ }, colorDescs.also(timeDescs).also(waitDescs).also(map[string]string{
+ "assertion": i18n.G("Print the raw assertion."),
+ "verbose": i18n.G("Print all specific assertion fields."),
+ "serial": i18n.G(
+ "Print the serial assertion instead of the model assertion."),
+ }),
+ []argDesc{},
+ )
+}
+
+func (x *cmdModel) Execute(args []string) error {
+ if x.Verbose && x.Assertion {
+ // can't do a verbose mode for the assertion
+ return errNoVerboseAssertion
+ }
+
+ var mainAssertion asserts.Assertion
+ serialAssertion, serialErr := x.client.CurrentSerialAssertion()
+ modelAssertion, modelErr := x.client.CurrentModelAssertion()
+
+ // if we didn't get a model assertion bail early
+ if modelErr != nil {
+ if client.IsAssertionNotFoundError(modelErr) {
+ // device is not registered yet - use specific error message
+ return errNoMainAssertion
+ }
+ return modelErr
+ }
+
+ // if the serial assertion error is anything other than not found, also
+ // bail early
+ // the serial assertion not being found may not be fatal
+ if serialErr != nil && !client.IsAssertionNotFoundError(serialErr) {
+ return serialErr
+ }
+
+ if x.Serial {
+ mainAssertion = serialAssertion
+ } else {
+ mainAssertion = modelAssertion
+ }
+
+ if x.Assertion {
+ // if we are using the serial assertion and we specifically didn't find the
+ // serial assertion, bail with specific error
+ if x.Serial && client.IsAssertionNotFoundError(serialErr) {
+ return errNoMainAssertion
+ }
+
+ _, err := Stdout.Write(asserts.Encode(mainAssertion))
+ return err
+ }
+
+ termWidth, _ := termSize()
+ termWidth -= 3
+ if termWidth > 100 {
+ // any wider than this and it gets hard to read
+ termWidth = 100
+ }
+
+ esc := x.getEscapes()
+
+ w := tabWriter()
+
+ if x.Serial && client.IsAssertionNotFoundError(serialErr) {
+ // for serial assertion, the primary keys are output (model and
+ // brand-id), but if we didn't find the serial assertion then we still
+ // output the brand-id and model from the model assertion, but also
+ // return a devNotReady error
+ fmt.Fprintf(w, "brand-id:\t%s\n", modelAssertion.HeaderString("brand-id"))
+ fmt.Fprintf(w, "model:\t%s\n", modelAssertion.HeaderString("model"))
+ w.Flush()
+ return errNoSerial
+ }
+
+ // the rest of this function is the main flow for outputting either the
+ // model or serial assertion in normal or verbose mode
+
+ // for the `snap model` case with no options, we don't want colons, we want
+ // to be like `snap version`
+ separator := ":"
+ if !x.Verbose && !x.Serial {
+ separator = ""
+ }
+
+ // ordering of the primary keys for model: brand, model, serial
+ // ordering of primary keys for serial is brand-id, model, serial
+
+ // output brand/brand-id
+ brandIDHeader := mainAssertion.HeaderString("brand-id")
+ modelHeader := mainAssertion.HeaderString("model")
+ // for the serial header, if there's no serial yet, it's not an error for
+ // model (and we already handled the serial error above) but need to add a
+ // parenthetical about the device not being registered yet
+ var serial string
+ if client.IsAssertionNotFoundError(serialErr) {
+ if x.Verbose || x.Serial {
+ // verbose and serial are yamlish, so we need to escape the dash
+ serial = esc.dash
+ } else {
+ serial = "-"
+ }
+ serial += " (device not registered yet)"
+ } else {
+ serial = serialAssertion.HeaderString("serial")
+ }
+
+ // handle brand/brand-id (the former is only on `snap model` w/o opts)
+ if x.Serial || x.Verbose {
+ fmt.Fprintf(w, "brand-id:\t%s\n", brandIDHeader)
+ } else {
+ // for the model command (not --serial) we want to show a publisher
+ // style display of "brand" instead of just "brand-id"
+ storeAccount, err := x.client.StoreAccount(brandIDHeader)
+ if err != nil {
+ return err
+ }
+ // use the longPublisher helper to format the brand store account
+ // like we do in `snap info`
+ fmt.Fprintf(w, "brand%s\t%s\n", separator, longPublisher(x.getEscapes(), storeAccount))
+ }
+
+ // handle model, on `snap model` we try to add display-name if it exists
+ if x.Serial {
+ fmt.Fprintf(w, "model:\t%s\n", modelHeader)
+ } else {
+ // for model, if there's a display-name, we show that first with the
+ // real model in parenthesis
+ if displayName := modelAssertion.HeaderString("display-name"); displayName != "" {
+ modelHeader = fmt.Sprintf("%s (%s)", displayName, modelHeader)
+ }
+ fmt.Fprintf(w, "model%s\t%s\n", separator, modelHeader)
+ }
+
+ // serial is same for all variants
+ fmt.Fprintf(w, "serial%s\t%s\n", separator, serial)
+
+ // --verbose means output more information
+ if x.Verbose {
+ allHeadersMap := mainAssertion.Headers()
+
+ for _, headerName := range niceOrdering {
+ invalidTypeErr := fmt.Errorf(invalidTypeMessage, headerName)
+
+ headerValue, ok := allHeadersMap[headerName]
+ // make sure the header is in the map
+ if !ok {
+ continue
+ }
+
+ // switch on which header it is to handle some special cases
+ switch headerName {
+ // list of scalars
+ case "required-snaps":
+ headerIfaceList, ok := headerValue.([]interface{})
+ if !ok {
+ return invalidTypeErr
+ }
+ if len(headerIfaceList) == 0 {
+ continue
+ }
+ fmt.Fprintf(w, "%s:\t\n", headerName)
+ for _, elem := range headerIfaceList {
+ headerStringElem, ok := elem.(string)
+ if !ok {
+ return invalidTypeErr
+ }
+ // note we don't wrap these, since for now this is
+ // specifically just required-snaps and so all of these
+ // will be snap names which are required to be short
+ fmt.Fprintf(w, " - %s\n", headerStringElem)
+ }
+
+ //timestamp needs to be formatted with fmtTime from the timeMixin
+ case "timestamp":
+ timestamp, ok := headerValue.(string)
+ if !ok {
+ return invalidTypeErr
+ }
+
+ // parse the time string as RFC3339, which is what the format is
+ // always in for assertions
+ t, err := time.Parse(time.RFC3339, timestamp)
+ if err != nil {
+ return err
+ }
+ fmt.Fprintf(w, "timestamp:\t%s\n", x.fmtTime(t))
+
+ // long string key we don't want to rewrap but can safely handle
+ // on "reasonable" width terminals
+ case "device-key-sha3-384":
+ // also flush the writer before continuing so the previous keys
+ // don't try to align with this key
+ w.Flush()
+ headerString, ok := headerValue.(string)
+ if !ok {
+ return invalidTypeErr
+ }
+
+ switch {
+ case termWidth > 86:
+ fmt.Fprintf(w, "device-key-sha3-384: %s\n", headerString)
+ case termWidth <= 86 && termWidth > 66:
+ fmt.Fprintln(w, "device-key-sha3-384: |")
+ wrapLine(w, []rune(headerString), " ", termWidth)
+ }
+
+ // long base64 key we can rewrap safely
+ case "device-key":
+ headerString, ok := headerValue.(string)
+ if !ok {
+ return invalidTypeErr
+ }
+ // the string value here has newlines inserted as part of the
+ // raw assertion, but base64 doesn't care about whitespace, so
+ // it's safe to split by newlines and re-wrap to make it
+ // prettier
+ headerString = strings.Join(
+ strings.Split(headerString, "\n"),
+ "")
+ fmt.Fprintln(w, "device-key: |")
+ wrapLine(w, []rune(headerString), " ", termWidth)
+
+ // the default is all the rest of short scalar values, which all
+ // should be strings
+ default:
+ headerString, ok := headerValue.(string)
+ if !ok {
+ return invalidTypeErr
+ }
+ fmt.Fprintf(w, "%s:\t%s\n", headerName, headerString)
+ }
+ }
+ }
+
+ return w.Flush()
+}
diff -Nru snapd-2.40/cmd/snap/cmd_model_test.go snapd-2.42.1/cmd/snap/cmd_model_test.go
--- snapd-2.40/cmd/snap/cmd_model_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_model_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,458 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package main_test
+
+import (
+ "fmt"
+ "net/http"
+
+ "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+const happyModelAssertionResponse = `type: model
+authority-id: mememe
+series: 16
+brand-id: mememe
+model: test-model
+architecture: amd64
+base: core18
+gadget: pc=18
+kernel: pc-kernel=18
+required-snaps:
+ - core
+ - hello-world
+timestamp: 2017-07-27T00:00:00.0Z
+sign-key-sha3-384: 8B3Wmemeu3H6i4dEV4Q85Q4gIUCHIBCNMHq49e085QeLGHi7v27l3Cqmemer4__t
+
+AcLBcwQAAQoAHRYhBMbX+t6MbKGH5C3nnLZW7+q0g6ELBQJdTdwTAAoJELZW7+q0g6ELEvgQAI3j
+jXTqR6kKOqvw94pArwdMDUaZ++tebASAZgso8ejrW2DQGWSc0Q7SQICIR8bvHxqS1GtupQswOzwS
+U8hjDTv7WEchH1jylyTj/1W1GernmitTKycecRlEkSOE+EpuqBFgTtj6PdA1Fj3CiCRi1rLMhgF2
+luCOitBLaP+E8P3fuATsLqqDLYzt1VY4Y14MU75hMn+CxAQdnOZTI+NzGMasPsldmOYCPNaN/b3N
+6/fDLU47RtNlMJ3K0Tz8kj0bqRbegKlD0RdNbAgo9iZwNmrr5E9WCu9f/0rUor/NIxO77H2ExIll
+zhmsZ7E6qlxvAgBmzKgAXrn68gGrBkIb0eXKiCaKy/i2ApvjVZ9HkOzA6Ldd+SwNJv/iA8rdiMsq
+p2BfKV5f3ju5b6+WktHxAakJ8iqQmj9Yh7piHjsOAUf1PEJd2s2nqQ+pEEn1F0B23gVCY/Fa9YRQ
+iKtWVeL3rBw4dSAaK9rpTMqlNcr+yrdXfTK5YzkCC6RU4yzc5MW0hKeseeSiEDSaRYxvftjFfVNa
+ZaVXKg8Lu+cHtCJDeYXEkPIDQzXswdBO1M8Mb9D0mYxQwHxwvsWv1DByB+Otq08EYgPh4kyHo7ag
+85yK2e/NQ/fxSwQJMhBF74jM1z9arq6RMiE/KOleFAOraKn2hcROKnEeinABW+sOn6vNuMVv
+`
+
+const happyModelWithDisplayNameAssertionResponse = `type: model
+authority-id: mememe
+series: 16
+brand-id: mememe
+model: test-model
+architecture: amd64
+display-name: Model Name
+base: core18
+gadget: pc=18
+kernel: pc-kernel=18
+required-snaps:
+ - core
+ - hello-world
+timestamp: 2017-07-27T00:00:00.0Z
+sign-key-sha3-384: 8B3Wmemeu3H6i4dEV4Q85Q4gIUCHIBCNMHq49e085QeLGHi7v27l3Cqmemer4__t
+
+AcLBcwQAAQoAHRYhBMbX+t6MbKGH5C3nnLZW7+q0g6ELBQJdTdwTAAoJELZW7+q0g6ELEvgQAI3j
+jXTqR6kKOqvw94pArwdMDUaZ++tebASAZgso8ejrW2DQGWSc0Q7SQICIR8bvHxqS1GtupQswOzwS
+U8hjDTv7WEchH1jylyTj/1W1GernmitTKycecRlEkSOE+EpuqBFgTtj6PdA1Fj3CiCRi1rLMhgF2
+luCOitBLaP+E8P3fuATsLqqDLYzt1VY4Y14MU75hMn+CxAQdnOZTI+NzGMasPsldmOYCPNaN/b3N
+6/fDLU47RtNlMJ3K0Tz8kj0bqRbegKlD0RdNbAgo9iZwNmrr5E9WCu9f/0rUor/NIxO77H2ExIll
+zhmsZ7E6qlxvAgBmzKgAXrn68gGrBkIb0eXKiCaKy/i2ApvjVZ9HkOzA6Ldd+SwNJv/iA8rdiMsq
+p2BfKV5f3ju5b6+WktHxAakJ8iqQmj9Yh7piHjsOAUf1PEJd2s2nqQ+pEEn1F0B23gVCY/Fa9YRQ
+iKtWVeL3rBw4dSAaK9rpTMqlNcr+yrdXfTK5YzkCC6RU4yzc5MW0hKeseeSiEDSaRYxvftjFfVNa
+ZaVXKg8Lu+cHtCJDeYXEkPIDQzXswdBO1M8Mb9D0mYxQwHxwvsWv1DByB+Otq08EYgPh4kyHo7ag
+85yK2e/NQ/fxSwQJMhBF74jM1z9arq6RMiE/KOleFAOraKn2hcROKnEeinABW+sOn6vNuMVv
+`
+
+const happyAccountAssertionResponse = `type: account
+authority-id: canonical
+account-id: mememe
+display-name: MeMeMe
+timestamp: 2016-04-01T00:00:00.0Z
+username: meuser
+validation: certified
+sign-key-sha3-384: -CvQKAwRQ5h3Ffn10FILJoEZUXOv6km9FwA80-Rcj-f-6jadQ89VRswHNiEB9Lxk
+
+AcLDXAQAAQoABgUCV7UYzwAKCRDUpVvql9g3IK7uH/4udqNOurx5WYVknzXdwekp0ovHCQJ0iBPw
+TSFxEVr9faZSzb7eqJ1WicHsShf97PYS3ClRYAiluFsjRA8Y03kkSVJHjC+sIwGFubsnkmgflt6D
+WEmYIl0UBmeaEDS8uY4Xvp9NsLTzNEj2kvzy/52gKaTc1ZSl5RDL9ppMav+0V9iBYpiDPBWH2rJ+
+aDSD8Rkyygm0UscfAKyDKH4lrvZ0WkYyi1YVNPrjQ/AtBySh6Q4iJ3LifzKa9woIyAuJET/4/FPY
+oirqHAfuvNod36yNQIyNqEc20AvTvZNH0PSsg4rq3DLjIPzv5KbJO9lhsasNJK1OdL6x8Yqrdsbk
+ldZp4qkzfjV7VOMQKaadfcZPRaVVeJWOBnBiaukzkhoNlQi1sdCdkBB/AJHZF8QXw6c7vPDcfnCV
+1lW7ddQ2p8IsJbT6LzpJu3GW/P4xhNgCjtCJ1AJm9a9RqLwQYgdLZwwDa9iCRtqTbRXBlfy3apps
+1VjbQ3h5iCd0hNfwDBnGVm1rhLKHCD1DUdNE43oN2ZlE7XGyh0HFV6vKlpqoW3eoXCIxWu+HBY96
++LSl/jQgCkb0nxYyzEYK4Reb31D0mYw1Nji5W+MIF5E09+DYZoOT0UvR05YMwMEOeSdI/hLWg/5P
+k+GDK+/KopMmpd4D1+jjtF7ZvqDpmAV98jJGB2F88RyVb4gcjmFFyTi4Kv6vzz/oLpbm0qrizC0W
+HLGDN/ymGA5sHzEgEx7U540vz/q9VX60FKqL2YZr/DcyY9GKX5kCG4sNqIIHbcJneZ4frM99oVDu
+7Jv+DIx/Di6D1ULXol2XjxbbJLKHFtHksR97ceaFvcZwTogC61IYUBJCvvMoqdXAWMhEXCr0QfQ5
+Xbi31XW2d4/lF/zWlAkRnGTzufIXFni7+nEuOK0SQEzO3/WaRedK1SGOOtTDjB8/3OJeW96AUYK5
+oTIynkYkEyHWMNCXALg+WQW6L4/YO7aUjZ97zOWIugd7Xy63aT3r/EHafqaY2nacOhLfkeKZ830b
+o/ezjoZQAxbh6ce7JnXRgE9ELxjdAhBTpGjmmmN2sYrJ7zP9bOgly0BnEPXGSQfFA+NNNw1FADx1
+MUY8q9DBjmVtgqY+1KGTV5X8KvQCBMODZIf/XJPHdCRAHxMd8COypcwgL2vDIIXpOFbi1J/B0GF+
+eklxk9wzBA8AecBMCwCzIRHDNpD1oa2we38bVFrOug6e/VId1k1jYFJjiLyLCDmV8IMYwEllHSXp
+LQAdm3xZ7t4WnxYC8YSCk9mXf3CZg59SpmnV5Q5Z6A5Pl7Nc3sj7hcsMBZEsOMPzNC9dPsBnZvjs
+WpPUffJzEdhHBFhvYMuD4Vqj6ejUv9l3oTrjQWVC`
+
+// note: this serial assertion was generated by adding print statements to the
+// test in api_model_test.go that generate a fake serial assertion
+const happySerialAssertionResponse = `type: serial
+authority-id: my-brand
+brand-id: my-brand
+model: my-old-model
+serial: serialserial
+device-key:
+ AcZrBFaFwYABAvCgEOrrLA6FKcreHxCcOoTgBUZ+IRG7Nb8tzmEAklaQPGpv7skapUjwD1luE2go
+ mTcoTssVHrfLpBoSDV1aBs44rg3NK40ZKPJP7d2zkds1GxUo1Ea5vfet3SJ4h3aRABEBAAE=
+device-key-sha3-384: iqLo9doLzK8De9925UrdUyuvPbBad72OTWVE9YJXqd6nz9dKvwJ_lHP5bVxrl3VO
+timestamp: 2019-08-26T16:34:21-05:00
+sign-key-sha3-384: anCEGC2NYq7DzDEi6y7OafQCVeVLS90XlLt9PNjrRl9sim5rmRHDDNFNO7ODcWQW
+
+AcJwBAABCgAGBQJdZFBdAADCLALwR6Sy24wm9PffwbvUhOEXneyY3BnxKC0+NgdHu1gU8go9vEP1
+i+Flh5uoS70+MBIO+nmF8T+9JWIx2QWFDDxvcuFosnIhvUajCEQohauys5FMz/H/WvB0vrbTBpvK
+eg==
+`
+
+const noModelAssertionYetResponse = `
+{
+ "type": "error",
+ "status-code": 404,
+ "status": "Not Found",
+ "result": {
+ "message": "no model assertion yet",
+ "kind": "assertion-not-found",
+ "value": "model"
+ }
+}`
+
+const noSerialAssertionYetResponse = `
+{
+ "type": "error",
+ "status-code": 404,
+ "status": "Not Found",
+ "result": {
+ "message": "no serial assertion yet",
+ "kind": "assertion-not-found",
+ "value": "serial"
+ }
+}`
+
+// helper for constructing different types of responses to the client
+type checkResponder func(c *check.C, w http.ResponseWriter, r *http.Request)
+
+func simpleHappyResponder(body string) checkResponder {
+ return func(c *check.C, w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.RawQuery, check.Equals, "")
+ fmt.Fprintln(w, body)
+ }
+}
+
+func simpleUnhappyResponder(errBody string) checkResponder {
+ return func(c *check.C, w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, check.Equals, "GET")
+ c.Check(r.URL.RawQuery, check.Equals, "")
+ w.Header().Set("Content-Type", "application/json")
+ w.WriteHeader(404)
+ fmt.Fprintln(w, errBody)
+ }
+}
+
+func simpleAssertionAccountResponder(body string) checkResponder {
+ return func(c *check.C, w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Method, check.Equals, "GET")
+ w.Header().Set("X-Ubuntu-Assertions-Count", "1")
+ fmt.Fprintln(w, body)
+ }
+}
+
+func makeHappyTestServerHandler(c *check.C, modelResp, serialResp, accountResp checkResponder) func(w http.ResponseWriter, r *http.Request) {
+ var nModelSerial, nModel, nKnown int
+ return func(w http.ResponseWriter, r *http.Request) {
+ switch r.URL.Path {
+ case "/v2/model":
+ switch nModel {
+ case 0:
+ modelResp(c, w, r)
+ default:
+ c.Fatalf("expected to get 1 request for /v2/model, now on %d", nModel+1)
+ }
+ nModel++
+ case "/v2/model/serial":
+ switch nModelSerial {
+ case 0:
+ serialResp(c, w, r)
+ default:
+ c.Fatalf("expected to get 1 request for /v2/model, now on %d", nModelSerial+1)
+ }
+ nModelSerial++
+ case "/v2/assertions/account":
+ switch nKnown {
+ case 0:
+ accountResp(c, w, r)
+ default:
+ c.Fatalf("expected to get 1 request for /v2/model, now on %d", nKnown+1)
+ }
+ nKnown++
+ default:
+ c.Fatalf("unexpected request to %s", r.URL.Path)
+ }
+ }
+}
+
+func (s *SnapSuite) TestNoModelYet(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleUnhappyResponder(noModelAssertionYetResponse),
+ simpleUnhappyResponder(noSerialAssertionYetResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"model"})
+ c.Assert(err, check.ErrorMatches, `device not ready yet \(no assertions found\)`)
+}
+
+func (s *SnapSuite) TestNoSerialYet(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleUnhappyResponder(noSerialAssertionYetResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--serial"})
+ c.Assert(err, check.ErrorMatches, `device not registered yet \(no serial assertion found\)`)
+ c.Check(s.Stderr(), check.Equals, "")
+ c.Check(s.Stdout(), check.Equals, `
+brand-id: mememe
+model: test-model
+`[1:])
+}
+
+func (s *SnapSuite) TestModel(c *check.C) {
+
+ for _, tt := range []struct {
+ comment string
+ modelF checkResponder
+ serialF checkResponder
+ outText string
+ }{
+ {
+ comment: "normal serial and model asserts",
+ modelF: simpleHappyResponder(happyModelAssertionResponse),
+ serialF: simpleHappyResponder(happySerialAssertionResponse),
+ outText: `
+brand MeMeMe (meuser*)
+model test-model
+serial serialserial
+`[1:],
+ },
+ {
+ comment: "model assert has display-name",
+ modelF: simpleHappyResponder(happyModelWithDisplayNameAssertionResponse),
+ serialF: simpleHappyResponder(happySerialAssertionResponse),
+ outText: `
+brand MeMeMe (meuser*)
+model Model Name (test-model)
+serial serialserial
+`[1:],
+ },
+ {
+ comment: "missing serial assert",
+ modelF: simpleHappyResponder(happyModelAssertionResponse),
+ serialF: simpleUnhappyResponder(noSerialAssertionYetResponse),
+ outText: `
+brand MeMeMe (meuser*)
+model test-model
+serial - (device not registered yet)
+`[1:],
+ },
+ } {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ tt.modelF,
+ tt.serialF,
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, tt.outText, check.Commentf("\n%s\n", tt.outText))
+ c.Check(s.Stderr(), check.Equals, "")
+ s.ResetStdStreams()
+ }
+}
+
+func (s *SnapSuite) TestModelVerbose(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleHappyResponder(happySerialAssertionResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--verbose", "--abs-time"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `
+brand-id: mememe
+model: test-model
+serial: serialserial
+architecture: amd64
+base: core18
+gadget: pc=18
+kernel: pc-kernel=18
+timestamp: 2017-07-27T00:00:00Z
+required-snaps:
+ - core
+ - hello-world
+`[1:])
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestModelVerboseNoSerialYet(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleUnhappyResponder(noSerialAssertionYetResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--verbose", "--abs-time"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `
+brand-id: mememe
+model: test-model
+serial: -- (device not registered yet)
+architecture: amd64
+base: core18
+gadget: pc=18
+kernel: pc-kernel=18
+timestamp: 2017-07-27T00:00:00Z
+required-snaps:
+ - core
+ - hello-world
+`[1:])
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestModelAssertion(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleHappyResponder(happySerialAssertionResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--assertion"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, happyModelAssertionResponse)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestModelAssertionVerbose(c *check.C) {
+ // check that no calls to the server happen
+ s.RedirectClientToTestServer(
+ func(w http.ResponseWriter, r *http.Request) {
+ c.Fatalf("unexpected request to %s", r.URL.Path)
+ },
+ )
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--assertion", "--verbose"})
+ c.Assert(err, check.ErrorMatches, "cannot use --verbose with --assertion")
+ c.Check(s.Stdout(), check.Equals, "")
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestSerial(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleHappyResponder(happySerialAssertionResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--serial"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `
+brand-id: my-brand
+model: my-old-model
+serial: serialserial
+`[1:])
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestSerialVerbose(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleHappyResponder(happySerialAssertionResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--serial", "--verbose", "--abs-time"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, `
+brand-id: my-brand
+model: my-old-model
+serial: serialserial
+timestamp: 2019-08-26T16:34:21-05:00
+device-key-sha3-384: |
+ iqLo9doLzK8De9925UrdUyuvPbBad72OTWVE9YJXqd6nz9dKvwJ_lHP5bVxrl3VO
+device-key: |
+ AcZrBFaFwYABAvCgEOrrLA6FKcreHxCcOoTgBUZ+IRG7Nb8tzmEAklaQPGpv7skapUjwD1luE2g
+ omTcoTssVHrfLpBoSDV1aBs44rg3NK40ZKPJP7d2zkds1GxUo1Ea5vfet3SJ4h3aRABEBAAE=
+`[1:])
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestSerialAssertion(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleHappyResponder(happySerialAssertionResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--serial", "--assertion"})
+ c.Assert(err, check.IsNil)
+ c.Assert(rest, check.DeepEquals, []string{})
+ c.Check(s.Stdout(), check.Equals, happySerialAssertionResponse)
+ c.Check(s.Stderr(), check.Equals, "")
+}
+
+func (s *SnapSuite) TestSerialAssertionSerialAssertionMissing(c *check.C) {
+ s.RedirectClientToTestServer(
+ makeHappyTestServerHandler(
+ c,
+ simpleHappyResponder(happyModelAssertionResponse),
+ simpleUnhappyResponder(noSerialAssertionYetResponse),
+ simpleAssertionAccountResponder(happyAccountAssertionResponse),
+ ))
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"model", "--serial", "--assertion"})
+ c.Assert(err, check.ErrorMatches, `device not ready yet \(no assertions found\)`)
+ c.Assert(s.Stdout(), check.Equals, "")
+ c.Assert(s.Stderr(), check.Equals, "")
+}
diff -Nru snapd-2.40/cmd/snap/cmd_prefer_test.go snapd-2.42.1/cmd/snap/cmd_prefer_test.go
--- snapd-2.40/cmd/snap/cmd_prefer_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_prefer_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -52,6 +52,7 @@
"action": "prefer",
"snap": "some-snap",
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
diff -Nru snapd-2.40/cmd/snap/cmd_remodel.go snapd-2.42.1/cmd/snap/cmd_remodel.go
--- snapd-2.40/cmd/snap/cmd_remodel.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_remodel.go 2019-10-30 12:17:43.000000000 +0000
@@ -81,6 +81,6 @@
}
return err
}
- fmt.Fprintf(Stdout, i18n.G("New model %s set"), newModelFile)
+ fmt.Fprintf(Stdout, i18n.G("New model %s set\n"), newModelFile)
return nil
}
diff -Nru snapd-2.40/cmd/snap/cmd_set.go snapd-2.42.1/cmd/snap/cmd_set.go
--- snapd-2.40/cmd/snap/cmd_set.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_set.go 2019-10-30 12:17:43.000000000 +0000
@@ -40,7 +40,10 @@
Nested values may be modified via a dotted path:
- $ snap set author.name=frank
+ $ snap set snap-name author.name=frank
+
+Configuration option may be unset with exclamation mark:
+ $ snap set snap-name author!
`)
type cmdSet struct {
@@ -61,7 +64,7 @@
// TRANSLATORS: This needs to begin with < and end with >
name: i18n.G(""),
// TRANSLATORS: This should not start with a lowercase letter.
- desc: i18n.G("Configuration value (key=value)"),
+ desc: i18n.G("Set (key=value) or unset (key!) configuration value"),
},
})
}
@@ -70,6 +73,10 @@
patchValues := make(map[string]interface{})
for _, patchValue := range x.Positional.ConfValues {
parts := strings.SplitN(patchValue, "=", 2)
+ if len(parts) == 1 && strings.HasSuffix(patchValue, "!") {
+ patchValues[strings.TrimSuffix(patchValue, "!")] = nil
+ continue
+ }
if len(parts) != 2 {
return fmt.Errorf(i18n.G("invalid configuration: %q (want key=value)"), patchValue)
}
diff -Nru snapd-2.40/cmd/snap/cmd_set_test.go snapd-2.42.1/cmd/snap/cmd_set_test.go
--- snapd-2.40/cmd/snap/cmd_set_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_set_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -27,78 +27,89 @@
"gopkg.in/check.v1"
snapset "github.com/snapcore/snapd/cmd/snap"
- "github.com/snapcore/snapd/snap"
- "github.com/snapcore/snapd/snap/snaptest"
)
-var validApplyYaml = []byte(`name: snapname
-version: 1.0
-hooks:
- configure:
-`)
+type snapSetSuite struct {
+ BaseSnapSuite
-func (s *SnapSuite) TestInvalidSetParameters(c *check.C) {
+ setConfApiCalls int
+}
+
+var _ = check.Suite(&snapSetSuite{})
+
+func (s *snapSetSuite) SetUpTest(c *check.C) {
+ s.BaseSnapSuite.SetUpTest(c)
+ s.setConfApiCalls = 0
+}
+
+func (s *snapSetSuite) TestInvalidSetParameters(c *check.C) {
invalidParameters := []string{"set", "snap-name", "key", "value"}
_, err := snapset.Parser(snapset.Client()).ParseArgs(invalidParameters)
c.Check(err, check.ErrorMatches, ".*invalid configuration:.*(want key=value).*")
+ c.Check(s.setConfApiCalls, check.Equals, 0)
}
-func (s *SnapSuite) TestSnapSetIntegrationString(c *check.C) {
- // mock installed snap
- snaptest.MockSnap(c, string(validApplyYaml), &snap.SideInfo{
- Revision: snap.R(42),
- })
-
+func (s *snapSetSuite) TestSnapSetIntegrationString(c *check.C) {
// and mock the server
s.mockSetConfigServer(c, "value")
// Set a config value for the active snap
_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=value"})
c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
}
-func (s *SnapSuite) TestSnapSetIntegrationNumber(c *check.C) {
- // mock installed snap
- snaptest.MockSnap(c, string(validApplyYaml), &snap.SideInfo{
- Revision: snap.R(42),
- })
-
+func (s *snapSetSuite) TestSnapSetIntegrationNumber(c *check.C) {
// and mock the server
s.mockSetConfigServer(c, json.Number("1.2"))
// Set a config value for the active snap
_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=1.2"})
c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
}
-func (s *SnapSuite) TestSnapSetIntegrationBigInt(c *check.C) {
- snaptest.MockSnap(c, string(validApplyYaml), &snap.SideInfo{
- Revision: snap.R(42),
- })
-
+func (s *snapSetSuite) TestSnapSetIntegrationBigInt(c *check.C) {
// and mock the server
s.mockSetConfigServer(c, json.Number("1234567890"))
// Set a config value for the active snap
_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=1234567890"})
c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
}
-func (s *SnapSuite) TestSnapSetIntegrationJson(c *check.C) {
- // mock installed snap
- snaptest.MockSnap(c, string(validApplyYaml), &snap.SideInfo{
- Revision: snap.R(42),
- })
-
+func (s *snapSetSuite) TestSnapSetIntegrationJson(c *check.C) {
// and mock the server
s.mockSetConfigServer(c, map[string]interface{}{"subkey": "value"})
// Set a config value for the active snap
_, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", `key={"subkey":"value"}`})
c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
+}
+
+func (s *snapSetSuite) TestSnapSetIntegrationUnsetWithExclamationMark(c *check.C) {
+ // and mock the server
+ s.mockSetConfigServer(c, nil)
+
+ // Unset config value via exclamation mark
+ _, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key!"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
+}
+
+func (s *snapSetSuite) TestSnapSetIntegrationStringWithExclamationMark(c *check.C) {
+ // and mock the server
+ s.mockSetConfigServer(c, "value!")
+
+ // Set a config value ending with exclamation mark
+ _, err := snapset.Parser(snapset.Client()).ParseArgs([]string{"set", "snapname", "key=value!"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
}
-func (s *SnapSuite) mockSetConfigServer(c *check.C, expectedValue interface{}) {
+func (s *snapSetSuite) mockSetConfigServer(c *check.C, expectedValue interface{}) {
s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/v2/snaps/snapname/conf":
@@ -106,7 +117,9 @@
c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
"key": expectedValue,
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
+ s.setConfApiCalls += 1
case "/v2/changes/zzz":
c.Check(r.Method, check.Equals, "GET")
fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`)
diff -Nru snapd-2.40/cmd/snap/cmd_snap_op.go snapd-2.42.1/cmd/snap/cmd_snap_op.go
--- snapd-2.40/cmd/snap/cmd_snap_op.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_snap_op.go 2019-10-30 12:17:43.000000000 +0000
@@ -35,7 +35,7 @@
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/osutil"
- "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/channel"
"github.com/snapcore/snapd/strutil"
)
@@ -281,14 +281,14 @@
}
var trackingRisk, currentRisk string
if tracking != "" {
- traCh, err := snap.ParseChannel(tracking, "")
+ traCh, err := channel.Parse(tracking, "")
if err != nil {
return false, err
}
trackingRisk = traCh.Risk
}
if current != "" {
- curCh, err := snap.ParseChannel(current, "")
+ curCh, err := channel.Parse(current, "")
if err != nil {
return false, err
}
@@ -345,13 +345,39 @@
case "revert":
// TRANSLATORS: first %s is a snap name, second %s is a revision
fmt.Fprintf(Stdout, i18n.G("%s reverted to %s\n"), snap.Name, snap.Version)
+ case "switch":
+ switchCohort := opts.CohortKey != ""
+ switchChannel := opts.Channel != ""
+ var msg string
+ // we have three boolean things to check, meaning 2³=8 possibilities,
+ // minus 3 error cases which are handled before the call to showDone.
+ switch {
+ case switchCohort && !opts.LeaveCohort && !switchChannel:
+ // TRANSLATORS: the first %q will be the (quoted) snap name, the second an ellipted cohort string
+ msg = fmt.Sprintf(i18n.G("%q switched to the %q cohort\n"), snap.Name, strutil.ElliptLeft(opts.CohortKey, 10))
+ case switchCohort && !opts.LeaveCohort && switchChannel:
+ // TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel, the third an ellipted cohort string
+ msg = fmt.Sprintf(i18n.G("%q switched to the %q channel and the %q cohort\n"), snap.Name, snap.TrackingChannel, strutil.ElliptLeft(opts.CohortKey, 10))
+ case !switchCohort && !opts.LeaveCohort && switchChannel:
+ // TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel
+ msg = fmt.Sprintf(i18n.G("%q switched to the %q channel\n"), snap.Name, snap.TrackingChannel)
+ case !switchCohort && opts.LeaveCohort && switchChannel:
+ // TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel
+ msg = fmt.Sprintf(i18n.G("%q left the cohort, and switched to the %q channel"), snap.Name, snap.TrackingChannel)
+ case !switchCohort && opts.LeaveCohort && !switchChannel:
+ // TRANSLATORS: %q will be the (quoted) snap name
+ msg = fmt.Sprintf(i18n.G("%q left the cohort"), snap.Name)
+ }
+ fmt.Fprintln(Stdout, msg)
default:
fmt.Fprintf(Stdout, "internal error: unknown op %q", op)
}
- if snap.TrackingChannel != snap.Channel && snap.Channel != "" {
- if sameRisk, err := isSameRisk(snap.TrackingChannel, snap.Channel); err == nil && !sameRisk {
- // TRANSLATORS: first %s is a channel name, following %s is a snap name, last %s is a channel name again.
- fmt.Fprintf(Stdout, i18n.G("Channel %s for %s is closed; temporarily forwarding to %s.\n"), snap.TrackingChannel, snap.Name, snap.Channel)
+ if op == "install" || op == "refresh" {
+ if snap.TrackingChannel != snap.Channel && snap.Channel != "" {
+ if sameRisk, err := isSameRisk(snap.TrackingChannel, snap.Channel); err == nil && !sameRisk {
+ // TRANSLATORS: first %s is a channel name, following %s is a snap name, last %s is a channel name again.
+ fmt.Fprintf(Stdout, i18n.G("Channel %s for %s is closed; temporarily forwarding to %s.\n"), snap.TrackingChannel, snap.Name, snap.Channel)
+ }
}
}
}
@@ -979,34 +1005,19 @@
name := string(x.Positional.Snap)
channel := string(x.Channel)
- var msg string
- // some duplication between this and the two other switch-summarisers...
- // in this one, we have three boolean things to check, meaning 2³=8 possibilities
- // of which 3 are errors (which is why we look at this before running it)
switchCohort := x.Cohort != ""
switchChannel := x.Channel != ""
- switch {
- case switchCohort && x.LeaveCohort:
+
+ // we have three boolean things to check, meaning 2³=8 possibilities
+ // of which 3 are errors (which is why we look at the errors first).
+ // the 5 valid cases are handled by showDone.
+ if switchCohort && x.LeaveCohort {
// this one counts as two (no channel filter)
return fmt.Errorf(i18n.G("cannot specify both --cohort and --leave-cohort"))
- case switchCohort && !x.LeaveCohort && !switchChannel:
- // TRANSLATORS: the first %q will be the (quoted) snap name, the second an ellipted cohort string
- msg = fmt.Sprintf(i18n.G("%q switched to the %q cohort\n"), name, strutil.ElliptLeft(x.Cohort, 10))
- case switchCohort && !x.LeaveCohort && switchChannel:
- // TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel, the third an ellipted cohort string
- msg = fmt.Sprintf(i18n.G("%q switched to the %q channel and the %q cohort\n"), name, channel, strutil.ElliptLeft(x.Cohort, 10))
- case !switchCohort && !x.LeaveCohort && switchChannel:
- // TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel
- msg = fmt.Sprintf(i18n.G("%q switched to the %q channel\n"), name, channel)
- case !switchCohort && x.LeaveCohort && switchChannel:
- // TRANSLATORS: the first %q will be the (quoted) snap name, the second a channel
- msg = fmt.Sprintf(i18n.G("%q left the cohort, and switched to the %q channel"), name, channel)
- case !switchCohort && x.LeaveCohort && !switchChannel:
- // TRANSLATORS: %q will be the (quoted) snap name
- msg = fmt.Sprintf(i18n.G("%q left the cohort"), name)
- case !switchCohort && !x.LeaveCohort && !switchChannel:
+ }
+ if !switchCohort && !x.LeaveCohort && !switchChannel {
return fmt.Errorf(i18n.G("nothing to switch; specify --channel (and/or one of --cohort/--leave-cohort)"))
- } // and that's the 8 \o/
+ }
opts := &client.SnapOptions{
Channel: channel,
@@ -1025,8 +1036,7 @@
return err
}
- fmt.Fprintln(Stdout, msg)
- return nil
+ return showDone(x.client, []string{name}, "switch", opts, nil)
}
func init() {
diff -Nru snapd-2.40/cmd/snap/cmd_snap_op_test.go snapd-2.42.1/cmd/snap/cmd_snap_op_test.go
--- snapd-2.40/cmd/snap/cmd_snap_op_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_snap_op_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -943,7 +943,6 @@
c.Assert(rest, check.DeepEquals, []string{})
// tracking channel is "" in the test server
c.Check(s.Stdout(), check.Equals, `foo reverted to 1.0
-Channel for foo is closed; temporarily forwarding to potato.
`)
c.Check(s.Stderr(), check.Equals, "")
// ensure that the fake server api was actually hit
@@ -1876,27 +1875,30 @@
}
func (s *SnapOpSuite) TestSwitchHappy(c *check.C) {
- s.srv.total = 3
+ s.srv.total = 4
s.srv.checker = func(r *http.Request) {
c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
"action": "switch",
"channel": "beta",
})
+ s.srv.trackingChannel = "beta"
}
s.RedirectClientToTestServer(s.srv.handle)
rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--beta", "foo"})
c.Assert(err, check.IsNil)
c.Assert(rest, check.DeepEquals, []string{})
- c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" switched to the "beta" channel`)
+ c.Check(s.Stdout(), check.Equals, `"foo" switched to the "beta" channel
+
+`)
c.Check(s.Stderr(), check.Equals, "")
// ensure that the fake server api was actually hit
c.Check(s.srv.n, check.Equals, s.srv.total)
}
func (s *SnapOpSuite) TestSwitchHappyCohort(c *check.C) {
- s.srv.total = 3
+ s.srv.total = 4
s.srv.checker = func(r *http.Request) {
c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
@@ -1916,7 +1918,7 @@
}
func (s *SnapOpSuite) TestSwitchHappyLeaveCohort(c *check.C) {
- s.srv.total = 3
+ s.srv.total = 4
s.srv.checker = func(r *http.Request) {
c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
@@ -1936,7 +1938,7 @@
}
func (s *SnapOpSuite) TestSwitchHappyChannelAndCohort(c *check.C) {
- s.srv.total = 3
+ s.srv.total = 4
s.srv.checker = func(r *http.Request) {
c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
@@ -1944,6 +1946,7 @@
"cohort-key": "what",
"channel": "edge",
})
+ s.srv.trackingChannel = "edge"
}
s.RedirectClientToTestServer(s.srv.handle)
@@ -1957,7 +1960,7 @@
}
func (s *SnapOpSuite) TestSwitchHappyChannelAndLeaveCohort(c *check.C) {
- s.srv.total = 3
+ s.srv.total = 4
s.srv.checker = func(r *http.Request) {
c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo")
c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{
@@ -1965,6 +1968,7 @@
"leave-cohort": true,
"channel": "edge",
})
+ s.srv.trackingChannel = "edge"
}
s.RedirectClientToTestServer(s.srv.handle)
diff -Nru snapd-2.40/cmd/snap/cmd_snapshot.go snapd-2.42.1/cmd/snap/cmd_snapshot.go
--- snapd-2.40/cmd/snap/cmd_snapshot.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_snapshot.go 2019-10-30 12:17:43.000000000 +0000
@@ -336,14 +336,34 @@
}, waitDescs.also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"users": i18n.G("Restore data of only specific users (comma-separated) (default: all users)"),
- }), nil)
+ }), []argDesc{
+ {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("The snap for which data will be restored"),
+ }, {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("Set id of snapshot to restore (see 'snap help saved')"),
+ },
+ })
addCommand("forget",
shortForgetHelp,
longForgetHelp,
func() flags.Commander {
return &forgetCmd{}
- }, waitDescs, nil)
+ }, waitDescs, []argDesc{
+ {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("Set id of snapshot to delete (see 'snap help saved')"),
+ }, {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("The snap for which data will be deleted"),
+ },
+ })
addCommand("check-snapshot",
shortCheckHelp,
@@ -353,5 +373,15 @@
}, waitDescs.also(map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"users": i18n.G("Check data of only specific users (comma-separated) (default: all users)"),
- }), nil)
+ }), []argDesc{
+ {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("Set id of snapshot to verify (see 'snap help saved')"),
+ }, {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("The snap for which data will be verified"),
+ },
+ })
}
diff -Nru snapd-2.40/cmd/snap/cmd_snapshot_test.go snapd-2.42.1/cmd/snap/cmd_snapshot_test.go
--- snapd-2.40/cmd/snap/cmd_snapshot_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_snapshot_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -33,10 +33,10 @@
var snapshotsTests = []getCmdArgs{{
args: "restore x",
- error: "invalid argument for set id: expected a non-negative integer argument",
+ error: `invalid argument for snapshot set id: expected a non-negative integer argument \(see 'snap help saved'\)`,
}, {
args: "saved --id=x",
- error: "invalid argument for set id: expected a non-negative integer argument",
+ error: `invalid argument for snapshot set id: expected a non-negative integer argument \(see 'snap help saved'\)`,
}, {
args: "saved --id=3",
stdout: "Set Snap Age Version Rev Size Notes\n3 htop .* 2 1168 1B auto\n",
@@ -45,10 +45,10 @@
stdout: "Set Snap Age Version Rev Size Notes\n1 htop .* 2 1168 1B -\n",
}, {
args: "forget x",
- error: "invalid argument for set id: expected a non-negative integer argument",
+ error: `invalid argument for snapshot set id: expected a non-negative integer argument \(see 'snap help saved'\)`,
}, {
args: "check-snapshot x",
- error: "invalid argument for set id: expected a non-negative integer argument",
+ error: `invalid argument for snapshot set id: expected a non-negative integer argument \(see 'snap help saved'\)`,
}, {
args: "restore 1",
stdout: "Restored snapshot #1.\n",
@@ -102,6 +102,7 @@
}
fmt.Fprintf(w, `{"type":"sync","status-code":200,"status":"OK","result":[{"id":1,"snapshots":[{"set":1,"time":%q,"snap":"htop","revision":"1168","snap-id":"Z","epoch":{"read":[0],"write":[0]},"summary":"","version":"2","sha3-384":{"archive.tgz":""},"size":1}]}]}`, snapshotTime)
} else {
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "9"}`)
}
case "/v2/changes/9":
diff -Nru snapd-2.40/cmd/snap/cmd_unalias_test.go snapd-2.42.1/cmd/snap/cmd_unalias_test.go
--- snapd-2.40/cmd/snap/cmd_unalias_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_unalias_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -53,6 +53,7 @@
"snap": "alias1",
"alias": "alias1",
})
+ w.WriteHeader(202)
fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`)
case "/v2/changes/zzz":
c.Check(r.Method, Equals, "GET")
diff -Nru snapd-2.40/cmd/snap/cmd_unset.go snapd-2.42.1/cmd/snap/cmd_unset.go
--- snapd-2.40/cmd/snap/cmd_unset.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_unset.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,85 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package main
+
+import (
+ "github.com/jessevdk/go-flags"
+
+ "github.com/snapcore/snapd/i18n"
+)
+
+var shortUnsetHelp = i18n.G("Remove configuration options")
+var longUnsetHelp = i18n.G(`
+The unset command removes the provided configuration options as requested.
+
+ $ snap unset snap-name name address
+
+All configuration changes are persisted at once, and only after the
+snap's configuration hook returns successfully.
+
+Nested values may be removed via a dotted path:
+
+ $ snap unset snap-name user.name
+`)
+
+type cmdUnset struct {
+ waitMixin
+ Positional struct {
+ Snap installedSnapName
+ ConfKeys []string `required:"1"`
+ } `positional-args:"yes" required:"yes"`
+}
+
+func init() {
+ addCommand("unset", shortUnsetHelp, longUnsetHelp, func() flags.Commander { return &cmdUnset{} }, waitDescs, []argDesc{
+ {
+ name: "",
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("The snap to configure (e.g. hello-world)"),
+ }, {
+ // TRANSLATORS: This needs to begin with < and end with >
+ name: i18n.G(""),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ desc: i18n.G("Configuration key to unset"),
+ },
+ })
+}
+
+func (x *cmdUnset) Execute(args []string) error {
+ patchValues := make(map[string]interface{})
+ for _, confKey := range x.Positional.ConfKeys {
+ patchValues[confKey] = nil
+ }
+
+ snapName := string(x.Positional.Snap)
+ id, err := x.client.SetConf(snapName, patchValues)
+ if err != nil {
+ return err
+ }
+
+ if _, err := x.wait(id); err != nil {
+ if err == noWait {
+ return nil
+ }
+ return err
+ }
+
+ return nil
+}
diff -Nru snapd-2.40/cmd/snap/cmd_unset_test.go snapd-2.42.1/cmd/snap/cmd_unset_test.go
--- snapd-2.40/cmd/snap/cmd_unset_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_unset_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,47 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package main_test
+
+import (
+ "gopkg.in/check.v1"
+
+ snapunset "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *snapSetSuite) TestInvalidUnsetParameters(c *check.C) {
+ invalidParameters := []string{"unset"}
+ _, err := snapunset.Parser(snapunset.Client()).ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, "the required arguments `` and ` \\(at least 1 argument\\)` were not provided")
+ c.Check(s.setConfApiCalls, check.Equals, 0)
+
+ invalidParameters = []string{"unset", "snap-name"}
+ _, err = snapunset.Parser(snapunset.Client()).ParseArgs(invalidParameters)
+ c.Check(err, check.ErrorMatches, "the required argument ` \\(at least 1 argument\\)` was not provided")
+ c.Check(s.setConfApiCalls, check.Equals, 0)
+}
+
+func (s *snapSetSuite) TestSnapUnset(c *check.C) {
+ // expected value is "nil" as the key is unset
+ s.mockSetConfigServer(c, nil)
+
+ _, err := snapunset.Parser(snapunset.Client()).ParseArgs([]string{"unset", "snapname", "key"})
+ c.Assert(err, check.IsNil)
+ c.Check(s.setConfApiCalls, check.Equals, 1)
+}
diff -Nru snapd-2.40/cmd/snap/cmd_userd.go snapd-2.42.1/cmd/snap/cmd_userd.go
--- snapd-2.40/cmd/snap/cmd_userd.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_userd.go 2019-10-30 12:17:43.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2017 Canonical Ltd
+ * Copyright (C) 2017-2019 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
@@ -27,14 +27,16 @@
"github.com/jessevdk/go-flags"
+ "github.com/snapcore/snapd/cmd"
"github.com/snapcore/snapd/i18n"
- "github.com/snapcore/snapd/userd"
+ "github.com/snapcore/snapd/usersession/agent"
+ "github.com/snapcore/snapd/usersession/autostart"
+ "github.com/snapcore/snapd/usersession/userd"
)
type cmdUserd struct {
- userd userd.Userd
-
Autostart bool `long:"autostart"`
+ Agent bool `long:"agent"`
}
var shortUserdHelp = i18n.G("Start the userd service")
@@ -51,6 +53,8 @@
}, map[string]string{
// TRANSLATORS: This should not start with a lowercase letter.
"autostart": i18n.G("Autostart user applications"),
+ // TRANSLATORS: This should not start with a lowercase letter.
+ "agent": i18n.G("Run the user session agent"),
}, nil)
cmd.hidden = true
}
@@ -64,26 +68,66 @@
return x.runAutostart()
}
- if err := x.userd.Init(); err != nil {
+ if x.Agent {
+ return x.runAgent()
+ }
+
+ return x.runUserd()
+}
+
+var signalNotify = signalNotifyImpl
+
+func (x *cmdUserd) runUserd() error {
+ var userd userd.Userd
+ if err := userd.Init(); err != nil {
+ return err
+ }
+ userd.Start()
+
+ ch, stop := signalNotify(syscall.SIGINT, syscall.SIGTERM)
+ defer stop()
+
+ select {
+ case sig := <-ch:
+ fmt.Fprintf(Stdout, "Exiting on %s.\n", sig)
+ case <-userd.Dying():
+ // something called Stop()
+ }
+
+ return userd.Stop()
+}
+
+func (x *cmdUserd) runAgent() error {
+ agent, err := agent.New()
+ if err != nil {
return err
}
- x.userd.Start()
+ agent.Version = cmd.Version
+ agent.Start()
+
+ ch, stop := signalNotify(syscall.SIGINT, syscall.SIGTERM)
+ defer stop()
- ch := make(chan os.Signal, 3)
- signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM, syscall.SIGUSR1)
select {
case sig := <-ch:
fmt.Fprintf(Stdout, "Exiting on %s.\n", sig)
- case <-x.userd.Dying():
+ case <-agent.Dying():
// something called Stop()
}
- return x.userd.Stop()
+ return agent.Stop()
}
func (x *cmdUserd) runAutostart() error {
- if err := userd.AutostartSessionApps(); err != nil {
+ if err := autostart.AutostartSessionApps(); err != nil {
return fmt.Errorf("autostart failed for the following apps:\n%v", err)
}
return nil
}
+
+func signalNotifyImpl(sig ...os.Signal) (ch chan os.Signal, stop func()) {
+ ch = make(chan os.Signal, len(sig))
+ signal.Notify(ch, sig...)
+ stop = func() { signal.Stop(ch) }
+ return ch, stop
+}
diff -Nru snapd-2.40/cmd/snap/cmd_userd_test.go snapd-2.42.1/cmd/snap/cmd_userd_test.go
--- snapd-2.40/cmd/snap/cmd_userd_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_userd_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016 Canonical Ltd
+ * Copyright (C) 2016-2019 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
@@ -20,6 +20,9 @@
package main_test
import (
+ "fmt"
+ "net"
+ "net/http"
"os"
"strings"
"syscall"
@@ -28,7 +31,9 @@
. "gopkg.in/check.v1"
snap "github.com/snapcore/snapd/cmd/snap"
+ "github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/testutil"
)
@@ -36,7 +41,7 @@
BaseSnapSuite
testutil.DBusTest
- restoreLogger func()
+ agentSocketPath string
}
var _ = Suite(&userdSuite{})
@@ -45,14 +50,17 @@
s.BaseSnapSuite.SetUpTest(c)
s.DBusTest.SetUpTest(c)
- _, s.restoreLogger = logger.MockLogger()
+ _, restore := logger.MockLogger()
+ s.AddCleanup(restore)
+
+ xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid())
+ c.Assert(os.MkdirAll(xdgRuntimeDir, 0700), IsNil)
+ s.agentSocketPath = fmt.Sprintf("%s/snapd-session-agent.socket", xdgRuntimeDir)
}
func (s *userdSuite) TearDownTest(c *C) {
s.BaseSnapSuite.TearDownTest(c)
s.DBusTest.TearDownTest(c)
-
- s.restoreLogger()
}
func (s *userdSuite) TestUserdBadCommandline(c *C) {
@@ -60,13 +68,29 @@
c.Assert(err, ErrorMatches, "too many arguments for command")
}
+type mockSignal struct{}
+
+func (m *mockSignal) String() string {
+ return ""
+}
+
+func (m *mockSignal) Signal() {}
+
func (s *userdSuite) TestUserdDBus(c *C) {
+ sigCh := make(chan os.Signal, 1)
+ sigStopCalls := 0
+
+ restore := snap.MockSignalNotify(func(sig ...os.Signal) (chan os.Signal, func()) {
+ c.Assert(sig, DeepEquals, []os.Signal{syscall.SIGINT, syscall.SIGTERM})
+ return sigCh, func() { sigStopCalls++ }
+ })
+ defer restore()
+
go func() {
myPid := os.Getpid()
+
defer func() {
- me, err := os.FindProcess(myPid)
- c.Assert(err, IsNil)
- me.Signal(syscall.SIGUSR1)
+ sigCh <- &mockSignal{}
}()
names := map[string]bool{
@@ -98,5 +122,72 @@
rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"userd"})
c.Assert(err, IsNil)
c.Check(rest, DeepEquals, []string{})
- c.Check(strings.ToLower(s.Stdout()), Equals, "exiting on user defined signal 1.\n")
+ c.Check(strings.ToLower(s.Stdout()), Equals, "exiting on .\n")
+ c.Check(sigStopCalls, Equals, 1)
+}
+
+func (s *userdSuite) makeAgentClient() *http.Client {
+ transport := &http.Transport{
+ Dial: func(_, _ string) (net.Conn, error) {
+ return net.Dial("unix", s.agentSocketPath)
+ },
+ DisableKeepAlives: true,
+ }
+ return &http.Client{Transport: transport}
+}
+
+func (s *userdSuite) TestSessionAgentSocket(c *C) {
+ sigCh := make(chan os.Signal, 1)
+ sigStopCalls := 0
+
+ restore := snap.MockSignalNotify(func(sig ...os.Signal) (chan os.Signal, func()) {
+ c.Assert(sig, DeepEquals, []os.Signal{syscall.SIGINT, syscall.SIGTERM})
+ return sigCh, func() { sigStopCalls++ }
+ })
+ defer restore()
+
+ go func() {
+ defer func() {
+ sigCh <- &mockSignal{}
+ }()
+
+ // Wait for command to create socket file
+ for i := 0; i < 1000; i++ {
+ if osutil.FileExists(s.agentSocketPath) {
+ break
+ }
+ time.Sleep(10 * time.Millisecond)
+ }
+
+ // Check that agent functions
+ client := s.makeAgentClient()
+ response, err := client.Get("http://localhost/v1/session-info")
+ c.Assert(err, IsNil)
+ defer response.Body.Close()
+ c.Check(response.StatusCode, Equals, 200)
+ }()
+
+ rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"userd", "--agent"})
+ c.Assert(err, IsNil)
+ c.Check(rest, DeepEquals, []string{})
+ c.Check(strings.ToLower(s.Stdout()), Equals, "exiting on .\n")
+ c.Check(sigStopCalls, Equals, 1)
+}
+
+func (s *userdSuite) TestSignalNotify(c *C) {
+ ch, stop := snap.SignalNotify(syscall.SIGUSR1)
+ defer stop()
+ go func() {
+ myPid := os.Getpid()
+ me, err := os.FindProcess(myPid)
+ c.Assert(err, IsNil)
+ err = me.Signal(syscall.SIGUSR1)
+ c.Assert(err, IsNil)
+ }()
+ select {
+ case sig := <-ch:
+ c.Assert(sig, Equals, syscall.SIGUSR1)
+ case <-time.After(5 * time.Second):
+ c.Fatal("signal not received within 5s")
+ }
}
diff -Nru snapd-2.40/cmd/snap/cmd_whoami_test.go snapd-2.42.1/cmd/snap/cmd_whoami_test.go
--- snapd-2.40/cmd/snap/cmd_whoami_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/cmd/snap/cmd_whoami_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,69 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package main_test
+
+import (
+ "net/http"
+
+ "github.com/snapcore/snapd/osutil"
+
+ . "gopkg.in/check.v1"
+
+ snap "github.com/snapcore/snapd/cmd/snap"
+)
+
+func (s *SnapSuite) TestWhoamiLoggedInUser(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ panic("unexpected call to snapd API")
+ })
+
+ s.Login(c)
+ defer s.Logout(c)
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"whoami"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, "email: hello@mail.com\n")
+}
+
+func (s *SnapSuite) TestWhoamiNotLoggedInUser(c *C) {
+ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) {
+ panic("unexpected call to snapd API")
+ })
+
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"whoami"})
+ c.Assert(err, IsNil)
+ c.Check(s.Stdout(), Equals, "email: -\n")
+}
+
+func (s *SnapSuite) TestWhoamiExtraParamError(c *C) {
+ _, err := snap.Parser(snap.Client()).ParseArgs([]string{"whoami", "test"})
+ c.Check(err, ErrorMatches, "too many arguments for command")
+}
+
+func (s *SnapSuite) TestWhoamiEmptyAuthFile(c *C) {
+ s.Login(c)
+ defer s.Logout(c)
+
+ err := osutil.AtomicWriteFile(s.AuthFile, []byte(``), 0600, 0)
+ c.Assert(err, IsNil)
+
+ _, err = snap.Parser(snap.Client()).ParseArgs([]string{"whoami"})
+ c.Check(err, ErrorMatches, "EOF")
+}
diff -Nru snapd-2.40/cmd/snap/complete.go snapd-2.42.1/cmd/snap/complete.go
--- snapd-2.40/cmd/snap/complete.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/complete.go 2019-10-30 12:17:43.000000000 +0000
@@ -508,7 +508,7 @@
func (s snapshotID) ToUint() (uint64, error) {
setID, err := strconv.ParseUint((string)(s), 10, 64)
if err != nil {
- return 0, fmt.Errorf(i18n.G("invalid argument for set id: expected a non-negative integer argument"))
+ return 0, fmt.Errorf(i18n.G("invalid argument for snapshot set id: expected a non-negative integer argument (see 'snap help saved')"))
}
return setID, nil
}
diff -Nru snapd-2.40/cmd/snap/error.go snapd-2.42.1/cmd/snap/error.go
--- snapd-2.40/cmd/snap/error.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/error.go 2019-10-30 12:17:43.000000000 +0000
@@ -35,7 +35,7 @@
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
- "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/channel"
"github.com/snapcore/snapd/strutil"
)
@@ -239,16 +239,16 @@
return msg, nil
}
-func snapRevisionNotAvailableMessage(kind, snapName, action, arch, channel string, releases []interface{}) string {
+func snapRevisionNotAvailableMessage(kind, snapName, action, arch, snapChannel string, releases []interface{}) string {
// releases contains all available (arch x channel)
// as reported by the store through the daemon
- req, err := snap.ParseChannel(channel, arch)
+ req, err := channel.Parse(snapChannel, arch)
if err != nil {
// TRANSLATORS: %q is the invalid request channel, %s is the snap name
- msg := fmt.Sprintf(i18n.G("requested channel %q is not valid (see 'snap info %s' for valid ones)"), channel, snapName)
+ msg := fmt.Sprintf(i18n.G("requested channel %q is not valid (see 'snap info %s' for valid ones)"), snapChannel, snapName)
return msg
}
- avail := make([]*snap.Channel, 0, len(releases))
+ avail := make([]*channel.Channel, 0, len(releases))
for _, v := range releases {
rel, _ := v.(map[string]interface{})
relCh, _ := rel["channel"].(string)
@@ -257,7 +257,7 @@
logger.Debugf("internal error: %q daemon error carries a release with invalid/empty architecture: %v", kind, v)
continue
}
- a, err := snap.ParseChannel(relCh, relArch)
+ a, err := channel.Parse(relCh, relArch)
if err != nil {
logger.Debugf("internal error: %q daemon error carries a release with invalid/empty channel (%v): %v", kind, err, v)
continue
@@ -265,7 +265,7 @@
avail = append(avail, &a)
}
- matches := map[string][]*snap.Channel{}
+ matches := map[string][]*channel.Channel{}
for _, a := range avail {
m := req.Match(a)
matchRepr := m.String()
@@ -298,7 +298,7 @@
if req.Branch != "" {
// there are matching arch+track+risk, give main track info
if len(matches["architecture:track:risk"]) != 0 {
- trackRisk := snap.Channel{Track: req.Track, Risk: req.Risk}
+ trackRisk := channel.Channel{Track: req.Track, Risk: req.Risk}
trackRisk = trackRisk.Clean()
// TRANSLATORS: %q is for the snap name, first %s is the full requested channel
@@ -350,7 +350,7 @@
return msg
}
-func installTable(snapName, action string, avail []*snap.Channel, full bool) string {
+func installTable(snapName, action string, avail []*channel.Channel, full bool) string {
b := &bytes.Buffer{}
w := tabwriter.NewWriter(b, len("candidate")+2, 1, 2, ' ', 0)
first := true
@@ -379,7 +379,7 @@
return strings.Join(lines, "")
}
-func channelOption(c *snap.Channel) string {
+func channelOption(c *channel.Channel) string {
if c.Branch == "" {
if c.Track == "" {
return fmt.Sprintf("--%s", c.Risk)
@@ -391,7 +391,7 @@
return fmt.Sprintf("--channel=%s", c)
}
-func archsForChannels(cs []*snap.Channel) []string {
+func archsForChannels(cs []*channel.Channel) []string {
archs := []string{}
for _, c := range cs {
if !strutil.ListContains(archs, c.Architecture) {
diff -Nru snapd-2.40/cmd/snap/export_test.go snapd-2.42.1/cmd/snap/export_test.go
--- snapd-2.40/cmd/snap/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,6 +20,7 @@
package main
import (
+ "os"
"os/user"
"time"
@@ -77,6 +78,8 @@
FixupArg = fixupArg
InterfacesDeprecationNotice = interfacesDeprecationNotice
+
+ SignalNotify = signalNotify
)
func NewInfoWriter(w writeflusher) *infoWriter {
@@ -109,6 +112,7 @@
MaybePrintPath = (*infoWriter).maybePrintPath
MaybePrintSum = (*infoWriter).maybePrintSum
MaybePrintCohortKey = (*infoWriter).maybePrintCohortKey
+ MaybePrintHealth = (*infoWriter).maybePrintHealth
)
func MockPollTime(d time.Duration) (restore func()) {
@@ -294,4 +298,12 @@
}
}
+func MockSignalNotify(newSignalNotify func(sig ...os.Signal) (chan os.Signal, func())) (restore func()) {
+ old := signalNotify
+ signalNotify = newSignalNotify
+ return func() {
+ signalNotify = old
+ }
+}
+
type ServiceName = serviceName
diff -Nru snapd-2.40/cmd/snap/notes.go snapd-2.42.1/cmd/snap/notes.go
--- snapd-2.40/cmd/snap/notes.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/notes.go 2019-10-30 12:17:43.000000000 +0000
@@ -51,7 +51,6 @@
// Notes encapsulate everything that might be interesting about a
// snap, in order to present a brief summary of it.
type Notes struct {
- Price string
SnapType snap.Type
Private bool
DevMode bool
@@ -62,6 +61,8 @@
Broken bool
IgnoreValidation bool
InCohort bool
+ Health string
+ Price string
}
func NotesFromChannelSnapInfo(ref *snap.ChannelSnapInfo) *Notes {
@@ -86,6 +87,10 @@
}
func NotesFromLocal(snp *client.Snap) *Notes {
+ var health string
+ if snp.Health != nil {
+ health = snp.Health.Status
+ }
return &Notes{
SnapType: snap.Type(snp.Type),
Private: snp.Private,
@@ -97,16 +102,7 @@
Broken: snp.Broken != "",
IgnoreValidation: snp.IgnoreValidation,
InCohort: snp.CohortKey != "",
- }
-}
-
-func NotesFromInfo(info *snap.Info) *Notes {
- return &Notes{
- SnapType: info.GetType(),
- Private: info.Private,
- DevMode: info.Confinement == client.DevModeConfinement,
- Classic: info.Confinement == client.ClassicConfinement,
- Broken: info.Broken != "",
+ Health: health,
}
}
@@ -166,6 +162,9 @@
if n.InCohort {
ns = append(ns, i18n.G("in-cohort"))
}
+ if n.Health != "" && n.Health != "okay" {
+ ns = append(ns, n.Health)
+ }
if len(ns) == 0 {
return "-"
diff -Nru snapd-2.40/cmd/snap/notes_test.go snapd-2.42.1/cmd/snap/notes_test.go
--- snapd-2.40/cmd/snap/notes_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap/notes_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -113,4 +113,5 @@
// check that a cohort key in a snap sets the InCohort note flag
c.Check(snap.NotesFromLocal(&client.Snap{CohortKey: ""}).InCohort, check.Equals, false)
c.Check(snap.NotesFromLocal(&client.Snap{CohortKey: "123"}).InCohort, check.Equals, true)
+ c.Check(snap.NotesFromLocal(&client.Snap{Health: &client.SnapHealth{Status: "blocked"}}).Health, check.Equals, "blocked")
}
diff -Nru snapd-2.40/cmd/snap-confine/cookie-support-test.c snapd-2.42.1/cmd/snap-confine/cookie-support-test.c
--- snapd-2.40/cmd/snap-confine/cookie-support-test.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-confine/cookie-support-test.c 2019-10-30 12:17:43.000000000 +0000
@@ -18,6 +18,7 @@
#include "cookie-support.h"
#include "cookie-support.c"
+#include "../libsnap-confine-private/cleanup-funcs.h"
#include "../libsnap-confine-private/test-utils.h"
#include
@@ -64,8 +65,8 @@
static void test_cookie_get_from_snapd__successful(void)
{
- struct sc_error *err = NULL;
- char *cookie;
+ struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ char *cookie SC_CLEANUP(sc_cleanup_string) = NULL;
char *dummy = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijmnopqrst";
@@ -81,8 +82,8 @@
static void test_cookie_get_from_snapd__nofile(void)
{
- struct sc_error *err = NULL;
- char *cookie;
+ struct sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ char *cookie SC_CLEANUP(sc_cleanup_string) = NULL;
set_fake_cookie_dir();
diff -Nru snapd-2.40/cmd/snap-confine/mount-support.c snapd-2.42.1/cmd/snap-confine/mount-support.c
--- snapd-2.40/cmd/snap-confine/mount-support.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-confine/mount-support.c 2019-10-30 12:17:43.000000000 +0000
@@ -241,10 +241,10 @@
// replicate the state of the root filesystem into the scratch directory.
sc_do_mount(config->rootfs_dir, scratch_dir, NULL, MS_REC | MS_BIND,
NULL);
- // Make the scratch directory recursively private. Nothing done there will
- // be shared with any peer group, This effectively detaches us from the
- // original namespace and coupled with pivot_root below serves as the
- // foundation of the mount sandbox.
+ // Make the scratch directory recursively slave. Nothing done there will be
+ // shared with the initial mount namespace. This effectively detaches us,
+ // in one way, from the original namespace and coupled with pivot_root
+ // below serves as the foundation of the mount sandbox.
sc_do_mount("none", scratch_dir, NULL, MS_REC | MS_SLAVE, NULL);
// Bind mount certain directories from the host filesystem to the scratch
// directory. By default mount events will propagate in both into and out
@@ -406,10 +406,14 @@
// the this directory on the host filesystem may not match the location in
// the desired root filesystem. In the "core" and "ubuntu-core" snaps the
// directory is always /snap. On the host it is a build-time configuration
- // option stored in SNAP_MOUNT_DIR.
- sc_must_snprintf(dst, sizeof dst, "%s/snap", scratch_dir);
- sc_do_mount(SNAP_MOUNT_DIR, dst, NULL, MS_BIND | MS_REC, NULL);
- sc_do_mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL);
+ // option stored in SNAP_MOUNT_DIR. In legacy mode (or in other words, not
+ // in normal mode), we don't need to do this because /snap is fixed and
+ // already contains the correct view of the mounted snaps.
+ if (config->normal_mode) {
+ sc_must_snprintf(dst, sizeof dst, "%s/snap", scratch_dir);
+ sc_do_mount(SNAP_MOUNT_DIR, dst, NULL, MS_BIND | MS_REC, NULL);
+ sc_do_mount("none", dst, NULL, MS_REC | MS_SLAVE, NULL);
+ }
// Create the hostfs directory if one is missing. This directory is a part
// of packaging now so perhaps this code can be removed later.
if (access(SC_HOSTFS_DIR, F_OK) != 0) {
@@ -585,6 +589,7 @@
{"/var/tmp"}, // to get access to the other temporary directory
{"/run"}, // to get /run with sockets and what not
{"/lib/modules",.is_optional = true}, // access to the modules of the running kernel
+ {"/lib/firmware",.is_optional = true}, // access to the firmware of the running kernel
{"/usr/src"}, // FIXME: move to SecurityMounts in system-trace interface
{"/var/log"}, // FIXME: move to SecurityMounts in log-observe interface
#ifdef MERGED_USR
diff -Nru snapd-2.40/cmd/snap-confine/ns-support.c snapd-2.42.1/cmd/snap-confine/ns-support.c
--- snapd-2.40/cmd/snap-confine/ns-support.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-confine/ns-support.c 2019-10-30 12:17:43.000000000 +0000
@@ -39,6 +39,7 @@
#include
#include "../libsnap-confine-private/cgroup-freezer-support.h"
+#include "../libsnap-confine-private/cgroup-support.h"
#include "../libsnap-confine-private/classic.h"
#include "../libsnap-confine-private/cleanup-funcs.h"
#include "../libsnap-confine-private/infofile.h"
@@ -486,6 +487,11 @@
debug("preserved mount is not stale, reusing");
return 0;
case SC_DISCARD_SHOULD:
+ if (sc_cgroup_is_v2()) {
+ debug
+ ("WARNING: cgroup v2 detected, preserved mount namespace process presence check unsupported, discarding");
+ break;
+ }
if (sc_cgroup_freezer_occupied(inv->snap_instance)) {
// Some processes are still using the namespace so we cannot discard it
// as that would fracture the view that the set of processes inside
@@ -691,8 +697,24 @@
if (kill(parent, 0) < 0) {
switch (errno) {
case ESRCH:
+ // When snap-confine executes it will fork a helper process. That
+ // process establishes an elaborate dance to ensure both itself and
+ // the parent are operating exactly as specified, so that no
+ // processes are left behind for unbound amount of time. As a part
+ // of that dance the child pings the parent to ensure it is still
+ // alive after establishing a notification signal to be sent in
+ // case the parent dies. This is a race avoidance mechanism, we set
+ // up the notification and then check if the parent is alive by the
+ // time we are done.
+ //
+ // In the case when the parent does go away we used to call
+ // abort(). On some distributions this would trigger an unclean
+ // process termination error report to be sent. One such example is
+ // the Ubuntu error tracker. Since the parent process can be
+ // legitimately interrupted and killed, this should not generate an
+ // error report. As such, perform clean exit in this specific case.
debug("parent process has terminated");
- abort();
+ exit(0);
default:
die("cannot confirm that parent process is alive");
break;
@@ -857,10 +879,20 @@
char info_path[PATH_MAX] = { 0 };
sc_must_snprintf(info_path, sizeof info_path,
"/run/snapd/ns/snap.%s.info", inv->snap_instance);
- stream = fopen(info_path, "w");
- if (stream == NULL) {
+ int fd = -1;
+ fd = open(info_path,
+ O_WRONLY | O_CREAT | O_TRUNC | O_CLOEXEC | O_NOFOLLOW, 0644);
+ if (fd < 0) {
die("cannot open %s", info_path);
}
+ if (fchown(fd, 0, 0) < 0) {
+ die("cannot chown %s to root.root", info_path);
+ }
+ // The stream now owns the file descriptor.
+ stream = fdopen(fd, "w");
+ if (stream == NULL) {
+ die("cannot get stream from file descriptor");
+ }
fprintf(stream, "base-snap-name=%s\n", inv->orig_base_snap_name);
if (ferror(stream) != 0) {
die("I/O error when writing to %s", info_path);
diff -Nru snapd-2.40/cmd/snap-confine/snap-confine.apparmor.in snapd-2.42.1/cmd/snap-confine/snap-confine.apparmor.in
--- snapd-2.40/cmd/snap-confine/snap-confine.apparmor.in 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-confine/snap-confine.apparmor.in 2019-10-30 12:17:43.000000000 +0000
@@ -193,6 +193,9 @@
mount options=(rw rbind) {,/usr}/lib{,32,64,x32}/modules/ -> /tmp/snap.rootfs_*{,/usr}/lib/modules/,
mount options=(rw rslave) -> /tmp/snap.rootfs_*{,/usr}/lib/modules/,
+ mount options=(rw rbind) {,/usr}/lib{,32,64,x32}/firmware/ -> /tmp/snap.rootfs_*{,/usr}/lib/firmware/,
+ mount options=(rw rslave) -> /tmp/snap.rootfs_*{,/usr}/lib/firmware/,
+
mount options=(rw rbind) /var/log/ -> /tmp/snap.rootfs_*/var/log/,
mount options=(rw rslave) -> /tmp/snap.rootfs_*/var/log/,
diff -Nru snapd-2.40/cmd/snap-confine/snap-confine-args.c snapd-2.42.1/cmd/snap-confine/snap-confine-args.c
--- snapd-2.40/cmd/snap-confine/snap-confine-args.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-confine/snap-confine-args.c 2019-10-30 12:17:43.000000000 +0000
@@ -21,6 +21,7 @@
#include "../libsnap-confine-private/utils.h"
#include "../libsnap-confine-private/string-utils.h"
+#include "../libsnap-confine-private/test-utils.h"
struct sc_args {
// The security tag that the application is intended to run with
diff -Nru snapd-2.40/cmd/snap-confine/snap-confine-args-test.c snapd-2.42.1/cmd/snap-confine/snap-confine-args-test.c
--- snapd-2.40/cmd/snap-confine/snap-confine-args-test.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-confine/snap-confine-args-test.c 2019-10-30 12:17:43.000000000 +0000
@@ -23,57 +23,6 @@
#include
-/**
- * Create an argc + argv pair out of a NULL terminated argument list.
- **/
-static void
- __attribute__((sentinel)) test_argc_argv(int *argcp, char ***argvp, ...)
-{
- int argc = 0;
- char **argv = NULL;
- g_test_queue_free(argv);
-
- va_list ap;
- va_start(ap, argvp);
- const char *arg;
- do {
- arg = va_arg(ap, const char *);
- // XXX: yeah, wrong way but the worse that can happen is for test to fail
- argv = realloc(argv, sizeof(const char **) * (argc + 1));
- g_assert_nonnull(argv);
- if (arg != NULL) {
- char *arg_copy = sc_strdup(arg);
- g_test_queue_free(arg_copy);
- argv[argc] = arg_copy;
- argc += 1;
- } else {
- argv[argc] = NULL;
- }
- } while (arg != NULL);
- va_end(ap);
-
- *argcp = argc;
- *argvp = argv;
-}
-
-static void test_test_argc_argv(void)
-{
- // Check that test_argc_argv() correctly stores data
- int argc;
- char **argv;
-
- test_argc_argv(&argc, &argv, NULL);
- g_assert_cmpint(argc, ==, 0);
- g_assert_null(argv[0]);
-
- test_argc_argv(&argc, &argv, "zero", "one", "two", NULL);
- g_assert_cmpint(argc, ==, 3);
- g_assert_cmpstr(argv[0], ==, "zero");
- g_assert_cmpstr(argv[1], ==, "one");
- g_assert_cmpstr(argv[2], ==, "two");
- g_assert_null(argv[3]);
-}
-
static void test_sc_nonfatal_parse_args__typical(void)
{
// Test that typical invocation of snap-confine is parsed correctly.
@@ -239,6 +188,7 @@
g_assert_null(args);
g_assert_cmpstr(sc_error_msg(err), ==,
"cannot parse arguments, argcp or argvp is NULL");
+ sc_cleanup_error(&err);
int argc;
char **argv;
@@ -252,6 +202,7 @@
g_assert_null(args);
g_assert_cmpstr(sc_error_msg(err), ==,
"cannot parse arguments, argc is zero or argv is NULL");
+ sc_cleanup_error(&err);
// NULL argv[i] attack
test_argc_argv(&argc, &argv,
@@ -451,7 +402,6 @@
static void __attribute__((constructor)) init(void)
{
- g_test_add_func("/args/test_argc_argv", test_test_argc_argv);
g_test_add_func("/args/sc_cleanup_args", test_sc_cleanup_args);
g_test_add_func("/args/sc_nonfatal_parse_args/typical",
test_sc_nonfatal_parse_args__typical);
diff -Nru snapd-2.40/cmd/snap-confine/snap-confine.c snapd-2.42.1/cmd/snap-confine/snap-confine.c
--- snapd-2.40/cmd/snap-confine/snap-confine.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-confine/snap-confine.c 2019-10-30 12:17:43.000000000 +0000
@@ -35,6 +35,7 @@
#include "../libsnap-confine-private/apparmor-support.h"
#include "../libsnap-confine-private/cgroup-freezer-support.h"
#include "../libsnap-confine-private/cgroup-pids-support.h"
+#include "../libsnap-confine-private/cgroup-support.h"
#include "../libsnap-confine-private/classic.h"
#include "../libsnap-confine-private/cleanup-funcs.h"
#include "../libsnap-confine-private/feature.h"
@@ -574,8 +575,11 @@
/** Populate and join the device control group. */
struct snappy_udev udev_s;
- if (snappy_udev_init(inv->security_tag, &udev_s) == 0)
- setup_devices_cgroup(inv->security_tag, &udev_s);
+ if (snappy_udev_init(inv->security_tag, &udev_s) == 0) {
+ if (!sc_cgroup_is_v2()) {
+ setup_devices_cgroup(inv->security_tag, &udev_s);
+ }
+ }
snappy_udev_cleanup(&udev_s);
/**
@@ -675,9 +679,11 @@
die("cannot set effective group id to root");
}
}
- sc_cgroup_freezer_join(inv->snap_instance, getpid());
- if (sc_feature_enabled(SC_FEATURE_REFRESH_APP_AWARENESS)) {
- sc_cgroup_pids_join(inv->security_tag, getpid());
+ if (!sc_cgroup_is_v2()) {
+ sc_cgroup_freezer_join(inv->snap_instance, getpid());
+ if (sc_feature_enabled(SC_FEATURE_REFRESH_APP_AWARENESS)) {
+ sc_cgroup_pids_join(inv->security_tag, getpid());
+ }
}
if (geteuid() == 0 && real_gid != 0) {
if (setegid(real_gid) != 0) {
diff -Nru snapd-2.40/cmd/snap-confine/snap-confine-invocation.c snapd-2.42.1/cmd/snap-confine/snap-confine-invocation.c
--- snapd-2.40/cmd/snap-confine/snap-confine-invocation.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-confine/snap-confine-invocation.c 2019-10-30 12:17:43.000000000 +0000
@@ -61,6 +61,10 @@
die("cannot run with NULL executable");
}
+ /* Instance name length + NULL termination */
+ char snap_name[SNAP_NAME_LEN + 1] = {0};
+ sc_snap_drop_instance_key(snap_instance, snap_name, sizeof snap_name);
+
/* Invocation helps to pass relevant data to various parts of snap-confine. */
memset(inv, 0, sizeof *inv);
inv->base_snap_name = sc_strdup(base_snap_name);
@@ -68,6 +72,7 @@
inv->executable = sc_strdup(executable);
inv->security_tag = sc_strdup(security_tag);
inv->snap_instance = sc_strdup(snap_instance);
+ inv->snap_name = sc_strdup(snap_name);
inv->classic_confinement = sc_args_is_classic_confinement(args);
// construct rootfs_dir based on base_snap_name
@@ -84,6 +89,7 @@
void sc_cleanup_invocation(sc_invocation *inv) {
if (inv != NULL) {
sc_cleanup_string(&inv->snap_instance);
+ sc_cleanup_string(&inv->snap_name);
sc_cleanup_string(&inv->base_snap_name);
sc_cleanup_string(&inv->orig_base_snap_name);
sc_cleanup_string(&inv->security_tag);
diff -Nru snapd-2.40/cmd/snap-confine/snap-confine-invocation.h snapd-2.42.1/cmd/snap-confine/snap-confine-invocation.h
--- snapd-2.40/cmd/snap-confine/snap-confine-invocation.h 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-confine/snap-confine-invocation.h 2019-10-30 12:17:43.000000000 +0000
@@ -29,7 +29,8 @@
**/
typedef struct sc_invocation {
/* Things declared by the system. */
- char *snap_instance;
+ char *snap_instance; /* snap instance name (_) */
+ char *snap_name; /* snap name (without instance key) */
char *orig_base_snap_name;
char *security_tag;
char *executable;
diff -Nru snapd-2.40/cmd/snap-confine/snap-confine-invocation-test.c snapd-2.42.1/cmd/snap-confine/snap-confine-invocation-test.c
--- snapd-2.40/cmd/snap-confine/snap-confine-invocation-test.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/cmd/snap-confine/snap-confine-invocation-test.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,162 @@
+/*
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#include "snap-confine-invocation.h"
+#include "snap-confine-args.h"
+#include "snap-confine-invocation.c"
+
+#include "../libsnap-confine-private/cleanup-funcs.h"
+#include "../libsnap-confine-private/test-utils.h"
+
+#include
+
+#include
+
+static struct sc_args *test_prepare_args(const char *base, const char *tag) {
+ struct sc_args *args = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ int argc;
+ char **argv;
+
+ test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", "--base", (base != NULL) ? base : "core",
+ (tag != NULL) ? tag : "snap.foo.app", "/usr/lib/snapd/snap-exec", NULL);
+ args = sc_nonfatal_parse_args(&argc, &argv, &err);
+ g_assert_null(err);
+ g_assert_nonnull(args);
+
+ return args;
+}
+
+static void test_sc_invocation_basic(void) {
+ struct sc_args *args SC_CLEANUP(sc_cleanup_args) = test_prepare_args("core", NULL);
+
+ sc_invocation inv SC_CLEANUP(sc_cleanup_invocation);
+ ;
+ sc_init_invocation(&inv, args, "foo");
+
+ g_assert_cmpstr(inv.base_snap_name, ==, "core");
+ g_assert_cmpstr(inv.executable, ==, "/usr/lib/snapd/snap-exec");
+ g_assert_cmpstr(inv.orig_base_snap_name, ==, "core");
+ g_assert_cmpstr(inv.rootfs_dir, ==, SNAP_MOUNT_DIR "/core/current");
+ g_assert_cmpstr(inv.security_tag, ==, "snap.foo.app");
+ g_assert_cmpstr(inv.snap_instance, ==, "foo");
+ g_assert_cmpstr(inv.snap_name, ==, "foo");
+ g_assert_false(inv.classic_confinement);
+ /* derived later */
+ g_assert_false(inv.is_normal_mode);
+}
+
+static void test_sc_invocation_instance_key(void) {
+ struct sc_args *args SC_CLEANUP(sc_cleanup_args) = test_prepare_args("core", "snap.foo_bar.app");
+
+ sc_invocation inv SC_CLEANUP(sc_cleanup_invocation);
+ ;
+ sc_init_invocation(&inv, args, "foo_bar");
+
+ // Check the error that we've got
+ g_assert_cmpstr(inv.snap_instance, ==, "foo_bar");
+ g_assert_cmpstr(inv.snap_name, ==, "foo");
+ g_assert_cmpstr(inv.orig_base_snap_name, ==, "core");
+ g_assert_cmpstr(inv.security_tag, ==, "snap.foo_bar.app");
+ g_assert_cmpstr(inv.executable, ==, "/usr/lib/snapd/snap-exec");
+ g_assert_false(inv.classic_confinement);
+ g_assert_cmpstr(inv.rootfs_dir, ==, SNAP_MOUNT_DIR "/core/current");
+ g_assert_cmpstr(inv.base_snap_name, ==, "core");
+ /* derived later */
+ g_assert_false(inv.is_normal_mode);
+}
+
+static void test_sc_invocation_base_name(void) {
+ struct sc_args *args SC_CLEANUP(sc_cleanup_args) = test_prepare_args("base-snap", NULL);
+
+ sc_invocation inv SC_CLEANUP(sc_cleanup_invocation);
+ sc_init_invocation(&inv, args, "foo");
+
+ g_assert_cmpstr(inv.base_snap_name, ==, "base-snap");
+ g_assert_cmpstr(inv.executable, ==, "/usr/lib/snapd/snap-exec");
+ g_assert_cmpstr(inv.orig_base_snap_name, ==, "base-snap");
+ g_assert_cmpstr(inv.rootfs_dir, ==, SNAP_MOUNT_DIR "/base-snap/current");
+ g_assert_cmpstr(inv.security_tag, ==, "snap.foo.app");
+ g_assert_cmpstr(inv.snap_instance, ==, "foo");
+ g_assert_cmpstr(inv.snap_name, ==, "foo");
+ g_assert_false(inv.classic_confinement);
+ /* derived later */
+ g_assert_false(inv.is_normal_mode);
+}
+
+static void test_sc_invocation_bad_instance_name(void) {
+ struct sc_args *args SC_CLEANUP(sc_cleanup_args) = test_prepare_args(NULL, NULL);
+
+ if (g_test_subprocess()) {
+ sc_invocation inv SC_CLEANUP(sc_cleanup_invocation) = {0};
+ sc_init_invocation(&inv, args, "foo_bar_bar_bar");
+ return;
+ }
+
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("snap instance name can contain only one underscore\n");
+}
+
+static void test_sc_invocation_classic(void) {
+ struct sc_args *args SC_CLEANUP(sc_cleanup_args) = NULL;
+ sc_error *err SC_CLEANUP(sc_cleanup_error) = NULL;
+ int argc;
+ char **argv = NULL;
+
+ test_argc_argv(&argc, &argv, "/usr/lib/snapd/snap-confine", "--classic", "--base", "core", "snap.foo-classic.app",
+ "/usr/lib/snapd/snap-exec", NULL);
+ args = sc_nonfatal_parse_args(&argc, &argv, &err);
+ g_assert_null(err);
+ g_assert_nonnull(args);
+
+ sc_invocation inv SC_CLEANUP(sc_cleanup_invocation) = {0};
+ sc_init_invocation(&inv, args, "foo-classic");
+
+ g_assert_cmpstr(inv.base_snap_name, ==, "core");
+ g_assert_cmpstr(inv.executable, ==, "/usr/lib/snapd/snap-exec");
+ g_assert_cmpstr(inv.orig_base_snap_name, ==, "core");
+ g_assert_cmpstr(inv.rootfs_dir, ==, SNAP_MOUNT_DIR "/core/current");
+ g_assert_cmpstr(inv.security_tag, ==, "snap.foo-classic.app");
+ g_assert_cmpstr(inv.snap_instance, ==, "foo-classic");
+ g_assert_cmpstr(inv.snap_name, ==, "foo-classic");
+ g_assert_true(inv.classic_confinement);
+}
+
+static void test_sc_invocation_tag_name_mismatch(void) {
+ struct sc_args *args SC_CLEANUP(sc_cleanup_args) = test_prepare_args("core", "snap.foo.app");
+
+ if (g_test_subprocess()) {
+ sc_invocation inv SC_CLEANUP(sc_cleanup_invocation);
+ ;
+ sc_init_invocation(&inv, args, "foo-not-foo");
+ return;
+ }
+
+ g_test_trap_subprocess(NULL, 0, 0);
+ g_test_trap_assert_failed();
+ g_test_trap_assert_stderr("security tag snap.foo.app not allowed\n");
+}
+
+static void __attribute__((constructor)) init(void) {
+ g_test_add_func("/invocation/bad_instance_name", test_sc_invocation_bad_instance_name);
+ g_test_add_func("/invocation/base_name", test_sc_invocation_base_name);
+ g_test_add_func("/invocation/basic", test_sc_invocation_basic);
+ g_test_add_func("/invocation/classic", test_sc_invocation_classic);
+ g_test_add_func("/invocation/instance_key", test_sc_invocation_instance_key);
+ g_test_add_func("/invocation/tag_name_mismatch", test_sc_invocation_tag_name_mismatch);
+}
diff -Nru snapd-2.40/cmd/snap-confine/udev-support.c snapd-2.42.1/cmd/snap-confine/udev-support.c
--- snapd-2.40/cmd/snap-confine/udev-support.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-confine/udev-support.c 2019-10-30 12:17:43.000000000 +0000
@@ -143,10 +143,10 @@
if (udev_s->devices == NULL)
die("udev_enumerate_new failed");
- if (udev_enumerate_add_match_tag(udev_s->devices, udev_s->tagname) != 0)
+ if (udev_enumerate_add_match_tag(udev_s->devices, udev_s->tagname) < 0)
die("udev_enumerate_add_match_tag");
- if (udev_enumerate_scan_devices(udev_s->devices) != 0)
+ if (udev_enumerate_scan_devices(udev_s->devices) < 0)
die("udev_enumerate_scan failed");
udev_s->assigned = udev_enumerate_get_list_entry(udev_s->devices);
diff -Nru snapd-2.40/cmd/snapd/main.go snapd-2.42.1/cmd/snapd/main.go
--- snapd-2.40/cmd/snapd/main.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snapd/main.go 2019-10-30 12:17:43.000000000 +0000
@@ -23,7 +23,6 @@
"fmt"
"os"
"os/signal"
- "runtime"
"syscall"
"time"
@@ -54,24 +53,6 @@
func main() {
cmd.ExecInSnapdOrCoreSnap()
- // The Go scheduler by default has a single operating system
- // thread per processor core, and does its own goroutine
- // scheduling inside of that thread. For I/O operations that
- // the Go runtime knows about, it has mechanisms to reschedule
- // goroutines so the system thread isn't blocked waiting for
- // I/O. If a goroutine performs a blocking system call which
- // the go runtime doesn't have special optimizations for, the
- // system thread can become blocked waiting for the syscall.
- // This can dramatically reduce runtime performance, and the
- // problem is much worse on single processor systems because
- // there is normally only a single system thread.
- //
- // We workaround by increasing the number of procs to a
- // minimum of two.
- if runtime.GOMAXPROCS(-1) == 1 {
- runtime.GOMAXPROCS(2)
- }
-
ch := make(chan os.Signal, 2)
signal.Notify(ch, syscall.SIGINT, syscall.SIGTERM)
if err := run(ch); err != nil {
@@ -147,7 +128,9 @@
d.Version = cmd.Version
- d.Start()
+ if err := d.Start(); err != nil {
+ return err
+ }
watchdog, err := runWatchdog(d)
if err != nil {
diff -Nru snapd-2.40/cmd/snapd/main_test.go snapd-2.42.1/cmd/snapd/main_test.go
--- snapd-2.40/cmd/snapd/main_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snapd/main_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -62,9 +62,7 @@
defer restore()
restore = apparmor.MockIsHomeUsingNFS(func() (bool, error) { return false, nil })
defer restore()
- restore = seccomp.MockSnapSeccompVersionInfo(func(s seccomp.Compiler) (string, error) {
- return "abcdef 1.2.3 1234abcd -", nil
- })
+ restore = seccomp.MockSnapSeccompVersionInfo("abcdef 1.2.3 1234abcd -")
defer restore()
sanityErr := fmt.Errorf("foo failed")
diff -Nru snapd-2.40/cmd/snap-mgmt/snap-mgmt-selinux.sh.in snapd-2.42.1/cmd/snap-mgmt/snap-mgmt-selinux.sh.in
--- snapd-2.40/cmd/snap-mgmt/snap-mgmt-selinux.sh.in 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-mgmt/snap-mgmt-selinux.sh.in 2019-10-30 12:17:43.000000000 +0000
@@ -1,6 +1,7 @@
#!/bin/bash
set -e
+set +x
SNAP_MOUNT_DIR="@SNAP_MOUNT_DIR@"
diff -Nru snapd-2.40/cmd/snap-mgmt/snap-mgmt.sh.in snapd-2.42.1/cmd/snap-mgmt/snap-mgmt.sh.in
--- snapd-2.40/cmd/snap-mgmt/snap-mgmt.sh.in 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-mgmt/snap-mgmt.sh.in 2019-10-30 12:17:43.000000000 +0000
@@ -6,6 +6,7 @@
# snapd.
set -e
+set +x
SNAP_MOUNT_DIR="@SNAP_MOUNT_DIR@"
diff -Nru snapd-2.40/cmd/snap-repair/runner.go snapd-2.42.1/cmd/snap-repair/runner.go
--- snapd-2.40/cmd/snap-repair/runner.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-repair/runner.go 2019-10-30 12:17:43.000000000 +0000
@@ -188,7 +188,7 @@
// wait for repair to finish or timeout
var scriptErr error
killTimerCh := time.After(defaultRepairTimeout)
- doneCh := make(chan error)
+ doneCh := make(chan error, 1)
go func() {
doneCh <- cmd.Wait()
close(doneCh)
@@ -776,7 +776,7 @@
if err != nil {
return false
}
- if len(archs) != 0 && !strutil.ListContains(archs, arch.UbuntuArchitecture()) {
+ if len(archs) != 0 && !strutil.ListContains(archs, arch.DpkgArchitecture()) {
return false
}
brandModel := fmt.Sprintf("%s/%s", run.state.Device.Brand, run.state.Device.Model)
diff -Nru snapd-2.40/cmd/snap-repair/runner_test.go snapd-2.42.1/cmd/snap-repair/runner_test.go
--- snapd-2.40/cmd/snap-repair/runner_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-repair/runner_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -782,10 +782,10 @@
{map[string]interface{}{"series": []interface{}{"18", "16"}}, true},
{map[string]interface{}{"series": "18"}, false},
{map[string]interface{}{"series": []interface{}{18}}, false},
- {map[string]interface{}{"architectures": []interface{}{arch.UbuntuArchitecture()}}, true},
+ {map[string]interface{}{"architectures": []interface{}{arch.DpkgArchitecture()}}, true},
{map[string]interface{}{"architectures": []interface{}{"other-arch"}}, false},
- {map[string]interface{}{"architectures": []interface{}{"other-arch", arch.UbuntuArchitecture()}}, true},
- {map[string]interface{}{"architectures": arch.UbuntuArchitecture()}, false},
+ {map[string]interface{}{"architectures": []interface{}{"other-arch", arch.DpkgArchitecture()}}, true},
+ {map[string]interface{}{"architectures": arch.DpkgArchitecture()}, false},
{map[string]interface{}{"models": []interface{}{"my-brand/my-model"}}, true},
{map[string]interface{}{"models": []interface{}{"other-brand/other-model"}}, false},
{map[string]interface{}{"models": []interface{}{"other-brand/other-model", "my-brand/my-model"}}, true},
diff -Nru snapd-2.40/cmd/snap-seccomp/export_test.go snapd-2.42.1/cmd/snap-seccomp/export_test.go
--- snapd-2.40/cmd/snap-seccomp/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-seccomp/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -26,19 +26,19 @@
GoSeccompFeatures = goSeccompFeatures
)
-func MockArchUbuntuArchitecture(f func() string) (restore func()) {
- realArchUbuntuArchitecture := archUbuntuArchitecture
- archUbuntuArchitecture = f
+func MockArchDpkgArchitecture(f func() string) (restore func()) {
+ realArchDpkgArchitecture := archDpkgArchitecture
+ archDpkgArchitecture = f
return func() {
- archUbuntuArchitecture = realArchUbuntuArchitecture
+ archDpkgArchitecture = realArchDpkgArchitecture
}
}
-func MockArchUbuntuKernelArchitecture(f func() string) (restore func()) {
- realArchUbuntuKernelArchitecture := archUbuntuKernelArchitecture
- archUbuntuKernelArchitecture = f
+func MockArchDpkgKernelArchitecture(f func() string) (restore func()) {
+ realArchDpkgKernelArchitecture := archDpkgKernelArchitecture
+ archDpkgKernelArchitecture = f
return func() {
- archUbuntuKernelArchitecture = realArchUbuntuKernelArchitecture
+ archDpkgKernelArchitecture = realArchDpkgKernelArchitecture
}
}
diff -Nru snapd-2.40/cmd/snap-seccomp/main.go snapd-2.42.1/cmd/snap-seccomp/main.go
--- snapd-2.40/cmd/snap-seccomp/main.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-seccomp/main.go 2019-10-30 12:17:43.000000000 +0000
@@ -426,10 +426,10 @@
"PTRACE_CONT": C.PTRACE_CONT,
}
-// UbuntuArchToScmpArch takes a dpkg architecture and converts it to
+// DpkgArchToScmpArch takes a dpkg architecture and converts it to
// the seccomp.ScmpArch as used in the libseccomp-golang library
-func UbuntuArchToScmpArch(ubuntuArch string) seccomp.ScmpArch {
- switch ubuntuArch {
+func DpkgArchToScmpArch(dpkgArch string) seccomp.ScmpArch {
+ switch dpkgArch {
case "amd64":
return seccomp.ArchAMD64
case "arm64":
@@ -447,7 +447,7 @@
case "s390x":
return seccomp.ArchS390X
}
- panic(fmt.Sprintf("cannot map ubuntu arch %q to a seccomp arch", ubuntuArch))
+ panic(fmt.Sprintf("cannot map dpkg arch %q to a seccomp arch", dpkgArch))
}
// important for unit testing
@@ -631,13 +631,13 @@
// used to mock in tests
var (
- archUbuntuArchitecture = arch.UbuntuArchitecture
- archUbuntuKernelArchitecture = arch.UbuntuKernelArchitecture
+ archDpkgArchitecture = arch.DpkgArchitecture
+ archDpkgKernelArchitecture = arch.DpkgKernelArchitecture
)
var (
- ubuntuArchitecture = archUbuntuArchitecture()
- ubuntuKernelArchitecture = archUbuntuKernelArchitecture()
+ dpkgArchitecture = archDpkgArchitecture()
+ dpkgKernelArchitecture = archDpkgKernelArchitecture()
)
// For architectures that support a compat architecture, when the
@@ -653,8 +653,8 @@
// add a compat architecture for some architectures that
// support it, e.g. on amd64 kernel and userland, we add
// compat i386 syscalls.
- if ubuntuArchitecture == ubuntuKernelArchitecture {
- switch archUbuntuArchitecture() {
+ if dpkgArchitecture == dpkgKernelArchitecture {
+ switch archDpkgArchitecture() {
case "amd64":
compatArch = seccomp.ArchX86
case "arm64":
@@ -672,7 +672,7 @@
// snaps. While unusual from a traditional Linux distribution
// perspective, certain classes of embedded devices are known
// to use this configuration.
- compatArch = UbuntuArchToScmpArch(archUbuntuKernelArchitecture())
+ compatArch = DpkgArchToScmpArch(archDpkgKernelArchitecture())
}
if compatArch != seccomp.ArchInvalid {
diff -Nru snapd-2.40/cmd/snap-seccomp/main_test.go snapd-2.42.1/cmd/snap-seccomp/main_test.go
--- snapd-2.40/cmd/snap-seccomp/main_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-seccomp/main_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -191,7 +191,7 @@
// Build 32bit runner on amd64 to test non-native syscall handling.
// Ideally we would build for ppc64el->powerpc and arm64->armhf but
// it seems tricky to find the right gcc-multilib for this.
- if arch.UbuntuArchitecture() == "amd64" && s.canCheckCompatArch {
+ if arch.DpkgArchitecture() == "amd64" && s.canCheckCompatArch {
cmd = exec.Command(cmd.Args[0], cmd.Args[1:]...)
cmd.Args = append(cmd.Args, "-m32")
for i, k := range cmd.Args {
@@ -274,7 +274,7 @@
// compiler that can produce the required binaries. Currently
// we only test amd64 running i386 here.
if syscallArch != "native" {
- syscallNr, err = seccomp.GetSyscallFromNameByArch(syscallName, main.UbuntuArchToScmpArch(syscallArch))
+ syscallNr, err = seccomp.GetSyscallFromNameByArch(syscallName, main.DpkgArchToScmpArch(syscallArch))
c.Assert(err, IsNil)
switch syscallArch {
@@ -825,10 +825,10 @@
// https://github.com/seccomp/libseccomp/issues/86
//
// This means we can not just
- // main.MockArchUbuntuArchitecture(t.arch)
+ // main.MockArchDpkgArchitecture(t.arch)
// here because on endian mismatch the arch will *not* be
// added
- if arch.UbuntuArchitecture() == t.arch {
+ if arch.DpkgArchitecture() == t.arch {
s.runBpf(c, t.seccompWhitelist, t.bpfInput, t.expected)
}
}
diff -Nru snapd-2.40/cmd/snap-update-ns/change.go snapd-2.42.1/cmd/snap-update-ns/change.go
--- snapd-2.40/cmd/snap-update-ns/change.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-update-ns/change.go 2019-10-30 12:17:43.000000000 +0000
@@ -330,10 +330,20 @@
flags := umountNoFollow
if c.Entry.XSnapdDetach() {
flags |= syscall.MNT_DETACH
+ // If we are detaching something then before performing the actual detach
+ // switch the entire hierarchy to private event propagation (that is,
+ // none). This works around a bit of peculiar kernel behavior when the
+ // kernel reports EBUSY during a detach operation, because the changes
+ // propagate in a way that conflicts with itself. This is also documented
+ // in umount(2).
+ err = sysMount("none", c.Entry.Dir, "", syscall.MS_REC|syscall.MS_PRIVATE, "")
+ logger.Debugf("mount --make-rprivate %q (error: %v)", c.Entry.Dir, err)
}
// Perform the raw unmount operation.
- err = sysUnmount(c.Entry.Dir, flags)
+ if err == nil {
+ err = sysUnmount(c.Entry.Dir, flags)
+ }
if err == nil {
as.AddChange(c)
}
@@ -392,6 +402,7 @@
}
return err
case Keep:
+ as.AddChange(c)
return nil
}
return fmt.Errorf("cannot process mount change: unknown action: %q", c.Action)
diff -Nru snapd-2.40/cmd/snap-update-ns/change_test.go snapd-2.42.1/cmd/snap-update-ns/change_test.go
--- snapd-2.40/cmd/snap-update-ns/change_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-update-ns/change_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -642,6 +642,7 @@
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`},
+ {C: `mount "none" "/tmp/.snap/rofs" "" MS_REC|MS_PRIVATE ""`},
{C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`},
// Perform clean up after the unmount operation.
@@ -805,6 +806,7 @@
synth, err := chg.Perform(s.as)
c.Assert(err, IsNil)
c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
+ {C: `mount "none" "/target" "" MS_REC|MS_PRIVATE ""`},
{C: `unmount "/target" UMOUNT_NOFOLLOW|MNT_DETACH`},
// Perform clean up after the unmount operation.
@@ -1151,6 +1153,7 @@
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`},
+ {C: `mount "none" "/tmp/.snap/rofs" "" MS_REC|MS_PRIVATE ""`},
{C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`},
// Perform clean up after the unmount operation.
@@ -1298,6 +1301,7 @@
{C: `close 4`},
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`},
+ {C: `mount "none" "/tmp/.snap/rofs" "" MS_REC|MS_PRIVATE ""`},
{C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`},
// Perform clean up after the unmount operation.
@@ -1690,6 +1694,7 @@
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`},
+ {C: `mount "none" "/tmp/.snap/rofs" "" MS_REC|MS_PRIVATE ""`},
{C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`},
// Perform clean up after the unmount operation.
@@ -2082,6 +2087,7 @@
{C: `lstat "/rofs"`, R: testutil.FileInfoDir},
{C: `mount "tmpfs" "/rofs" "tmpfs" 0 "mode=0755,uid=0,gid=0"`},
+ {C: `mount "none" "/tmp/.snap/rofs" "" MS_REC|MS_PRIVATE ""`},
{C: `unmount "/tmp/.snap/rofs" UMOUNT_NOFOLLOW|MNT_DETACH`},
// Perform clean up after the unmount operation.
@@ -2340,6 +2346,7 @@
{C: `close 7`},
// We're done restoring now.
+ {C: `mount "none" "/tmp/.snap/etc" "" MS_REC|MS_PRIVATE ""`},
{C: `unmount "/tmp/.snap/etc" UMOUNT_NOFOLLOW|MNT_DETACH`},
// Perform clean up after the unmount operation.
@@ -2413,9 +2420,91 @@
chg = &update.Change{Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}}
_, err = chg.Perform(s.as)
c.Assert(err, IsNil)
+
+ chg = &update.Change{Action: update.Keep, Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/target", Type: "tmpfs"}}
+ _, err = chg.Perform(s.as)
+ c.Assert(err, IsNil)
c.Assert(s.as.PastChanges(), DeepEquals, []*update.Change{
// past changes stack in order.
{Action: update.Mount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}},
{Action: update.Unmount, Entry: osutil.MountEntry{Name: "device", Dir: "/target", Type: "type"}},
+ {Action: update.Keep, Entry: osutil.MountEntry{Name: "tmpfs", Dir: "/target", Type: "tmpfs"}},
+ })
+}
+
+func (s *changeSuite) TestComplexPropagatingChanges(c *C) {
+ // This problem is more subtle. It is a variant of the regression test
+ // implemented in tests/regression/lp-1831010. Here, we have four directories:
+ //
+ // - $SNAP/a
+ // - $SNAP/b
+ // - $SNAP/b/c
+ // - $SNAP/d
+ //
+ // but snapd's mount profile contains only two entries:
+ //
+ // 1) recursive-bind $SNAP/a -> $SNAP/b/c (ie, mount --rbind $SNAP/a $SNAP/b/c)
+ // 2) recursive-bind $SNAP/b -> $SNAP/d (ie, mount --rbind $SNAP/b $SNAP/d)
+ //
+ // Both mount operations are performed under a substrate that is MS_SHARED.
+ // Therefore, due to the rules that decide upon propagation of bind mounts
+ // the propagation of the new mount entries is also shared. This is
+ // documented in section 5b of
+ // https://www.kernel.org/doc/Documentation/filesystems/sharedsubtree.txt.
+ //
+ // Interactive experimentation shows that the following three mount points exist
+ // after this operation, as illustrated by findmnt:
+ //
+ // TARGET SOURCE FSTYPE OPTIONS
+ // ...
+ // └─/snap/test-snapd-layout/x1 /dev/loop1 squashfs ro,nodev,relatime
+ // ├─/snap/test-snapd-layout/x1/b/c /dev/loop1[/a] squashfs ro,nodev,relatime
+ // └─/snap/test-snapd-layout/x1/d /dev/loop1[/b] squashfs ro,nodev,relatime
+ // └─/snap/test-snapd-layout/x1/d/c /dev/loop1[/a] squashfs ro,nodev,relatime
+ //
+ // Note that after the first mount operation only one mount point is created, namely
+ // $SNAP/a -> $SNAP/b/c. The second recursive bind mount not only creates
+ // $SNAP/b -> $SNAP/d, but also replicates $SNAP/a -> $SNAP/b/c as
+ // $SNAP/a -> $SNAP/d/c.
+ //
+ // The test will simulate a refresh of the snap from revision x1 to revision
+ // x2. When this happens the mount profile associated with x1 must be undone
+ // and the mount profile associated with x2 must be constructed. Because
+ // ordering matters, let's first consider the order of construction of x1
+ // itself. Starting from nothing, apply x1 as follows:
+ x1 := &osutil.MountProfile{
+ Entries: []osutil.MountEntry{
+ {Name: "/snap/app/x1/a", Dir: "/snap/app/x1/b/c", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}},
+ {Name: "/snap/app/x1/b", Dir: "/snap/app/x1/d", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}},
+ },
+ }
+ changes := update.NeededChanges(&osutil.MountProfile{}, x1)
+ c.Assert(changes, DeepEquals, []*update.Change{
+ {Action: update.Mount, Entry: osutil.MountEntry{Name: "/snap/app/x1/a", Dir: "/snap/app/x1/b/c", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}},
+ {Action: update.Mount, Entry: osutil.MountEntry{Name: "/snap/app/x1/b", Dir: "/snap/app/x1/d", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}},
+ })
+ // We can see that x1 is constructed in alphabetical order, first recursively
+ // bind mount at $SNAP/a the directory $SNAP/b/c, second recursively bind
+ // mount at $SNAP/b the directory $SNAP/d.
+ x2 := &osutil.MountProfile{
+ Entries: []osutil.MountEntry{
+ {Name: "/snap/app/x2/a", Dir: "/snap/app/x2/b/c", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}},
+ {Name: "/snap/app/x2/b", Dir: "/snap/app/x2/d", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}},
+ },
+ }
+ // When we are asked to refresh to revision x2, using the same layout, we
+ // simply undo x1 and then create x2, which apart from the difference in
+ // revision name, is exactly the same. The undo code, however, does not take
+ // the replicated mount point under consideration and therefore attempts to
+ // detach "x1/d", which normally fails with EBUSY. To counter this, the
+ // unmount operation first switches the mount point to recursive private
+ // propagation, before actually unmounting it. This ensures that propagation
+ // doesn't self-conflict, simply because there isn't any left.
+ changes = update.NeededChanges(x1, x2)
+ c.Assert(changes, DeepEquals, []*update.Change{
+ {Action: update.Unmount, Entry: osutil.MountEntry{Name: "/snap/app/x1/b", Dir: "/snap/app/x1/d", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout", "x-snapd.detach"}}},
+ {Action: update.Unmount, Entry: osutil.MountEntry{Name: "/snap/app/x1/a", Dir: "/snap/app/x1/b/c", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout", "x-snapd.detach"}}},
+ {Action: update.Mount, Entry: osutil.MountEntry{Name: "/snap/app/x2/a", Dir: "/snap/app/x2/b/c", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}},
+ {Action: update.Mount, Entry: osutil.MountEntry{Name: "/snap/app/x2/b", Dir: "/snap/app/x2/d", Type: "none", Options: []string{"rbind", "rw", "x-snapd.origin=layout"}}},
})
}
diff -Nru snapd-2.40/cmd/snap-update-ns/trespassing.go snapd-2.42.1/cmd/snap-update-ns/trespassing.go
--- snapd-2.40/cmd/snap-update-ns/trespassing.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-update-ns/trespassing.go 2019-10-30 12:17:43.000000000 +0000
@@ -248,7 +248,7 @@
for i := len(changes) - 1; i >= 0; i-- {
change := changes[i]
if change.Entry.Type == "tmpfs" && change.Entry.Dir == dirName {
- return change.Action == Mount
+ return change.Action == Mount || change.Action == Keep
}
}
return false
diff -Nru snapd-2.40/cmd/snap-update-ns/trespassing_test.go snapd-2.42.1/cmd/snap-update-ns/trespassing_test.go
--- snapd-2.40/cmd/snap-update-ns/trespassing_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-update-ns/trespassing_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -147,6 +147,27 @@
c.Assert(ok, Equals, true)
}
+// We are allowed to write to tmpfs that was mounted by snapd in another run.
+func (s *trespassingSuite) TestCanWriteToDirectoryTmpfsMountedBySnapdEarlier(c *C) {
+ a := &update.Assumptions{}
+
+ path := "/etc"
+ fd, err := s.sys.Open(path, syscall.O_DIRECTORY, 0)
+ c.Assert(err, IsNil)
+ defer s.sys.Close(fd)
+
+ s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.TmpfsMagic})
+ s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{})
+
+ a.AddChange(&update.Change{
+ Action: update.Keep,
+ Entry: osutil.MountEntry{Type: "tmpfs", Dir: path}})
+
+ ok, err := a.CanWriteToDirectory(fd, path)
+ c.Assert(err, IsNil)
+ c.Assert(ok, Equals, true)
+}
+
// We are allowed to write to directory beneath a tmpfs that was mounted by snapd.
func (s *trespassingSuite) TestCanWriteToDirectoryUnderTmpfsMountedBySnapd(c *C) {
a := &update.Assumptions{}
diff -Nru snapd-2.40/cmd/snap-update-ns/utils_test.go snapd-2.42.1/cmd/snap-update-ns/utils_test.go
--- snapd-2.40/cmd/snap-update-ns/utils_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/snap-update-ns/utils_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -136,6 +136,40 @@
})
}
+// Ensure that trespassing for prefix is matched using clean base path.
+func (s *utilsSuite) TestTrespassingMatcher(c *C) {
+ // We mounted tmpfs at "/path".
+ s.as.AddChange(&update.Change{Action: update.Mount, Entry: osutil.MountEntry{Dir: "/path", Type: "tmpfs", Name: "tmpfs"}})
+ s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic})
+ s.sys.InsertFstatResult(`fstat 3 `, syscall.Stat_t{})
+ s.sys.InsertFstatfsResult(`fstatfs 4 `, syscall.Statfs_t{Type: update.TmpfsMagic})
+ s.sys.InsertFstatResult(`fstat 4 `, syscall.Stat_t{})
+ s.sys.InsertFault(`mkdirat 3 "path" 0755`, syscall.EEXIST)
+ rs := s.as.RestrictionsFor("/path/to/something")
+ // Trespassing detector checked "/path", not "/path/" (which would not match).
+ c.Assert(update.MkdirAll("/path/to/something", 0755, 123, 456, rs), IsNil)
+ c.Assert(s.sys.RCalls(), testutil.SyscallsEqual, []testutil.CallResultError{
+ {C: `open "/" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
+ {C: `fstatfs 3 `, R: syscall.Statfs_t{Type: update.SquashfsMagic}},
+ {C: `fstat 3 `, R: syscall.Stat_t{}},
+ {C: `mkdirat 3 "path" 0755`, E: syscall.EEXIST},
+ {C: `openat 3 "path" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 4},
+ {C: `fstatfs 4 `, R: syscall.Statfs_t{Type: update.TmpfsMagic}},
+ {C: `fstat 4 `, R: syscall.Stat_t{}},
+
+ {C: `mkdirat 4 "to" 0755`},
+ {C: `openat 4 "to" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 5},
+ {C: `fchown 5 123 456`},
+ {C: `close 4`},
+ {C: `close 3`},
+ {C: `mkdirat 5 "something" 0755`},
+ {C: `openat 5 "something" O_NOFOLLOW|O_CLOEXEC|O_DIRECTORY 0`, R: 3},
+ {C: `fchown 3 123 456`},
+ {C: `close 3`},
+ {C: `close 5`},
+ })
+}
+
// Ensure that writes to /etc/demo are interrupted if /etc is restricted.
func (s *utilsSuite) TestSecureMkdirAllWithRestrictedEtc(c *C) {
s.sys.InsertFstatfsResult(`fstatfs 3 `, syscall.Statfs_t{Type: update.SquashfsMagic})
diff -Nru snapd-2.40/cmd/system-shutdown/system-shutdown.c snapd-2.42.1/cmd/system-shutdown/system-shutdown.c
--- snapd-2.40/cmd/system-shutdown/system-shutdown.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/system-shutdown/system-shutdown.c 2019-10-30 12:17:43.000000000 +0000
@@ -32,10 +32,32 @@
#include // SYS_reboot
#include "system-shutdown-utils.h"
+#include "../libsnap-confine-private/panic.h"
#include "../libsnap-confine-private/string-utils.h"
+#include "../libsnap-confine-private/utils.h"
+
+static void show_error(const char *fmt, va_list ap, int errno_copy)
+{
+ fprintf(stderr, "snapd system-shutdown helper: ");
+ fprintf(stderr, "*** ");
+ vfprintf(stderr, fmt, ap);
+ if (errno_copy != 0) {
+ fprintf(stderr, ": %s", strerror(errno_copy));
+ }
+ fprintf(stderr, "\n");
+}
+
+static void sync_and_halt(void)
+{
+ sync();
+ reboot(RB_HALT_SYSTEM);
+}
int main(int argc, char *argv[])
{
+ sc_set_panic_msg_fn(show_error);
+ sc_set_panic_exit_fn(sync_and_halt);
+
// 256 should be more than enough...
char reboot_arg[256] = { 0 };
diff -Nru snapd-2.40/cmd/system-shutdown/system-shutdown-utils.c snapd-2.42.1/cmd/system-shutdown/system-shutdown-utils.c
--- snapd-2.40/cmd/system-shutdown/system-shutdown-utils.c 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/system-shutdown/system-shutdown-utils.c 2019-10-30 12:17:43.000000000 +0000
@@ -33,6 +33,7 @@
#include "../libsnap-confine-private/mountinfo.h"
#include "../libsnap-confine-private/string-utils.h"
+#include "../libsnap-confine-private/utils.h"
__attribute__((format(printf, 1, 2)))
void kmsg(const char *fmt, ...)
@@ -53,19 +54,6 @@
va_end(va);
}
-__attribute__((noreturn))
-void die(const char *msg)
-{
- if (errno == 0) {
- kmsg("*** %s", msg);
- } else {
- kmsg("*** %s: %s", msg, strerror(errno));
- }
- sync();
- reboot(RB_HALT_SYSTEM);
- exit(1);
-}
-
int sc_read_reboot_arg(char *arg, size_t max_size)
{
FILE *f;
diff -Nru snapd-2.40/cmd/system-shutdown/system-shutdown-utils.h snapd-2.42.1/cmd/system-shutdown/system-shutdown-utils.h
--- snapd-2.40/cmd/system-shutdown/system-shutdown-utils.h 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/cmd/system-shutdown/system-shutdown-utils.h 2019-10-30 12:17:43.000000000 +0000
@@ -25,8 +25,6 @@
// no longer found writable.
bool umount_all(void);
-__attribute__((noreturn))
-void die(const char *msg);
__attribute__((format(printf, 1, 2)))
void kmsg(const char *fmt, ...);
diff -Nru snapd-2.40/daemon/api_asserts.go snapd-2.42.1/daemon/api_asserts.go
--- snapd-2.40/daemon/api_asserts.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/daemon/api_asserts.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,7 +20,9 @@
package daemon
import (
+ "errors"
"net/http"
+ "net/url"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/overlord/assertstate"
@@ -43,6 +45,40 @@
}
)
+// a helper type for parsing the options specified to /v2/assertions and other
+// such endpoints that can either do JSON or assertion depending on the value
+// of the the URL query parameters
+type daemonAssertFormatOptions struct {
+ jsonResult bool
+ headersOnly bool
+ headers map[string]string
+}
+
+// helper for parsing url query options into formatting option vars
+func parseHeadersFormatOptionsFromURL(q url.Values) (*daemonAssertFormatOptions, error) {
+ res := daemonAssertFormatOptions{}
+ res.headers = make(map[string]string)
+ for k := range q {
+ if k == "json" {
+ switch q.Get(k) {
+ case "false":
+ res.jsonResult = false
+ case "headers":
+ res.headersOnly = true
+ fallthrough
+ case "true":
+ res.jsonResult = true
+ default:
+ return nil, errors.New(`"json" query parameter when used must be set to "true" or "headers"`)
+ }
+ continue
+ }
+ res.headers[k] = q.Get(k)
+ }
+
+ return &res, nil
+}
+
func getAssertTypeNames(c *Command, r *http.Request, user *auth.UserState) Response {
return SyncResponse(map[string][]string{
"types": asserts.TypeNames(),
@@ -50,7 +86,7 @@
}
func doAssert(c *Command, r *http.Request, user *auth.UserState) Response {
- batch := assertstate.NewBatch()
+ batch := asserts.NewBatch(nil)
_, err := batch.AddStream(r.Body)
if err != nil {
return BadRequest("cannot decode request body into assertions: %v", err)
@@ -60,7 +96,9 @@
state.Lock()
defer state.Unlock()
- if err := batch.Commit(state); err != nil {
+ if err := assertstate.AddBatch(state, batch, &asserts.CommitOptions{
+ Precheck: true,
+ }); err != nil {
return BadRequest("assert failed: %v", err)
}
// TODO: what more info do we want to return on success?
@@ -76,46 +114,28 @@
if assertType == nil {
return BadRequest("invalid assert type: %q", assertTypeName)
}
- jsonResult := false
- headersOnly := false
- headers := map[string]string{}
- q := r.URL.Query()
- for k := range q {
- if k == "json" {
- switch q.Get(k) {
- case "false":
- jsonResult = false
- case "headers":
- headersOnly = true
- fallthrough
- case "true":
- jsonResult = true
- default:
- return BadRequest(`"json" query parameter when used must be set to "true" or "headers"`)
- }
- continue
- }
- headers[k] = q.Get(k)
+ opts, err := parseHeadersFormatOptionsFromURL(r.URL.Query())
+ if err != nil {
+ return BadRequest(err.Error())
}
-
state := c.d.overlord.State()
state.Lock()
db := assertstate.DB(state)
state.Unlock()
- assertions, err := db.FindMany(assertType, headers)
+ assertions, err := db.FindMany(assertType, opts.headers)
if err != nil && !asserts.IsNotFound(err) {
return InternalError("searching assertions failed: %v", err)
}
- if jsonResult {
+ if opts.jsonResult {
assertsJSON := make([]struct {
Headers map[string]interface{} `json:"headers,omitempty"`
Body string `json:"body,omitempty"`
}, len(assertions))
for i := range assertions {
assertsJSON[i].Headers = assertions[i].Headers()
- if !headersOnly {
+ if !opts.headersOnly {
assertsJSON[i].Body = string(assertions[i].Body())
}
}
diff -Nru snapd-2.40/daemon/api.go snapd-2.42.1/daemon/api.go
--- snapd-2.40/daemon/api.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/daemon/api.go 2019-10-30 12:17:43.000000000 +0000
@@ -103,6 +103,7 @@
connectionsCmd,
modelCmd,
cohortsCmd,
+ serialModelCmd,
}
var (
@@ -753,6 +754,7 @@
// The fields below should not be unmarshalled into. Do not export them.
userID int
+ ctx context.Context
}
func (inst *snapInstruction) revnoOpts() *snapstate.RevisionOptions {
@@ -952,7 +954,7 @@
ckey = strutil.ElliptLeft(inst.CohortKey, 10)
logger.Noticef("Installing snap %q from cohort %q", inst.Snaps[0], ckey)
}
- tset, err := snapstateInstall(st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags)
+ tset, err := snapstateInstall(inst.ctx, st, inst.Snaps[0], inst.revnoOpts(), inst.userID, flags)
if err != nil {
return "", nil, err
}
@@ -1165,6 +1167,7 @@
if err := decoder.Decode(&inst); err != nil {
return BadRequest("cannot decode request body into snap instruction: %v", err)
}
+ inst.ctx = r.Context()
state := c.d.overlord.State()
state.Lock()
diff -Nru snapd-2.40/daemon/api_model.go snapd-2.42.1/daemon/api_model.go
--- snapd-2.40/daemon/api_model.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/daemon/api_model.go 2019-10-30 12:17:43.000000000 +0000
@@ -26,13 +26,29 @@
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/devicestate"
+ "github.com/snapcore/snapd/overlord/state"
)
-var modelCmd = &Command{
- Path: "/v2/model",
- POST: postModel,
- // TODO: provide GET here too once we decided on the details of the API
-}
+var (
+ serialModelCmd = &Command{
+ Path: "/v2/model/serial",
+ GET: getSerial,
+ UserOK: true,
+ }
+ modelCmd = &Command{
+ Path: "/v2/model",
+ POST: postModel,
+ GET: getModel,
+ UserOK: true,
+ }
+)
+
+type assertType int
+
+const (
+ serialType assertType = iota
+ modelType
+)
var devicestateRemodel = devicestate.Remodel
@@ -40,6 +56,11 @@
NewModel string `json:"new-model"`
}
+type modelAssertJSONResponse struct {
+ Headers map[string]interface{} `json:"headers,omitempty"`
+ Body string `json:"body,omitempty"`
+}
+
func postModel(c *Command, r *http.Request, _ *auth.UserState) Response {
defer r.Body.Close()
var data postModelData
@@ -69,3 +90,93 @@
return AsyncResponse(nil, &Meta{Change: chg.ID()})
}
+
+// getModel gets the current model assertion using the DeviceManager
+func getModel(c *Command, r *http.Request, _ *auth.UserState) Response {
+ opts, err := parseHeadersFormatOptionsFromURL(r.URL.Query())
+ if err != nil {
+ return BadRequest(err.Error())
+ }
+
+ st := c.d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ devmgr := c.d.overlord.DeviceManager()
+
+ model, err := devmgr.Model()
+ if err == state.ErrNoState {
+ res := &errorResult{
+ Message: "no model assertion yet",
+ Kind: errorKindAssertionNotFound,
+ Value: "model",
+ }
+
+ return &resp{
+ Type: ResponseTypeError,
+ Result: res,
+ Status: 404,
+ }
+ }
+ if err != nil {
+ return InternalError("accessing model failed: %v", err)
+ }
+
+ if opts.jsonResult {
+ modelJSON := modelAssertJSONResponse{}
+
+ modelJSON.Headers = model.Headers()
+ if !opts.headersOnly {
+ modelJSON.Body = string(model.Body())
+ }
+
+ return SyncResponse(modelJSON, nil)
+ }
+
+ return AssertResponse([]asserts.Assertion{model}, true)
+}
+
+// getSerial gets the current serial assertion using the DeviceManager
+func getSerial(c *Command, r *http.Request, _ *auth.UserState) Response {
+ opts, err := parseHeadersFormatOptionsFromURL(r.URL.Query())
+ if err != nil {
+ return BadRequest(err.Error())
+ }
+
+ st := c.d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+
+ devmgr := c.d.overlord.DeviceManager()
+
+ serial, err := devmgr.Serial()
+ if err == state.ErrNoState {
+ res := &errorResult{
+ Message: "no serial assertion yet",
+ Kind: errorKindAssertionNotFound,
+ Value: "serial",
+ }
+
+ return &resp{
+ Type: ResponseTypeError,
+ Result: res,
+ Status: 404,
+ }
+ }
+ if err != nil {
+ return InternalError("accessing serial failed: %v", err)
+ }
+
+ if opts.jsonResult {
+ serialJSON := modelAssertJSONResponse{}
+
+ serialJSON.Headers = serial.Headers()
+ if !opts.headersOnly {
+ serialJSON.Body = string(serial.Body())
+ }
+
+ return SyncResponse(serialJSON, nil)
+ }
+
+ return AssertResponse([]asserts.Assertion{serial}, true)
+}
diff -Nru snapd-2.40/daemon/api_model_test.go snapd-2.42.1/daemon/api_model_test.go
--- snapd-2.40/daemon/api_model_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/daemon/api_model_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -23,12 +23,16 @@
"bytes"
"encoding/json"
"net/http"
+ "time"
"gopkg.in/check.v1"
"github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
"github.com/snapcore/snapd/overlord/assertstate/assertstatetest"
+ "github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/devicestate"
+ "github.com/snapcore/snapd/overlord/devicestate/devicestatetest"
"github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/state"
)
@@ -105,3 +109,254 @@
c.Assert(soon, check.Equals, 1)
}
+
+func (s *apiSuite) TestGetModelNoModelAssertion(c *check.C) {
+
+ d := s.daemonWithOverlordMock(c)
+ hookMgr, err := hookstate.Manager(d.overlord.State(), d.overlord.TaskRunner())
+ c.Assert(err, check.IsNil)
+ deviceMgr, err := devicestate.Manager(d.overlord.State(), hookMgr, d.overlord.TaskRunner(), nil)
+ c.Assert(err, check.IsNil)
+ d.overlord.AddManager(deviceMgr)
+
+ req, err := http.NewRequest("GET", "/v2/model", nil)
+ c.Assert(err, check.IsNil)
+ response := getModel(appsCmd, req, nil)
+ c.Assert(response, check.FitsTypeOf, &resp{})
+ rsp := response.(*resp)
+ c.Assert(rsp.Status, check.Equals, 404)
+ c.Assert(rsp.Result, check.FitsTypeOf, &errorResult{})
+ errRes := rsp.Result.(*errorResult)
+ c.Assert(errRes.Kind, check.Equals, errorKindAssertionNotFound)
+ c.Assert(errRes.Value, check.Equals, "model")
+ c.Assert(errRes.Message, check.Equals, "no model assertion yet")
+}
+
+func (s *apiSuite) TestGetModelHasModelAssertion(c *check.C) {
+ // make a model assertion
+ theModel := s.brands.Model("my-brand", "my-old-model", modelDefaults)
+
+ // model assertion setup
+ d := s.daemonWithOverlordMock(c)
+ hookMgr, err := hookstate.Manager(d.overlord.State(), d.overlord.TaskRunner())
+ c.Assert(err, check.IsNil)
+ deviceMgr, err := devicestate.Manager(d.overlord.State(), hookMgr, d.overlord.TaskRunner(), nil)
+ c.Assert(err, check.IsNil)
+ d.overlord.AddManager(deviceMgr)
+ st := d.overlord.State()
+ st.Lock()
+ assertstatetest.AddMany(st, s.storeSigning.StoreAccountKey(""))
+ assertstatetest.AddMany(st, s.brands.AccountsAndKeys("my-brand")...)
+ s.mockModel(c, st, theModel)
+ st.Unlock()
+
+ // make a new get request to the model endpoint
+ req, err := http.NewRequest("GET", "/v2/model", nil)
+ c.Assert(err, check.IsNil)
+ response := getModel(appsCmd, req, nil)
+
+ // check that we get an assertion response
+ c.Assert(response, check.FitsTypeOf, &assertResponse{})
+
+ // check that there is only one assertion
+ assertions := response.(*assertResponse).assertions
+ c.Assert(assertions, check.HasLen, 1)
+
+ // check that one of the assertion keys matches what's in the model we
+ // provided
+ assert := assertions[0]
+ arch := assert.Header("architecture")
+ c.Assert(arch, check.FitsTypeOf, "")
+ c.Assert(arch.(string), check.Equals, modelDefaults["architecture"])
+}
+
+func (s *apiSuite) TestGetModelJSONHasModelAssertion(c *check.C) {
+ // make a model assertion
+ theModel := s.brands.Model("my-brand", "my-old-model", modelDefaults)
+
+ // model assertion setup
+ d := s.daemonWithOverlordMock(c)
+ hookMgr, err := hookstate.Manager(d.overlord.State(), d.overlord.TaskRunner())
+ c.Assert(err, check.IsNil)
+ deviceMgr, err := devicestate.Manager(d.overlord.State(), hookMgr, d.overlord.TaskRunner(), nil)
+ c.Assert(err, check.IsNil)
+ d.overlord.AddManager(deviceMgr)
+ st := d.overlord.State()
+ st.Lock()
+ assertstatetest.AddMany(st, s.storeSigning.StoreAccountKey(""))
+ assertstatetest.AddMany(st, s.brands.AccountsAndKeys("my-brand")...)
+ s.mockModel(c, st, theModel)
+ st.Unlock()
+
+ // make a new get request to the model endpoint with json as true
+ req, err := http.NewRequest("GET", "/v2/model?json=true", nil)
+ c.Assert(err, check.IsNil)
+ response := getModel(appsCmd, req, nil)
+
+ // check that we get an generic response type
+ c.Assert(response, check.FitsTypeOf, &resp{})
+
+ // get the body and try to unmarshal into modelAssertJSONResponse
+ c.Assert(response.(*resp).Result, check.FitsTypeOf, modelAssertJSONResponse{})
+
+ jsonResponse := response.(*resp).Result.(modelAssertJSONResponse)
+
+ // get the architecture key from the headers
+ arch, ok := jsonResponse.Headers["architecture"]
+ c.Assert(ok, check.Equals, true)
+
+ // ensure that the architecture key is what we set in the model defaults
+ c.Assert(arch, check.FitsTypeOf, "")
+ c.Assert(arch.(string), check.Equals, modelDefaults["architecture"])
+}
+
+func (s *apiSuite) TestGetModelNoSerialAssertion(c *check.C) {
+
+ d := s.daemonWithOverlordMock(c)
+ hookMgr, err := hookstate.Manager(d.overlord.State(), d.overlord.TaskRunner())
+ c.Assert(err, check.IsNil)
+ deviceMgr, err := devicestate.Manager(d.overlord.State(), hookMgr, d.overlord.TaskRunner(), nil)
+ c.Assert(err, check.IsNil)
+ d.overlord.AddManager(deviceMgr)
+
+ req, err := http.NewRequest("GET", "/v2/model/serial", nil)
+ c.Assert(err, check.IsNil)
+ response := getSerial(appsCmd, req, nil)
+ c.Assert(response, check.FitsTypeOf, &resp{})
+ rsp := response.(*resp)
+ c.Assert(rsp.Status, check.Equals, 404)
+ c.Assert(rsp.Result, check.FitsTypeOf, &errorResult{})
+ errRes := rsp.Result.(*errorResult)
+ c.Assert(errRes.Kind, check.Equals, errorKindAssertionNotFound)
+ c.Assert(errRes.Value, check.Equals, "serial")
+ c.Assert(errRes.Message, check.Equals, "no serial assertion yet")
+}
+
+func (s *apiSuite) TestGetModelHasSerialAssertion(c *check.C) {
+ // make a model assertion
+ theModel := s.brands.Model("my-brand", "my-old-model", modelDefaults)
+
+ deviceKey, _ := assertstest.GenerateKey(752)
+
+ encDevKey, err := asserts.EncodePublicKey(deviceKey.PublicKey())
+ c.Assert(err, check.IsNil)
+
+ // model assertion setup
+ d := s.daemonWithOverlordMock(c)
+ hookMgr, err := hookstate.Manager(d.overlord.State(), d.overlord.TaskRunner())
+ c.Assert(err, check.IsNil)
+ deviceMgr, err := devicestate.Manager(d.overlord.State(), hookMgr, d.overlord.TaskRunner(), nil)
+ c.Assert(err, check.IsNil)
+ d.overlord.AddManager(deviceMgr)
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ assertstatetest.AddMany(st, s.storeSigning.StoreAccountKey(""))
+ assertstatetest.AddMany(st, s.brands.AccountsAndKeys("my-brand")...)
+ s.mockModel(c, st, theModel)
+
+ serial, err := s.brands.Signing("my-brand").Sign(asserts.SerialType, map[string]interface{}{
+ "authority-id": "my-brand",
+ "brand-id": "my-brand",
+ "model": "my-old-model",
+ "serial": "serialserial",
+ "device-key": string(encDevKey),
+ "device-key-sha3-384": deviceKey.PublicKey().ID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, check.IsNil)
+ assertstatetest.AddMany(st, serial)
+ devicestatetest.SetDevice(st, &auth.DeviceState{
+ Brand: "my-brand",
+ Model: "my-old-model",
+ Serial: "serialserial",
+ })
+
+ st.Unlock()
+ defer st.Lock()
+
+ // make a new get request to the serial endpoint
+ req, err := http.NewRequest("GET", "/v2/model/serial", nil)
+ c.Assert(err, check.IsNil)
+ response := getSerial(appsCmd, req, nil)
+
+ // check that we get an assertion response
+ c.Assert(response, check.FitsTypeOf, &assertResponse{})
+
+ // check that there is only one assertion
+ assertions := response.(*assertResponse).assertions
+ c.Assert(assertions, check.HasLen, 1)
+
+ // check that the device key in the returned assertion matches what we
+ // created above
+ assert := assertions[0]
+ devKey := assert.Header("device-key")
+ c.Assert(devKey, check.FitsTypeOf, "")
+ c.Assert(devKey.(string), check.Equals, string(encDevKey))
+}
+
+func (s *apiSuite) TestGetModelJSONHasSerialAssertion(c *check.C) {
+ // make a model assertion
+ theModel := s.brands.Model("my-brand", "my-old-model", modelDefaults)
+
+ deviceKey, _ := assertstest.GenerateKey(752)
+
+ encDevKey, err := asserts.EncodePublicKey(deviceKey.PublicKey())
+ c.Assert(err, check.IsNil)
+
+ // model assertion setup
+ d := s.daemonWithOverlordMock(c)
+ hookMgr, err := hookstate.Manager(d.overlord.State(), d.overlord.TaskRunner())
+ c.Assert(err, check.IsNil)
+ deviceMgr, err := devicestate.Manager(d.overlord.State(), hookMgr, d.overlord.TaskRunner(), nil)
+ c.Assert(err, check.IsNil)
+ d.overlord.AddManager(deviceMgr)
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ assertstatetest.AddMany(st, s.storeSigning.StoreAccountKey(""))
+ assertstatetest.AddMany(st, s.brands.AccountsAndKeys("my-brand")...)
+ s.mockModel(c, st, theModel)
+
+ serial, err := s.brands.Signing("my-brand").Sign(asserts.SerialType, map[string]interface{}{
+ "authority-id": "my-brand",
+ "brand-id": "my-brand",
+ "model": "my-old-model",
+ "serial": "serialserial",
+ "device-key": string(encDevKey),
+ "device-key-sha3-384": deviceKey.PublicKey().ID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, check.IsNil)
+ assertstatetest.AddMany(st, serial)
+ devicestatetest.SetDevice(st, &auth.DeviceState{
+ Brand: "my-brand",
+ Model: "my-old-model",
+ Serial: "serialserial",
+ })
+
+ st.Unlock()
+ defer st.Lock()
+
+ // make a new get request to the model endpoint with json as true
+ req, err := http.NewRequest("GET", "/v2/model/serial?json=true", nil)
+ c.Assert(err, check.IsNil)
+ response := getSerial(appsCmd, req, nil)
+
+ // check that we get an generic response type
+ c.Assert(response, check.FitsTypeOf, &resp{})
+
+ // get the body and try to unmarshal into modelAssertJSONResponse
+ c.Assert(response.(*resp).Result, check.FitsTypeOf, modelAssertJSONResponse{})
+
+ jsonResponse := response.(*resp).Result.(modelAssertJSONResponse)
+
+ // get the architecture key from the headers
+ devKey, ok := jsonResponse.Headers["device-key"]
+ c.Assert(ok, check.Equals, true)
+
+ // check that the device key in the returned assertion matches what we
+ // created above
+ c.Assert(devKey, check.FitsTypeOf, "")
+ c.Assert(devKey.(string), check.Equals, string(encDevKey))
+}
diff -Nru snapd-2.40/daemon/api_test.go snapd-2.42.1/daemon/api_test.go
--- snapd-2.40/daemon/api_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/daemon/api_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -63,6 +63,7 @@
"github.com/snapcore/snapd/overlord/configstate/config"
"github.com/snapcore/snapd/overlord/devicestate"
"github.com/snapcore/snapd/overlord/devicestate/devicestatetest"
+ "github.com/snapcore/snapd/overlord/healthstate"
"github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
"github.com/snapcore/snapd/overlord/ifacestate"
@@ -71,6 +72,7 @@
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/channel"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/store"
"github.com/snapcore/snapd/store/storetest"
@@ -311,6 +313,7 @@
func (s *apiBaseSuite) TearDownTest(c *check.C) {
s.trustedRestorer()
s.d = nil
+ s.ctx = nil
s.restoreBackends()
unsafeReadSnapInfo = unsafeReadSnapInfoImpl
ensureStateSoon = ensureStateSoonImpl
@@ -361,6 +364,8 @@
c.Assert(err, check.IsNil)
d.addRoutes()
+ c.Assert(d.overlord.StartUp(), check.IsNil)
+
st := d.overlord.State()
st.Lock()
defer st.Unlock()
@@ -427,6 +432,7 @@
runner := d.overlord.TaskRunner()
d.overlord.AddManager(newFakeSnapManager(st, runner))
d.overlord.AddManager(runner)
+ c.Assert(d.overlord.StartUp(), check.IsNil)
return d
}
@@ -662,6 +668,9 @@
var snapst snapstate.SnapState
st := s.d.overlord.State()
st.Lock()
+ st.Set("health", map[string]healthstate.HealthState{
+ "foo": {Status: healthstate.OkayStatus},
+ })
err := snapstate.Get(st, "foo", &snapst)
st.Unlock()
c.Assert(err, check.IsNil)
@@ -713,6 +722,7 @@
Validation: "unproven",
},
Status: "active",
+ Health: &client.SnapHealth{Status: "okay"},
Icon: "/v2/icons/foo/icon",
Type: string(snap.TypeApp),
Base: "base18",
@@ -1031,8 +1041,9 @@
// reload dirs for release info to have effect
dirs.SetRootDir(dirs.GlobalRootDir)
- buildID, err := osutil.MyBuildID()
- c.Assert(err, check.IsNil)
+ buildID := "this-is-my-build-id"
+ restore = MockBuildID(buildID)
+ defer restore()
sysInfoCmd.GET(sysInfoCmd, nil, nil).ServeHTTP(rec, nil)
c.Check(rec.Code, check.Equals, 200)
@@ -1101,8 +1112,9 @@
})
c.Assert(err, check.IsNil)
- buildID, err := osutil.MyBuildID()
- c.Assert(err, check.IsNil)
+ buildID := "this-is-my-build-id"
+ restore = MockBuildID(buildID)
+ defer restore()
sysInfoCmd.GET(sysInfoCmd, nil, nil).ServeHTTP(rec, nil)
c.Check(rec.Code, check.Equals, 200)
@@ -1630,6 +1642,12 @@
},
}}
s.mkInstalledInState(c, d, "local", "foo", "v1", snap.R(10), true, "")
+ st := s.d.overlord.State()
+ st.Lock()
+ st.Set("health", map[string]healthstate.HealthState{
+ "local": {Status: healthstate.OkayStatus},
+ })
+ st.Unlock()
req, err := http.NewRequest("GET", "/v2/snaps?sources=local", nil)
c.Assert(err, check.IsNil)
@@ -1641,6 +1659,11 @@
snaps := snapList(rsp.Result)
c.Assert(snaps, check.HasLen, 1)
c.Assert(snaps[0]["name"], check.Equals, "local")
+ c.Check(snaps[0]["health"], check.DeepEquals, map[string]interface{}{
+ "status": "okay",
+ "revision": "unset",
+ "timestamp": "0001-01-01T00:00:00Z",
+ })
}
func (s *apiSuite) TestSnapsInfoAllMixedPublishers(c *check.C) {
@@ -2717,7 +2740,7 @@
return &snap.Info{SuggestedName: mockedName}, nil
}
- snapstateInstall = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
// NOTE: ubuntu-core is not installed in developer mode
c.Check(flags, check.Equals, snapstate.Flags{})
installQueue = append(installQueue, name)
@@ -3103,7 +3126,7 @@
return state.NewTaskSet(t), nil
}
- snapstateInstall = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
if name != "core" {
c.Check(flags, check.DeepEquals, t.flags, check.Commentf(t.desc))
}
@@ -3589,7 +3612,7 @@
restore := release.MockForcedDevmode(forcedDevmode)
defer restore()
- snapstateInstall = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
calledFlags = flags
installQueue = append(installQueue, name)
c.Check(revision, check.Equals, opts.Revision)
@@ -3638,6 +3661,32 @@
c.Check(chg.Summary(), check.Equals, `Install "some-snap" snap`)
}
+func (s *apiSuite) TestInstallUserAgentContextCreated(c *check.C) {
+ snapstateInstall = func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ s.ctx = ctx
+ t := st.NewTask("fake-install-snap", "Doing a fake install")
+ return state.NewTaskSet(t), nil
+ }
+ defer func() {
+ snapstateInstall = nil
+ }()
+
+ s.daemonWithFakeSnapManager(c)
+
+ var buf bytes.Buffer
+ buf.WriteString(`{"action": "install"}`)
+ req, err := http.NewRequest("POST", "/v2/snaps/some-snap", &buf)
+ req.RemoteAddr = "pid=100;uid=0;socket=;"
+ c.Assert(err, check.IsNil)
+ req.Header.Add("User-Agent", "some-agent/1.0")
+
+ s.vars = map[string]string{"name": "some-snap"}
+ rec := httptest.NewRecorder()
+ snapCmd.ServeHTTP(rec, req)
+ c.Assert(rec.Code, check.Equals, 202)
+ c.Check(store.ClientUserAgent(s.ctx), check.Equals, "some-agent/1.0")
+}
+
func (s *apiSuite) TestRefresh(c *check.C) {
var calledFlags snapstate.Flags
calledUserID := 0
@@ -4087,7 +4136,7 @@
}
func (s *apiSuite) TestInstallFails(c *check.C) {
- snapstateInstall = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
t := s.NewTask("fake-install-snap-error", "Install task")
return state.NewTaskSet(t), nil
}
@@ -4121,7 +4170,7 @@
c.Skip("temporarily dropped half-baked support while sorting out flag mess")
var calledFlags snapstate.Flags
- snapstateInstall = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
calledFlags = flags
t := s.NewTask("fake-install-snap", "Doing a fake install")
@@ -4147,7 +4196,7 @@
func (s *apiSuite) TestInstall(c *check.C) {
var calledName string
- snapstateInstall = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
calledName = name
t := s.NewTask("fake-install-snap", "Doing a fake install")
@@ -4174,7 +4223,7 @@
var calledName string
var calledCohort string
- snapstateInstall = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
calledName = name
calledCohort = opts.CohortKey
@@ -4202,7 +4251,7 @@
func (s *apiSuite) TestInstallDevMode(c *check.C) {
var calledFlags snapstate.Flags
- snapstateInstall = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
calledFlags = flags
t := s.NewTask("fake-install-snap", "Doing a fake install")
@@ -4229,7 +4278,7 @@
func (s *apiSuite) TestInstallJailMode(c *check.C) {
var calledFlags snapstate.Flags
- snapstateInstall = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
calledFlags = flags
t := s.NewTask("fake-install-snap", "Doing a fake install")
@@ -4271,7 +4320,7 @@
}
func (s *apiSuite) TestInstallEmptyName(c *check.C) {
- snapstateInstall = func(_ *state.State, _ string, _ *snapstate.RevisionOptions, _ int, _ snapstate.Flags) (*state.TaskSet, error) {
+ snapstateInstall = func(ctx context.Context, _ *state.State, _ string, _ *snapstate.RevisionOptions, _ int, _ snapstate.Flags) (*state.TaskSet, error) {
return nil, errors.New("should not be called")
}
d := s.daemon(c)
@@ -6365,7 +6414,7 @@
func (s *apiSuite) TestInstallUnaliased(c *check.C) {
var calledFlags snapstate.Flags
- snapstateInstall = func(s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
+ snapstateInstall = func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) {
calledFlags = flags
t := s.NewTask("fake-install-snap", "Doing a fake install")
@@ -7076,12 +7125,12 @@
func (s *apiSuite) TestErrToResponseForRevisionNotAvailable(c *check.C) {
si := &snapInstruction{Action: "frobble", Snaps: []string{"foo"}}
- thisArch := arch.UbuntuArchitecture()
+ thisArch := arch.DpkgArchitecture()
err := &store.RevisionNotAvailableError{
Action: "install",
Channel: "stable",
- Releases: []snap.Channel{
+ Releases: []channel.Channel{
snaptest.MustParseChannel("beta", thisArch),
},
}
@@ -7107,7 +7156,7 @@
err = &store.RevisionNotAvailableError{
Action: "install",
Channel: "stable",
- Releases: []snap.Channel{
+ Releases: []channel.Channel{
snaptest.MustParseChannel("beta", "other-arch"),
},
}
diff -Nru snapd-2.40/daemon/api_users_test.go snapd-2.42.1/daemon/api_users_test.go
--- snapd-2.40/daemon/api_users_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/daemon/api_users_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -320,9 +320,13 @@
c.Check(users, check.HasLen, 1)
}
-func (s *userSuite) TestPostCreateUserFromAssertionWithForcePasswordChnage(c *check.C) {
- lusers := []map[string]interface{}{goodUser}
- lusers[0]["force-password-change"] = "true"
+func (s *userSuite) TestPostCreateUserFromAssertionWithForcePasswordChange(c *check.C) {
+ user := make(map[string]interface{})
+ for k, v := range goodUser {
+ user[k] = v
+ }
+ user["force-password-change"] = "true"
+ lusers := []map[string]interface{}{user}
s.makeSystemUsers(c, lusers)
// mock the calls that create the user
diff -Nru snapd-2.40/daemon/daemon.go snapd-2.42.1/daemon/daemon.go
--- snapd-2.40/daemon/daemon.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/daemon/daemon.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,6 +20,7 @@
package daemon
import (
+ "context"
"fmt"
"net"
"net/http"
@@ -46,6 +47,7 @@
"github.com/snapcore/snapd/overlord/standby"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/polkit"
+ "github.com/snapcore/snapd/store"
"github.com/snapcore/snapd/systemd"
)
@@ -57,10 +59,11 @@
type Daemon struct {
Version string
overlord *overlord.Overlord
+ state *state.State
snapdListener net.Listener
- snapdServe *shutdownServer
snapListener net.Listener
- snapServe *shutdownServer
+ connTracker *connTracker
+ serve *http.Server
tomb tomb.Tomb
router *mux.Router
standbyOpinions *standby.StandbyOpinions
@@ -73,6 +76,8 @@
// degradedErr is set when the daemon is in degraded mode
degradedErr error
+ expectedRebootDidNotHappen bool
+
mu sync.Mutex
}
@@ -208,7 +213,7 @@
}
func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) {
- st := c.d.overlord.State()
+ st := c.d.state
st.Lock()
// TODO Look at the error and fail if there's an attempt to authenticate with invalid data.
user, _ := UserFromRequest(st, r)
@@ -234,6 +239,9 @@
return
}
+ ctx := store.WithClientUserAgent(r.Context(), r)
+ r = r.WithContext(ctx)
+
var rspf ResponseFunc
var rsp = MethodNotAllowed("method %q not allowed", r.Method)
@@ -320,7 +328,7 @@
// The SnapdSocket is required-- without it, die.
if listener, err := netutil.GetListener(dirs.SnapdSocket, listenerMap); err == nil {
- d.snapdListener = &ucrednetListener{listener}
+ d.snapdListener = &ucrednetListener{Listener: listener}
} else {
return fmt.Errorf("when trying to listen on %s: %v", dirs.SnapdSocket, err)
}
@@ -328,7 +336,7 @@
if listener, err := netutil.GetListener(dirs.SnapSocket, listenerMap); err == nil {
// This listener may also be nil if that socket wasn't among
// the listeners, so check it before using it.
- d.snapListener = &ucrednetListener{listener}
+ d.snapListener = &ucrednetListener{Listener: listener}
} else {
logger.Debugf("cannot get listener for %q: %v", dirs.SnapSocket, err)
}
@@ -375,97 +383,33 @@
shutdownTimeout = 25 * time.Second
)
-// shutdownServer supplements a http.Server with graceful shutdown.
-// TODO: with go1.8 http.Server itself grows a graceful Shutdown method
-type shutdownServer struct {
- l net.Listener
- httpSrv *http.Server
-
- mu sync.Mutex
- conns map[net.Conn]http.ConnState
- shuttingDown bool
+type connTracker struct {
+ mu sync.Mutex
+ conns map[net.Conn]struct{}
}
-func newShutdownServer(l net.Listener, h http.Handler) *shutdownServer {
- srv := &http.Server{
- Handler: h,
- }
- ssrv := &shutdownServer{
- l: l,
- httpSrv: srv,
- conns: make(map[net.Conn]http.ConnState),
- }
- srv.ConnState = ssrv.trackConn
- return ssrv
-}
+func (ct *connTracker) CanStandby() bool {
+ ct.mu.Lock()
+ defer ct.mu.Unlock()
-func (srv *shutdownServer) Serve() error {
- return srv.httpSrv.Serve(srv.l)
+ return len(ct.conns) == 0
}
-func (srv *shutdownServer) CanStandby() bool {
- srv.mu.Lock()
- defer srv.mu.Unlock()
-
- for _, state := range srv.conns {
- if state != http.StateIdle {
- return false
- }
- }
- return true
-}
-
-func (srv *shutdownServer) trackConn(conn net.Conn, state http.ConnState) {
- srv.mu.Lock()
- defer srv.mu.Unlock()
+func (ct *connTracker) trackConn(conn net.Conn, state http.ConnState) {
+ ct.mu.Lock()
+ defer ct.mu.Unlock()
// we ignore hijacked connections, if we do things with websockets
// we'll need custom shutdown handling for them
- if state == http.StateClosed || state == http.StateHijacked {
- delete(srv.conns, conn)
- return
- }
- if srv.shuttingDown && state == http.StateIdle {
- conn.Close()
- delete(srv.conns, conn)
- return
- }
- srv.conns[conn] = state
-}
-
-func (srv *shutdownServer) finishShutdown() error {
- toutC := time.After(shutdownTimeout)
-
- srv.mu.Lock()
- defer srv.mu.Unlock()
-
- srv.shuttingDown = true
- for conn, state := range srv.conns {
- if state == http.StateIdle {
- conn.Close()
- delete(srv.conns, conn)
- }
- }
-
- doWait := true
- for doWait {
- if len(srv.conns) == 0 {
- return nil
- }
- srv.mu.Unlock()
- select {
- case <-time.After(200 * time.Millisecond):
- case <-toutC:
- doWait = false
- }
- srv.mu.Lock()
+ if state == http.StateNew || state == http.StateActive {
+ ct.conns[conn] = struct{}{}
+ } else {
+ delete(ct.conns, conn)
}
- return fmt.Errorf("cannot gracefully finish, still active connections on %v after %v", srv.l.Addr(), shutdownTimeout)
}
func (d *Daemon) initStandbyHandling() {
- d.standbyOpinions = standby.New(d.overlord.State())
- d.standbyOpinions.AddOpinion(d.snapdServe)
- d.standbyOpinions.AddOpinion(d.snapServe)
+ d.standbyOpinions = standby.New(d.state)
+ d.standbyOpinions.AddOpinion(d.connTracker)
d.standbyOpinions.AddOpinion(d.overlord)
d.standbyOpinions.AddOpinion(d.overlord.SnapManager())
d.standbyOpinions.AddOpinion(d.overlord.DeviceManager())
@@ -473,39 +417,38 @@
}
// Start the Daemon
-func (d *Daemon) Start() {
- // die when asked to restart (systemd should get us back up!)
- d.overlord.SetRestartHandler(func(t state.RestartType) {
- switch t {
- case state.RestartDaemon:
- d.tomb.Kill(nil)
- case state.RestartSystem:
- // try to schedule a fallback slow reboot already here
- // in case we get stuck shutting down
- if err := reboot(rebootWaitTimeout); err != nil {
- logger.Noticef("%s", err)
- }
+func (d *Daemon) Start() error {
+ if d.expectedRebootDidNotHappen {
+ // we need to schedule and wait for a system restart
+ d.tomb.Kill(nil)
+ // avoid systemd killing us again while we wait
+ systemdSdNotify("READY=1")
+ return nil
+ }
+ if d.overlord == nil {
+ panic("internal error: no Overlord")
+ }
- d.mu.Lock()
- defer d.mu.Unlock()
- // remember we need to restart the system
- d.restartSystem = true
- d.tomb.Kill(nil)
- case state.RestartSocket:
- d.mu.Lock()
- defer d.mu.Unlock()
- d.restartSocket = true
- d.tomb.Kill(nil)
- default:
- logger.Noticef("internal error: restart handler called with unknown restart type: %v", t)
- d.tomb.Kill(nil)
- }
- })
+ to, reasoning, err := d.overlord.StartupTimeout()
+ if err != nil {
+ return err
+ }
+ if to > 0 {
+ to = to.Round(time.Microsecond)
+ us := to.Nanoseconds() / 1000
+ logger.Noticef("adjusting startup timeout by %v (%s)", to, reasoning)
+ systemdSdNotify(fmt.Sprintf("EXTEND_TIMEOUT_USEC=%d", us))
+ }
+ // now perform expensive overlord/manages initiliazation
+ if err := d.overlord.StartUp(); err != nil {
+ return err
+ }
- if d.snapListener != nil {
- d.snapServe = newShutdownServer(d.snapListener, logit(d.router))
+ d.connTracker = &connTracker{conns: make(map[net.Conn]struct{})}
+ d.serve = &http.Server{
+ Handler: logit(d.router),
+ ConnState: d.connTracker.trackConn,
}
- d.snapdServe = newShutdownServer(d.snapdListener, logit(d.router))
// enable standby handling
d.initStandbyHandling()
@@ -516,7 +459,7 @@
d.tomb.Go(func() error {
if d.snapListener != nil {
d.tomb.Go(func() error {
- if err := d.snapServe.Serve(); err != nil && d.tomb.Err() == tomb.ErrStillAlive {
+ if err := d.serve.Serve(d.snapListener); err != http.ErrServerClosed && d.tomb.Err() == tomb.ErrStillAlive {
return err
}
@@ -524,7 +467,7 @@
})
}
- if err := d.snapdServe.Serve(); err != nil && d.tomb.Err() == tomb.ErrStillAlive {
+ if err := d.serve.Serve(d.snapdListener); err != http.ErrServerClosed && d.tomb.Err() == tomb.ErrStillAlive {
return err
}
@@ -533,31 +476,52 @@
// notify systemd that we are ready
systemdSdNotify("READY=1")
+ return nil
}
-var shutdownMsg = i18n.G("reboot scheduled to update the system")
-
-func rebootImpl(rebootDelay time.Duration) error {
- if rebootDelay < 0 {
- rebootDelay = 0
- }
- mins := int64((rebootDelay + time.Minute - 1) / time.Minute)
- cmd := exec.Command("shutdown", "-r", fmt.Sprintf("+%d", mins), shutdownMsg)
- if out, err := cmd.CombinedOutput(); err != nil {
- return osutil.OutputErr(out, err)
+// HandleRestart implements overlord.RestartBehavior.
+func (d *Daemon) HandleRestart(t state.RestartType) {
+ // die when asked to restart (systemd should get us back up!) etc
+ switch t {
+ case state.RestartDaemon:
+ case state.RestartSystem:
+ // try to schedule a fallback slow reboot already here
+ // in case we get stuck shutting down
+ if err := reboot(rebootWaitTimeout); err != nil {
+ logger.Noticef("%s", err)
+ }
+
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ // remember we need to restart the system
+ d.restartSystem = true
+ case state.RestartSocket:
+ d.mu.Lock()
+ defer d.mu.Unlock()
+ d.restartSocket = true
+ default:
+ logger.Noticef("internal error: restart handler called with unknown restart type: %v", t)
}
- return nil
+ d.tomb.Kill(nil)
}
-var reboot = rebootImpl
-
var (
- rebootNoticeWait = 3 * time.Second
- rebootWaitTimeout = 10 * time.Minute
+ rebootNoticeWait = 3 * time.Second
+ rebootWaitTimeout = 10 * time.Minute
+ rebootRetryWaitTimeout = 5 * time.Minute
+ rebootMaxTentatives = 3
)
// Stop shuts down the Daemon
func (d *Daemon) Stop(sigCh chan<- os.Signal) error {
+ // we need to schedule/wait for a system restart again
+ if d.expectedRebootDidNotHappen {
+ return d.doReboot(sigCh, rebootRetryWaitTimeout)
+ }
+ if d.overlord == nil {
+ return fmt.Errorf("internal error: no Overlord")
+ }
+
d.tomb.Kill(nil)
d.mu.Lock()
@@ -572,7 +536,7 @@
// stop running hooks first
// and do it more gracefully if we are restarting
hookMgr := d.overlord.HookManager()
- if ok, _ := d.overlord.State().Restarting(); ok {
+ if ok, _ := d.state.Restarting(); ok {
logger.Noticef("gracefully waiting for running hooks")
hookMgr.GracefullyWaitRunningHooks()
logger.Noticef("done waiting for running hooks")
@@ -586,10 +550,12 @@
time.Sleep(rebootNoticeWait)
}
- d.tomb.Kill(d.snapdServe.finishShutdown())
- if d.snapListener != nil {
- d.tomb.Kill(d.snapServe.finishShutdown())
- }
+ // We're using the background context here because the tomb's
+ // context will likely already have been cancelled when we are
+ // called.
+ ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout)
+ d.tomb.Kill(d.serve.Shutdown(ctx))
+ cancel()
if !restartSystem {
// tell systemd that we are stopping
@@ -626,9 +592,30 @@
}
if restartSystem {
- // ask for shutdown and wait for it to happen.
- // if we exit snapd will be restared by systemd
- rebootDelay := 1 * time.Minute
+ return d.doReboot(sigCh, rebootWaitTimeout)
+ }
+
+ if d.restartSocket {
+ return ErrRestartSocket
+ }
+
+ return nil
+}
+
+func (d *Daemon) rebootDelay() (time.Duration, error) {
+ d.state.Lock()
+ defer d.state.Unlock()
+ now := time.Now()
+ // see whether a reboot had already been scheduled
+ var rebootAt time.Time
+ err := d.state.Get("daemon-system-restart-at", &rebootAt)
+ if err != nil && err != state.ErrNoState {
+ return 0, err
+ }
+ rebootDelay := 1 * time.Minute
+ if err == nil {
+ rebootDelay = rebootAt.Sub(now)
+ } else {
ovr := os.Getenv("SNAPD_REBOOT_DELAY") // for tests
if ovr != "" {
d, err := time.ParseDuration(ovr)
@@ -636,39 +623,106 @@
rebootDelay = d
}
}
- if err := reboot(rebootDelay); err != nil {
- return err
- }
- // wait for reboot to happen
- logger.Noticef("Waiting for system reboot")
- if sigCh != nil {
- signal.Stop(sigCh)
- if len(sigCh) > 0 {
- // a signal arrived in between
- return nil
- }
- close(sigCh)
- }
- time.Sleep(rebootWaitTimeout)
- return fmt.Errorf("expected reboot did not happen")
+ rebootAt = now.Add(rebootDelay)
+ d.state.Set("daemon-system-restart-at", rebootAt)
}
- if d.restartSocket {
- return ErrRestartSocket
+ return rebootDelay, nil
+}
+
+func (d *Daemon) doReboot(sigCh chan<- os.Signal, waitTimeout time.Duration) error {
+ rebootDelay, err := d.rebootDelay()
+ if err != nil {
+ return err
+ }
+ // ask for shutdown and wait for it to happen.
+ // if we exit snapd will be restared by systemd
+ if err := reboot(rebootDelay); err != nil {
+ return err
}
+ // wait for reboot to happen
+ logger.Noticef("Waiting for system reboot")
+ if sigCh != nil {
+ signal.Stop(sigCh)
+ if len(sigCh) > 0 {
+ // a signal arrived in between
+ return nil
+ }
+ close(sigCh)
+ }
+ time.Sleep(waitTimeout)
+ return fmt.Errorf("expected reboot did not happen")
+}
+var shutdownMsg = i18n.G("reboot scheduled to update the system")
+
+func rebootImpl(rebootDelay time.Duration) error {
+ if rebootDelay < 0 {
+ rebootDelay = 0
+ }
+ mins := int64(rebootDelay / time.Minute)
+ cmd := exec.Command("shutdown", "-r", fmt.Sprintf("+%d", mins), shutdownMsg)
+ if out, err := cmd.CombinedOutput(); err != nil {
+ return osutil.OutputErr(out, err)
+ }
return nil
}
+var reboot = rebootImpl
+
// Dying is a tomb-ish thing
func (d *Daemon) Dying() <-chan struct{} {
return d.tomb.Dying()
}
+func clearReboot(st *state.State) {
+ st.Set("daemon-system-restart-at", nil)
+ st.Set("daemon-system-restart-tentative", nil)
+}
+
+// RebootAsExpected implements part of overlord.RestartBehavior.
+func (d *Daemon) RebootAsExpected(st *state.State) error {
+ clearReboot(st)
+ return nil
+}
+
+// RebootDidNotHappen implements part of overlord.RestartBehavior.
+func (d *Daemon) RebootDidNotHappen(st *state.State) error {
+ var nTentative int
+ err := st.Get("daemon-system-restart-tentative", &nTentative)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ nTentative++
+ if nTentative > rebootMaxTentatives {
+ // giving up, proceed normally, some in-progress refresh
+ // might get rolled back!!
+ st.ClearReboot()
+ clearReboot(st)
+ logger.Noticef("snapd was restarted while a system restart was expected, snapd retried to schedule and waited again for a system restart %d times and is giving up", rebootMaxTentatives)
+ return nil
+ }
+ st.Set("daemon-system-restart-tentative", nTentative)
+ d.state = st
+ logger.Noticef("snapd was restarted while a system restart was expected, snapd will try to schedule and wait for a system restart again (tenative %d/%d)", nTentative, rebootMaxTentatives)
+ return state.ErrExpectedReboot
+}
+
// New Daemon
func New() (*Daemon, error) {
- ovld, err := overlord.New()
+ d := &Daemon{}
+ ovld, err := overlord.New(d)
+ if err == state.ErrExpectedReboot {
+ // we proceed without overlord until we reach Stop
+ // where we will schedule and wait again for a system restart.
+ // ATM we cannot do that in New because we need to satisfy
+ // systemd notify mechanisms.
+ d.expectedRebootDidNotHappen = true
+ return d, nil
+ }
if err != nil {
return nil, err
}
- return &Daemon{overlord: ovld}, nil
+ d.overlord = ovld
+ d.state = ovld.State()
+ return d, nil
}
diff -Nru snapd-2.40/daemon/daemon_test.go snapd-2.42.1/daemon/daemon_test.go
--- snapd-2.40/daemon/daemon_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/daemon/daemon_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -42,14 +42,17 @@
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/devicestate/devicestatetest"
"github.com/snapcore/snapd/overlord/ifacestate"
+ "github.com/snapcore/snapd/overlord/patch"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/standby"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/polkit"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/store"
"github.com/snapcore/snapd/systemd"
"github.com/snapcore/snapd/testutil"
)
@@ -122,17 +125,16 @@
mck.lastMethod = r.Method
}
-func mkRF(c *check.C, cmd *Command, mck *mockHandler) ResponseFunc {
- return func(innerCmd *Command, req *http.Request, user *auth.UserState) Response {
- c.Assert(cmd, check.Equals, innerCmd)
- return mck
- }
-}
-
func (s *daemonSuite) TestCommandMethodDispatch(c *check.C) {
+ fakeUserAgent := "some-agent-talking-to-snapd/1.0"
+
cmd := &Command{d: newTestDaemon(c)}
mck := &mockHandler{cmd: cmd}
- rf := mkRF(c, cmd, mck)
+ rf := func(innerCmd *Command, req *http.Request, user *auth.UserState) Response {
+ c.Assert(cmd, check.Equals, innerCmd)
+ c.Check(store.ClientUserAgent(req.Context()), check.Equals, fakeUserAgent)
+ return mck
+ }
cmd.GET = rf
cmd.PUT = rf
cmd.POST = rf
@@ -140,6 +142,7 @@
for _, method := range []string{"GET", "POST", "PUT", "DELETE"} {
req, err := http.NewRequest(method, "", nil)
+ req.Header.Add("User-Agent", fakeUserAgent)
c.Assert(err, check.IsNil)
rec := httptest.NewRecorder()
@@ -476,9 +479,9 @@
accept chan struct{}
accept1 bool
- closed chan struct{}
- closed1 bool
- closedLck sync.Mutex
+ idempotClose sync.Once
+ closeErr error
+ closed chan struct{}
}
func (l *witnessAcceptListener) Accept() (net.Conn, error) {
@@ -490,16 +493,13 @@
}
func (l *witnessAcceptListener) Close() error {
- err := l.Listener.Close()
- if l.closed != nil {
- l.closedLck.Lock()
- defer l.closedLck.Unlock()
- if !l.closed1 {
- l.closed1 = true
+ l.idempotClose.Do(func() {
+ l.closeErr = l.Listener.Close()
+ if l.closed != nil {
close(l.closed)
}
- }
- return err
+ })
+ return l.closeErr
}
func (s *daemonSuite) markSeeded(d *Daemon) {
@@ -529,19 +529,23 @@
Current: snap.R(1),
})
st.Unlock()
+ // 1 snap => extended timeout 30s + 5s
+ const extendedTimeoutUSec = "EXTEND_TIMEOUT_USEC=35000000"
- l, err := net.Listen("tcp", "127.0.0.1:0")
+ l1, err := net.Listen("tcp", "127.0.0.1:0")
+ c.Assert(err, check.IsNil)
+ l2, err := net.Listen("tcp", "127.0.0.1:0")
c.Assert(err, check.IsNil)
snapdAccept := make(chan struct{})
- d.snapdListener = &witnessAcceptListener{Listener: l, accept: snapdAccept}
+ d.snapdListener = &witnessAcceptListener{Listener: l1, accept: snapdAccept}
snapAccept := make(chan struct{})
- d.snapListener = &witnessAcceptListener{Listener: l, accept: snapAccept}
+ d.snapListener = &witnessAcceptListener{Listener: l2, accept: snapAccept}
- d.Start()
+ c.Assert(d.Start(), check.IsNil)
- c.Check(s.notified, check.DeepEquals, []string{"READY=1"})
+ c.Check(s.notified, check.DeepEquals, []string{extendedTimeoutUSec, "READY=1"})
snapdDone := make(chan struct{})
go func() {
@@ -569,7 +573,7 @@
err = d.Stop(nil)
c.Check(err, check.IsNil)
- c.Check(s.notified, check.DeepEquals, []string{"READY=1", "STOPPING=1"})
+ c.Check(s.notified, check.DeepEquals, []string{extendedTimeoutUSec, "READY=1", "STOPPING=1"})
}
func (s *daemonSuite) TestRestartWiring(c *check.C) {
@@ -586,7 +590,7 @@
snapAccept := make(chan struct{})
d.snapListener = &witnessAcceptListener{Listener: l, accept: snapAccept}
- d.Start()
+ c.Assert(d.Start(), check.IsNil)
defer d.Stop(nil)
snapdDone := make(chan struct{})
@@ -664,7 +668,7 @@
snapAccept := make(chan struct{})
d.snapListener = &witnessAcceptListener{Listener: snapL, accept: snapAccept}
- d.Start()
+ c.Assert(d.Start(), check.IsNil)
snapdAccepting := make(chan struct{})
go func() {
@@ -733,9 +737,11 @@
snapAccept := make(chan struct{})
d.snapListener = &witnessAcceptListener{Listener: l, accept: snapAccept}
- d.Start()
+ c.Assert(d.Start(), check.IsNil)
defer d.Stop(nil)
+ st := d.overlord.State()
+
snapdDone := make(chan struct{})
go func() {
select {
@@ -775,7 +781,9 @@
return nil
}
- d.overlord.State().RequestRestart(state.RestartSystem)
+ st.Lock()
+ st.RequestRestart(state.RestartSystem)
+ st.Unlock()
defer func() {
d.mu.Lock()
@@ -798,14 +806,25 @@
c.Check(delays, check.HasLen, 1)
c.Check(delays[0], check.DeepEquals, rebootWaitTimeout)
+ now := time.Now()
+
err = d.Stop(nil)
c.Check(err, check.ErrorMatches, "expected reboot did not happen")
+
c.Check(delays, check.HasLen, 2)
c.Check(delays[1], check.DeepEquals, 1*time.Minute)
// we are not stopping, we wait for the reboot instead
- c.Check(s.notified, check.DeepEquals, []string{"READY=1"})
+ c.Check(s.notified, check.DeepEquals, []string{"EXTEND_TIMEOUT_USEC=30000000", "READY=1"})
+
+ st.Lock()
+ defer st.Unlock()
+ var rebootAt time.Time
+ err = st.Get("daemon-system-restart-at", &rebootAt)
+ c.Assert(err, check.IsNil)
+ approxAt := now.Add(time.Minute)
+ c.Check(rebootAt.After(approxAt) || rebootAt.Equal(approxAt), check.Equals, true)
}
func (s *daemonSuite) TestRebootHelper(c *check.C) {
@@ -820,7 +839,7 @@
{0, "+0"},
{time.Minute, "+1"},
{10 * time.Minute, "+10"},
- {30 * time.Second, "+1"},
+ {30 * time.Second, "+0"},
}
for _, t := range tests {
@@ -866,8 +885,12 @@
makeDaemonListeners(c, d)
s.markSeeded(d)
- d.Start()
- d.overlord.State().RequestRestart(state.RestartSystem)
+ c.Assert(d.Start(), check.IsNil)
+ st := d.overlord.State()
+
+ st.Lock()
+ st.RequestRestart(state.RestartSystem)
+ st.Unlock()
ch := make(chan os.Signal, 2)
ch <- syscall.SIGTERM
@@ -882,7 +905,6 @@
oldRebootNoticeWait := rebootNoticeWait
oldRebootWaitTimeout := rebootWaitTimeout
defer func() {
- reboot = rebootImpl
rebootNoticeWait = oldRebootNoticeWait
rebootWaitTimeout = oldRebootWaitTimeout
}()
@@ -896,8 +918,12 @@
makeDaemonListeners(c, d)
s.markSeeded(d)
- d.Start()
- d.overlord.State().RequestRestart(state.RestartSystem)
+ c.Assert(d.Start(), check.IsNil)
+ st := d.overlord.State()
+
+ st.Lock()
+ st.RequestRestart(state.RestartSystem)
+ st.Unlock()
sigCh := make(chan os.Signal, 2)
// stop (this will timeout but thats not relevant for this test)
@@ -908,6 +934,102 @@
c.Assert(chOpen, check.Equals, false)
}
+func (s *daemonSuite) TestRestartExpectedRebootDidNotHappen(c *check.C) {
+ curBootID, err := osutil.BootID()
+ c.Assert(err, check.IsNil)
+
+ fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"patch-sublevel":%d,"some":"data","refresh-privacy-key":"0123456789ABCDEF","system-restart-from-boot-id":%q,"daemon-system-restart-at":"%s"},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0,"last-lane-id":0}`, patch.Level, patch.Sublevel, curBootID, time.Now().UTC().Format(time.RFC3339)))
+ err = ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
+ c.Assert(err, check.IsNil)
+
+ oldRebootNoticeWait := rebootNoticeWait
+ oldRebootRetryWaitTimeout := rebootRetryWaitTimeout
+ defer func() {
+ rebootNoticeWait = oldRebootNoticeWait
+ rebootRetryWaitTimeout = oldRebootRetryWaitTimeout
+ }()
+ rebootRetryWaitTimeout = 100 * time.Millisecond
+ rebootNoticeWait = 150 * time.Millisecond
+
+ cmd := testutil.MockCommand(c, "shutdown", "")
+ defer cmd.Restore()
+
+ d := newTestDaemon(c)
+ c.Check(d.overlord, check.IsNil)
+ c.Check(d.expectedRebootDidNotHappen, check.Equals, true)
+
+ var n int
+ d.state.Lock()
+ err = d.state.Get("daemon-system-restart-tentative", &n)
+ d.state.Unlock()
+ c.Check(err, check.IsNil)
+ c.Check(n, check.Equals, 1)
+
+ c.Assert(d.Start(), check.IsNil)
+
+ c.Check(s.notified, check.DeepEquals, []string{"READY=1"})
+
+ select {
+ case <-d.Dying():
+ case <-time.After(2 * time.Second):
+ c.Fatal("expected reboot not happening should proceed to try to shutdown again")
+ }
+
+ sigCh := make(chan os.Signal, 2)
+ // stop (this will timeout but thats not relevant for this test)
+ d.Stop(sigCh)
+
+ // an immediate shutdown was scheduled again
+ c.Check(cmd.Calls(), check.DeepEquals, [][]string{
+ {"shutdown", "-r", "+0", "reboot scheduled to update the system"},
+ })
+}
+
+func (s *daemonSuite) TestRestartExpectedRebootOK(c *check.C) {
+ fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"patch-sublevel":%d,"some":"data","refresh-privacy-key":"0123456789ABCDEF","system-restart-from-boot-id":%q,"daemon-system-restart-at":"%s"},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0,"last-lane-id":0}`, patch.Level, patch.Sublevel, "boot-id-0", time.Now().UTC().Format(time.RFC3339)))
+ err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
+ c.Assert(err, check.IsNil)
+
+ cmd := testutil.MockCommand(c, "shutdown", "")
+ defer cmd.Restore()
+
+ d := newTestDaemon(c)
+ c.Assert(d.overlord, check.NotNil)
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ var v interface{}
+ // these were cleared
+ c.Check(st.Get("daemon-system-restart-at", &v), check.Equals, state.ErrNoState)
+ c.Check(st.Get("system-restart-from-boot-id", &v), check.Equals, state.ErrNoState)
+}
+
+func (s *daemonSuite) TestRestartExpectedRebootGiveUp(c *check.C) {
+ // we give up trying to restart the system after 3 retry tentatives
+ curBootID, err := osutil.BootID()
+ c.Assert(err, check.IsNil)
+
+ fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"patch-sublevel":%d,"some":"data","refresh-privacy-key":"0123456789ABCDEF","system-restart-from-boot-id":%q,"daemon-system-restart-at":"%s","daemon-system-restart-tentative":3},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0,"last-lane-id":0}`, patch.Level, patch.Sublevel, curBootID, time.Now().UTC().Format(time.RFC3339)))
+ err = ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
+ c.Assert(err, check.IsNil)
+
+ cmd := testutil.MockCommand(c, "shutdown", "")
+ defer cmd.Restore()
+
+ d := newTestDaemon(c)
+ c.Assert(d.overlord, check.NotNil)
+
+ st := d.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ var v interface{}
+ // these were cleared
+ c.Check(st.Get("daemon-system-restart-at", &v), check.Equals, state.ErrNoState)
+ c.Check(st.Get("system-restart-from-boot-id", &v), check.Equals, state.ErrNoState)
+ c.Check(st.Get("daemon-system-restart-tentative", &v), check.Equals, state.ErrNoState)
+}
+
func (s *daemonSuite) TestRestartIntoSocketModeNoNewChanges(c *check.C) {
restore := standby.MockStandbyWait(5 * time.Millisecond)
defer restore()
@@ -919,10 +1041,10 @@
// go into socket activation mode
s.markSeeded(d)
- d.Start()
+ c.Assert(d.Start(), check.IsNil)
// pretend some ensure happened
for i := 0; i < 5; i++ {
- d.overlord.StateEngine().Ensure()
+ c.Check(d.overlord.StateEngine().Ensure(), check.IsNil)
time.Sleep(5 * time.Millisecond)
}
@@ -949,10 +1071,10 @@
s.markSeeded(d)
st := d.overlord.State()
- d.Start()
+ c.Assert(d.Start(), check.IsNil)
// pretend some ensure happened
for i := 0; i < 5; i++ {
- d.overlord.StateEngine().Ensure()
+ c.Check(d.overlord.StateEngine().Ensure(), check.IsNil)
time.Sleep(5 * time.Millisecond)
}
@@ -980,16 +1102,16 @@
c.Check(d.restartSocket, check.Equals, false)
}
-func (s *daemonSuite) TestShutdownServerCanShutdown(c *check.C) {
- shush := newShutdownServer(nil, nil)
- c.Check(shush.CanStandby(), check.Equals, true)
+func (s *daemonSuite) TestConnTrackerCanShutdown(c *check.C) {
+ ct := &connTracker{conns: make(map[net.Conn]struct{})}
+ c.Check(ct.CanStandby(), check.Equals, true)
con := &net.IPConn{}
- shush.conns[con] = http.StateActive
- c.Check(shush.CanStandby(), check.Equals, false)
+ ct.trackConn(con, http.StateActive)
+ c.Check(ct.CanStandby(), check.Equals, false)
- shush.conns[con] = http.StateIdle
- c.Check(shush.CanStandby(), check.Equals, true)
+ ct.trackConn(con, http.StateIdle)
+ c.Check(ct.CanStandby(), check.Equals, true)
}
func doTestReq(c *check.C, cmd *Command, mth string) *httptest.ResponseRecorder {
diff -Nru snapd-2.40/daemon/export_test.go snapd-2.42.1/daemon/export_test.go
--- snapd-2.40/daemon/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/daemon/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -41,3 +41,11 @@
muxVars = old
}
}
+
+func MockBuildID(mock string) (restore func()) {
+ old := buildID
+ buildID = mock
+ return func() {
+ buildID = old
+ }
+}
diff -Nru snapd-2.40/daemon/response.go snapd-2.42.1/daemon/response.go
--- snapd-2.40/daemon/response.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/daemon/response.go 2019-10-30 12:17:43.000000000 +0000
@@ -188,6 +188,8 @@
errorKindDaemonRestart = errorKind("daemon-restart")
errorKindSystemRestart = errorKind("system-restart")
+
+ errorKindAssertionNotFound = errorKind("assertion-not-found")
)
type errorValue interface{}
@@ -418,7 +420,7 @@
kind := errorKindSnapRevisionNotAvailable
msg := rnaErr.Error()
if len(rnaErr.Releases) != 0 && rnaErr.Channel != "" {
- thisArch := arch.UbuntuArchitecture()
+ thisArch := arch.DpkgArchitecture()
values := map[string]interface{}{
"snap-name": snapName,
"action": rnaErr.Action,
diff -Nru snapd-2.40/daemon/snap.go snapd-2.42.1/daemon/snap.go
--- snapd-2.40/daemon/snap.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/daemon/snap.go 2019-10-30 12:17:43.000000000 +0000
@@ -31,6 +31,7 @@
"github.com/snapcore/snapd/cmd"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/overlord/assertstate"
+ "github.com/snapcore/snapd/overlord/healthstate"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
@@ -68,6 +69,20 @@
type aboutSnap struct {
info *snap.Info
snapst *snapstate.SnapState
+ health *client.SnapHealth
+}
+
+func clientHealthFromHealthstate(h *healthstate.HealthState) *client.SnapHealth {
+ if h == nil {
+ return nil
+ }
+ return &client.SnapHealth{
+ Revision: h.Revision,
+ Timestamp: h.Timestamp,
+ Status: h.Status.String(),
+ Message: h.Message,
+ Code: h.Code,
+ }
}
// localSnapInfo returns the information about the current snap for the given name plus the SnapState with the active flag and other snap revisions.
@@ -94,9 +109,15 @@
return aboutSnap{}, err
}
+ health, err := healthstate.Get(st, name)
+ if err != nil {
+ return aboutSnap{}, err
+ }
+
return aboutSnap{
info: info,
snapst: &snapst,
+ health: clientHealthFromHealthstate(health),
}, nil
}
@@ -111,11 +132,17 @@
}
about := make([]aboutSnap, 0, len(snapStates))
+ healths, err := healthstate.All(st)
+ if err != nil {
+ return nil, err
+ }
+
var firstErr error
for name, snapst := range snapStates {
if len(wanted) > 0 && !wanted[name] {
continue
}
+ health := clientHealthFromHealthstate(healths[name])
var aboutThis []aboutSnap
var info *snap.Info
var err error
@@ -137,13 +164,13 @@
if err != nil && firstErr == nil {
firstErr = err
}
- aboutThis = append(aboutThis, aboutSnap{info, snapst})
+ aboutThis = append(aboutThis, aboutSnap{info, snapst, health})
}
} else {
info, err = snapst.CurrentInfo()
if err == nil {
info.Publisher, err = publisherAccount(st, info.SnapID)
- aboutThis = append(aboutThis, aboutSnap{info, snapst})
+ aboutThis = append(aboutThis, aboutSnap{info, snapst, health})
}
}
@@ -290,6 +317,7 @@
// prime dir)
result.MountedFrom, _ = os.Readlink(result.MountedFrom)
}
+ result.Health = about.health
return result
}
diff -Nru snapd-2.40/daemon/ucrednet.go snapd-2.42.1/daemon/ucrednet.go
--- snapd-2.40/daemon/ucrednet.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/daemon/ucrednet.go 2019-10-30 12:17:43.000000000 +0000
@@ -25,6 +25,7 @@
"net"
"regexp"
"strconv"
+ "sync"
sys "syscall"
)
@@ -94,7 +95,12 @@
return &ucrednetAddr{wc.Conn.RemoteAddr(), wc.ucrednet}
}
-type ucrednetListener struct{ net.Listener }
+type ucrednetListener struct {
+ net.Listener
+
+ idempotClose sync.Once
+ closeErr error
+}
var getUcred = sys.GetsockoptUcred
@@ -127,3 +133,10 @@
return &ucrednetConn{con, unet}, nil
}
+
+func (wl *ucrednetListener) Close() error {
+ wl.idempotClose.Do(func() {
+ wl.closeErr = wl.Listener.Close()
+ })
+ return wl.closeErr
+}
diff -Nru snapd-2.40/daemon/ucrednet_test.go snapd-2.42.1/daemon/ucrednet_test.go
--- snapd-2.40/daemon/ucrednet_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/daemon/ucrednet_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -58,7 +58,9 @@
l, err := net.Listen("unix", sock)
c.Assert(err, check.IsNil)
- defer l.Close()
+ wl := &ucrednetListener{Listener: l}
+
+ defer wl.Close()
go func() {
cli, err := net.Dial("unix", sock)
@@ -66,8 +68,6 @@
cli.Close()
}()
- wl := &ucrednetListener{l}
-
conn, err := wl.Accept()
c.Assert(err, check.IsNil)
defer conn.Close()
@@ -83,7 +83,9 @@
func (s *ucrednetSuite) TestNonUnix(c *check.C) {
l, err := net.Listen("tcp", "localhost:0")
c.Assert(err, check.IsNil)
- defer l.Close()
+
+ wl := &ucrednetListener{Listener: l}
+ defer wl.Close()
addr := l.Addr().String()
@@ -93,8 +95,6 @@
cli.Close()
}()
- wl := &ucrednetListener{l}
-
conn, err := wl.Accept()
c.Assert(err, check.IsNil)
defer conn.Close()
@@ -116,7 +116,7 @@
c.Assert(err, check.IsNil)
c.Assert(l.Close(), check.IsNil)
- wl := &ucrednetListener{l}
+ wl := &ucrednetListener{Listener: l}
_, err = wl.Accept()
c.Assert(err, check.NotNil)
@@ -129,7 +129,9 @@
l, err := net.Listen("unix", sock)
c.Assert(err, check.IsNil)
- defer l.Close()
+
+ wl := &ucrednetListener{Listener: l}
+ defer wl.Close()
go func() {
cli, err := net.Dial("unix", sock)
@@ -137,12 +139,23 @@
cli.Close()
}()
- wl := &ucrednetListener{l}
-
_, err = wl.Accept()
c.Assert(err, check.Equals, s.err)
}
+func (s *ucrednetSuite) TestIdempotentClose(c *check.C) {
+ s.ucred = &sys.Ucred{Pid: 100, Uid: 42}
+ d := c.MkDir()
+ sock := filepath.Join(d, "sock")
+
+ l, err := net.Listen("unix", sock)
+ c.Assert(err, check.IsNil)
+ wl := &ucrednetListener{Listener: l}
+
+ c.Assert(wl.Close(), check.IsNil)
+ c.Assert(wl.Close(), check.IsNil)
+}
+
func (s *ucrednetSuite) TestGetNoUid(c *check.C) {
pid, uid, _, err := ucrednetGet("pid=100;uid=;socket=;")
c.Check(err, check.Equals, errNoID)
diff -Nru snapd-2.40/data/completion/snap snapd-2.42.1/data/completion/snap
--- snapd-2.40/data/completion/snap 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/data/completion/snap 2019-10-30 12:17:43.000000000 +0000
@@ -38,7 +38,7 @@
fi
done
- command=${words[1]}
+ command="${words[1]}"
# Only split on newlines
local IFS=$'\n'
@@ -46,7 +46,12 @@
# now we pass _just the bit that's being completed_ of the command
# to snap for it to figure it out. go-flags isn't smart enough to
# look at COMP_WORDS etc. itself.
- COMPREPLY=($(GO_FLAGS_COMPLETION=1 snap "$command" "$cur"))
+ if [ "$command" = "debug" ]; then
+ command="${words[2]}"
+ COMPREPLY=($(GO_FLAGS_COMPLETION=1 snap debug "$command" "$cur"))
+ else
+ COMPREPLY=($(GO_FLAGS_COMPLETION=1 snap "$command" "$cur"))
+ fi
case $command in
install|info|sign-build)
diff -Nru snapd-2.40/data/desktop/snap-userd-autostart.desktop.in snapd-2.42.1/data/desktop/snap-userd-autostart.desktop.in
--- snapd-2.40/data/desktop/snap-userd-autostart.desktop.in 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/data/desktop/snap-userd-autostart.desktop.in 2019-10-30 12:17:43.000000000 +0000
@@ -3,3 +3,4 @@
Comment=Helper program for launching snap applications that are configured to start automatically.
Exec=@bindir@/snap userd --autostart
Type=Application
+NoDisplay=true
diff -Nru snapd-2.40/data/Makefile snapd-2.42.1/data/Makefile
--- snapd-2.40/data/Makefile 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/data/Makefile 2019-10-30 12:17:43.000000000 +0000
@@ -1,5 +1,6 @@
all install clean:
$(MAKE) -C systemd $@
+ $(MAKE) -C systemd-user $@
$(MAKE) -C systemd-env $@
$(MAKE) -C dbus $@
$(MAKE) -C env $@
diff -Nru snapd-2.40/data/selinux/snappy.te snapd-2.42.1/data/selinux/snappy.te
--- snapd-2.40/data/selinux/snappy.te 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/data/selinux/snappy.te 2019-10-30 12:17:43.000000000 +0000
@@ -100,7 +100,10 @@
# Allow transitions from init_t to snappy for sockets
# init_named_socket_activation() is not supported by core policy in RHEL7
-gen_require(` type init_t; type var_run_t; ')
+gen_require(`
+ type init_t;
+ type var_run_t;
+')
filetrans_pattern(init_t, var_run_t, snappy_var_run_t, sock_file, "snapd.socket")
filetrans_pattern(init_t, var_run_t, snappy_var_run_t, sock_file, "snapd-snap.socket")
@@ -108,7 +111,9 @@
allow init_t snappy_var_lib_t:dir read;
# Allow snapd to read procfs
-gen_require(` type proc_t; ')
+gen_require(`
+ type proc_t;
+')
allow snappy_t proc_t:file { getattr open read };
# Allow snapd to read sysfs
@@ -116,7 +121,9 @@
dev_search_sysfs(snappy_t)
# This silences a read AVC denial event on the lost+found directory.
-gen_require(` type lost_found_t; ')
+gen_require(`
+ type lost_found_t;
+')
dontaudit snappy_t lost_found_t:dir read;
# Allow snapd to read SSL cert store
@@ -134,7 +141,9 @@
sysnet_dns_name_resolve(snappy_t)
# When managed by NetworkManager, DNS config is in its rundata
-gen_require(` type NetworkManager_var_run_t; ')
+gen_require(`
+ type NetworkManager_var_run_t;
+')
allow snappy_t NetworkManager_var_run_t:dir search;
# Allow snapd to read sysctl files
@@ -196,17 +205,23 @@
read_files_pattern(snappy_t, snappy_var_run_t, snappy_var_run_t)
getattr_files_pattern(snappy_t, snappy_var_run_t, snappy_var_run_t)
-gen_require(` type user_tmp_t; ')
+gen_require(`
+ type user_tmp_t;
+')
allow snappy_t user_tmp_t:dir { read };
# Allow snapd to clean up /run/user sockets
userdom_manage_tmp_dirs(snappy_t)
userdom_manage_tmp_sockets(snappy_t)
-gen_require(` type systemd_unit_file_t; ')
+gen_require(`
+ type systemd_unit_file_t;
+')
allow snappy_t systemd_unit_file_t:dir { rmdir };
-gen_require(` type home_root_t; ')
+gen_require(`
+ type home_root_t;
+')
allow snappy_t home_root_t:dir { read };
# Allow snapd to manage its persistent data
@@ -233,7 +248,9 @@
files_tmp_filetrans(snappy_t, snappy_tmp_t, { file dir })
# snap command completions, symlinks going back to snap mount directory
-gen_require(` type usr_t; ')
+gen_require(`
+ type usr_t;
+')
allow snappy_t usr_t:dir { write remove_name add_name };
allow snappy_t usr_t:lnk_file { create unlink };
@@ -263,7 +280,9 @@
allow snappy_t self:capability2 block_suspend;
# snapd needs to check for ipv6 support
-gen_require(` type node_t; ')
+gen_require(`
+ type node_t;
+')
allow snappy_t node_t:tcp_socket node_bind;
corenet_all_recvfrom_unlabeled(snappy_t)
@@ -306,6 +325,8 @@
# snapd runs journalctl to fetch logs
optional_policy(`
journalctl_run(snappy_t, snappy_roles)
+ # and kills journalctl once the logs have been fetched
+ allow snappy_t journalctl_t:process sigkill;
')
# only pops up in cloud images where cloud-init.target is incorrectly labeled
@@ -321,6 +342,10 @@
libs_manage_lib_dirs(snappy_t)
libs_manage_lib_files(snappy_t)
fs_getattr_xattr_fs(snappy_t)
+# snapd checks whether .mnt exists before running the mount namespace
+# helper tool
+# fs_getattr_nsfs_files() is not available in selinux devel on CentOS 7.x
+getattr_files_pattern(snappy_t, nsfs_t, nsfs_t)
# snapd attempts to read /run/cloud-init/instance-data.json
sysnet_read_config(snappy_t)
@@ -359,12 +384,17 @@
files_pid_filetrans(snappy_mount_t, snappy_var_run_t, {file dir})
# Allow snap-{update,discard}-ns to manage mounts
-gen_require(` type fs_t; type mount_var_run_t; ')
+gen_require(`
+ type fs_t;
+ type mount_var_run_t;
+')
allow snappy_mount_t fs_t:filesystem { mount unmount };
allow snappy_mount_t mount_var_run_t:dir { add_name remove_name write search };
allow snappy_mount_t mount_var_run_t:file { create getattr setattr open read write rename unlink lock };
# for discard-ns, because a preserved mount ns is a bind-mounted /proc//ns/mnt
-gen_require(` type proc_t; ')
+gen_require(`
+ type proc_t;
+')
allow snappy_mount_t proc_t:filesystem { getattr unmount };
allow snappy_mount_t self:capability { sys_chroot sys_admin };
@@ -379,6 +409,10 @@
fs_getattr_all_fs(snappy_mount_t)
fs_getattr_tmpfs(snappy_mount_t)
fs_getattr_xattr_fs(snappy_mount_t)
+# snap-discard-ns pokes, reads and unmounts the mount ns captured at .mnt
+fs_read_nsfs_files(snappy_mount_t)
+fs_unmount_nsfs(snappy_mount_t)
+
# freezer
fs_manage_cgroup_dirs(snappy_mount_t)
fs_manage_cgroup_files(snappy_mount_t)
@@ -387,7 +421,9 @@
fs_read_tmpfs_symlinks(snappy_mount_t)
# because /run/snapd/ns/*.mnt gets a label of the process context
-gen_require(` type unconfined_t; ')
+gen_require(`
+ type unconfined_t;
+')
allow snappy_mount_t unconfined_t:file { open read getattr };
allow snappy_mount_t snappy_confine_t:file { open read getattr };
@@ -395,6 +431,7 @@
kernel_read_system_state(snappy_mount_t)
kernel_read_net_sysctls(snappy_mount_t)
kernel_search_network_sysctl(snappy_mount_t)
+dev_read_sysfs(snappy_mount_t)
########################################
#
@@ -444,6 +481,7 @@
fs_write_cgroup_files(snappy_confine_t)
kernel_getattr_debugfs(snappy_confine_t)
kernel_getattr_proc(snappy_confine_t)
+fs_read_nsfs_files(snappy_confine_t)
term_getattr_pty_fs(snappy_confine_t)
# term_getattr_generic_ptys() is not supported by core policy in RHEL7
allow snappy_confine_t devpts_t:chr_file getattr;
@@ -458,6 +496,7 @@
type modules_object_t;
type ifconfig_var_run_t;
type var_log_t;
+ type lib_t;
')
allow snappy_confine_t admin_home_t:dir mounton;
allow snappy_confine_t bin_t:dir mounton;
@@ -468,6 +507,7 @@
allow snappy_confine_t home_root_t:dir mounton;
allow snappy_confine_t ifconfig_var_run_t:dir mounton;
allow snappy_confine_t modules_object_t:dir mounton;
+allow snappy_confine_t lib_t:dir mounton;
allow snappy_confine_t ptmx_t:chr_file { getattr mounton };
allow snappy_confine_t snappy_snap_t:dir { mounton read };
allow snappy_confine_t snappy_snap_t:file mounton;
@@ -508,7 +548,9 @@
read_files_pattern(snappy_confine_t, snappy_snap_t, snappy_snap_t)
# and allow transition by snap-confine
allow snappy_confine_t snappy_unconfined_snap_t:process { noatsecure rlimitinh siginh transition dyntransition };
-gen_require(` type unconfined_service_t; ')
+gen_require(`
+ type unconfined_service_t;
+')
allow snappy_confine_t unconfined_service_t:process { noatsecure rlimitinh siginh transition dyntransition };
# for classic snaps, snap-confine executes snap-exec from the host (labeled as
@@ -573,6 +615,7 @@
init_ioctl_stream_sockets(snappy_cli_t)
kernel_read_net_sysctls(snappy_cli_t)
kernel_search_network_sysctl(snappy_cli_t)
+dev_read_sysfs(snappy_cli_t)
# talk to snapd
snappy_stream_connect(snappy_cli_t)
@@ -597,7 +640,9 @@
domain_entry_file(unconfined_service_t, snappy_snap_t)
# for journald
-gen_require(` type syslogd_t; ')
+gen_require(`
+ type syslogd_t;
+')
allow syslogd_t snappy_unconfined_snap_t:dir search_dir_perms;
allow snappy_unconfined_snap_t self:process { fork getsched };
@@ -620,7 +665,9 @@
# snap tools can be invoked by the regular user, make sure that things get
# proper labels
-gen_require(` type unconfined_t; ')
+gen_require(`
+ type unconfined_t;
+')
userdom_user_home_dir_filetrans(unconfined_t, snappy_home_t, dir, "snap")
userdom_admin_home_dir_filetrans(unconfined_t, snappy_home_t, dir, "snap")
files_pid_filetrans(unconfined_t, snappy_var_run_t, dir, "snapd")
@@ -641,3 +688,13 @@
# declare fs_type(snappy_snap_t) outside of core policy, add explicit permission
# instead
allow init_t snappy_snap_t:filesystem remount;
+
+########################################
+#
+# extra policy for mandb_t
+#
+gen_require(`
+ type mandb_t;
+')
+# mandb cache update scans whe whole directory tree looking for 'man'
+allow mandb_t snappy_var_lib_t:dir search_dir_perms;
diff -Nru snapd-2.40/data/systemd-user/Makefile snapd-2.42.1/data/systemd-user/Makefile
--- snapd-2.40/data/systemd-user/Makefile 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/data/systemd-user/Makefile 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,40 @@
+#
+# Copyright (C) 2019 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 .
+
+SNAPD_ENVIRONMENT_FILE := /etc/environment
+BINDIR := /usr/bin
+SYSTEMDUSERUNITDIR := /usr/lib/systemd/user
+
+SYSTEMD_UNITS_GENERATED := $(wildcard *.in)
+# NOTE: sort removes duplicates so this gives us all the units, generated or otherwise
+SYSTEMD_UNITS = $(sort $(SYSTEMD_UNITS_GENERATED:.in=) $(wildcard *.service) $(wildcard *.timer) $(wildcard *.socket))
+
+.PHONY: all
+all: $(SYSTEMD_UNITS)
+
+.PHONY: install
+install:: $(SYSTEMD_UNITS)
+ # NOTE: old (e.g. 14.04) GNU coreutils doesn't -D with -t
+ install -d -m 0755 $(DESTDIR)/$(SYSTEMDUSERUNITDIR)
+ install -m 0644 -t $(DESTDIR)/$(SYSTEMDUSERUNITDIR) $^
+
+.PHONY: clean
+clean:
+ rm -f $(SYSTEMD_UNITS_GENERATED:.in=)
+
+%: %.in
+ cat $< | \
+ sed s:@bindir@:$(BINDIR):g | \
+ cat > $@
diff -Nru snapd-2.40/data/systemd-user/snapd.session-agent.service.in snapd-2.42.1/data/systemd-user/snapd.session-agent.service.in
--- snapd-2.40/data/systemd-user/snapd.session-agent.service.in 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/data/systemd-user/snapd.session-agent.service.in 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,7 @@
+[Unit]
+Description=snapd user session agent
+Requires=snapd.session-agent.socket
+
+[Service]
+Type=notify
+ExecStart=@bindir@/snap userd --agent
diff -Nru snapd-2.40/data/systemd-user/snapd.session-agent.socket snapd-2.42.1/data/systemd-user/snapd.session-agent.socket
--- snapd-2.40/data/systemd-user/snapd.session-agent.socket 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/data/systemd-user/snapd.session-agent.socket 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,8 @@
+[Unit]
+Description=REST API socket for snapd user session agent
+
+[Socket]
+ListenStream=%t/snapd-session-agent.socket
+
+[Install]
+WantedBy=sockets.target
diff -Nru snapd-2.40/debian/changelog snapd-2.42.1/debian/changelog
--- snapd-2.40/debian/changelog 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/debian/changelog 2019-10-30 12:17:43.000000000 +0000
@@ -1,3 +1,459 @@
+snapd (2.42.1) xenial; urgency=medium
+
+ * New upstream release, LP: #1846181
+ - interfaces: de-duplicate emitted update-ns profiles
+ - packaging: tweak handling of usr.lib.snapd.snap-confine
+ - interfaces: allow introspecting network-manager on core
+ - tests/main/interfaces-contacts-service: disable on openSUSE
+ Tumbleweed
+ - tests/lib/lxd-snapfuse: restore mount changes introduced by LXD
+ - snap: fix default-provider in seed validation
+ - tests: update system-usernames test now that opensuse-15.1 works
+ - overlord: set fake sertial in TestRemodelSwitchToDifferentKernel
+ - gadget: rename "boot{select,img}" -> system-boot-{select,image}
+ - tests: listing test, make accepted snapd/core versions consistent
+
+ -- Michael Vogt Wed, 30 Oct 2019 13:17:43 +0100
+
+snapd (2.42) xenial; urgency=medium
+
+ * New upstream release, LP: #1846181
+ - tests: disable {contacts,calendar}-service tests on debian-sid
+ - tests/main/snap-run: disable strace test cases on Arch
+ - cmd/system-shutdown: include correct prototype for die
+ - snap/naming: add test for hook name connect-plug-i2c
+ - cmd/snap-confine: allow digits in hook names
+ - gadget: do not fail the update when old gadget snap is missing
+ bare content
+ - tests: disable {contacts,calendar}-service tests on Arch Linux
+ - tests: move "centos-7" to unstable systems
+ - interfaces/docker-support,kubernetes-support: misc updates for
+ strict k8s
+ - packaging: remove obsolete usr.lib.snapd.snap-confine in
+ postinst
+ - tests: add test that ensures our snapfuse binary actually works
+ - packaging: use snapfuse_ll to speed up snapfuse performance
+ - usersession/userd: make sure to export DBus interfaces before
+ requesting a name
+ - data/selinux: allow snapd to issue sigkill to journalctl
+ - store: download propagates options to delta download
+ - wrappers: allow snaps to install icon theme icons
+ - debug: state-inspect debugging utility
+ - sandbox/cgroup: introduce cgroup wrappers package
+ - snap-confine: fix return value checks for udev functions
+ - cmd/model: output tweaks, add'l tests
+ - wrappers/services: add ServicesEnableState + unit tests
+ - tests: fix newline and wrong test name pointed out in previous PRs
+ - tests: extend mount-ns test to handle mimics
+ - run-checks, tests/main/go: allow gofmt checks to be skipped on
+ 19.10
+ - tests/main/interfaces-{calendar,contacts}-service: disable on
+ 19.10
+ - tests: part3 making tests work on ubuntu-core-18
+ - tests: fix interfaces-timeserver-control on 19.10
+ - overlord/snapstate: config revision code cleanup and extra tests
+ - devicestate: allow remodel to different kernels
+ - overlord,daemon: adjust startup timeout via EXTEND_TIMEOUT_USEC
+ using an estimate
+ - tests/main/many: increase kill-timeout to 5m
+ - interfaces/kubernetes-support: allow systemd-run to ptrace read
+ unconfined
+ - snapstate: auto transition on experimental.snapd-snap=true
+ - tests: retry checking until the written file on desktop-portal-
+ filechooser
+ - tests: unit test for a refresh failing on configure hook
+ - tests: remove mount_id and parent_id from mount-ns test data
+ - tests: move classic-ubuntu-core-transition* to nightly
+ - tests/mountinfo-tool: proper formatting of opt_fields
+ - overlord/configstate: special-case "null" in transaction Changes()
+ - snap-confine: fallback gracefully on a cgroup v2 only system
+ - tests: debian sid now ships new seccomp, adjust tests
+ - tests: explicitly restore after using LXD
+ - snapstate: make progress reporting less granular
+ - bootloader: little kernel support
+ - fixme: rename ubuntu*architectures to dpkg*architectures
+ - tests: run dbus-launch inside a systemd unit
+ - channel: introduce Resolve and ResolveLocked
+ - tests: run failing tests on ubuntu eoan due to is now set as
+ unstable
+ - systemd: detach rather than unmount .mount units
+ - cmd/snap-confine: add unit tests for sc_invocation, cleanup memory
+ leaks in tests
+ - boot,dirs,image: introduce boot.MakeBootable, use it in image
+ instead of ad hoc code
+ - cmd/snap-update-ns: clarify sharing comment
+ - tests/overlord/snapstate: refactor for cleaner test failures
+ - cmd/snap-update-ns: don't propagate detaching changes
+ - interfaces: allow reading mutter Xauthority file
+ - cmd/snap-confine: fix /snap duplication in legacy mode
+ - tests: fix mountinfo-tool filtering when used with rewriting
+ - seed,image,o/devicestate: extract seed loading to seed/seed16.go
+ - many: pass the rootdir and options to bootloader.Find
+ - tests: part5 making tests work on ubuntu-core-18
+ - cmd/snap-confine: keep track of snap instance name and the snap
+ name
+ - cmd: unify die() across C programs
+ - tests: add functions to make an abstraction for the snaps
+ - packaging/fedora, tests/lib/prepare-restore: helper tool for
+ packing sources for RPM
+ - cmd/snap: improve help and error msg for snapshot commands
+ - hookstate/ctlcmd: fix snapctl set help message
+ - cmd/snap: don't append / to snap name just because a dir exists
+ - tests: support fastly-global.cdn.snapcraft.io url on proxy-no-core
+ test
+ - tests: add --quiet switch to retry-tool
+ - tests: add unstable stage for travis execution
+ - tests: disable interfaces-timeserver-control on 19.10
+ - tests: don't guess in is_classic_confinement_supported
+ - boot, etc: simplify BootParticipant (etc) usage
+ - tests: verify retry-tool not retrying missing commands
+ - tests: rewrite "retry" command as retry-tool
+ - tests: move debug section after restore
+ - cmd/libsnap-confine-private, cmd/s-c: use constants for
+ snap/instance name lengths
+ - tests: measure behavior of the device cgroup
+ - boot, bootloader, o/devicestate: boot env manip goes in boot
+ - tests: enabling ubuntu 19.10-64 on spread.yaml
+ - tests: fix ephemeral mount table in left over by prepare
+ - tests: add version-tool for comparing versions
+ - cmd/libsnap: make feature flag enum 1< Tue, 01 Oct 2019 11:24:41 +0200
+
+snapd (2.41) xenial; urgency=medium
+
+ * New upstream release, LP: #1840740
+ - overlord/snapstate: revert track-risk behavior
+ - tests: fix snap info test
+ - httputil: rework protocol error detection
+ - gadget: do not error on gadget refreshes with multiple volumes
+ - i18n, vendor, packaging: drop github.com/ojii/gettext.go, use
+ github.com/snapcore/go-gettext
+ - snapstate: validate all system-usernames before creating them
+ - mkversion.sh: fix version from git checkouts
+ - interfaces/network-{control,manager}: allow 'k' on
+ /run/resolvconf/**
+ - interfaces/wayland,x11: allow reading an Xwayland Xauth file
+ - interfaces: k8s worker node updates
+ - debian: re-enable systemd environment generator
+ - many: create system-usernames user/group if both don't exist
+ - packaging: fix symlink for snapd.session-agent.socket
+ - tests: change cgroups so that LXD doesn't have to
+ - interfaces/network-setup-control: allow dbus netplan apply
+ messages
+ - tests: add /var/cache/snapd to the snapd state to prevent error on
+ the store
+ - tests: add test for services disabled during refresh hook
+ - many: simpler access to snap-seccomp version-info
+ - snap: cleanup some tests, clarify some errorsThis is a follow up
+ from work on system usernames:
+ - osutil: add osutil.Find{Uid,Gid}
+ - tests: use a different archive based on the spread backend on go-
+ build test
+ - cmd/snap-update-ns: fix pair of bugs affecting refresh of snap
+ with layouts
+ - overlord/devicestate: detect clashing concurrent (ongoing, just
+ finished) remodels or changes
+ - interfaces/docker-support: declare controls-device-cgroup
+ - packaging: fix removal of old apparmor profile
+ - store: use track/risk for "channel" name when parsing store
+ details
+ - many: allow 'system-usernames' with libseccomp > 2.4 and golang-
+ seccomp > 0.9.0
+ - overlord/devicestate, tests: use gadget.Update() proper, spread
+ test
+ - overlord/configstate/configcore: allow setting start_x=1 to enable
+ CSI camera on RPi
+ - interfaces: remove BeforePrepareSlot from commonInterface
+ - many: support system-usernames for 'snap_daemon' user
+ - overlord/devicestate,o/snapstate: queue service commands before
+ mark-seeded and other final tasks
+ - interfaces/mount: discard mount ns on backend Remove
+ - packaging/fedora: build on RHEL8
+ - overlord/devicestate: support seeding a classic system with the
+ snapd snap and no core
+ - interfaces: fix test failure in gpio_control_test
+ - interfaces, policy: remove sanitize helpers and use minimal policy
+ check
+ - packaging: use %systemd_user_* macros to enable session agent
+ socket according to presets
+ - snapstate, store: handle 429s on catalog refresh a little bit
+ better
+ - tests: part4 making tests work on ubuntu-core-18
+ - many: drop snap.ReadGadgetInfo wrapper
+ - xdgopenproxy: update test API to match upstream
+ - tests: show why sbuild failed
+ - data/selinux: allow mandb_t to search /var/lib/snapd
+ - tests: be less verbose when checking service status
+ - tests: set sbuild test as manual
+ - overlord: DeviceCtx must find the remodel context for a remodel
+ change
+ - tests: use snap info --verbose to check for base
+ - sanity: unmount squashfs with --lazy
+ - overlord/snapstate: keep current track if only risk is specified
+ - interfaces/firewall-control: support nft routing expressions and
+ device groups
+ - gadget: support for writing symlinks
+ - tests: mountinfo-tool fail if there are no matches
+ - tests: sync journal log before start the test
+ - cmd/snap, data/completion: improve completion for 'snap debug'
+ - httputil: retry for http2 PROTOCOL_ERROR
+ - Errata commit: pulseaudio still auto-connects on classic
+ - interfaces/misc: updates for k8s 1.15 (and greengrass test)
+ - tests: set GOTRACEBACK=1 when running tests
+ - cmd/libsnap: don't leak memory in sc_die_on_error
+ - tests: improve how the system is restored when the upgrade-
+ from-2.15 test fails
+ - interfaces/bluetooth-control: add udev rules for BT_chrdev devices
+ - interfaces: add audio-playback/audio-record and make pulseaudio
+ manually connect
+ - tests: split the sbuild test in 2 depending on the type of build
+ - interfaces: add an interface granting access to AppStream metadata
+ - gadget: ensure filesystem labels are unique
+ - usersession/agent: use background context when stopping the agent
+ - HACKING.md: update spread section, other updates
+ - data/selinux: allow snap-confine to read entries on nsfs
+ - tests: respect SPREAD_DEBUG_EACH on the main suite
+ - packaging/debian-sid: set GOCACHE to a known writable location
+ - interfaces: add gpio-control interface
+ - cmd/snap: use showDone helper with 'snap switch'
+ - gadget: effective structure role fallback, extra tests
+ - many: fix unit tests getting stuck
+ - tests: remove installed snap on restore
+ - daemon: do not modify test data in user suite
+ - data/selinux: allow read on sysfs
+ - packaging/debian: don't md5sum absent files
+ - tests: remove test-snapd-curl
+ - tests: remove test-snapd-snapctl-core18 in restore
+ - tests: remove installed snap in the restore section
+ - tests: remove installed test snap
+ - tests: correctly escape mount unit path
+ - cmd/Makefile.am: support building with the go snap
+ - tests: work around classic snap affecting the host
+ - tests: fix typo "current"
+ - overlord/assertstate: add Batch.Precheck to check for the full
+ validity of the batch before Commit
+ - tests: restore cpuset clone_children clobbered by lxd
+ - usersession: move userd package to usersession/userd
+ - tests: reformat and fix markdown in snapd-state.md
+ - gadget: select the right updater for given structure
+ - tests: show stderr only if it exists
+ - sessionagent: add a REST interface with socket activation
+ - tests: remove locally installed core in more tests
+ - tests: remove local revision of core
+ - packaging/debian-sid: use correct apparmor Depends for Debian
+ - packaging/debian-sid: merge debian upload changes back into master
+ - cmd/snap-repair: make sure the goroutine doesn't stick around on
+ timeout
+ - packaging/fedora: github.com/cheggaaa/pb is no longer used
+ - configstate/config: fix crash in purgeNulls
+ - boot, o/snapst, o/devicest: limit knowledge of boot vars to boot
+ - client,cmd/snap: stop depending on status/status-code in the JSON
+ responses in client
+ - tests: unmount leftover /run/netns
+ - tests: switch mount-ns test to manual
+ - overlord,daemon,cmd/snapd: move expensive startup to dedicated
+ StartUp methods
+ - osutil: add EnsureTreeState helper
+ - tests: measure properties of various mount namespaces
+ - tests: part2 making tests work on ubuntu-core-18
+ - interfaces/policy: minimal policy check for replacing
+ sanitizeReservedFor helpers (1/2)
+ - interfaces: add an interface that grants access to the PackageKit
+ service
+ - overlord/devicestate: update gadget update handlers and mocks
+ - tests: add mountinfo-tool --ref-x1000
+ - tests: remove lxd / lxcfs if pre-installed
+ - tests: removing support for ubuntu cosmic on spread test suite
+ - tests: don't leak /run/netns mount
+ - image: clean up the validateSuite
+ - bootloader: remove "Dir()" from Bootloader interface
+ - many: retry to reboot if snapd gets restarted before expected
+ reboot
+ - overlord: implement re-registration remodeling
+ - cmd: revert PR#6933 (tweak of GOMAXPROCS)
+ - cmd/snap: add snap unset command
+ - many: add Client-User-Agent to "SnapAction" install API call
+ - tests: first part making tests run on ubuntu-core-18
+ - hookstate/ctlcmd: support hidden commands in snapctl
+ - many: replace snapd snap name checks with type checks (3/4)
+ - overlord: mostly stop needing Kernel/CoreInfo, make GadgetInfo
+ consider a DeviceContext
+ - snapctl: handle unsetting of config options with "!"
+ - tests: move core migration snaps to tests/lib/snaps dir
+ - cmd/snap: handle unsetting of config options with "!"
+ - cmd/snap, etc: add health to 'snap list' and 'snap info'
+ - gadget: use struct field names when intializing data in mounted
+ updater unit tests
+ - cmd/snap-confine: bring /lib/firmware from the host
+ - snap: set snapd snap type (1/4)
+ - snap: add checks in validate-seed for missing base/default-
+ provider
+ - daemon: replace shutdownServer with net/http's native shutdown
+ support
+ - interfaces/builtin: add exec "/bin/runc" to docker-support
+ - gadget: mounted filesystem updater
+ - overlord/patch: simplify conditions for re-applying sublevel
+ patches for level 6
+ - seccomp/compiler: adjust test case names and comment for later
+ changes
+ - tests: fix error doing snap pack running failover test
+ - tests: don't preserve size= when rewriting mount tables
+ - tests: allow reordering of rewrite operations
+ - gadget: main update routine
+ - overlord/config: normalize nulls to support config unsetting
+ semantics
+ - snap-userd-autostart: don't list as a startup application on the
+ GUI
+ - tests: renumber snap revisions as seen via writable
+ - tests: change allocation for mount options
+ - tests: re-enable ns-re-associate test
+ - tests: mountinfo-tool allow many --refs
+ - overlord/devicestate: implement reregRemodelContext with the
+ essential re-registration logic
+ - tests: replace various numeric mount options
+ - gadget: filesystem image writer
+ - tests: add more unit tests for mountinfo-tool
+ - tests: introduce mountinfo-tool --ref feature
+ - tests: refactor mountinfo-tool rewrite state
+ - tests: allow renumbering mount namespace identifiers
+ - snap: refactor and explain layout blacklisting
+ - tests: renumber snap revisions as seen via hostfs
+ - daemon, interfaces, travis: workaround build ID with Go 1.9, use
+ 1.9 for travis tests
+ - cmd/libsnap: add sc_error_init_{simple,api_misuse}
+ - gadget: make raw updater handle shifted structures
+ - tests/lib/nested: create WORK_DIR before accessing it
+ - cmd/libsnap: rename SC_LIBSNAP_ERROR to SC_LIBSNAP_DOMAIN
+ - cmd,tests: forcibly discard mount namespace when bases change
+ - many: introduce healthstate, run check-health
+ post-(install/refresh/try/revert)
+ - interfaces/optical-drive: add scsi-generic type 4 and 5 support
+ - cmd/snap-confine: exit from helper when parent dies
+
+ -- Michael Vogt Fri, 30 Aug 2019 08:56:16 +0200
+
snapd (2.40) xenial; urgency=medium
* New upstream release, LP: #1836327
@@ -102,7 +558,7 @@
- tests/lib/nested: fix multi argument copy_remote
- tests/lib/nested: have mkfs.ext4 use a rootdir instead of mounting
an image
- - packaging: fix permissons powerpc docs dir
+ - packaging: fix permissions powerpc docs dir
- overlord: mock store to avoid net requests
- debian: rework how we run autopkgtests
- interface: builtin: avahi-observe/control: allow slots
diff -Nru snapd-2.40/debian/rules snapd-2.42.1/debian/rules
--- snapd-2.40/debian/rules 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/debian/rules 2019-10-30 12:17:43.000000000 +0000
@@ -26,7 +26,7 @@
# problem on "apt purge snapd". To ensure this won't happen add the
# right dependency on 18.04.
ifeq (${VERSION_ID},"18.04")
- SUBSTVARS = -Vsnapd:Breaks="apt (<< 1.6.3)"
+ SUBSTVARS = -Vsnapd:Breaks="systemd (<< 237-3ubuntu10.24), apt (<< 1.6.3)"
endif
# Same as above for 18.10 just a different version.
ifeq (${VERSION_ID},"18.10")
@@ -168,9 +168,9 @@
# Generate static snap-exec, snapctl and snap-update-ns - it somehow includes CGO so
# we must force a static build here. We need a static snap-{exec,update-ns}/snapctl
# inside the core snap because not all bases will have a libc
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-exec)
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snapctl)
- (cd _build/bin && GOPATH=$$(pwd)/.. go build --ldflags '-extldflags "-static"' $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-update-ns)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-exec)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snapctl)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build go build --ldflags '-extldflags "-static"' $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-update-ns)
# ensure we generated a static build
$(shell if ldd _build/bin/snap-exec; then false "need static build"; fi)
@@ -180,7 +180,7 @@
# ensure snap-seccomp is build with a static libseccomp on Ubuntu
ifeq ($(shell dpkg-vendor --query Vendor),Ubuntu)
sed -i "s|#cgo LDFLAGS:|#cgo LDFLAGS: /usr/lib/$(shell dpkg-architecture -qDEB_TARGET_MULTIARCH)/libseccomp.a|" _build/src/$(DH_GOPKG)/cmd/snap-seccomp/main.go
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_LDFLAGS_ALLOW="/.*/libseccomp.a" go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-seccomp)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_LDFLAGS_ALLOW="/.*/libseccomp.a" go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-seccomp)
# ensure that libseccomp is not dynamically linked
ldd _build/bin/snap-seccomp
test "$$(ldd _build/bin/snap-seccomp | grep libseccomp)" = ""
@@ -197,7 +197,7 @@
$(MAKE) -C data all
# build squashfuse and rename to snapfuse
- (cd vendor/github.com/snapcore/squashfuse/src && mkdir -p autom4te.cache && ./autogen.sh --disable-demo && ./configure --disable-demo && make && mv squashfuse snapfuse)
+ (cd vendor/github.com/snapcore/squashfuse/src && mkdir -p autom4te.cache && ./autogen.sh --disable-demo && ./configure --disable-demo && make && mv squashfuse_ll snapfuse)
override_dh_auto_test:
dh_auto_test -- $(GCCGOFLAGS)
@@ -246,12 +246,6 @@
$(MAKE) -C cmd install DESTDIR=$(CURDIR)/debian/tmp
- # We have to disable the systemd generator on bionic because of
- # the systemd bug LP: 1771858. Enabling it caused bug #1811233
-ifeq (${VERSION_ID},"18.04")
- chmod -x ${CURDIR}/debian/tmp/usr/lib/systemd/system-environment-generators/snapd-env-generator
-endif
-
# Rename the apparmor profile, see dh_apparmor call above for an explanation.
mv $(CURDIR)/debian/tmp/etc/apparmor.d/usr.lib.snapd.snap-confine $(CURDIR)/debian/tmp/etc/apparmor.d/usr.lib.snapd.snap-confine.real
diff -Nru snapd-2.40/debian/snapd.links snapd-2.42.1/debian/snapd.links
--- snapd-2.40/debian/snapd.links 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/debian/snapd.links 2019-10-30 12:17:43.000000000 +0000
@@ -1,2 +1,6 @@
-../../usr/lib/snapd/snap-device-helper /lib/udev/snappy-app-dev
+/usr/lib/snapd/snap-device-helper /lib/udev/snappy-app-dev
usr/lib/snapd/snapctl usr/bin/snapctl
+
+# This should be removed once we can rely on debhelper >= 11.5:
+# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=764678
+/usr/lib/systemd/user/snapd.session-agent.socket /usr/lib/systemd/user/sockets.target.wants/snapd.session-agent.socket
diff -Nru snapd-2.40/debian/snapd.postinst snapd-2.42.1/debian/snapd.postinst
--- snapd-2.40/debian/snapd.postinst 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/debian/snapd.postinst 2019-10-30 12:17:43.000000000 +0000
@@ -41,12 +41,12 @@
# In commit 0dce4704a5d (2017-03-28, snapd v2.23.6) we renamed
# /etc/apparmor.d/usr.lib.snap-confine to usr.lib.snap-confine.real
- # to fix LP: #1673247 - however some people (upgrades?) still have
+ # to fix LP: #1673247 - however some people (developers?) still have
# the old usr.lib.snap-confine file. This seems to be loaded instead
# of the correct usr.lib.snap-confine.real profile. To fix this we
- # use the rather blunt approach to remove the file forcefully.
- if test "$(md5sum /etc/apparmor.d/usr.lib.snapd.snap-confine | cut -f1 -d' ' 2>/dev/null)" = "2a38d40fe662f46fedd0aefbe78f23e9"; then
- rm -f /etc/apparmor.d/usr.lib.snapd.snap-confine
+ # use the rather blunt approach to rename the file forcefully.
+ if test -f /etc/apparmor.d/usr.lib.snapd.snap-confine && test -f /etc/apparmor.d/usr.lib.snapd.snap-confine.real; then
+ mv /etc/apparmor.d/usr.lib.snapd.snap-confine /etc/apparmor.d/usr.lib.snapd.snap-confine.dpkg-bak
fi
# Ensure that the void directory has correct permissions.
diff -Nru snapd-2.40/dirs/dirs.go snapd-2.42.1/dirs/dirs.go
--- snapd-2.40/dirs/dirs.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/dirs/dirs.go 2019-10-30 12:17:43.000000000 +0000
@@ -85,8 +85,10 @@
SnapBinariesDir string
SnapServicesDir string
+ SnapUserServicesDir string
SnapSystemdConfDir string
SnapDesktopFilesDir string
+ SnapDesktopIconsDir string
SnapBusPolicyDir string
SystemApparmorDir string
@@ -205,6 +207,16 @@
return err == nil, err
}
+// SnapBlobDirUnder returns the path to the snap blob dir under rootdir.
+func SnapBlobDirUnder(rootdir string) string {
+ return filepath.Join(rootdir, snappyDir, "snaps")
+}
+
+// SnapSeedDirUnder returns the path to the snap seed dir under rootdir.
+func SnapSeedDirUnder(rootdir string) string {
+ return filepath.Join(rootdir, snappyDir, "seed")
+}
+
// SetRootDir allows settings a new global root directory, this is useful
// for e.g. chroot operations
func SetRootDir(rootdir string) {
@@ -230,8 +242,12 @@
SnapSeccompDir = filepath.Join(rootdir, snappyDir, "seccomp", "bpf")
SnapMountPolicyDir = filepath.Join(rootdir, snappyDir, "mount")
SnapMetaDir = filepath.Join(rootdir, snappyDir, "meta")
- SnapBlobDir = filepath.Join(rootdir, snappyDir, "snaps")
+ SnapBlobDir = SnapBlobDirUnder(rootdir)
+ // ${snappyDir}/desktop is added to $XDG_DATA_DIRS.
+ // Subdirectories are interpreted according to the relevant
+ // freedesktop.org specifications
SnapDesktopFilesDir = filepath.Join(rootdir, snappyDir, "desktop", "applications")
+ SnapDesktopIconsDir = filepath.Join(rootdir, snappyDir, "desktop", "icons")
SnapRunDir = filepath.Join(rootdir, "/run/snapd")
SnapRunNsDir = filepath.Join(SnapRunDir, "/ns")
SnapRunLockDir = filepath.Join(SnapRunDir, "/lock")
@@ -254,7 +270,7 @@
SnapCommandsDB = filepath.Join(SnapCacheDir, "commands.db")
SnapAuxStoreInfoDir = filepath.Join(SnapCacheDir, "aux")
- SnapSeedDir = filepath.Join(rootdir, snappyDir, "seed")
+ SnapSeedDir = SnapSeedDirUnder(rootdir)
SnapDeviceDir = filepath.Join(rootdir, snappyDir, "device")
SnapRepairDir = filepath.Join(rootdir, snappyDir, "repair")
@@ -267,6 +283,7 @@
SnapBinariesDir = filepath.Join(SnapMountDir, "bin")
SnapServicesDir = filepath.Join(rootdir, "/etc/systemd/system")
+ SnapUserServicesDir = filepath.Join(rootdir, "/etc/systemd/user")
SnapSystemdConfDir = filepath.Join(rootdir, "/etc/systemd/system.conf.d")
SnapBusPolicyDir = filepath.Join(rootdir, "/etc/dbus-1/system.d")
diff -Nru snapd-2.40/dirs/dirs_test.go snapd-2.42.1/dirs/dirs_test.go
--- snapd-2.40/dirs/dirs_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/dirs/dirs_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -156,3 +156,13 @@
c.Check(dirs.IsCompleteShSymlink("/etc/passwd"), Equals, false)
c.Check(dirs.IsCompleteShSymlink("/does-not-exist"), Equals, false)
}
+
+func (s *DirsTestSuite) TestUnder(c *C) {
+ dirs.SetRootDir("/nowhere")
+ defer dirs.SetRootDir("")
+
+ rootdir := "/other-root"
+
+ c.Check(dirs.SnapBlobDirUnder(rootdir), Equals, "/other-root/var/lib/snapd/snaps")
+ c.Check(dirs.SnapSeedDirUnder(rootdir), Equals, "/other-root/var/lib/snapd/seed")
+}
diff -Nru snapd-2.40/errtracker/errtracker.go snapd-2.42.1/errtracker/errtracker.go
--- snapd-2.40/errtracker/errtracker.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/errtracker/errtracker.go 2019-10-30 12:17:43.000000000 +0000
@@ -429,7 +429,7 @@
}
report := map[string]string{
- "Architecture": arch.UbuntuArchitecture(),
+ "Architecture": arch.DpkgArchitecture(),
"SnapdVersion": SnapdVersion,
"DistroRelease": distroRelease(),
"HostSnapdBuildID": hostBuildID,
diff -Nru snapd-2.40/errtracker/errtracker_test.go snapd-2.42.1/errtracker/errtracker_test.go
--- snapd-2.40/errtracker/errtracker_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/errtracker/errtracker_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -167,7 +167,7 @@
"KernelVersion": osutil.KernelVersion(),
"ErrorMessage": "failed to do stuff",
"DuplicateSignature": "[failed to do stuff]",
- "Architecture": arch.UbuntuArchitecture(),
+ "Architecture": arch.DpkgArchitecture(),
"DidSnapdReExec": "yes",
"ProblemType": "Snap",
@@ -312,7 +312,7 @@
"SnapdVersion": "some-snapd-version",
"Date": "Fri Feb 17 09:51:00 2017",
"KernelVersion": osutil.KernelVersion(),
- "Architecture": arch.UbuntuArchitecture(),
+ "Architecture": arch.DpkgArchitecture(),
"DidSnapdReExec": "yes",
"ProblemType": "Repair",
diff -Nru snapd-2.40/features/features.go snapd-2.42.1/features/features.go
--- snapd-2.40/features/features.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/features/features.go 2019-10-30 12:17:43.000000000 +0000
@@ -76,6 +76,7 @@
var featuresExported = map[SnapdFeature]bool{
PerUserMountNamespace: true,
RefreshAppAwareness: true,
+ ParallelInstances: true,
}
// String returns the name of a snapd feature.
diff -Nru snapd-2.40/features/features_test.go snapd-2.42.1/features/features_test.go
--- snapd-2.40/features/features_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/features/features_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -57,9 +57,10 @@
func (*featureSuite) TestIsExported(c *C) {
c.Check(features.Layouts.IsExported(), Equals, false)
- c.Check(features.ParallelInstances.IsExported(), Equals, false)
c.Check(features.Hotplug.IsExported(), Equals, false)
c.Check(features.SnapdSnap.IsExported(), Equals, false)
+
+ c.Check(features.ParallelInstances.IsExported(), Equals, true)
c.Check(features.PerUserMountNamespace.IsExported(), Equals, true)
c.Check(features.RefreshAppAwareness.IsExported(), Equals, true)
}
@@ -95,6 +96,7 @@
func (*featureSuite) TestControlFile(c *C) {
c.Check(features.PerUserMountNamespace.ControlFile(), Equals, "/var/lib/snapd/features/per-user-mount-namespace")
c.Check(features.RefreshAppAwareness.ControlFile(), Equals, "/var/lib/snapd/features/refresh-app-awareness")
+ c.Check(features.ParallelInstances.ControlFile(), Equals, "/var/lib/snapd/features/parallel-instances")
// Features that are not exported don't have a control file.
c.Check(features.Layouts.ControlFile, PanicMatches, `cannot compute the control file of feature "layouts" because that feature is not exported`)
}
diff -Nru snapd-2.40/gadget/device_darwin.go snapd-2.42.1/gadget/device_darwin.go
--- snapd-2.40/gadget/device_darwin.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/device_darwin.go 2019-10-30 12:17:43.000000000 +0000
@@ -24,10 +24,14 @@
var errNotImplemented = errors.New("not implemented")
-func FindDeviceForStructure(ps *PositionedStructure) (string, error) {
+func FindDeviceForStructure(ps *LaidOutStructure) (string, error) {
return "", errNotImplemented
}
-func FindMountPointForStructure(ps *PositionedStructure) (string, error) {
+func FindDeviceForStructureWithFallback(ps *LaidOutStructure) (string, Size, error) {
+ return "", 0, errNotImplemented
+}
+
+func FindMountPointForStructure(ps *LaidOutStructure) (string, error) {
return "", errNotImplemented
}
diff -Nru snapd-2.40/gadget/device_linux.go snapd-2.42.1/gadget/device_linux.go
--- snapd-2.40/gadget/device_linux.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/device_linux.go 2019-10-30 12:17:43.000000000 +0000
@@ -40,7 +40,7 @@
// given volume structure, by inspecting its name and, optionally, the
// filesystem label. Assumes that the host's udev has set up device symlinks
// correctly.
-func FindDeviceForStructure(ps *PositionedStructure) (string, error) {
+func FindDeviceForStructure(ps *LaidOutStructure) (string, error) {
var candidates []string
if ps.Name != "" {
@@ -102,7 +102,7 @@
//
// Returns the device name and an offset at which the structure content starts
// within the device or an error.
-func FindDeviceForStructureWithFallback(ps *PositionedStructure) (dev string, offs Size, err error) {
+func FindDeviceForStructureWithFallback(ps *LaidOutStructure) (dev string, offs Size, err error) {
if !ps.IsBare() {
return "", 0, fmt.Errorf("internal error: cannot use with filesystem structures")
}
@@ -157,7 +157,7 @@
// FindMountPointForStructure locates a mount point of a device that matches
// given structure. The structure must have a filesystem defined, otherwise an
// error is raised.
-func FindMountPointForStructure(ps *PositionedStructure) (string, error) {
+func FindMountPointForStructure(ps *LaidOutStructure) (string, error) {
if ps.IsBare() {
return "", ErrNoFilesystemDefined
}
diff -Nru snapd-2.40/gadget/device_test.go snapd-2.42.1/gadget/device_test.go
--- snapd-2.40/gadget/device_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/device_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -90,7 +90,7 @@
for _, tc := range names {
c.Logf("trying: %q", tc)
- found, err := gadget.FindDeviceForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindDeviceForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Name: tc.structure},
})
c.Check(err, IsNil)
@@ -102,7 +102,7 @@
err := os.Symlink("../../fakedevice", filepath.Join(d.dir, "/dev/disk/by-partlabel/relative"))
c.Assert(err, IsNil)
- found, err := gadget.FindDeviceForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindDeviceForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Name: "relative"},
})
c.Check(err, IsNil)
@@ -128,7 +128,7 @@
for _, tc := range names {
c.Logf("trying: %q", tc)
- found, err := gadget.FindDeviceForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindDeviceForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Label: tc.structure},
})
c.Check(err, IsNil)
@@ -144,7 +144,7 @@
err = os.Symlink(fakedevice, filepath.Join(d.dir, "/dev/disk/by-partlabel/bar"))
c.Assert(err, IsNil)
- found, err := gadget.FindDeviceForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindDeviceForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "bar",
Label: "foo",
@@ -166,7 +166,7 @@
err = os.Symlink(fakedeviceOther, filepath.Join(d.dir, "/dev/disk/by-partlabel/bar"))
c.Assert(err, IsNil)
- found, err := gadget.FindDeviceForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindDeviceForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "bar",
Label: "foo",
@@ -177,7 +177,7 @@
}
func (d *deviceSuite) TestDeviceFindNotFound(c *C) {
- found, err := gadget.FindDeviceForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindDeviceForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "bar",
Label: "foo",
@@ -189,7 +189,7 @@
func (d *deviceSuite) TestDeviceFindNotFoundEmpty(c *C) {
// neither name nor filesystem label set
- found, err := gadget.FindDeviceForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindDeviceForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "",
Label: "",
@@ -204,7 +204,7 @@
err := os.Symlink(fakedevice, filepath.Join(d.dir, "/dev/disk/by-label/foo"))
c.Assert(err, IsNil)
- found, err := gadget.FindDeviceForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindDeviceForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Label: "foo",
},
@@ -217,7 +217,7 @@
err := ioutil.WriteFile(filepath.Join(d.dir, "/dev/disk/by-label/foo"), nil, 0644)
c.Assert(err, IsNil)
- found, err := gadget.FindDeviceForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindDeviceForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Label: "foo",
},
@@ -238,7 +238,7 @@
})
defer restore()
- found, err := gadget.FindDeviceForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindDeviceForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Label: "foo",
},
@@ -254,7 +254,7 @@
mockProcSelfFilesystem(c, d.dir, badMountInfo)
- found, offs, err := gadget.FindDeviceForStructureWithFallback(&gadget.PositionedStructure{
+ found, offs, err := gadget.FindDeviceForStructureWithFallback(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Type: "bare",
},
@@ -268,7 +268,7 @@
func (d *deviceSuite) TestDeviceFindFallbackBadWritable(c *C) {
mockProcSelfFilesystem(c, d.dir, writableMountInfo)
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Type: "bare",
},
@@ -292,25 +292,25 @@
func (d *deviceSuite) TestDeviceFindFallbackHappyWritable(c *C) {
d.setUpWritableFallback(c, writableMountInfo)
- psJustBare := &gadget.PositionedStructure{
+ psJustBare := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Type: "bare",
},
StartOffset: 123,
}
- psBareWithName := &gadget.PositionedStructure{
+ psBareWithName := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Type: "bare",
Name: "foo",
},
StartOffset: 123,
}
- psNoName := &gadget.PositionedStructure{
+ psNoName := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{},
StartOffset: 123,
}
- for _, ps := range []*gadget.PositionedStructure{psJustBare, psBareWithName, psNoName} {
+ for _, ps := range []*gadget.LaidOutStructure{psJustBare, psBareWithName, psNoName} {
found, offs, err := gadget.FindDeviceForStructureWithFallback(ps)
c.Check(err, IsNil)
c.Check(found, Equals, filepath.Join(d.dir, "/dev/fakedevice0"))
@@ -322,7 +322,7 @@
d.setUpWritableFallback(c, writableMountInfo)
// should not hit the fallback path
- psNamed := &gadget.PositionedStructure{
+ psNamed := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "foo",
},
@@ -337,7 +337,7 @@
func (d *deviceSuite) TestDeviceFindFallbackNotForFilesystem(c *C) {
d.setUpWritableFallback(c, writableMountInfo)
- psFs := &gadget.PositionedStructure{
+ psFs := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Label: "foo",
Filesystem: "ext4",
@@ -352,7 +352,7 @@
func (d *deviceSuite) TestDeviceFindFallbackBadMountInfo(c *C) {
d.setUpWritableFallback(c, "garbage")
- psFs := &gadget.PositionedStructure{
+ psFs := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "foo",
Type: "bare",
@@ -369,7 +369,7 @@
err := ioutil.WriteFile(filepath.Join(d.dir, "/dev/disk/by-partlabel/foo"), nil, 0644)
c.Assert(err, IsNil)
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "foo",
},
@@ -432,7 +432,7 @@
}
func (d *deviceSuite) TestDeviceFindMountPointErrorsWithBare(c *C) {
- p, err := gadget.FindMountPointForStructure(&gadget.PositionedStructure{
+ p, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
// no filesystem
Filesystem: "",
@@ -441,7 +441,7 @@
c.Assert(err, ErrorMatches, "no filesystem defined")
c.Check(p, Equals, "")
- p, err = gadget.FindMountPointForStructure(&gadget.PositionedStructure{
+ p, err = gadget.FindMountPointForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
// also counts as bare structure
Filesystem: "none",
@@ -452,7 +452,7 @@
}
func (d *deviceSuite) TestDeviceFindMountPointErrorsFromDevice(c *C) {
- p, err := gadget.FindMountPointForStructure(&gadget.PositionedStructure{
+ p, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Label: "bar",
Filesystem: "ext4",
@@ -461,7 +461,7 @@
c.Assert(err, ErrorMatches, "device not found")
c.Check(p, Equals, "")
- p, err = gadget.FindMountPointForStructure(&gadget.PositionedStructure{
+ p, err = gadget.FindMountPointForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "bar",
Filesystem: "ext4",
@@ -490,7 +490,7 @@
mockProcSelfFilesystem(c, d.dir, "garbage")
- found, err := gadget.FindMountPointForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "EFI System",
Label: "system-boot",
@@ -518,7 +518,7 @@
`
mockProcSelfFilesystem(c, d.dir, strings.Replace(mountInfo[1:], "${rootDir}", d.dir, -1))
- found, err := gadget.FindMountPointForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "EFI System",
Label: "system-boot",
@@ -547,7 +547,7 @@
mockProcSelfFilesystem(c, d.dir, strings.Replace(mountInfoReversed[1:], "${rootDir}", d.dir, -1))
- found, err := gadget.FindMountPointForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "EFI System",
Label: "system-boot",
@@ -576,7 +576,7 @@
mockProcSelfFilesystem(c, d.dir, strings.Replace(mountInfo[1:], "${rootDir}", d.dir, -1))
- found, err := gadget.FindMountPointForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "EFI System",
Label: "system-boot",
@@ -600,7 +600,7 @@
mockProcSelfFilesystem(c, d.dir, strings.Replace(mountInfo[1:], "${rootDir}", d.dir, -1))
- found, err := gadget.FindMountPointForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "pinkié pie",
Filesystem: "ext4",
@@ -623,7 +623,7 @@
mockProcSelfFilesystem(c, d.dir, strings.Replace(mountInfo[1:], "${rootDir}", d.dir, -1))
- found, err := gadget.FindMountPointForStructure(&gadget.PositionedStructure{
+ found, err := gadget.FindMountPointForStructure(&gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Name: "label",
// different fs than mount entry
diff -Nru snapd-2.40/gadget/export_test.go snapd-2.42.1/gadget/export_test.go
--- snapd-2.40/gadget/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -31,10 +31,12 @@
EncodeLabel = encodeLabel
- WriteFile = writeFile
+ WriteFile = writeFileOrSymlink
WriteDirectory = writeDirectory
RawContentBackupPath = rawContentBackupPath
+
+ UpdaterForStructure = updaterForStructure
)
func MockEvalSymlinks(mock func(path string) (string, error)) (restore func()) {
@@ -44,3 +46,11 @@
evalSymlinks = oldEvalSymlinks
}
}
+
+func MockMkfsHandlers(mock map[string]MkfsFunc) (restore func()) {
+ old := mkfsHandlers
+ mkfsHandlers = mock
+ return func() {
+ mkfsHandlers = old
+ }
+}
diff -Nru snapd-2.40/gadget/filesystemimage.go snapd-2.42.1/gadget/filesystemimage.go
--- snapd-2.40/gadget/filesystemimage.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/gadget/filesystemimage.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,136 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package gadget
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
+)
+
+type MkfsFunc func(imgFile, label, contentsRootDir string) error
+
+var (
+ mkfsHandlers = map[string]MkfsFunc{
+ "vfat": MkfsVfat,
+ "ext4": MkfsExt4,
+ }
+)
+
+// FilesystemImageWriter is capable of creating filesystem images described by
+// laid out structures.
+type FilesystemImageWriter struct {
+ contentDir string
+ ps *LaidOutStructure
+ workDir string
+}
+
+// PostStageFunc is called after the filesystem contents for the given structure
+// have been staged at a temporary location, but before the filesystem image is
+// created. The function can be used to manipulate the staged data.
+type PostStageFunc func(rootDir string, ps *LaidOutStructure) error
+
+// NewFilesystemImageWriter returns a writer capable of creating filesystem
+// images corresponding to the provided structure, with content from the given
+// content directory. A staging directory will be created in either, the
+// optionally provided work directory, or the default temp location.
+func NewFilesystemImageWriter(contentDir string, ps *LaidOutStructure, workDir string) (*FilesystemImageWriter, error) {
+ if ps == nil {
+ return nil, fmt.Errorf("internal error: *LaidOutStructure is nil")
+ }
+ if ps.IsBare() {
+ return nil, fmt.Errorf("internal error: structure has no filesystem")
+ }
+ if contentDir == "" {
+ return nil, fmt.Errorf("internal error: gadget content directory cannot be unset")
+ }
+ if _, ok := mkfsHandlers[ps.Filesystem]; !ok {
+ return nil, fmt.Errorf("internal error: filesystem %q has no handler", ps.Filesystem)
+ }
+
+ fiw := &FilesystemImageWriter{
+ contentDir: contentDir,
+ ps: ps,
+ workDir: workDir,
+ }
+ return fiw, nil
+}
+
+// Write creates the filesystem inside the provided image file and populates it
+// with data according to content declartion of the structure. Content data is
+// staged in a temporary location. An optional post-stage helper function can be
+// used to manipulate the data before it is copied over to the image.
+func (f *FilesystemImageWriter) Write(fname string, postStage PostStageFunc) error {
+ st, err := os.Stat(fname)
+ if err != nil {
+ return fmt.Errorf("cannot stat image file: %v", err)
+ }
+ if sz := st.Size(); sz != int64(f.ps.Size) {
+ return fmt.Errorf("size of image file %v is different from declared structure size %v", sz, f.ps.Size)
+ }
+
+ mkfsWithContent := mkfsHandlers[f.ps.Filesystem]
+ if mkfsWithContent == nil {
+ return fmt.Errorf("internal error: filesystem %q has no handler", f.ps.Filesystem)
+ }
+
+ stagingDir := filepath.Join(f.workDir, fmt.Sprintf("snap-stage-content-part-%04d", f.ps.Index))
+ if osutil.IsDirectory(stagingDir) {
+ return fmt.Errorf("cannot prepare staging directory %s: path exists", stagingDir)
+ }
+
+ if err := os.Mkdir(stagingDir, 0755); err != nil {
+ return fmt.Errorf("cannot prepare staging directory: %v", err)
+ }
+
+ if os.Getenv("SNAP_DEBUG_IMAGE_NO_CLEANUP") == "" {
+ defer func() {
+ if err := os.RemoveAll(stagingDir); err != nil {
+ logger.Noticef("cannot remove filesystem staging directory %q: %v", stagingDir, err)
+ }
+ }()
+ }
+
+ // use a mounted filesystem writer to populate the staging directory
+ // with contents of given structure
+ mrw, err := NewMountedFilesystemWriter(f.contentDir, f.ps)
+ if err != nil {
+ return fmt.Errorf("cannot prepare filesystem writer for %v: %v", f.ps, err)
+ }
+ // drop all contents to the staging directory
+ if err := mrw.Write(stagingDir, nil); err != nil {
+ return fmt.Errorf("cannot prepare filesystem content: %v", err)
+ }
+
+ if postStage != nil {
+ if err := postStage(stagingDir, f.ps); err != nil {
+ return fmt.Errorf("post stage callback failed: %v", err)
+ }
+ }
+
+ // create a filesystem with contents of the staging directory
+ if err := mkfsWithContent(fname, f.ps.Label, stagingDir); err != nil {
+ return fmt.Errorf("cannot create %q filesystem: %v", f.ps.Filesystem, err)
+ }
+
+ return nil
+}
diff -Nru snapd-2.40/gadget/filesystemimage_test.go snapd-2.42.1/gadget/filesystemimage_test.go
--- snapd-2.40/gadget/filesystemimage_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/gadget/filesystemimage_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,439 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package gadget_test
+
+import (
+ "errors"
+ "fmt"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/gadget"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type filesystemImageTestSuite struct {
+ testutil.BaseTest
+
+ dir string
+ work string
+ content string
+ psTrivial *gadget.LaidOutStructure
+}
+
+var _ = Suite(&filesystemImageTestSuite{})
+
+func (s *filesystemImageTestSuite) SetUpTest(c *C) {
+ s.BaseTest.SetUpTest(c)
+
+ s.dir = c.MkDir()
+ // work directory
+ s.work = filepath.Join(s.dir, "work")
+ err := os.MkdirAll(s.work, 0755)
+ c.Assert(err, IsNil)
+
+ // gadget content directory
+ s.content = filepath.Join(s.dir, "content")
+
+ s.psTrivial = &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Filesystem: "happyfs",
+ Size: 2 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{},
+ },
+ Index: 1,
+ }
+}
+
+func (s *filesystemImageTestSuite) TearDownTest(c *C) {
+ s.BaseTest.TearDownTest(c)
+}
+
+func (s *filesystemImageTestSuite) imgForPs(c *C, ps *gadget.LaidOutStructure) string {
+ c.Assert(ps, NotNil)
+ img := filepath.Join(s.dir, "img")
+ makeSizedFile(c, img, ps.Size, nil)
+ return img
+}
+
+type filesystemImageMockedTestSuite struct {
+ filesystemImageTestSuite
+}
+
+var _ = Suite(&filesystemImageMockedTestSuite{})
+
+func (s *filesystemImageMockedTestSuite) SetUpTest(c *C) {
+ s.filesystemImageTestSuite.SetUpTest(c)
+
+ unexpectedMkfs := func(imgFile, label, contentsRootDir string) error {
+ return errors.New("unexpected mkfs call")
+ }
+ s.AddCleanup(gadget.MockMkfsHandlers(map[string]gadget.MkfsFunc{
+ "happyfs": unexpectedMkfs,
+ }))
+}
+
+func (s *filesystemImageMockedTestSuite) TearDownTest(c *C) {
+ s.filesystemImageTestSuite.TearDownTest(c)
+}
+
+func (s *filesystemImageMockedTestSuite) TestSimpleErrors(c *C) {
+ psValid := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Filesystem: "ext4",
+ Size: 2 * gadget.SizeMiB,
+ },
+ }
+
+ fiw, err := gadget.NewFilesystemImageWriter("", psValid, "")
+ c.Assert(err, ErrorMatches, "internal error: gadget content directory cannot be unset")
+ c.Assert(fiw, IsNil)
+
+ fiw, err = gadget.NewFilesystemImageWriter(s.dir, nil, "")
+ c.Assert(err, ErrorMatches, `internal error: \*LaidOutStructure is nil`)
+ c.Assert(fiw, IsNil)
+
+ psNoFs := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Filesystem: "none",
+ Size: 2 * gadget.SizeMiB,
+ },
+ }
+
+ fiw, err = gadget.NewFilesystemImageWriter(s.dir, psNoFs, "")
+ c.Assert(err, ErrorMatches, "internal error: structure has no filesystem")
+ c.Assert(fiw, IsNil)
+
+ psInvalidFs := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Filesystem: "xfs",
+ Size: 2 * gadget.SizeMiB,
+ },
+ }
+ fiw, err = gadget.NewFilesystemImageWriter(s.dir, psInvalidFs, "")
+ c.Assert(err, ErrorMatches, `internal error: filesystem "xfs" has no handler`)
+ c.Assert(fiw, IsNil)
+}
+
+func (s *filesystemImageMockedTestSuite) TestHappyFull(c *C) {
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Filesystem: "happyfs",
+ Label: "so-happy",
+ Size: 2 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{
+ {Source: "/foo", Target: "/"},
+ },
+ },
+ Index: 2,
+ }
+
+ // image file
+ img := s.imgForPs(c, ps)
+
+ // mock gadget data
+ gd := []gadgetData{
+ {name: "foo", target: "foo", content: "hello foo"},
+ }
+ makeGadgetData(c, s.content, gd)
+
+ var cbCalled bool
+ var mkfsCalled bool
+
+ cb := func(rootDir string, cbPs *gadget.LaidOutStructure) error {
+ c.Assert(cbPs, DeepEquals, ps)
+ c.Assert(rootDir, Equals, filepath.Join(s.work, "snap-stage-content-part-0002"))
+ verifyWrittenGadgetData(c, rootDir, gd)
+
+ cbCalled = true
+ return nil
+ }
+
+ mkfsHappyFs := func(imgFile, label, contentsRootDir string) error {
+ c.Assert(imgFile, Equals, img)
+ c.Assert(label, Equals, "so-happy")
+ c.Assert(contentsRootDir, Equals, filepath.Join(s.work, "snap-stage-content-part-0002"))
+ mkfsCalled = true
+ return nil
+ }
+
+ restore := gadget.MockMkfsHandlers(map[string]gadget.MkfsFunc{
+ "happyfs": mkfsHappyFs,
+ })
+ defer restore()
+
+ fiw, err := gadget.NewFilesystemImageWriter(s.content, ps, s.work)
+ c.Assert(err, IsNil)
+
+ err = fiw.Write(img, cb)
+ c.Assert(err, IsNil)
+ c.Assert(cbCalled, Equals, true)
+ c.Assert(mkfsCalled, Equals, true)
+ // nothing left in temporary staging location
+ matches, err := filepath.Glob(s.work + "/*")
+ c.Assert(err, IsNil)
+ c.Assert(matches, HasLen, 0)
+}
+
+func (s *filesystemImageMockedTestSuite) TestPostStageOptional(c *C) {
+ var mkfsCalled bool
+ mkfsHappyFs := func(imgFile, label, contentsRootDir string) error {
+ mkfsCalled = true
+ return nil
+ }
+
+ restore := gadget.MockMkfsHandlers(map[string]gadget.MkfsFunc{
+ "happyfs": mkfsHappyFs,
+ })
+ defer restore()
+
+ fiw, err := gadget.NewFilesystemImageWriter(s.content, s.psTrivial, s.work)
+ c.Assert(err, IsNil)
+
+ img := s.imgForPs(c, s.psTrivial)
+
+ err = fiw.Write(img, nil)
+ c.Assert(err, IsNil)
+ c.Assert(mkfsCalled, Equals, true)
+}
+
+func (s *filesystemImageMockedTestSuite) TestChecksImage(c *C) {
+ cb := func(rootDir string, cbPs *gadget.LaidOutStructure) error {
+ return errors.New("unexpected call")
+ }
+
+ fiw, err := gadget.NewFilesystemImageWriter(s.content, s.psTrivial, s.work)
+ c.Assert(err, IsNil)
+
+ img := filepath.Join(s.dir, "img")
+
+ // no image file
+ err = fiw.Write(img, cb)
+ c.Assert(err, ErrorMatches, "cannot stat image file: .*/img: no such file or directory")
+
+ // image file smaller than expected
+ makeSizedFile(c, img, s.psTrivial.Size/2, nil)
+
+ err = fiw.Write(img, cb)
+ c.Assert(err, ErrorMatches, fmt.Sprintf("size of image file %v is different from declared structure size %v", s.psTrivial.Size/2, s.psTrivial.Size))
+}
+
+func (s *filesystemImageMockedTestSuite) TestPostStageError(c *C) {
+ cb := func(rootDir string, cbPs *gadget.LaidOutStructure) error {
+ return errors.New("post stage exploded")
+ }
+
+ fiw, err := gadget.NewFilesystemImageWriter(s.content, s.psTrivial, s.work)
+ c.Assert(err, IsNil)
+
+ img := s.imgForPs(c, s.psTrivial)
+
+ err = fiw.Write(img, cb)
+ c.Assert(err, ErrorMatches, "post stage callback failed: post stage exploded")
+}
+
+func (s *filesystemImageMockedTestSuite) TestMkfsError(c *C) {
+ mkfsHappyFs := func(imgFile, label, contentsRootDir string) error {
+ return errors.New("will not mkfs")
+ }
+ restore := gadget.MockMkfsHandlers(map[string]gadget.MkfsFunc{
+ "happyfs": mkfsHappyFs,
+ })
+ defer restore()
+
+ fiw, err := gadget.NewFilesystemImageWriter(s.content, s.psTrivial, s.work)
+ c.Assert(err, IsNil)
+
+ img := s.imgForPs(c, s.psTrivial)
+
+ err = fiw.Write(img, nil)
+ c.Assert(err, ErrorMatches, `cannot create "happyfs" filesystem: will not mkfs`)
+}
+
+func (s *filesystemImageMockedTestSuite) TestFilesystemExtraCheckError(c *C) {
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Filesystem: "happyfs",
+ Size: 2 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{},
+ },
+ }
+
+ fiw, err := gadget.NewFilesystemImageWriter(s.content, ps, s.work)
+ c.Assert(err, IsNil)
+
+ img := s.imgForPs(c, ps)
+
+ // modify filesystem
+ ps.Filesystem = "foofs"
+
+ err = fiw.Write(img, nil)
+ c.Assert(err, ErrorMatches, `internal error: filesystem "foofs" has no handler`)
+}
+
+func (s *filesystemImageMockedTestSuite) TestMountedWriterError(c *C) {
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Filesystem: "happyfs",
+ Size: 2 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{
+ {Source: "/foo", Target: "/"},
+ },
+ },
+ }
+
+ cb := func(rootDir string, cbPs *gadget.LaidOutStructure) error {
+ return errors.New("unexpected call")
+ }
+
+ fiw, err := gadget.NewFilesystemImageWriter(s.content, ps, s.work)
+ c.Assert(err, IsNil)
+
+ img := s.imgForPs(c, ps)
+
+ // declared content does not exist in the content directory
+ err = fiw.Write(img, cb)
+ c.Assert(err, ErrorMatches, `cannot prepare filesystem content: cannot write filesystem content of source:/foo: .* no such file or directory`)
+}
+
+func (s *filesystemImageMockedTestSuite) TestBadWorkDirError(c *C) {
+ cb := func(rootDir string, cbPs *gadget.LaidOutStructure) error {
+ return errors.New("unexpected call")
+ }
+
+ badWork := filepath.Join(s.dir, "bad-work")
+ fiw, err := gadget.NewFilesystemImageWriter(s.content, s.psTrivial, badWork)
+ c.Assert(err, IsNil)
+
+ img := s.imgForPs(c, s.psTrivial)
+
+ err = fiw.Write(img, cb)
+ c.Assert(err, ErrorMatches, `cannot prepare staging directory: mkdir .*/bad-work/snap-stage-content-part-0001: no such file or directory`)
+
+ err = os.MkdirAll(filepath.Join(badWork, "snap-stage-content-part-0001"), 0755)
+ c.Assert(err, IsNil)
+
+ err = fiw.Write(img, cb)
+ c.Assert(err, ErrorMatches, `cannot prepare staging directory .*/bad-work/snap-stage-content-part-0001: path exists`)
+}
+
+func (s *filesystemImageMockedTestSuite) TestKeepsStagingDir(c *C) {
+ cb := func(rootDir string, cbPs *gadget.LaidOutStructure) error {
+ return nil
+ }
+ mkfsHappyFs := func(imgFile, label, contentsRootDir string) error {
+ return nil
+ }
+ restore := gadget.MockMkfsHandlers(map[string]gadget.MkfsFunc{
+ "happyfs": mkfsHappyFs,
+ })
+ defer restore()
+
+ fiw, err := gadget.NewFilesystemImageWriter(s.content, s.psTrivial, s.work)
+ c.Assert(err, IsNil)
+
+ img := s.imgForPs(c, s.psTrivial)
+
+ os.Setenv("SNAP_DEBUG_IMAGE_NO_CLEANUP", "1")
+ defer os.Unsetenv("SNAP_DEBUG_IMAGE_NO_CLEANUP")
+ err = fiw.Write(img, cb)
+ c.Assert(err, IsNil)
+
+ matches, err := filepath.Glob(s.work + "/*")
+ c.Assert(err, IsNil)
+ c.Assert(matches, HasLen, 1)
+ c.Assert(osutil.IsDirectory(filepath.Join(s.work, "snap-stage-content-part-0001")), Equals, true)
+}
+
+type filesystemImageMkfsTestSuite struct {
+ filesystemImageTestSuite
+
+ cmdFakeroot *testutil.MockCmd
+ cmdMkfsVfat *testutil.MockCmd
+ cmdMcopy *testutil.MockCmd
+}
+
+func (s *filesystemImageMkfsTestSuite) SetUpTest(c *C) {
+ s.filesystemImageTestSuite.SetUpTest(c)
+
+ // mkfs.ext4 is called via fakeroot wrapper
+ s.cmdFakeroot = testutil.MockCommand(c, "fakeroot", "")
+ s.AddCleanup(s.cmdFakeroot.Restore)
+
+ s.cmdMkfsVfat = testutil.MockCommand(c, "mkfs.vfat", "")
+ s.AddCleanup(s.cmdMkfsVfat.Restore)
+
+ s.cmdMcopy = testutil.MockCommand(c, "mcopy", "")
+ s.AddCleanup(s.cmdMcopy.Restore)
+}
+
+func (s *filesystemImageMkfsTestSuite) TearDownTest(c *C) {
+ s.filesystemImageTestSuite.TearDownTest(c)
+}
+
+var _ = Suite(&filesystemImageMkfsTestSuite{})
+
+func (s *filesystemImageMkfsTestSuite) TestExt4(c *C) {
+ psExt4 := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Filesystem: "ext4",
+ Size: 2 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{{Source: "/", Target: "/"}},
+ },
+ }
+
+ makeSizedFile(c, filepath.Join(s.content, "foo"), 1024, nil)
+
+ fiw, err := gadget.NewFilesystemImageWriter(s.content, psExt4, s.work)
+ c.Assert(err, IsNil)
+
+ img := s.imgForPs(c, psExt4)
+ err = fiw.Write(img, nil)
+ c.Assert(err, IsNil)
+
+ c.Check(s.cmdFakeroot.Calls(), HasLen, 1)
+ c.Check(s.cmdMkfsVfat.Calls(), HasLen, 0)
+ c.Check(s.cmdMcopy.Calls(), HasLen, 0)
+}
+
+func (s *filesystemImageMkfsTestSuite) TestVfat(c *C) {
+ psVfat := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Filesystem: "vfat",
+ Size: 2 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{{Source: "/", Target: "/"}},
+ },
+ }
+
+ makeSizedFile(c, filepath.Join(s.content, "foo"), 1024, nil)
+
+ fiw, err := gadget.NewFilesystemImageWriter(s.content, psVfat, s.work)
+ c.Assert(err, IsNil)
+
+ img := s.imgForPs(c, psVfat)
+ err = fiw.Write(img, nil)
+ c.Assert(err, IsNil)
+
+ c.Check(s.cmdFakeroot.Calls(), HasLen, 0)
+ c.Check(s.cmdMkfsVfat.Calls(), HasLen, 1)
+ c.Check(s.cmdMcopy.Calls(), HasLen, 1)
+}
diff -Nru snapd-2.40/gadget/gadget.go snapd-2.42.1/gadget/gadget.go
--- snapd-2.40/gadget/gadget.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/gadget.go 2019-10-30 12:17:43.000000000 +0000
@@ -47,9 +47,17 @@
SystemBoot = "system-boot"
SystemData = "system-data"
+
+ BootImage = "system-boot-image"
+ BootSelect = "system-boot-select"
+
// ImplicitSystemDataLabel is the implicit filesystem label of structure
// of system-data role
ImplicitSystemDataLabel = "writable"
+
+ // only supported for legacy reasons
+ LegacyBootImage = "bootimg"
+ LegacyBootSelect = "bootselect"
)
var (
@@ -112,7 +120,7 @@
// structure is treated as if it is of role 'mbr'.
Type string `yaml:"type"`
// Role describes the role of given structure, can be one of 'mbr',
- // 'system-data', 'system-boot'. Structures of type 'mbr', must have a
+ // 'system-data', 'system-boot', 'bootimg', 'bootselect'. Structures of type 'mbr', must have a
// size of 446 bytes and must start at 0 offset.
Role string `yaml:"role"`
// ID is the GPT partition ID
@@ -138,6 +146,10 @@
if vs.Role == "" && vs.Type == MBR {
return MBR
}
+ if vs.Label == SystemBoot {
+ // for gadgets that only specify a filesystem-label, eg. pc
+ return SystemBoot
+ }
return ""
}
@@ -324,10 +336,10 @@
switch v.Bootloader {
case "":
// pass
- case "grub", "u-boot", "android-boot":
+ case "grub", "u-boot", "android-boot", "lk":
bootloadersFound += 1
default:
- return nil, errors.New("bootloader must be one of grub, u-boot or android-boot")
+ return nil, errors.New("bootloader must be one of grub, u-boot, android-boot or lk")
}
}
switch {
@@ -356,9 +368,11 @@
}
// named structures, for cross-referencing relative offset-write names
- knownStructures := make(map[string]*PositionedStructure, len(vol.Structure))
+ knownStructures := make(map[string]*LaidOutStructure, len(vol.Structure))
+ // for uniqueness of filesystem labels
+ knownFsLabels := make(map[string]bool, len(vol.Structure))
// for validating structure overlap
- structures := make([]PositionedStructure, len(vol.Structure))
+ structures := make([]LaidOutStructure, len(vol.Structure))
previousEnd := Size(0)
for idx, s := range vol.Structure {
@@ -372,7 +386,7 @@
start = previousEnd
}
end := start + s.Size
- ps := PositionedStructure{
+ ps := LaidOutStructure{
VolumeStructure: &vol.Structure[idx],
StartOffset: start,
Index: idx,
@@ -385,6 +399,12 @@
// keep track of named structures
knownStructures[s.Name] = &ps
}
+ if s.Label != "" {
+ if seen := knownFsLabels[s.Label]; seen {
+ return fmt.Errorf("filesystem label %q is not unique", s.Label)
+ }
+ knownFsLabels[s.Label] = true
+ }
previousEnd = end
}
@@ -395,12 +415,12 @@
return validateCrossVolumeStructure(structures, knownStructures)
}
-func validateCrossVolumeStructure(structures []PositionedStructure, knownStructures map[string]*PositionedStructure) error {
+func validateCrossVolumeStructure(structures []LaidOutStructure, knownStructures map[string]*LaidOutStructure) error {
previousEnd := Size(0)
// cross structure validation:
// - relative offsets that reference other structures by name
- // - positioned structure overlap
- // use structures positioned within the volume
+ // - laid out structure overlap
+ // use structures laid out within the volume
for pidx, ps := range structures {
if ps.EffectiveRole() == MBR {
if ps.StartOffset != 0 {
@@ -579,8 +599,12 @@
if vs.Filesystem != "" && vs.Filesystem != "none" {
return errors.New("mbr structures must not specify a file system")
}
- case SystemBoot, "":
+ case SystemBoot, BootImage, BootSelect, "":
+ // noop
+ case LegacyBootImage, LegacyBootSelect:
// noop
+ // legacy role names were added in 2.42 can be removed
+ // on snapd epoch bump
default:
return fmt.Errorf("unsupported role")
}
diff -Nru snapd-2.40/gadget/gadget_test.go snapd-2.42.1/gadget/gadget_test.go
--- snapd-2.40/gadget/gadget_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/gadget_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -192,6 +192,78 @@
target: /
`)
+var gadgetYamlLk = []byte(`
+volumes:
+ volumename:
+ schema: mbr
+ bootloader: lk
+ structure:
+ - name: BOOTIMG1
+ size: 25165824
+ role: system-boot-image
+ type: 27
+ content:
+ - image: boot.img
+ - name: BOOTIMG2
+ size: 25165824
+ role: system-boot-image
+ type: 27
+ - name: snapbootsel
+ size: 131072
+ role: system-boot-select
+ type: B2
+ content:
+ - image: snapbootsel.bin
+ - name: snapbootselbak
+ size: 131072
+ role: system-boot-select
+ type: B2
+ content:
+ - image: snapbootsel.bin
+ - name: writable
+ type: 83
+ filesystem: ext4
+ filesystem-label: writable
+ size: 500M
+ role: system-data
+`)
+
+var gadgetYamlLkLegacy = []byte(`
+volumes:
+ volumename:
+ schema: mbr
+ bootloader: lk
+ structure:
+ - name: BOOTIMG1
+ size: 25165824
+ role: bootimg
+ type: 27
+ content:
+ - image: boot.img
+ - name: BOOTIMG2
+ size: 25165824
+ role: bootimg
+ type: 27
+ - name: snapbootsel
+ size: 131072
+ role: bootselect
+ type: B2
+ content:
+ - image: snapbootsel.bin
+ - name: snapbootselbak
+ size: 131072
+ role: bootselect
+ type: B2
+ content:
+ - image: snapbootsel.bin
+ - name: writable
+ type: 83
+ filesystem: ext4
+ filesystem-label: writable
+ size: 500M
+ role: system-data
+`)
+
func TestRun(t *testing.T) { TestingT(t) }
func mustParseGadgetSize(c *C, s string) gadget.Size {
@@ -374,7 +446,7 @@
c.Assert(err, IsNil)
_, err = gadget.ReadInfo(s.dir, false)
- c.Assert(err, ErrorMatches, "bootloader must be one of grub, u-boot or android-boot")
+ c.Assert(err, ErrorMatches, "bootloader must be one of grub, u-boot, android-boot or lk")
}
func (s *gadgetYamlTestSuite) TestReadGadgetYamlEmptyBootloader(c *C) {
@@ -583,6 +655,22 @@
c.Assert(err, IsNil)
}
+func (s *gadgetYamlTestSuite) TestReadGadgetYamlLkHappy(c *C) {
+ err := ioutil.WriteFile(s.gadgetYamlPath, gadgetYamlLk, 0644)
+ c.Assert(err, IsNil)
+
+ _, err = gadget.ReadInfo(s.dir, false)
+ c.Assert(err, IsNil)
+}
+
+func (s *gadgetYamlTestSuite) TestReadGadgetYamlLkLegacyHappy(c *C) {
+ err := ioutil.WriteFile(s.gadgetYamlPath, gadgetYamlLkLegacy, 0644)
+ c.Assert(err, IsNil)
+
+ _, err = gadget.ReadInfo(s.dir, false)
+ c.Assert(err, IsNil)
+}
+
func (s *gadgetYamlTestSuite) TestValidateStructureType(c *C) {
for i, tc := range []struct {
s string
@@ -835,6 +923,50 @@
c.Assert(err, ErrorMatches, `structure name "duplicate" is not unique`)
}
+func (s *gadgetYamlTestSuite) TestValidateVolumeDuplicateFsLabel(c *C) {
+ err := gadget.ValidateVolume("name", &gadget.Volume{
+ Structure: []gadget.VolumeStructure{
+ {Label: "foo", Type: "21686148-6449-6E6F-744E-656564454123", Size: gadget.SizeMiB},
+ {Label: "foo", Type: "21686148-6449-6E6F-744E-656564454649", Size: gadget.SizeMiB},
+ },
+ })
+ c.Assert(err, ErrorMatches, `filesystem label "foo" is not unique`)
+
+ // writable isn't special
+ err = gadget.ValidateVolume("name", &gadget.Volume{
+ Structure: []gadget.VolumeStructure{{
+ Name: "data1",
+ Role: gadget.SystemData,
+ Label: "writable",
+ Type: "21686148-6449-6E6F-744E-656564454123",
+ Size: gadget.SizeMiB,
+ }, {
+ Name: "data2",
+ Role: gadget.SystemData,
+ Label: "writable",
+ Type: "21686148-6449-6E6F-744E-656564454649",
+ Size: gadget.SizeMiB,
+ }},
+ })
+ c.Assert(err, ErrorMatches, `filesystem label "writable" is not unique`)
+
+ // nor is system-boot
+ err = gadget.ValidateVolume("name", &gadget.Volume{
+ Structure: []gadget.VolumeStructure{{
+ Name: "boot1",
+ Label: "system-boot",
+ Type: "EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+ Size: gadget.SizeMiB,
+ }, {
+ Name: "boot2",
+ Label: "system-boot",
+ Type: "EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B",
+ Size: gadget.SizeMiB,
+ }},
+ })
+ c.Assert(err, ErrorMatches, `filesystem label "system-boot" is not unique`)
+}
+
func (s *gadgetYamlTestSuite) TestValidateVolumeErrorsWrapped(c *C) {
err := gadget.ValidateVolume("name", &gadget.Volume{
Structure: []gadget.VolumeStructure{
@@ -1047,7 +1179,7 @@
c.Check(err, IsNil)
}
-func (s *gadgetYamlTestSuite) TestValidatePositioningOverlapPreceding(c *C) {
+func (s *gadgetYamlTestSuite) TestValidateLayoutOverlapPreceding(c *C) {
overlappingGadgetYaml := `
volumes:
pc:
@@ -1072,7 +1204,7 @@
c.Check(err, ErrorMatches, `invalid volume "pc": structure #1 \("other-name"\) overlaps with the preceding structure #0 \("mbr"\)`)
}
-func (s *gadgetYamlTestSuite) TestValidatePositioningOverlapOutOfOrder(c *C) {
+func (s *gadgetYamlTestSuite) TestValidateLayoutOverlapOutOfOrder(c *C) {
outOfOrderGadgetYaml := `
volumes:
pc:
@@ -1147,3 +1279,51 @@
_, err = gadget.ReadInfo(s.dir, false)
c.Check(err, ErrorMatches, `invalid volume "pc": structure #1 \("mbr"\) has "mbr" role and must start at offset 0`)
}
+
+type gadgetTestSuite struct{}
+
+var _ = Suite(&gadgetTestSuite{})
+
+func (s *gadgetTestSuite) TestEffectiveRole(c *C) {
+ // no role set
+ vs := gadget.VolumeStructure{Role: ""}
+ c.Check(vs.EffectiveRole(), Equals, "")
+
+ // explicitly set role trumps all
+ vs = gadget.VolumeStructure{Role: "foobar", Type: gadget.MBR, Label: gadget.SystemBoot}
+
+ c.Check(vs.EffectiveRole(), Equals, "foobar")
+
+ vs = gadget.VolumeStructure{Role: gadget.MBR}
+ c.Check(vs.EffectiveRole(), Equals, gadget.MBR)
+
+ // legacy fallback
+ vs = gadget.VolumeStructure{Role: "", Type: gadget.MBR}
+ c.Check(vs.EffectiveRole(), Equals, gadget.MBR)
+
+ // fallback role based on fs label applies only to system-boot
+ vs = gadget.VolumeStructure{Role: "", Label: gadget.SystemBoot}
+ c.Check(vs.EffectiveRole(), Equals, gadget.SystemBoot)
+ vs = gadget.VolumeStructure{Role: "", Label: gadget.SystemData}
+ c.Check(vs.EffectiveRole(), Equals, "")
+}
+
+func (s *gadgetTestSuite) TestEffectiveFilesystemLabel(c *C) {
+ // no label, and no role set
+ vs := gadget.VolumeStructure{Role: ""}
+ c.Check(vs.EffectiveFilesystemLabel(), Equals, "")
+
+ // explicitly set label
+ vs = gadget.VolumeStructure{Label: "my-label"}
+ c.Check(vs.EffectiveFilesystemLabel(), Equals, "my-label")
+
+ // inferred based on role
+ vs = gadget.VolumeStructure{Role: gadget.SystemData, Label: "unused-label"}
+ c.Check(vs.EffectiveFilesystemLabel(), Equals, gadget.ImplicitSystemDataLabel)
+ vs = gadget.VolumeStructure{Role: gadget.SystemData}
+ c.Check(vs.EffectiveFilesystemLabel(), Equals, gadget.ImplicitSystemDataLabel)
+
+ // only system-data role is special
+ vs = gadget.VolumeStructure{Role: gadget.SystemBoot}
+ c.Check(vs.EffectiveFilesystemLabel(), Equals, "")
+}
diff -Nru snapd-2.40/gadget/layout.go snapd-2.42.1/gadget/layout.go
--- snapd-2.40/gadget/layout.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/gadget/layout.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,353 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package gadget
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+)
+
+// LayoutConstraints defines the constraints for arranging structures within a
+// volume
+type LayoutConstraints struct {
+ // NonMBRStartOffset is the default start offset of non-MBR structure in
+ // the volume.
+ NonMBRStartOffset Size
+ // SectorSize is the size of the sector to be used for calculations
+ SectorSize Size
+}
+
+// LaidOutVolume defines the size of a volume and arrangement of all the
+// structures within it
+type LaidOutVolume struct {
+ *Volume
+ // Size is the total size of the volume
+ Size Size
+ // SectorSize sector size of the volume
+ SectorSize Size
+ // LaidOutStructure is a list of structures within the volume, sorted
+ // by their start offsets
+ LaidOutStructure []LaidOutStructure
+ // RootDir is the root directory for volume data
+ RootDir string
+}
+
+// PartiallyLaidOutVolume defines the layout of volume structures, but lacks the
+// details about the layout of raw image content within the bare structures.
+type PartiallyLaidOutVolume struct {
+ *Volume
+ // SectorSize sector size of the volume
+ SectorSize Size
+ // LaidOutStructure is a list of structures within the volume, sorted
+ // by their start offsets
+ LaidOutStructure []LaidOutStructure
+}
+
+// LaidOutStructure describes a VolumeStructure that has been placed within the
+// volume
+type LaidOutStructure struct {
+ *VolumeStructure
+ // StartOffset defines the start offset of the structure within the
+ // enclosing volume
+ StartOffset Size
+ // AbsoluteOffsetWrite is the resolved absolute position of offset-write
+ // for this structure element within the enclosing volume
+ AbsoluteOffsetWrite *Size
+ // Index of the structure definition in gadget YAML
+ Index int
+ // LaidOutContent is a list of raw content inside the structure
+ LaidOutContent []LaidOutContent
+}
+
+func (p LaidOutStructure) String() string {
+ return fmtIndexAndName(p.Index, p.Name)
+}
+
+type byStartOffset []LaidOutStructure
+
+func (b byStartOffset) Len() int { return len(b) }
+func (b byStartOffset) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
+func (b byStartOffset) Less(i, j int) bool { return b[i].StartOffset < b[j].StartOffset }
+
+// LaidOutContent describes raw content that has been placed within the
+// encompassing structure and volume
+type LaidOutContent struct {
+ *VolumeContent
+
+ // StartOffset defines the start offset of this content image
+ StartOffset Size
+ // AbsoluteOffsetWrite is the resolved absolute position of offset-write
+ // for this content element within the enclosing volume
+ AbsoluteOffsetWrite *Size
+ // Size is the maximum size occupied by this image
+ Size Size
+ // Index of the content in structure declaration inside gadget YAML
+ Index int
+}
+
+func (p LaidOutContent) String() string {
+ if p.Image != "" {
+ return fmt.Sprintf("#%v (%q@%#x{%v})", p.Index, p.Image, p.StartOffset, p.Size)
+ }
+ return fmt.Sprintf("#%v (source:%q)", p.Index, p.Source)
+}
+
+func layoutVolumeStructures(volume *Volume, constraints LayoutConstraints) (structures []LaidOutStructure, byName map[string]*LaidOutStructure, err error) {
+ previousEnd := Size(0)
+ structures = make([]LaidOutStructure, len(volume.Structure))
+ byName = make(map[string]*LaidOutStructure, len(volume.Structure))
+
+ if constraints.SectorSize == 0 {
+ return nil, nil, fmt.Errorf("cannot lay out volume, invalid constraints: sector size cannot be 0")
+ }
+
+ for idx, s := range volume.Structure {
+ var start Size
+ if s.Offset == nil {
+ if s.EffectiveRole() != MBR && previousEnd < constraints.NonMBRStartOffset {
+ start = constraints.NonMBRStartOffset
+ } else {
+ start = previousEnd
+ }
+ } else {
+ start = *s.Offset
+ }
+
+ end := start + s.Size
+ ps := LaidOutStructure{
+ VolumeStructure: &volume.Structure[idx],
+ StartOffset: start,
+ Index: idx,
+ }
+
+ if ps.EffectiveRole() != MBR {
+ if s.Size%constraints.SectorSize != 0 {
+ return nil, nil, fmt.Errorf("cannot lay out volume, structure %v size is not a multiple of sector size %v",
+ ps, constraints.SectorSize)
+ }
+ }
+
+ if ps.Name != "" {
+ byName[ps.Name] = &ps
+ }
+
+ structures[idx] = ps
+
+ previousEnd = end
+ }
+
+ // sort by starting offset
+ sort.Sort(byStartOffset(structures))
+
+ previousEnd = Size(0)
+ for idx, ps := range structures {
+ if ps.StartOffset < previousEnd {
+ return nil, nil, fmt.Errorf("cannot lay out volume, structure %v overlaps with preceding structure %v", ps, structures[idx-1])
+ }
+ previousEnd = ps.StartOffset + ps.Size
+
+ offsetWrite, err := resolveOffsetWrite(ps.OffsetWrite, byName)
+ if err != nil {
+ return nil, nil, fmt.Errorf("cannot resolve offset-write of structure %v: %v", ps, err)
+ }
+ structures[idx].AbsoluteOffsetWrite = offsetWrite
+ }
+
+ return structures, byName, nil
+}
+
+// LayoutVolumePartially attempts to lay out only the structures in the volume using provided constraints
+func LayoutVolumePartially(volume *Volume, constraints LayoutConstraints) (*PartiallyLaidOutVolume, error) {
+ structures, _, err := layoutVolumeStructures(volume, constraints)
+ if err != nil {
+ return nil, err
+ }
+ vol := &PartiallyLaidOutVolume{
+ Volume: volume,
+ SectorSize: constraints.SectorSize,
+ LaidOutStructure: structures,
+ }
+ return vol, nil
+}
+
+// LayoutVolume attempts to completely lay out the volume, that is the
+// structures and their content, using provided constraints
+func LayoutVolume(gadgetRootDir string, volume *Volume, constraints LayoutConstraints) (*LaidOutVolume, error) {
+
+ structures, byName, err := layoutVolumeStructures(volume, constraints)
+ if err != nil {
+ return nil, err
+ }
+
+ farthestEnd := Size(0)
+ fartherstOffsetWrite := Size(0)
+
+ for idx, ps := range structures {
+ if ps.AbsoluteOffsetWrite != nil && *ps.AbsoluteOffsetWrite > fartherstOffsetWrite {
+ fartherstOffsetWrite = *ps.AbsoluteOffsetWrite
+ }
+ if end := ps.StartOffset + ps.Size; end > farthestEnd {
+ farthestEnd = end
+ }
+
+ content, err := layOutStructureContent(gadgetRootDir, &structures[idx], byName)
+ if err != nil {
+ return nil, err
+ }
+
+ for _, c := range content {
+ if c.AbsoluteOffsetWrite != nil && *c.AbsoluteOffsetWrite > fartherstOffsetWrite {
+ fartherstOffsetWrite = *c.AbsoluteOffsetWrite
+ }
+ }
+
+ structures[idx].LaidOutContent = content
+ }
+
+ volumeSize := farthestEnd
+ if fartherstOffsetWrite+SizeLBA48Pointer > farthestEnd {
+ volumeSize = fartherstOffsetWrite + SizeLBA48Pointer
+ }
+
+ vol := &LaidOutVolume{
+ Volume: volume,
+ Size: volumeSize,
+ SectorSize: constraints.SectorSize,
+ LaidOutStructure: structures,
+ RootDir: gadgetRootDir,
+ }
+ return vol, nil
+}
+
+type byContentStartOffset []LaidOutContent
+
+func (b byContentStartOffset) Len() int { return len(b) }
+func (b byContentStartOffset) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
+func (b byContentStartOffset) Less(i, j int) bool { return b[i].StartOffset < b[j].StartOffset }
+
+func getImageSize(path string) (Size, error) {
+ stat, err := os.Stat(path)
+ if err != nil {
+ return 0, err
+ }
+ return Size(stat.Size()), nil
+}
+
+func layOutStructureContent(gadgetRootDir string, ps *LaidOutStructure, known map[string]*LaidOutStructure) ([]LaidOutContent, error) {
+ if !ps.IsBare() {
+ // structures with a filesystem do not need any extra layout
+ return nil, nil
+ }
+ if len(ps.Content) == 0 {
+ return nil, nil
+ }
+
+ content := make([]LaidOutContent, len(ps.Content))
+ previousEnd := Size(0)
+
+ for idx, c := range ps.Content {
+ imageSize, err := getImageSize(filepath.Join(gadgetRootDir, c.Image))
+ if err != nil {
+ return nil, fmt.Errorf("cannot lay out structure %v: content %q: %v", ps, c.Image, err)
+ }
+
+ var start Size
+ if c.Offset != nil {
+ start = *c.Offset
+ } else {
+ start = previousEnd
+ }
+
+ actualSize := imageSize
+
+ if c.Size != 0 {
+ if c.Size < imageSize {
+ return nil, fmt.Errorf("cannot lay out structure %v: content %q size %v is larger than declared %v", ps, c.Image, actualSize, c.Size)
+ }
+ actualSize = c.Size
+ }
+
+ offsetWrite, err := resolveOffsetWrite(c.OffsetWrite, known)
+ if err != nil {
+ return nil, fmt.Errorf("cannot resolve offset-write of structure %v content %q: %v", ps, c.Image, err)
+ }
+
+ content[idx] = LaidOutContent{
+ VolumeContent: &ps.Content[idx],
+ Size: actualSize,
+ StartOffset: ps.StartOffset + start,
+ Index: idx,
+ // break for gofmt < 1.11
+ AbsoluteOffsetWrite: offsetWrite,
+ }
+ previousEnd = start + actualSize
+ if previousEnd > ps.Size {
+ return nil, fmt.Errorf("cannot lay out structure %v: content %q does not fit in the structure", ps, c.Image)
+ }
+ }
+
+ sort.Sort(byContentStartOffset(content))
+
+ previousEnd = ps.StartOffset
+ for idx, pc := range content {
+ if pc.StartOffset < previousEnd {
+ return nil, fmt.Errorf("cannot lay out structure %v: content %q overlaps with preceding image %q", ps, pc.Image, content[idx-1].Image)
+ }
+ previousEnd = pc.StartOffset + pc.Size
+ }
+
+ return content, nil
+}
+
+func resolveOffsetWrite(offsetWrite *RelativeOffset, knownStructs map[string]*LaidOutStructure) (*Size, error) {
+ if offsetWrite == nil {
+ return nil, nil
+ }
+
+ var relativeToOffset Size
+ if offsetWrite.RelativeTo != "" {
+ otherStruct, ok := knownStructs[offsetWrite.RelativeTo]
+ if !ok {
+ return nil, fmt.Errorf("refers to an unknown structure %q", offsetWrite.RelativeTo)
+ }
+ relativeToOffset = otherStruct.StartOffset
+ }
+
+ resolvedOffsetWrite := relativeToOffset + offsetWrite.Offset
+ return &resolvedOffsetWrite, nil
+}
+
+// ShiftStructureTo translates the starting offset of a laid out structure and
+// its content to the provided offset.
+func ShiftStructureTo(ps LaidOutStructure, offset Size) LaidOutStructure {
+ change := int64(offset - ps.StartOffset)
+
+ newPs := ps
+ newPs.StartOffset = Size(int64(ps.StartOffset) + change)
+
+ newPs.LaidOutContent = make([]LaidOutContent, len(ps.LaidOutContent))
+ for idx, pc := range ps.LaidOutContent {
+ newPc := pc
+ newPc.StartOffset = Size(int64(pc.StartOffset) + change)
+ newPs.LaidOutContent[idx] = newPc
+ }
+ return newPs
+}
diff -Nru snapd-2.40/gadget/layout_test.go snapd-2.42.1/gadget/layout_test.go
--- snapd-2.40/gadget/layout_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/gadget/layout_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,1141 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package gadget_test
+
+import (
+ "bytes"
+ "fmt"
+ "io"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/yaml.v2"
+
+ "github.com/snapcore/snapd/gadget"
+)
+
+type layoutTestSuite struct {
+ dir string
+}
+
+var _ = Suite(&layoutTestSuite{})
+
+func (p *layoutTestSuite) SetUpTest(c *C) {
+ p.dir = c.MkDir()
+}
+
+var defaultConstraints = gadget.LayoutConstraints{
+ NonMBRStartOffset: 1 * gadget.SizeMiB,
+ SectorSize: 512,
+}
+
+func (p *layoutTestSuite) TestVolumeSize(c *C) {
+ vol := gadget.Volume{
+ Structure: []gadget.VolumeStructure{
+ {Size: 2 * gadget.SizeMiB},
+ },
+ }
+ v, err := gadget.LayoutVolume(p.dir, &vol, defaultConstraints)
+ c.Assert(err, IsNil)
+
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: &gadget.Volume{
+ Structure: []gadget.VolumeStructure{
+ {Size: 2 * gadget.SizeMiB},
+ },
+ },
+ Size: 3 * gadget.SizeMiB,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {VolumeStructure: &gadget.VolumeStructure{Size: 2 * gadget.SizeMiB}, StartOffset: 1 * gadget.SizeMiB},
+ },
+ })
+}
+
+func mustParseVolume(c *C, gadgetYaml, volume string) *gadget.Volume {
+ var gi gadget.Info
+ err := yaml.Unmarshal([]byte(gadgetYaml), &gi)
+ c.Assert(err, IsNil)
+ v, ok := gi.Volumes[volume]
+ c.Assert(ok, Equals, true, Commentf("volume %q not found in gadget", volume))
+ err = gadget.ValidateVolume("foo", &v)
+ c.Assert(err, IsNil)
+ return &v
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeMinimal(c *C) {
+ gadgetYaml := `
+volumes:
+ first-image:
+ bootloader: u-boot
+ structure:
+ - type: 00000000-0000-0000-0000-0000deadbeef
+ size: 400M
+ - type: 83,00000000-0000-0000-0000-0000feedface
+ role: system-data
+ size: 100M
+`
+ vol := mustParseVolume(c, gadgetYaml, "first-image")
+ c.Assert(vol.Structure, HasLen, 2)
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 501 * gadget.SizeMiB,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 1 * gadget.SizeMiB,
+ Index: 0,
+ },
+ {
+ VolumeStructure: &vol.Structure[1],
+ StartOffset: 401 * gadget.SizeMiB,
+ Index: 1,
+ },
+ },
+ })
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeImplicitOrdering(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-dd00deadbeef
+ size: 400M
+ - type: 00000000-0000-0000-0000-cc00deadbeef
+ role: system-data
+ size: 500M
+ - type: 00000000-0000-0000-0000-bb00deadbeef
+ size: 100M
+ - type: 00000000-0000-0000-0000-aa00deadbeef
+ size: 100M
+`
+ vol := mustParseVolume(c, gadgetYaml, "first")
+ c.Assert(vol.Structure, HasLen, 4)
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 1101 * gadget.SizeMiB,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 1 * gadget.SizeMiB,
+ Index: 0,
+ },
+ {
+ VolumeStructure: &vol.Structure[1],
+ StartOffset: 401 * gadget.SizeMiB,
+ Index: 1,
+ },
+ {
+ VolumeStructure: &vol.Structure[2],
+ StartOffset: 901 * gadget.SizeMiB,
+ Index: 2,
+ },
+ {
+ VolumeStructure: &vol.Structure[3],
+ StartOffset: 1001 * gadget.SizeMiB,
+ Index: 3,
+ },
+ },
+ })
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeExplicitOrdering(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-dd00deadbeef
+ size: 400M
+ offset: 800M
+ - type: 00000000-0000-0000-0000-cc00deadbeef
+ role: system-data
+ size: 500M
+ offset: 200M
+ - type: 00000000-0000-0000-0000-bb00deadbeef
+ size: 100M
+ offset: 1200M
+ - type: 00000000-0000-0000-0000-aa00deadbeef
+ size: 100M
+ offset: 1M
+`
+ vol := mustParseVolume(c, gadgetYaml, "first")
+ c.Assert(vol.Structure, HasLen, 4)
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 1300 * gadget.SizeMiB,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ VolumeStructure: &vol.Structure[3],
+ StartOffset: 1 * gadget.SizeMiB,
+ Index: 3,
+ },
+ {
+ VolumeStructure: &vol.Structure[1],
+ StartOffset: 200 * gadget.SizeMiB,
+ Index: 1,
+ },
+ {
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 800 * gadget.SizeMiB,
+ Index: 0,
+ },
+ {
+ VolumeStructure: &vol.Structure[2],
+ StartOffset: 1200 * gadget.SizeMiB,
+ Index: 2,
+ },
+ },
+ })
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeMixedOrdering(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-dd00deadbeef
+ size: 400M
+ offset: 800M
+ - type: 00000000-0000-0000-0000-cc00deadbeef
+ role: system-data
+ size: 500M
+ offset: 200M
+ - type: 00000000-0000-0000-0000-bb00deadbeef
+ size: 100M
+ - type: 00000000-0000-0000-0000-aa00deadbeef
+ size: 100M
+ offset: 1M
+`
+ vol := mustParseVolume(c, gadgetYaml, "first")
+ c.Assert(vol.Structure, HasLen, 4)
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 1200 * gadget.SizeMiB,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ VolumeStructure: &vol.Structure[3],
+ StartOffset: 1 * gadget.SizeMiB,
+ Index: 3,
+ },
+ {
+ VolumeStructure: &vol.Structure[1],
+ StartOffset: 200 * gadget.SizeMiB,
+ Index: 1,
+ },
+ {
+ VolumeStructure: &vol.Structure[2],
+ StartOffset: 700 * gadget.SizeMiB,
+ Index: 2,
+ },
+ {
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 800 * gadget.SizeMiB,
+ Index: 0,
+ },
+ },
+ })
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeErrorsContentNoSuchFile(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-dd00deadbeef
+ size: 400M
+ offset: 800M
+ content:
+ - image: foo.img
+`
+ vol := mustParseVolume(c, gadgetYaml, "first")
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(v, IsNil)
+ c.Assert(err, ErrorMatches, `cannot lay out structure #0: content "foo.img":.*no such file or directory`)
+}
+
+func makeSizedFile(c *C, path string, size gadget.Size, content []byte) {
+ err := os.MkdirAll(filepath.Dir(path), 0755)
+ c.Assert(err, IsNil)
+
+ f, err := os.Create(path)
+ c.Assert(err, IsNil)
+ defer f.Close()
+ if size != 0 {
+ err = f.Truncate(int64(size))
+ c.Assert(err, IsNil)
+ }
+ if content != nil {
+ _, err := io.Copy(f, bytes.NewReader(content))
+ c.Assert(err, IsNil)
+ }
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeErrorsContentTooLargeSingle(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-dd00deadbeef
+ size: 1M
+ content:
+ - image: foo.img
+`
+ makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB+1, nil)
+
+ vol := mustParseVolume(c, gadgetYaml, "first")
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(v, IsNil)
+ c.Assert(err, ErrorMatches, `cannot lay out structure #0: content "foo.img" does not fit in the structure`)
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeErrorsContentTooLargeMany(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-dd00deadbeef
+ size: 2M
+ content:
+ - image: foo.img
+ - image: bar.img
+`
+ makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB+1, nil)
+ makeSizedFile(c, filepath.Join(p.dir, "bar.img"), gadget.SizeMiB+1, nil)
+
+ vol := mustParseVolume(c, gadgetYaml, "first")
+
+ constraints := gadget.LayoutConstraints{
+ NonMBRStartOffset: 1 * gadget.SizeMiB,
+ SectorSize: 512,
+ }
+ v, err := gadget.LayoutVolume(p.dir, vol, constraints)
+ c.Assert(v, IsNil)
+ c.Assert(err, ErrorMatches, `cannot lay out structure #0: content "bar.img" does not fit in the structure`)
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeErrorsContentTooLargeWithOffset(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-dd00deadbeef
+ size: 1M
+ content:
+ - image: foo.img
+ # 512kB
+ offset: 524288
+`
+ makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB, nil)
+
+ vol := mustParseVolume(c, gadgetYaml, "first")
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(v, IsNil)
+ c.Assert(err, ErrorMatches, `cannot lay out structure #0: content "foo.img" does not fit in the structure`)
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeErrorsContentLargerThanDeclared(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-dd00deadbeef
+ size: 2M
+ content:
+ - image: foo.img
+ size: 1M
+`
+ makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB+1, nil)
+
+ vol := mustParseVolume(c, gadgetYaml, "first")
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(v, IsNil)
+ c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot lay out structure #0: content "foo.img" size %v is larger than declared %v`, gadget.SizeMiB+1, gadget.SizeMiB))
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeErrorsContentOverlap(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-dd00deadbeef
+ size: 2M
+ content:
+ - image: foo.img
+ size: 1M
+ # 512kB
+ offset: 524288
+ - image: bar.img
+ size: 1M
+ offset: 0
+`
+ makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB, nil)
+ makeSizedFile(c, filepath.Join(p.dir, "bar.img"), gadget.SizeMiB, nil)
+
+ vol := mustParseVolume(c, gadgetYaml, "first")
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(v, IsNil)
+ c.Assert(err, ErrorMatches, `cannot lay out structure #0: content "foo.img" overlaps with preceding image "bar.img"`)
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeContentExplicitOrder(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-0000deadbeef
+ size: 2M
+ content:
+ - image: foo.img
+ size: 1M
+ offset: 1M
+ - image: bar.img
+ size: 1M
+ offset: 0
+`
+ makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB, nil)
+ makeSizedFile(c, filepath.Join(p.dir, "bar.img"), gadget.SizeMiB, nil)
+
+ vol := mustParseVolume(c, gadgetYaml, "first")
+ c.Assert(vol.Structure, HasLen, 1)
+ c.Assert(vol.Structure[0].Content, HasLen, 2)
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 3 * gadget.SizeMiB,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 1 * gadget.SizeMiB,
+ LaidOutContent: []gadget.LaidOutContent{
+ {
+ VolumeContent: &vol.Structure[0].Content[1],
+ StartOffset: 1 * gadget.SizeMiB,
+ Size: gadget.SizeMiB,
+ Index: 1,
+ },
+ {
+ VolumeContent: &vol.Structure[0].Content[0],
+ StartOffset: 2 * gadget.SizeMiB,
+ Size: gadget.SizeMiB,
+ Index: 0,
+ },
+ },
+ },
+ },
+ })
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeContentImplicitOrder(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-0000deadbeef
+ size: 2M
+ content:
+ - image: foo.img
+ size: 1M
+ - image: bar.img
+ size: 1M
+`
+ makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB, nil)
+ makeSizedFile(c, filepath.Join(p.dir, "bar.img"), gadget.SizeMiB, nil)
+
+ vol := mustParseVolume(c, gadgetYaml, "first")
+ c.Assert(vol.Structure, HasLen, 1)
+ c.Assert(vol.Structure[0].Content, HasLen, 2)
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 3 * gadget.SizeMiB,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 1 * gadget.SizeMiB,
+ LaidOutContent: []gadget.LaidOutContent{
+ {
+ VolumeContent: &vol.Structure[0].Content[0],
+ StartOffset: 1 * gadget.SizeMiB,
+ Size: gadget.SizeMiB,
+ Index: 0,
+ },
+ {
+ VolumeContent: &vol.Structure[0].Content[1],
+ StartOffset: 2 * gadget.SizeMiB,
+ Size: gadget.SizeMiB,
+ Index: 1,
+ },
+ },
+ },
+ },
+ })
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeContentImplicitSize(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-0000deadbeef
+ size: 2M
+ content:
+ - image: foo.img
+`
+ size1_5MiB := gadget.SizeMiB + gadget.SizeMiB/2
+ makeSizedFile(c, filepath.Join(p.dir, "foo.img"), size1_5MiB, nil)
+
+ vol := mustParseVolume(c, gadgetYaml, "first")
+ c.Assert(vol.Structure, HasLen, 1)
+ c.Assert(vol.Structure[0].Content, HasLen, 1)
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 3 * gadget.SizeMiB,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 1 * gadget.SizeMiB,
+ LaidOutContent: []gadget.LaidOutContent{
+ {
+ VolumeContent: &vol.Structure[0].Content[0],
+ StartOffset: 1 * gadget.SizeMiB,
+ Size: size1_5MiB,
+ },
+ },
+ },
+ },
+ })
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeContentNonBare(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-0000deadbeef
+ filesystem: ext4
+ size: 2M
+ content:
+ - source: foo.txt
+ target: /boot
+`
+ makeSizedFile(c, filepath.Join(p.dir, "foo.txt"), 0, []byte("foobar\n"))
+
+ vol := mustParseVolume(c, gadgetYaml, "first")
+ c.Assert(vol.Structure, HasLen, 1)
+ c.Assert(vol.Structure[0].Content, HasLen, 1)
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 3 * gadget.SizeMiB,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 1 * gadget.SizeMiB,
+ },
+ },
+ })
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeConstraintsChange(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - role: mbr
+ type: bare
+ size: 446
+ offset: 0
+ - type: 00000000-0000-0000-0000-0000deadbeef
+ filesystem: ext4
+ size: 2M
+ content:
+ - source: foo.txt
+ target: /boot
+`
+ makeSizedFile(c, filepath.Join(p.dir, "foo.txt"), 0, []byte("foobar\n"))
+
+ vol := mustParseVolume(c, gadgetYaml, "first")
+ c.Assert(vol.Structure, HasLen, 2)
+ c.Assert(vol.Structure[1].Content, HasLen, 1)
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 3 * gadget.SizeMiB,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 0,
+ Index: 0,
+ },
+ {
+ VolumeStructure: &vol.Structure[1],
+ StartOffset: 1 * gadget.SizeMiB,
+ Index: 1,
+ },
+ },
+ })
+
+ // still valid
+ constraints := gadget.LayoutConstraints{
+ // 512kiB
+ NonMBRStartOffset: 512 * gadget.SizeKiB,
+ SectorSize: 512,
+ }
+ v, err = gadget.LayoutVolume(p.dir, vol, constraints)
+ c.Assert(err, IsNil)
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 2*gadget.SizeMiB + 512*gadget.SizeKiB,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 0,
+ Index: 0,
+ },
+ {
+ VolumeStructure: &vol.Structure[1],
+ StartOffset: 512 * gadget.SizeKiB,
+ Index: 1,
+ },
+ },
+ })
+
+ // constraints would make a non MBR structure overlap with MBR, but
+ // structures start one after another unless offset is specified
+ // explicitly
+ constraintsBad := gadget.LayoutConstraints{
+ NonMBRStartOffset: 400,
+ SectorSize: 512,
+ }
+ v, err = gadget.LayoutVolume(p.dir, vol, constraintsBad)
+ c.Assert(err, IsNil)
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 2*gadget.SizeMiB + 446,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ VolumeStructure: &vol.Structure[0],
+ Index: 0,
+ },
+ {
+ VolumeStructure: &vol.Structure[1],
+ StartOffset: 446,
+ Index: 1,
+ },
+ },
+ })
+
+ // sector size is properly recorded
+ constraintsSector := gadget.LayoutConstraints{
+ NonMBRStartOffset: 1 * gadget.SizeMiB,
+ SectorSize: 1024,
+ }
+ v, err = gadget.LayoutVolume(p.dir, vol, constraintsSector)
+ c.Assert(err, IsNil)
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 3 * gadget.SizeMiB,
+ SectorSize: 1024,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ VolumeStructure: &vol.Structure[0],
+ Index: 0,
+ },
+ {
+ VolumeStructure: &vol.Structure[1],
+ StartOffset: 1 * gadget.SizeMiB,
+ Index: 1,
+ },
+ },
+ })
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeConstraintsSectorSize(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - role: mbr
+ type: bare
+ size: 446
+ offset: 0
+ - type: 00000000-0000-0000-0000-0000deadbeef
+ filesystem: ext4
+ size: 2M
+ content:
+ - source: foo.txt
+ target: /boot
+`
+ makeSizedFile(c, filepath.Join(p.dir, "foo.txt"), 0, []byte("foobar\n"))
+
+ vol := mustParseVolume(c, gadgetYaml, "first")
+
+ constraintsBadSectorSize := gadget.LayoutConstraints{
+ NonMBRStartOffset: 1 * gadget.SizeMiB,
+ SectorSize: 384,
+ }
+ _, err := gadget.LayoutVolume(p.dir, vol, constraintsBadSectorSize)
+ c.Assert(err, ErrorMatches, "cannot lay out volume, structure #1 size is not a multiple of sector size 384")
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeConstraintsNeedsSectorSize(c *C) {
+ constraintsBadSectorSize := gadget.LayoutConstraints{
+ NonMBRStartOffset: 1 * gadget.SizeMiB,
+ // SectorSize left unspecified
+ }
+ _, err := gadget.LayoutVolume(p.dir, &gadget.Volume{}, constraintsBadSectorSize)
+ c.Assert(err, ErrorMatches, "cannot lay out volume, invalid constraints: sector size cannot be 0")
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeMBRImplicitConstraints(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - name: mbr
+ type: bare
+ role: mbr
+ size: 446
+ - name: other
+ type: 00000000-0000-0000-0000-0000deadbeef
+ size: 1M
+`
+ vol := mustParseVolume(c, gadgetYaml, "first")
+ c.Assert(vol.Structure, HasLen, 2)
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 2 * gadget.SizeMiB,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ // MBR
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 0,
+ Index: 0,
+ }, {
+ VolumeStructure: &vol.Structure[1],
+ StartOffset: 1 * gadget.SizeMiB,
+ Index: 1,
+ },
+ },
+ })
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeOffsetWriteAll(c *C) {
+ var gadgetYaml = `
+volumes:
+ pc:
+ bootloader: grub
+ structure:
+ - name: mbr
+ type: mbr
+ size: 440
+ - name: foo
+ type: DA,21686148-6449-6E6F-744E-656564454649
+ size: 1M
+ offset: 1M
+ offset-write: mbr+92
+ content:
+ - image: foo.img
+ offset-write: bar+10
+ - name: bar
+ type: DA,21686148-6449-6E6F-744E-656564454649
+ size: 1M
+ offset-write: 600
+ content:
+ - image: bar.img
+ offset-write: 450
+`
+ makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*gadget.SizeKiB, []byte(""))
+ makeSizedFile(c, filepath.Join(p.dir, "bar.img"), 150*gadget.SizeKiB, []byte(""))
+
+ vol := mustParseVolume(c, gadgetYaml, "pc")
+ c.Assert(vol.Structure, HasLen, 3)
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+ c.Assert(v, DeepEquals, &gadget.LaidOutVolume{
+ Volume: vol,
+ Size: 3 * gadget.SizeMiB,
+ SectorSize: 512,
+ RootDir: p.dir,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ // mbr
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 0,
+ Index: 0,
+ }, {
+ // foo
+ VolumeStructure: &vol.Structure[1],
+ StartOffset: 1 * gadget.SizeMiB,
+ Index: 1,
+ // break for gofmt < 1.11
+ AbsoluteOffsetWrite: asSizePtr(92),
+ LaidOutContent: []gadget.LaidOutContent{
+ {
+ VolumeContent: &vol.Structure[1].Content[0],
+ Size: 200 * gadget.SizeKiB,
+ StartOffset: 1 * gadget.SizeMiB,
+ // offset-write: bar+10
+ AbsoluteOffsetWrite: asSizePtr(2*gadget.SizeMiB + 10),
+ },
+ },
+ }, {
+ // bar
+ VolumeStructure: &vol.Structure[2],
+ StartOffset: 2 * gadget.SizeMiB,
+ Index: 2,
+ // break for gofmt < 1.11
+ AbsoluteOffsetWrite: asSizePtr(600),
+ LaidOutContent: []gadget.LaidOutContent{
+ {
+ VolumeContent: &vol.Structure[2].Content[0],
+ Size: 150 * gadget.SizeKiB,
+ StartOffset: 2 * gadget.SizeMiB,
+ // offset-write: bar+10
+ AbsoluteOffsetWrite: asSizePtr(450),
+ },
+ },
+ },
+ },
+ })
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeOffsetWriteBadRelativeTo(c *C) {
+ // define volumes explicitly as those would not pass validation
+ volBadStructure := gadget.Volume{
+ Structure: []gadget.VolumeStructure{
+ {
+ Name: "foo",
+ Type: "DA,21686148-6449-6E6F-744E-656564454649",
+ Size: 1 * gadget.SizeMiB,
+ OffsetWrite: &gadget.RelativeOffset{
+ RelativeTo: "bar",
+ Offset: 10,
+ },
+ },
+ },
+ }
+ volBadContent := gadget.Volume{
+ Structure: []gadget.VolumeStructure{
+ {
+ Name: "foo",
+ Type: "DA,21686148-6449-6E6F-744E-656564454649",
+ Size: 1 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{
+ {
+ Image: "foo.img",
+ OffsetWrite: &gadget.RelativeOffset{
+ RelativeTo: "bar",
+ Offset: 10,
+ },
+ },
+ },
+ },
+ },
+ }
+
+ makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*gadget.SizeKiB, []byte(""))
+
+ v, err := gadget.LayoutVolume(p.dir, &volBadStructure, defaultConstraints)
+ c.Check(v, IsNil)
+ c.Check(err, ErrorMatches, `cannot resolve offset-write of structure #0 \("foo"\): refers to an unknown structure "bar"`)
+
+ v, err = gadget.LayoutVolume(p.dir, &volBadContent, defaultConstraints)
+ c.Check(v, IsNil)
+ c.Check(err, ErrorMatches, `cannot resolve offset-write of structure #0 \("foo"\) content "foo.img": refers to an unknown structure "bar"`)
+}
+
+func (p *layoutTestSuite) TestLayoutVolumeOffsetWriteEnlargesVolume(c *C) {
+ var gadgetYamlStructure = `
+volumes:
+ pc:
+ bootloader: grub
+ structure:
+ - name: mbr
+ type: mbr
+ size: 440
+ - name: foo
+ type: DA,21686148-6449-6E6F-744E-656564454649
+ size: 1M
+ offset: 1M
+ # 1GB
+ offset-write: mbr+1073741824
+
+`
+ vol := mustParseVolume(c, gadgetYamlStructure, "pc")
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+ // offset-write is at 1GB
+ c.Check(v.Size, Equals, 1*gadget.SizeGiB+gadget.SizeLBA48Pointer)
+
+ var gadgetYamlContent = `
+volumes:
+ pc:
+ bootloader: grub
+ structure:
+ - name: mbr
+ type: mbr
+ size: 440
+ - name: foo
+ type: DA,21686148-6449-6E6F-744E-656564454649
+ size: 1M
+ offset: 1M
+ content:
+ - image: foo.img
+ # 2GB
+ offset-write: mbr+2147483648
+ - image: bar.img
+ # 1GB
+ offset-write: mbr+1073741824
+ - image: baz.img
+ # 3GB
+ offset-write: mbr+3221225472
+
+`
+ makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*gadget.SizeKiB, []byte(""))
+ makeSizedFile(c, filepath.Join(p.dir, "bar.img"), 150*gadget.SizeKiB, []byte(""))
+ makeSizedFile(c, filepath.Join(p.dir, "baz.img"), 100*gadget.SizeKiB, []byte(""))
+
+ vol = mustParseVolume(c, gadgetYamlContent, "pc")
+
+ v, err = gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+ // foo.img offset-write is at 3GB
+ c.Check(v.Size, Equals, 3*gadget.SizeGiB+gadget.SizeLBA48Pointer)
+}
+
+func (p *layoutTestSuite) TestLayoutVolumePartialNoSuchFile(c *C) {
+ gadgetYaml := `
+volumes:
+ first:
+ schema: gpt
+ bootloader: grub
+ structure:
+ - type: 00000000-0000-0000-0000-dd00deadbeef
+ size: 400M
+ offset: 800M
+ content:
+ - image: foo.img
+`
+ vol := mustParseVolume(c, gadgetYaml, "first")
+ c.Assert(vol.Structure, HasLen, 1)
+
+ v, err := gadget.LayoutVolumePartially(vol, defaultConstraints)
+ c.Assert(v, DeepEquals, &gadget.PartiallyLaidOutVolume{
+ Volume: vol,
+ SectorSize: 512,
+ LaidOutStructure: []gadget.LaidOutStructure{
+ {
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 800 * gadget.SizeMiB,
+ Index: 0,
+ },
+ },
+ })
+ c.Assert(err, IsNil)
+}
+
+func (p *layoutTestSuite) TestLaidOutStructureShift(c *C) {
+ var gadgetYamlContent = `
+volumes:
+ pc:
+ bootloader: grub
+ structure:
+ - name: foo
+ type: DA,21686148-6449-6E6F-744E-656564454649
+ size: 1M
+ offset: 1M
+ content:
+ - image: foo.img
+ - image: bar.img
+ # 300KB
+ offset: 307200
+
+`
+ makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*gadget.SizeKiB, []byte(""))
+ makeSizedFile(c, filepath.Join(p.dir, "bar.img"), 150*gadget.SizeKiB, []byte(""))
+
+ vol := mustParseVolume(c, gadgetYamlContent, "pc")
+
+ v, err := gadget.LayoutVolume(p.dir, vol, defaultConstraints)
+ c.Assert(err, IsNil)
+ c.Assert(v.LaidOutStructure, HasLen, 1)
+ c.Assert(v.LaidOutStructure[0].LaidOutContent, HasLen, 2)
+
+ ps := v.LaidOutStructure[0]
+
+ c.Assert(ps, DeepEquals, gadget.LaidOutStructure{
+ // foo
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 1 * gadget.SizeMiB,
+ Index: 0,
+ LaidOutContent: []gadget.LaidOutContent{
+ {
+ VolumeContent: &vol.Structure[0].Content[0],
+ Size: 200 * gadget.SizeKiB,
+ StartOffset: 1 * gadget.SizeMiB,
+ Index: 0,
+ }, {
+ VolumeContent: &vol.Structure[0].Content[1],
+ Size: 150 * gadget.SizeKiB,
+ StartOffset: 1*gadget.SizeMiB + 300*gadget.SizeKiB,
+ Index: 1,
+ },
+ },
+ })
+
+ shiftedTo0 := gadget.ShiftStructureTo(ps, 0)
+ c.Assert(shiftedTo0, DeepEquals, gadget.LaidOutStructure{
+ // foo
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 0,
+ Index: 0,
+ LaidOutContent: []gadget.LaidOutContent{
+ {
+ VolumeContent: &vol.Structure[0].Content[0],
+ Size: 200 * gadget.SizeKiB,
+ StartOffset: 0,
+ Index: 0,
+ }, {
+ VolumeContent: &vol.Structure[0].Content[1],
+ Size: 150 * gadget.SizeKiB,
+ StartOffset: 300 * gadget.SizeKiB,
+ Index: 1,
+ },
+ },
+ })
+
+ shiftedTo2M := gadget.ShiftStructureTo(ps, 2*gadget.SizeMiB)
+ c.Assert(shiftedTo2M, DeepEquals, gadget.LaidOutStructure{
+ // foo
+ VolumeStructure: &vol.Structure[0],
+ StartOffset: 2 * gadget.SizeMiB,
+ Index: 0,
+ LaidOutContent: []gadget.LaidOutContent{
+ {
+ VolumeContent: &vol.Structure[0].Content[0],
+ Size: 200 * gadget.SizeKiB,
+ StartOffset: 2 * gadget.SizeMiB,
+ Index: 0,
+ }, {
+ VolumeContent: &vol.Structure[0].Content[1],
+ Size: 150 * gadget.SizeKiB,
+ StartOffset: 2*gadget.SizeMiB + 300*gadget.SizeKiB,
+ Index: 1,
+ },
+ },
+ })
+}
diff -Nru snapd-2.40/gadget/mountedfilesystem.go snapd-2.42.1/gadget/mountedfilesystem.go
--- snapd-2.40/gadget/mountedfilesystem.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/mountedfilesystem.go 2019-10-30 12:17:43.000000000 +0000
@@ -19,29 +19,54 @@
package gadget
import (
+ "bytes"
+ "crypto"
+ _ "crypto/sha1"
"fmt"
+ "io"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
+ "github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/strutil"
)
+func checkSourceIsDir(src string) error {
+ if !osutil.IsDirectory(src) {
+ if strings.HasSuffix(src, "/") {
+ return fmt.Errorf("cannot specify trailing / for a source which is not a directory")
+ }
+ return fmt.Errorf("source is not a directory")
+ }
+ return nil
+}
+
+func checkContent(content *VolumeContent) error {
+ if content.Source == "" {
+ return fmt.Errorf("internal error: source cannot be unset")
+ }
+ if content.Target == "" {
+ return fmt.Errorf("internal error: target cannot be unset")
+ }
+ return nil
+}
+
// MountedFilesystemWriter assists in writing contents of a structure to a
// mounted filesystem.
type MountedFilesystemWriter struct {
contentDir string
- ps *PositionedStructure
+ ps *LaidOutStructure
}
-// NewMountedFilesystemWriter returns a writer capable of deploying provided
+// NewMountedFilesystemWriter returns a writer capable of writing provided
// structure, with content of the structure stored in the given root directory.
-func NewMountedFilesystemWriter(contentDir string, ps *PositionedStructure) (*MountedFilesystemWriter, error) {
+func NewMountedFilesystemWriter(contentDir string, ps *LaidOutStructure) (*MountedFilesystemWriter, error) {
if ps == nil {
- return nil, fmt.Errorf("internal error: *PositionedStructure is nil")
+ return nil, fmt.Errorf("internal error: *LaidOutStructure is nil")
}
if ps.IsBare() {
return nil, fmt.Errorf("structure %v has no filesystem", ps)
@@ -56,14 +81,20 @@
return fw, nil
}
-func prefixPreserve(dstDir string, preserve []string) []string {
+func mapPreserve(dstDir string, preserve []string) ([]string, error) {
preserveInDst := make([]string, len(preserve))
for i, p := range preserve {
- preserveInDst[i] = filepath.Join(dstDir, p)
+ inDst := filepath.Join(dstDir, p)
+
+ if osutil.IsDirectory(inDst) {
+ return nil, fmt.Errorf("preserved entry %q cannot be a directory", p)
+ }
+
+ preserveInDst[i] = inDst
}
sort.Strings(preserveInDst)
- return preserveInDst
+ return preserveInDst, nil
}
// Write writes structure data into provided directory. All existing files are
@@ -74,7 +105,12 @@
if whereDir == "" {
return fmt.Errorf("internal error: destination directory cannot be unset")
}
- preserveInDst := prefixPreserve(whereDir, preserve)
+
+ preserveInDst, err := mapPreserve(whereDir, preserve)
+ if err != nil {
+ return fmt.Errorf("cannot map preserve entries for destination %q: %v", whereDir, err)
+ }
+
for _, c := range m.ps.Content {
if err := m.writeVolumeContent(whereDir, &c, preserveInDst); err != nil {
return fmt.Errorf("cannot write filesystem content of %s: %v", c, err)
@@ -85,21 +121,17 @@
// writeDirectory copies the source directory, or its contents under target
// location dst. Follows rsync like semantics, that is:
-// /foo/ -> /bar - deploys contents of foo under /bar
-// /foo -> /bar - deploys foo and its subtree under /bar
+// /foo/ -> /bar - writes contents of foo under /bar
+// /foo -> /bar - writes foo and its subtree under /bar
func writeDirectory(src, dst string, preserveInDst []string) error {
hasDirSourceSlash := strings.HasSuffix(src, "/")
- if !osutil.IsDirectory(src) {
- if hasDirSourceSlash {
- // make the error sufficiently descriptive
- return fmt.Errorf("cannot specify trailing / for a source which is not a directory")
- }
- return fmt.Errorf("source is not a directory")
+ if err := checkSourceIsDir(src); err != nil {
+ return err
}
if !hasDirSourceSlash {
- // /foo -> /bar (deploy foo and subtree)
+ // /foo -> /bar (write foo and subtree)
dst = filepath.Join(dst, filepath.Base(src))
}
@@ -112,7 +144,7 @@
pSrc := filepath.Join(src, fi.Name())
pDst := filepath.Join(dst, fi.Name())
- write := writeFile
+ write := writeFileOrSymlink
if fi.IsDir() {
if err := os.MkdirAll(pDst, 0755); err != nil {
return fmt.Errorf("cannot create directory prefix: %v", err)
@@ -129,14 +161,14 @@
return nil
}
-// writeFile copies the source file at given location or under given directory.
-// Follows rsync like semantics, that is:
-// /foo -> /bar/ - deploys foo as /bar/foo
-// /foo -> /bar - deploys foo as /bar
+// writeFileOrSymlink writes the source file or a symlink at given location or
+// under given directory. Follows rsync like semantics, that is:
+// /foo -> /bar/ - writes foo as /bar/foo
+// /foo -> /bar - writes foo as /bar
// The destination location is overwritten.
-func writeFile(src, dst string, preserveInDst []string) error {
+func writeFileOrSymlink(src, dst string, preserveInDst []string) error {
if strings.HasSuffix(dst, "/") {
- // deploy to directory
+ // write to directory
dst = filepath.Join(dst, filepath.Base(src))
}
@@ -149,26 +181,34 @@
return fmt.Errorf("cannot create prefix directory: %v", err)
}
- // overwrite & sync by default
- copyFlags := osutil.CopyFlagOverwrite | osutil.CopyFlagSync
+ if osutil.IsSymlink(src) {
+ // recreate the symlinks as they are
+ to, err := os.Readlink(src)
+ if err != nil {
+ return fmt.Errorf("cannot read symlink: %v", err)
+ }
+ if err := os.Symlink(to, dst); err != nil {
+ return fmt.Errorf("cannot write a symlink: %v", err)
+ }
+ } else {
+ // overwrite & sync by default
+ copyFlags := osutil.CopyFlagOverwrite | osutil.CopyFlagSync
- // TODO use osutil.AtomicFile
- // TODO try to preserve ownership and permission bits
- if err := osutil.CopyFile(src, dst, copyFlags); err != nil {
- return fmt.Errorf("cannot copy %s: %v", src, err)
+ // TODO use osutil.AtomicFile
+ // TODO try to preserve ownership and permission bits
+ if err := osutil.CopyFile(src, dst, copyFlags); err != nil {
+ return fmt.Errorf("cannot copy %s: %v", src, err)
+ }
}
return nil
}
-func (m *MountedFilesystemWriter) writeVolumeContent(whereDir string, content *VolumeContent, preserveInDst []string) error {
- if content.Source == "" {
- return fmt.Errorf("internal error: source cannot be unset")
- }
- if content.Target == "" {
- return fmt.Errorf("internal error: target cannot be unset")
+func (m *MountedFilesystemWriter) writeVolumeContent(volumeRoot string, content *VolumeContent, preserveInDst []string) error {
+ if err := checkContent(content); err != nil {
+ return err
}
realSource := filepath.Join(m.contentDir, content.Source)
- realTarget := filepath.Join(whereDir, content.Target)
+ realTarget := filepath.Join(volumeRoot, content.Target)
// filepath trims the trailing /, restore if needed
if strings.HasSuffix(content.Target, "/") {
@@ -179,10 +219,586 @@
}
if osutil.IsDirectory(realSource) || strings.HasSuffix(content.Source, "/") {
- // deploy a directory
+ // write a directory
return writeDirectory(realSource, realTarget, preserveInDst)
} else {
- // deploy a file
- return writeFile(realSource, realTarget, preserveInDst)
+ // write a file
+ return writeFileOrSymlink(realSource, realTarget, preserveInDst)
+ }
+}
+
+func newStampFile(stamp string) (*osutil.AtomicFile, error) {
+ if err := os.MkdirAll(filepath.Dir(stamp), 0755); err != nil {
+ return nil, fmt.Errorf("cannot create stamp file prefix: %v", err)
+ }
+ return osutil.NewAtomicFile(stamp, 0644, 0, osutil.NoChown, osutil.NoChown)
+}
+
+func makeStamp(stamp string) error {
+ f, err := newStampFile(stamp)
+ if err != nil {
+ return err
+ }
+ return f.Commit()
+}
+
+type mountLookupFunc func(ps *LaidOutStructure) (string, error)
+
+// MountedFilesystemUpdater assits in applying updates to a mounted filesystem.
+//
+// The update process is composed of 2 main passes, and an optional rollback:
+//
+// 1) backup, where update data and current data is analyzed to identify
+// identical content, stamp files are created for entries that are to be
+// preserved, modified or otherwise touched by the update, that is, for existing
+// files that would be created/overwritten, ones that are explicitly listed as
+// preserved, or directories to be written to
+//
+// 2) update, where update data is written to the target location
+//
+// 3) rollback (optional), where update data is rolled back and replaced with
+// backup copies of files, newly created directories are removed
+type MountedFilesystemUpdater struct {
+ *MountedFilesystemWriter
+ backupDir string
+ mountLookup mountLookupFunc
+}
+
+// NewMountedFilesystemUpdater returns an updater for given filesystem
+// structure, with structure content coming from provided root directory. The
+// mount is located by calling a mount lookup helper. The backup directory
+// contains backup state information for use during rollback.
+func NewMountedFilesystemUpdater(rootDir string, ps *LaidOutStructure, backupDir string, mountLookup mountLookupFunc) (*MountedFilesystemUpdater, error) {
+ fw, err := NewMountedFilesystemWriter(rootDir, ps)
+ if err != nil {
+ return nil, err
}
+ if mountLookup == nil {
+ return nil, fmt.Errorf("internal error: mount lookup helper must be provided")
+ }
+ if backupDir == "" {
+ return nil, fmt.Errorf("internal error: backup directory must not be unset")
+ }
+ fu := &MountedFilesystemUpdater{
+ MountedFilesystemWriter: fw,
+ backupDir: backupDir,
+ mountLookup: mountLookup,
+ }
+ return fu, nil
+}
+
+func fsStructBackupPath(backupDir string, ps *LaidOutStructure) string {
+ return filepath.Join(backupDir, fmt.Sprintf("struct-%v", ps.Index))
+}
+
+// entryDestPaths resolves destination and backup paths for given
+// source/target combination. Backup location is within provided
+// backup directory or empty if directory was not provided.
+func (f *MountedFilesystemUpdater) entryDestPaths(dstRoot, source, target, backupDir string) (dstPath, backupPath string) {
+ dstBasePath := target
+ if strings.HasSuffix(target, "/") {
+ // write to a directory
+ dstBasePath = filepath.Join(dstBasePath, filepath.Base(source))
+ }
+ dstPath = filepath.Join(dstRoot, dstBasePath)
+
+ if backupDir != "" {
+ backupPath = filepath.Join(backupDir, dstBasePath)
+ }
+
+ return dstPath, backupPath
+}
+
+// entrySourcePath returns the path of given source entry within the root
+// directory provided during initialization.
+func (f *MountedFilesystemUpdater) entrySourcePath(source string) string {
+ srcPath := filepath.Join(f.contentDir, source)
+
+ if strings.HasSuffix(source, "/") {
+ // restore trailing / if one was there
+ srcPath += "/"
+ }
+ return srcPath
+}
+
+// Update applies an update to a mounted filesystem. The caller must have
+// executed a Backup() before, to prepare a data set for rollback purpose.
+func (f *MountedFilesystemUpdater) Update() error {
+ mount, err := f.mountLookup(f.ps)
+ if err != nil {
+ return fmt.Errorf("cannot find mount location of structure %v: %v", f.ps, err)
+ }
+
+ preserveInDst, err := mapPreserve(mount, f.ps.Update.Preserve)
+ if err != nil {
+ return fmt.Errorf("cannot map preserve entries for mount location %q: %v", mount, err)
+ }
+
+ backupRoot := fsStructBackupPath(f.backupDir, f.ps)
+
+ for _, c := range f.ps.Content {
+ if err := f.updateVolumeContent(mount, &c, preserveInDst, backupRoot); err != nil {
+ return fmt.Errorf("cannot update content: %v", err)
+ }
+ }
+
+ return nil
+}
+
+func (f *MountedFilesystemUpdater) sourceDirectoryEntries(source string) ([]os.FileInfo, error) {
+ srcPath := f.entrySourcePath(source)
+
+ if err := checkSourceIsDir(srcPath); err != nil {
+ return nil, err
+ }
+
+ // TODO: enable support for symlinks when needed
+ if osutil.IsSymlink(srcPath) {
+ return nil, fmt.Errorf("source is a symbolic link")
+ }
+
+ return ioutil.ReadDir(srcPath)
+}
+
+// targetInSourceDir resolves the actual target for given source directory name
+// and target specification.
+// source: /foo/bar/ target: /baz => /bar/ (contents of /foo/bar/ under /baz)
+// source: /foo/bar target: /baz => /bar/bar (directory /foo/bar under /baz, contents under /baz/bar)
+func targetForSourceDir(source, target string) string {
+ if strings.HasSuffix(source, "/") {
+ // contents of source directory land under target
+ return target
+ }
+ // source directory lands under target
+ return filepath.Join(target, filepath.Base(source))
+}
+
+func (f *MountedFilesystemUpdater) updateDirectory(dstRoot, source, target string, preserveInDst []string, backupDir string) error {
+ fis, err := f.sourceDirectoryEntries(source)
+ if err != nil {
+ return fmt.Errorf("cannot list source directory %q: %v", source, err)
+ }
+
+ target = targetForSourceDir(source, target)
+
+ // create current target directory if needed
+ if err := os.MkdirAll(filepath.Join(dstRoot, target), 0755); err != nil {
+ return fmt.Errorf("cannot write directory: %v", err)
+ }
+ // and write the content of source to target
+ for _, fi := range fis {
+ pSrc := filepath.Join(source, fi.Name())
+ pDst := filepath.Join(target, fi.Name())
+
+ update := f.updateOrSkipFile
+ if fi.IsDir() {
+ // continue updating contents of the directory rather
+ // than the directory itself
+ pSrc += "/"
+ pDst += "/"
+ update = f.updateDirectory
+ }
+ if err := update(dstRoot, pSrc, pDst, preserveInDst, backupDir); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (f *MountedFilesystemUpdater) updateOrSkipFile(dstRoot, source, target string, preserveInDst []string, backupDir string) error {
+ srcPath := f.entrySourcePath(source)
+ dstPath, backupPath := f.entryDestPaths(dstRoot, source, target, backupDir)
+
+ // TODO: enable support for symlinks when needed
+ if osutil.IsSymlink(srcPath) {
+ return fmt.Errorf("cannot update file %s: symbolic links are not supported", source)
+ }
+
+ if osutil.FileExists(dstPath) {
+ if strutil.SortedListContains(preserveInDst, dstPath) {
+ // file is to be preserved
+ return nil
+ }
+ if osutil.FileExists(backupPath + ".same") {
+ // file is the same as current copy
+ return nil
+ }
+ if !osutil.FileExists(backupPath + ".backup") {
+ // not preserved & different than the update, error out
+ // as there is no backup
+ return fmt.Errorf("missing backup file %q for %v", backupPath+".backup", target)
+ }
+ }
+
+ return writeFileOrSymlink(srcPath, dstPath, preserveInDst)
+}
+
+func (f *MountedFilesystemUpdater) updateVolumeContent(volumeRoot string, content *VolumeContent, preserveInDst []string, backupDir string) error {
+ if err := checkContent(content); err != nil {
+ return err
+ }
+
+ srcPath := f.entrySourcePath(content.Source)
+
+ if osutil.IsDirectory(srcPath) || strings.HasSuffix(content.Source, "/") {
+ return f.updateDirectory(volumeRoot, content.Source, content.Target, preserveInDst, backupDir)
+ } else {
+ return f.updateOrSkipFile(volumeRoot, content.Source, content.Target, preserveInDst, backupDir)
+ }
+}
+
+// Backup analyzes a mounted filesystem and prepares a rollback state should the
+// update be applied. The content of the filesystem is processed, files and
+// directories that would be modified by the update are backed up, while
+// identical/preserved files may be stamped to improve the later step of update
+// process.
+//
+// The backup directory structure mirrors the the structure of destination
+// location. Given the following destination structure:
+//
+// foo
+// ├── a
+// ├── b
+// ├── bar
+// │ ├── baz
+// │ │ └── d
+// │ └── z
+// └── c
+//
+// The structure of backup looks like this:
+//
+// foo-backup
+// ├── a.backup <-- backup copy of ./a
+// ├── bar
+// │ ├── baz
+// │ │ └── d.backup <-- backup copy of ./bar/baz/d
+// │ └── baz.backup <-- stamp indicating ./bar/baz existed before the update
+// ├── bar.backup <-- stamp indicating ./bar existed before the update
+// ├── b.same <-- stamp indicating ./b is identical to the update data
+// └── c.preserve <-- stamp indicating ./c is to be preserved
+//
+func (f *MountedFilesystemUpdater) Backup() error {
+ mount, err := f.mountLookup(f.ps)
+ if err != nil {
+ return fmt.Errorf("cannot find mount location of structure %v: %v", f.ps, err)
+ }
+
+ backupRoot := fsStructBackupPath(f.backupDir, f.ps)
+
+ if err := os.MkdirAll(backupRoot, 0755); err != nil {
+ return fmt.Errorf("cannot create backup directory: %v", err)
+ }
+
+ preserveInDst, err := mapPreserve(mount, f.ps.Update.Preserve)
+ if err != nil {
+ return fmt.Errorf("cannot map preserve entries for mount location %q: %v", mount, err)
+ }
+
+ for _, c := range f.ps.Content {
+ if err := f.backupVolumeContent(mount, &c, preserveInDst, backupRoot); err != nil {
+ return fmt.Errorf("cannot backup content: %v", err)
+ }
+ }
+
+ return nil
+}
+
+func (f *MountedFilesystemUpdater) backupOrCheckpointDirectory(dstRoot, source, target string, preserveInDst []string, backupDir string) error {
+ fis, err := f.sourceDirectoryEntries(source)
+ if err != nil {
+ return fmt.Errorf("cannot backup directory %q: %v", source, err)
+ }
+
+ target = targetForSourceDir(source, target)
+
+ for _, fi := range fis {
+ pSrc := filepath.Join(source, fi.Name())
+ pDst := filepath.Join(target, fi.Name())
+
+ backup := f.backupOrCheckpointFile
+ if fi.IsDir() {
+ // continue backing up the contents of the directory
+ // rather than the directory itself
+ pSrc += "/"
+ pDst += "/"
+ backup = f.backupOrCheckpointDirectory
+ }
+ if err := f.checkpointPrefix(dstRoot, pDst, backupDir); err != nil {
+ return err
+ }
+ if err := backup(dstRoot, pSrc, pDst, preserveInDst, backupDir); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+// checkpointPrefix creates stamps for each part of the destination prefix that exists
+func (f *MountedFilesystemUpdater) checkpointPrefix(dstRoot, target string, backupDir string) error {
+ // check how much of the prefix needs to be created
+ for prefix := filepath.Dir(target); prefix != "." && prefix != "/"; prefix = filepath.Dir(prefix) {
+ prefixDst, prefixBackupBase := f.entryDestPaths(dstRoot, "", prefix, backupDir)
+
+ // TODO: enable support for symlinks when needed
+ if osutil.IsSymlink(prefixDst) {
+ return fmt.Errorf("cannot create a checkpoint for directory %v: symbolic links are not supported", prefix)
+ }
+
+ prefixBackupName := prefixBackupBase + ".backup"
+ if osutil.FileExists(prefixBackupName) {
+ continue
+ }
+ if !osutil.IsDirectory(prefixDst) {
+ // does not exist now, will be created on the fly and
+ // removed during rollback
+ continue
+ }
+ if err := os.MkdirAll(filepath.Dir(prefixBackupName), 0755); err != nil {
+ return fmt.Errorf("cannot create backup prefix: %v", err)
+ }
+ if err := makeStamp(prefixBackupName); err != nil {
+ return fmt.Errorf("cannot create a checkpoint for directory: %v", err)
+ }
+ }
+ return nil
+}
+
+func (f *MountedFilesystemUpdater) backupOrCheckpointFile(dstRoot, source, target string, preserveInDst []string, backupDir string) error {
+ srcPath := f.entrySourcePath(source)
+ dstPath, backupPath := f.entryDestPaths(dstRoot, source, target, backupDir)
+
+ backupName := backupPath + ".backup"
+ sameStamp := backupPath + ".same"
+ preserveStamp := backupPath + ".preserve"
+
+ // TODO: enable support for symlinks when needed
+ if osutil.IsSymlink(dstPath) {
+ return fmt.Errorf("cannot backup file %s: symbolic links are not supported", target)
+ }
+
+ if !osutil.FileExists(dstPath) {
+ // destination does not exist and will be created when writing
+ // the udpate, no need for backup
+ return nil
+ }
+
+ if osutil.FileExists(backupName) || osutil.FileExists(sameStamp) {
+ // file already checked, either has a backup or is the same as
+ // the update, move on
+ return nil
+ }
+
+ if strutil.SortedListContains(preserveInDst, dstPath) {
+ // file is to be preserved, create a relevant stamp
+
+ if !osutil.FileExists(dstPath) {
+ // preserve, but does not exist, will be written anyway
+ return nil
+ }
+ if osutil.FileExists(preserveStamp) {
+ // already stamped
+ return nil
+ }
+ // make a stamp
+ if err := makeStamp(preserveStamp); err != nil {
+ return fmt.Errorf("cannot create preserve stamp: %v", err)
+ }
+ return nil
+ }
+
+ // try to find out whether the update and the existing file are
+ // identical
+
+ orig, err := os.Open(dstPath)
+ if err != nil {
+ return fmt.Errorf("cannot open destination file: %v", err)
+ }
+
+ // backup of the original content
+ backup, err := newStampFile(backupName)
+ if err != nil {
+ return fmt.Errorf("cannot create backup file: %v", err)
+ }
+ // becomes a backup copy or a noop if canceled
+ defer backup.Commit()
+
+ // checksum the original data while it's being copied
+ origHash := crypto.SHA1.New()
+ htr := io.TeeReader(orig, origHash)
+
+ _, err = io.Copy(backup, htr)
+ if err != nil {
+ backup.Cancel()
+ return fmt.Errorf("cannot backup original file: %v", err)
+ }
+
+ // digest of the update
+ updateDigest, _, err := osutil.FileDigest(srcPath, crypto.SHA1)
+ if err != nil {
+ backup.Cancel()
+ return fmt.Errorf("cannot checksum update file: %v", err)
+ }
+ // digest of the currently present data
+ origDigest := origHash.Sum(nil)
+
+ // TODO: look into comparing the streams directly
+ if bytes.Equal(origDigest, updateDigest) {
+ // mark that files are identical and update can be skipped, no
+ // backup is needed
+ if err := makeStamp(sameStamp); err != nil {
+ return fmt.Errorf("cannot create a checkpoint file: %v", err)
+ }
+
+ // makes the deferred commit a noop
+ backup.Cancel()
+ return nil
+ }
+
+ // update will overwrite existing file, a backup copy is created on
+ // Commit()
+ return nil
+}
+
+func (f *MountedFilesystemUpdater) backupVolumeContent(volumeRoot string, content *VolumeContent, preserveInDst []string, backupDir string) error {
+ if err := checkContent(content); err != nil {
+ return err
+ }
+
+ srcPath := f.entrySourcePath(content.Source)
+
+ if err := f.checkpointPrefix(volumeRoot, content.Target, backupDir); err != nil {
+ return err
+ }
+ if osutil.IsDirectory(srcPath) || strings.HasSuffix(content.Source, "/") {
+ // backup directory contents
+ return f.backupOrCheckpointDirectory(volumeRoot, content.Source, content.Target, preserveInDst, backupDir)
+ } else {
+ // backup a file
+ return f.backupOrCheckpointFile(volumeRoot, content.Source, content.Target, preserveInDst, backupDir)
+ }
+}
+
+// Rollback attempts to revert changes done by the update step, using state
+// information collected during backup phase. Files that were modified by the
+// update are stored from their backup copies, newly added directories are
+// removed.
+func (f *MountedFilesystemUpdater) Rollback() error {
+ mount, err := f.mountLookup(f.ps)
+ if err != nil {
+ return fmt.Errorf("cannot find mount location of structure %v: %v", f.ps, err)
+ }
+
+ backupRoot := fsStructBackupPath(f.backupDir, f.ps)
+
+ preserveInDst, err := mapPreserve(mount, f.ps.Update.Preserve)
+ if err != nil {
+ return fmt.Errorf("cannot map preserve entries for mount location %q: %v", mount, err)
+ }
+
+ for _, c := range f.ps.Content {
+ if err := f.rollbackVolumeContent(mount, &c, preserveInDst, backupRoot); err != nil {
+ return fmt.Errorf("cannot rollback content: %v", err)
+ }
+ }
+
+ return nil
+}
+
+func (f *MountedFilesystemUpdater) rollbackPrefix(dstRoot, target string, backupDir string) error {
+ for prefix := filepath.Dir(target); prefix != "/" && prefix != "."; prefix = filepath.Dir(prefix) {
+ prefixDstPath, prefixBackupPath := f.entryDestPaths(dstRoot, "", prefix, backupDir)
+ if !osutil.FileExists(prefixBackupPath + ".backup") {
+ // try remove
+ if err := os.Remove(prefixDstPath); err != nil {
+ logger.Noticef("cannot remove gadget directory %q: %v", prefix, err)
+ }
+ }
+ }
+ return nil
+}
+
+func (f *MountedFilesystemUpdater) rollbackDirectory(dstRoot, source, target string, preserveInDst []string, backupDir string) error {
+ fis, err := f.sourceDirectoryEntries(source)
+ if err != nil {
+ return fmt.Errorf("cannot rollback directory %q: %v", source, err)
+ }
+
+ target = targetForSourceDir(source, target)
+
+ for _, fi := range fis {
+ pSrc := filepath.Join(source, fi.Name())
+ pDst := filepath.Join(target, fi.Name())
+
+ rollback := f.rollbackFile
+ if fi.IsDir() {
+ // continue rolling back the contents of the directory
+ // rather than the directory itself
+ rollback = f.rollbackDirectory
+ pSrc += "/"
+ pDst += "/"
+ }
+ if err := rollback(dstRoot, pSrc, pDst, preserveInDst, backupDir); err != nil {
+ return err
+ }
+ if err := f.rollbackPrefix(dstRoot, pDst, backupDir); err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
+
+func (f *MountedFilesystemUpdater) rollbackFile(dstRoot, source, target string, preserveInDst []string, backupDir string) error {
+ dstPath, backupPath := f.entryDestPaths(dstRoot, source, target, backupDir)
+
+ backupName := backupPath + ".backup"
+ sameStamp := backupPath + ".same"
+ preserveStamp := backupPath + ".preserve"
+
+ if strutil.SortedListContains(preserveInDst, dstPath) && osutil.FileExists(preserveStamp) {
+ // file was preserved at original location, do nothing
+ return nil
+ }
+ if osutil.FileExists(sameStamp) {
+ // contents are the same as original, do nothing
+ return nil
+ }
+
+ if osutil.FileExists(backupName) {
+ // restore backup -> destination
+ return writeFileOrSymlink(backupName, dstPath, nil)
+ }
+
+ // none of the markers exists, file is not preserved, meaning, it has
+ // been added by the update
+
+ if err := os.Remove(dstPath); err != nil && !os.IsNotExist(err) {
+ return fmt.Errorf("cannot remove written update: %v", err)
+ }
+
+ return nil
+}
+
+func (f *MountedFilesystemUpdater) rollbackVolumeContent(volumeRoot string, content *VolumeContent, preserveInDst []string, backupDir string) error {
+ if err := checkContent(content); err != nil {
+ return err
+ }
+
+ srcPath := f.entrySourcePath(content.Source)
+
+ var err error
+ if osutil.IsDirectory(srcPath) || strings.HasSuffix(content.Source, "/") {
+ // rollback directory
+ err = f.rollbackDirectory(volumeRoot, content.Source, content.Target, preserveInDst, backupDir)
+ } else {
+ // rollback file
+ err = f.rollbackFile(volumeRoot, content.Source, content.Target, preserveInDst, backupDir)
+ }
+ if err != nil {
+ return err
+ }
+
+ return f.rollbackPrefix(volumeRoot, content.Target, backupDir)
}
diff -Nru snapd-2.40/gadget/mountedfilesystem_test.go snapd-2.42.1/gadget/mountedfilesystem_test.go
--- snapd-2.40/gadget/mountedfilesystem_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/mountedfilesystem_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -19,9 +19,11 @@
package gadget_test
import (
+ "errors"
"fmt"
"os"
"path/filepath"
+ "strings"
. "gopkg.in/check.v1"
@@ -43,22 +45,97 @@
}
type gadgetData struct {
- name, target, content string
+ name, target, symlinkTo, content string
}
func makeGadgetData(c *C, where string, data []gadgetData) {
for _, en := range data {
+ if en.name == "" {
+ continue
+ }
+ if strings.HasSuffix(en.name, "/") {
+ err := os.MkdirAll(filepath.Join(where, en.name), 0755)
+ c.Check(en.content, HasLen, 0)
+ c.Assert(err, IsNil)
+ continue
+ }
+ if en.symlinkTo != "" {
+ err := os.Symlink(en.symlinkTo, filepath.Join(where, en.name))
+ c.Assert(err, IsNil)
+ continue
+ }
makeSizedFile(c, filepath.Join(where, en.name), 0, []byte(en.content))
}
}
-func verifyDeployedGadgetData(c *C, where string, data []gadgetData) {
+func verifyWrittenGadgetData(c *C, where string, data []gadgetData) {
for _, en := range data {
+ if en.target == "" {
+ continue
+ }
+ if en.symlinkTo != "" {
+ symlinkTarget, err := os.Readlink(filepath.Join(where, en.target))
+ c.Assert(err, IsNil)
+ c.Check(symlinkTarget, Equals, en.symlinkTo)
+ continue
+ }
target := filepath.Join(where, en.target)
c.Check(target, testutil.FileContains, en.content)
}
}
+func makeExistingData(c *C, where string, data []gadgetData) {
+ for _, en := range data {
+ if en.target == "" {
+ continue
+ }
+ if strings.HasSuffix(en.target, "/") {
+ err := os.MkdirAll(filepath.Join(where, en.target), 0755)
+ c.Check(en.content, HasLen, 0)
+ c.Assert(err, IsNil)
+ continue
+ }
+ if en.symlinkTo != "" {
+ err := os.Symlink(en.symlinkTo, filepath.Join(where, en.target))
+ c.Assert(err, IsNil)
+ continue
+ }
+ makeSizedFile(c, filepath.Join(where, en.target), 0, []byte(en.content))
+ }
+}
+
+type contentType int
+
+const (
+ typeFile contentType = iota
+ typeDir
+)
+
+func verifyDirContents(c *C, where string, expected map[string]contentType) {
+ cleanWhere := filepath.Clean(where)
+
+ got := make(map[string]contentType)
+ filepath.Walk(where, func(name string, fi os.FileInfo, err error) error {
+ if name == where {
+ return nil
+ }
+ suffixName := name[len(cleanWhere)+1:]
+ t := typeFile
+ if fi.IsDir() {
+ t = typeDir
+ }
+ got[suffixName] = t
+
+ for prefix := filepath.Dir(name); prefix != where; prefix = filepath.Dir(prefix) {
+ delete(got, prefix[len(cleanWhere)+1:])
+ }
+
+ return nil
+ })
+
+ c.Assert(got, DeepEquals, expected)
+}
+
func (s *mountedfilesystemTestSuite) TestWriteFile(c *C) {
makeSizedFile(c, filepath.Join(s.dir, "foo"), 0, []byte("foo foo foo"))
@@ -74,7 +151,7 @@
c.Assert(err, IsNil)
c.Check(filepath.Join(outDir, "bar/foo"), testutil.FileEquals, []byte("foo foo foo"))
- // deploy overwrites
+ // overwrites
makeSizedFile(c, filepath.Join(outDir, "overwrite"), 0, []byte("disappear"))
err = gadget.WriteFile(filepath.Join(s.dir, "foo"), filepath.Join(outDir, "overwrite"), nil)
c.Assert(err, IsNil)
@@ -93,11 +170,11 @@
func (s *mountedfilesystemTestSuite) TestWriteDirectoryContents(c *C) {
gd := []gadgetData{
- {"boot-assets/splash", "splash", "splash"},
- {"boot-assets/some-dir/data", "some-dir/data", "data"},
- {"boot-assets/some-dir/empty-file", "some-dir/empty-file", ""},
- {"boot-assets/nested-dir/nested", "/nested-dir/nested", "nested"},
- {"boot-assets/nested-dir/more-nested/more", "/nested-dir/more-nested/more", "more"},
+ {name: "boot-assets/splash", target: "splash", content: "splash"},
+ {name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"},
+ {name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file"},
+ {name: "boot-assets/nested-dir/nested", target: "/nested-dir/nested", content: "nested"},
+ {name: "boot-assets/nested-dir/more-nested/more", target: "/nested-dir/more-nested/more", content: "more"},
}
makeGadgetData(c, s.dir, gd)
@@ -106,16 +183,16 @@
err := gadget.WriteDirectory(filepath.Join(s.dir, "boot-assets")+"/", outDir+"/", nil)
c.Assert(err, IsNil)
- verifyDeployedGadgetData(c, outDir, gd)
+ verifyWrittenGadgetData(c, outDir, gd)
}
func (s *mountedfilesystemTestSuite) TestWriteDirectoryWhole(c *C) {
gd := []gadgetData{
- {"boot-assets/splash", "boot-assets/splash", "splash"},
- {"boot-assets/some-dir/data", "boot-assets/some-dir/data", "data"},
- {"boot-assets/some-dir/empty-file", "boot-assets/some-dir/empty-file", ""},
- {"boot-assets/nested-dir/nested", "boot-assets/nested-dir/nested", "nested"},
- {"boot-assets/nested-dir/more-nested/more", "boot-assets//nested-dir/more-nested/more", "more"},
+ {name: "boot-assets/splash", target: "boot-assets/splash", content: "splash"},
+ {name: "boot-assets/some-dir/data", target: "boot-assets/some-dir/data", content: "data"},
+ {name: "boot-assets/some-dir/empty-file", target: "boot-assets/some-dir/empty-file"},
+ {name: "boot-assets/nested-dir/nested", target: "boot-assets/nested-dir/nested", content: "nested"},
+ {name: "boot-assets/nested-dir/more-nested/more", target: "boot-assets//nested-dir/more-nested/more", content: "more"},
}
makeGadgetData(c, s.dir, gd)
@@ -124,7 +201,7 @@
err := gadget.WriteDirectory(filepath.Join(s.dir, "boot-assets"), outDir+"/", nil)
c.Assert(err, IsNil)
- verifyDeployedGadgetData(c, outDir, gd)
+ verifyWrittenGadgetData(c, outDir, gd)
}
func (s *mountedfilesystemTestSuite) TestWriteNonDirectory(c *C) {
@@ -144,20 +221,20 @@
func (s *mountedfilesystemTestSuite) TestMountedWriterHappy(c *C) {
gd := []gadgetData{
- {"foo", "foo-dir/foo", "foo foo foo"},
- {"bar", "bar-name", "bar bar bar"},
- {"boot-assets/splash", "splash", "splash"},
- {"boot-assets/some-dir/data", "some-dir/data", "data"},
- {"boot-assets/some-dir/data", "data-copy", "data"},
- {"boot-assets/some-dir/empty-file", "some-dir/empty-file", ""},
- {"boot-assets/nested-dir/nested", "/nested-copy/nested", "nested"},
- {"boot-assets/nested-dir/more-nested/more", "/nested-copy/more-nested/more", "more"},
+ {name: "foo", target: "foo-dir/foo", content: "foo foo foo"},
+ {name: "bar", target: "bar-name", content: "bar bar bar"},
+ {name: "boot-assets/splash", target: "splash", content: "splash"},
+ {name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"},
+ {name: "boot-assets/some-dir/data", target: "data-copy", content: "data"},
+ {name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file"},
+ {name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested", content: "nested"},
+ {name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/more-nested/more", content: "more"},
}
makeGadgetData(c, s.dir, gd)
err := os.MkdirAll(filepath.Join(s.dir, "boot-assets/empty-dir"), 0755)
c.Assert(err, IsNil)
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
Filesystem: "ext4",
@@ -196,7 +273,7 @@
err = rw.Write(outDir, nil)
c.Assert(err, IsNil)
- verifyDeployedGadgetData(c, outDir, gd)
+ verifyWrittenGadgetData(c, outDir, gd)
c.Assert(osutil.IsDirectory(filepath.Join(outDir, "empty-dir")), Equals, true)
}
@@ -206,7 +283,7 @@
}
makeGadgetData(c, s.dir, gd)
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
Filesystem: "ext4",
@@ -231,7 +308,7 @@
}
func (s *mountedfilesystemTestSuite) TestMountedWriterErrorMissingSource(c *C) {
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
Filesystem: "ext4",
@@ -258,7 +335,7 @@
func (s *mountedfilesystemTestSuite) TestMountedWriterErrorBadDestination(c *C) {
makeSizedFile(c, filepath.Join(s.dir, "foo"), 0, []byte("foo foo foo"))
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
Filesystem: "vfat",
@@ -291,7 +368,7 @@
{name: "foo-dir", content: "bar bar bar"},
})
- psOverwritesDirectoryWithFile := &gadget.PositionedStructure{
+ psOverwritesDirectoryWithFile := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
Filesystem: "ext4",
@@ -326,7 +403,7 @@
{name: "foo", content: "foo foo foo"},
{name: "bar", content: "bar bar bar"},
})
- psOverwritesFile := &gadget.PositionedStructure{
+ psOverwritesFile := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
Filesystem: "ext4",
@@ -363,7 +440,7 @@
{name: "foo/bar/baz", content: "data"},
})
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
Filesystem: "ext4",
@@ -391,20 +468,20 @@
func (s *mountedfilesystemTestSuite) TestMountedWriterPreserve(c *C) {
// some data for the gadget
- gdDeployed := []gadgetData{
- {"foo", "foo-dir/foo", "data"},
- {"bar", "bar-name", "data"},
- {"boot-assets/splash", "splash", "data"},
- {"boot-assets/some-dir/data", "some-dir/data", "data"},
- {"boot-assets/some-dir/empty-file", "some-dir/empty-file", "data"},
- {"boot-assets/nested-dir/more-nested/more", "/nested-copy/more-nested/more", "data"},
- }
- gdNotDeployed := []gadgetData{
- {"foo", "/foo", "data"},
- {"boot-assets/some-dir/data", "data-copy", "data"},
- {"boot-assets/nested-dir/nested", "/nested-copy/nested", "data"},
+ gdWritten := []gadgetData{
+ {name: "foo", target: "foo-dir/foo", content: "data"},
+ {name: "bar", target: "bar-name", content: "data"},
+ {name: "boot-assets/splash", target: "splash", content: "data"},
+ {name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"},
+ {name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file", content: "data"},
+ {name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/more-nested/more", content: "data"},
+ }
+ gdNotWritten := []gadgetData{
+ {name: "foo", target: "/foo", content: "data"},
+ {name: "boot-assets/some-dir/data", target: "data-copy", content: "data"},
+ {name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested", content: "data"},
}
- makeGadgetData(c, s.dir, append(gdDeployed, gdNotDeployed...))
+ makeGadgetData(c, s.dir, append(gdWritten, gdNotWritten...))
// these exist in the root directory and are preserved
preserve := []string{
@@ -415,7 +492,7 @@
"not-listed", // not present in 'gadget' contents
}
// these are preserved, but don't exist in the root, so data from gadget
- // will be deployed
+ // will be written
preserveButNotPresent := []string{
"/bar-name",
"some-dir/data",
@@ -427,7 +504,7 @@
makeSizedFile(c, p, 0, []byte("can't touch this"))
}
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
Filesystem: "ext4",
@@ -441,12 +518,12 @@
Target: "/",
}, {
// preserved, but not present, will be
- // deployed
+ // written
Source: "bar",
Target: "/bar-name",
}, {
// some-dir/data is preserved, but not
- // preset, hence will be deployed
+ // preset, hence will be written
Source: "boot-assets/",
Target: "/",
}, {
@@ -474,18 +551,55 @@
p := filepath.Join(outDir, en)
c.Check(p, testutil.FileEquals, "can't touch this")
}
- // everything else was deployed
- verifyDeployedGadgetData(c, outDir, gdDeployed)
+ // everything else was written
+ verifyWrittenGadgetData(c, outDir, gdWritten)
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedWriterNonFilePreserveError(c *C) {
+ // some data for the gadget
+ gd := []gadgetData{
+ {name: "foo", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ preserve := []string{
+ // this will be a directory
+ "foo",
+ }
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+ // will conflict with preserve entry
+ err := os.MkdirAll(filepath.Join(outDir, "foo"), 0755)
+ c.Assert(err, IsNil)
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "/",
+ Target: "/",
+ },
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps)
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Write(outDir, preserve)
+ c.Assert(err, ErrorMatches, `cannot map preserve entries for destination ".*/out-dir": preserved entry "foo" cannot be a directory`)
}
func (s *mountedfilesystemTestSuite) TestMountedWriterImplicitDir(c *C) {
gd := []gadgetData{
- {"boot-assets/nested-dir/nested", "/nested-copy/nested-dir/nested", "nested"},
- {"boot-assets/nested-dir/more-nested/more", "/nested-copy/nested-dir/more-nested/more", "more"},
+ {name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested-dir/nested", content: "nested"},
+ {name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/nested-dir/more-nested/more", content: "more"},
}
makeGadgetData(c, s.dir, gd)
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
Filesystem: "ext4",
@@ -508,11 +622,11 @@
err = rw.Write(outDir, nil)
c.Assert(err, IsNil)
- verifyDeployedGadgetData(c, outDir, gd)
+ verifyWrittenGadgetData(c, outDir, gd)
}
func (s *mountedfilesystemTestSuite) TestMountedWriterNoFs(c *C) {
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
// no filesystem
@@ -533,10 +647,10 @@
func (s *mountedfilesystemTestSuite) TestMountedWriterTrivialValidation(c *C) {
rw, err := gadget.NewMountedFilesystemWriter(s.dir, nil)
- c.Assert(err, ErrorMatches, `internal error: \*PositionedStructure.*`)
+ c.Assert(err, ErrorMatches, `internal error: \*LaidOutStructure.*`)
c.Assert(rw, IsNil)
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
Filesystem: "ext4",
@@ -568,3 +682,1746 @@
err = rw.Write(d, nil)
c.Assert(err, ErrorMatches, "cannot write filesystem content .* target cannot be unset")
}
+
+func (s *mountedfilesystemTestSuite) TestMountedWriterSymlinks(c *C) {
+ // some data for the gadget
+ gd := []gadgetData{
+ {name: "foo", target: "foo", content: "data"},
+ {name: "nested/foo", target: "nested/foo", content: "nested-data"},
+ {name: "link", symlinkTo: "foo"},
+ {name: "nested-link", symlinkTo: "nested"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {Source: "/", Target: "/"},
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemWriter(s.dir, ps)
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Write(outDir, nil)
+ c.Assert(err, IsNil)
+
+ // everything else was written
+ verifyWrittenGadgetData(c, outDir, []gadgetData{
+ {target: "foo", content: "data"},
+ {target: "link", symlinkTo: "foo"},
+ {target: "nested/foo", content: "nested-data"},
+ {target: "nested-link", symlinkTo: "nested"},
+ // when read via symlink
+ {target: "nested-link/foo", content: "nested-data"},
+ })
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupSimple(c *C) {
+ // some data for the gadget
+ gdWritten := []gadgetData{
+ {name: "bar", target: "bar-name", content: "data"},
+ {name: "foo", target: "foo", content: "data"},
+ {name: "zed", target: "zed", content: "data"},
+ {name: "same-data", target: "same", content: "same"},
+ // not included in volume contents
+ {name: "not-written", target: "not-written", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gdWritten)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ // these exist in the destination directory and will be backed up
+ backedUp := []gadgetData{
+ {target: "foo", content: "can't touch this"},
+ {target: "nested/foo", content: "can't touch this"},
+ // listed in preserve
+ {target: "zed", content: "preserved"},
+ // same content as the update
+ {target: "same", content: "same"},
+ }
+ makeExistingData(c, outDir, backedUp)
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/bar-name",
+ }, {
+ Source: "foo",
+ Target: "/",
+ }, {
+ Source: "foo",
+ Target: "/nested/",
+ }, {
+ Source: "zed",
+ Target: "/",
+ }, {
+ Source: "same-data",
+ Target: "/same",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ Preserve: []string{"/zed"},
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Backup()
+ c.Assert(err, IsNil)
+
+ // files that existed were backed up
+ for _, en := range backedUp {
+ backup := filepath.Join(s.backup, "struct-0", en.target+".backup")
+ same := filepath.Join(s.backup, "struct-0", en.target+".same")
+ switch en.content {
+ case "preserved":
+ c.Check(osutil.FileExists(backup), Equals, false, Commentf("file: %v", backup))
+ c.Check(osutil.FileExists(same), Equals, false, Commentf("file: %v", same))
+ case "same":
+ c.Check(osutil.FileExists(same), Equals, true, Commentf("file: %v", same))
+ default:
+ c.Check(backup, testutil.FileEquals, "can't touch this")
+ }
+ }
+
+ // running backup again does not error out
+ err = rw.Backup()
+ c.Assert(err, IsNil)
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupWithDirectories(c *C) {
+ // some data for the gadget
+ gdWritten := []gadgetData{
+ {name: "bar", content: "data"},
+ {name: "some-dir/foo", content: "data"},
+ {name: "empty-dir/"},
+ }
+ makeGadgetData(c, s.dir, gdWritten)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ // these exist in the destination directory and will be backed up
+ backedUp := []gadgetData{
+ // overwritten by "bar" -> "/foo"
+ {target: "foo", content: "can't touch this"},
+ // overwritten by some-dir/ -> /nested/
+ {target: "nested/foo", content: "can't touch this"},
+ // written to by bar -> /this/is/some/nested/
+ {target: "this/is/some/"},
+ {target: "lone-dir/"},
+ }
+ makeExistingData(c, outDir, backedUp)
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/foo",
+ }, {
+ Source: "bar",
+ Target: "/this/is/some/nested/",
+ }, {
+ Source: "some-dir/",
+ Target: "/nested/",
+ }, {
+ Source: "empty-dir/",
+ Target: "/lone-dir/",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ Preserve: []string{"/zed"},
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Backup()
+ c.Assert(err, IsNil)
+
+ verifyDirContents(c, filepath.Join(s.backup, "struct-0"), map[string]contentType{
+ "this/is/some.backup": typeFile,
+ "this/is.backup": typeFile,
+ "this.backup": typeFile,
+
+ "nested/foo.backup": typeFile,
+ "nested.backup": typeFile,
+
+ "foo.backup": typeFile,
+
+ "lone-dir.backup": typeFile,
+ })
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupNonexistent(c *C) {
+ // some data for the gadget
+ gd := []gadgetData{
+ {name: "bar", target: "foo", content: "data"},
+ {name: "bar", target: "some-dir/foo", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/foo",
+ }, {
+ Source: "bar",
+ Target: "/some-dir/foo",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ // bar not in preserved files
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Backup()
+ c.Assert(err, IsNil)
+
+ backupRoot := filepath.Join(s.backup, "struct-0")
+ // actually empty
+ verifyDirContents(c, backupRoot, map[string]contentType{})
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFailsOnBackupDirErrors(c *C) {
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/foo",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = os.Chmod(s.backup, 0555)
+ c.Assert(err, IsNil)
+ defer os.Chmod(s.backup, 0755)
+
+ err = rw.Backup()
+ c.Assert(err, ErrorMatches, "cannot create backup directory: .*/struct-0: permission denied")
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFailsOnDestinationErrors(c *C) {
+ // some data for the gadget
+ gd := []gadgetData{
+ {name: "bar", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+ makeExistingData(c, outDir, []gadgetData{
+ {target: "foo", content: "same"},
+ })
+
+ err := os.Chmod(filepath.Join(outDir, "foo"), 0000)
+ c.Assert(err, IsNil)
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/foo",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Backup()
+ c.Assert(err, ErrorMatches, "cannot backup content: cannot open destination file: open .*/out-dir/foo: permission denied")
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFailsOnBadSrcComparison(c *C) {
+ // some data for the gadget
+ gd := []gadgetData{
+ {name: "bar", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gd)
+ err := os.Chmod(filepath.Join(s.dir, "bar"), 0000)
+ c.Assert(err, IsNil)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+ makeExistingData(c, outDir, []gadgetData{
+ {target: "foo", content: "same"},
+ })
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/foo",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Backup()
+ c.Assert(err, ErrorMatches, "cannot backup content: cannot checksum update file: open .*/bar: permission denied")
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFunnyNamesConflictBackup(c *C) {
+ gdWritten := []gadgetData{
+ {name: "bar.backup/foo", content: "data"},
+ {name: "bar", content: "data"},
+ {name: "foo.same/foo", content: "same-as-current"},
+ {name: "foo", content: "same-as-current"},
+ }
+ makeGadgetData(c, s.dir, gdWritten)
+
+ // backup stamps conflicts with bar.backup
+ existingUpBar := []gadgetData{
+ // will be listed first
+ {target: "bar", content: "can't touch this"},
+ {target: "bar.backup/foo", content: "can't touch this"},
+ }
+ // backup stamps conflicts with foo.same
+ existingUpFoo := []gadgetData{
+ // will be listed first
+ {target: "foo", content: "same-as-current"},
+ {target: "foo.same/foo", content: "can't touch this"},
+ }
+
+ outDirConflictsBar := filepath.Join(c.MkDir(), "out-dir-bar")
+ makeExistingData(c, outDirConflictsBar, existingUpBar)
+
+ outDirConflictsFoo := filepath.Join(c.MkDir(), "out-dir-foo")
+ makeExistingData(c, outDirConflictsFoo, existingUpFoo)
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {Source: "/", Target: "/"},
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ backupBar := filepath.Join(s.backup, "backup-bar")
+ backupFoo := filepath.Join(s.backup, "backup-foo")
+
+ prefix := `cannot backup content: cannot create backup file: cannot create stamp file prefix: `
+ for _, tc := range []struct {
+ backupDir string
+ outDir string
+ err string
+ }{
+ {backupBar, outDirConflictsBar, prefix + `mkdir .*/bar.backup: not a directory`},
+ {backupFoo, outDirConflictsFoo, prefix + `mkdir .*/foo.same: not a directory`},
+ } {
+ err := os.MkdirAll(tc.backupDir, 0755)
+ c.Assert(err, IsNil)
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, tc.backupDir, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return tc.outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Backup()
+ c.Assert(err, ErrorMatches, tc.err)
+ }
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupFunnyNamesOk(c *C) {
+ gdWritten := []gadgetData{
+ {name: "bar.backup/foo", target: "bar.backup/foo", content: "data"},
+ {name: "foo.same/foo.same", target: "foo.same/foo.same", content: "same-as-current"},
+ {name: "zed.preserve", target: "zed.preserve", content: "this-is-preserved"},
+ {name: "new-file.same", target: "new-file.same", content: "this-is-new"},
+ }
+ makeGadgetData(c, s.dir, gdWritten)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ // these exist in the destination directory and will be backed up
+ backedUp := []gadgetData{
+ // will be listed first
+ {target: "bar.backup/foo", content: "not-data"},
+ {target: "foo.same/foo.same", content: "same-as-current"},
+ {target: "zed.preserve", content: "to-be-preserved"},
+ }
+ makeExistingData(c, outDir, backedUp)
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {Source: "/", Target: "/"},
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ Preserve: []string{
+ "zed.preserve",
+ },
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Backup()
+ c.Assert(err, IsNil)
+
+ verifyDirContents(c, filepath.Join(s.backup, "struct-0"), map[string]contentType{
+ "bar.backup.backup": typeFile,
+ "bar.backup/foo.backup": typeFile,
+
+ "foo.same.backup": typeFile,
+ "foo.same/foo.same.same": typeFile,
+
+ "zed.preserve.preserve": typeFile,
+ })
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupErrorOnSymlinkFile(c *C) {
+ gd := []gadgetData{
+ {name: "bar/data", target: "bar/data", content: "some data"},
+ {name: "bar/foo", target: "bar/foo", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ existing := []gadgetData{
+ {target: "bar/data", content: "some data"},
+ {target: "bar/foo", symlinkTo: "data"},
+ }
+ makeExistingData(c, outDir, existing)
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {Source: "/", Target: "/"},
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Backup()
+ c.Assert(err, ErrorMatches, "cannot backup content: cannot backup file /bar/foo: symbolic links are not supported")
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupErrorOnSymlinkInPrefixDir(c *C) {
+ gd := []gadgetData{
+ {name: "bar/nested/data", target: "bar/data", content: "some data"},
+ {name: "baz/foo", target: "baz/foo", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ existing := []gadgetData{
+ {target: "bar/nested-target/data", content: "some data"},
+ }
+ makeExistingData(c, outDir, existing)
+ // bar/nested-target -> nested
+ os.Symlink("nested-target", filepath.Join(outDir, "bar/nested"))
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {Source: "/", Target: "/"},
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Backup()
+ c.Assert(err, ErrorMatches, "cannot backup content: cannot create a checkpoint for directory /bar/nested: symbolic links are not supported")
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterUpdate(c *C) {
+ // some data for the gadget
+ gdWritten := []gadgetData{
+ {name: "foo", target: "foo-dir/foo", content: "data"},
+ {name: "bar", target: "bar-name", content: "data"},
+ {name: "boot-assets/splash", target: "splash", content: "data"},
+ {name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"},
+ {name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file", content: ""},
+ {name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/more-nested/more", content: "data"},
+ }
+ gdNotWritten := []gadgetData{
+ {name: "foo", target: "/foo", content: "data"},
+ {name: "boot-assets/some-dir/data", target: "data-copy", content: "data"},
+ {name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested", content: "data"},
+ }
+ makeGadgetData(c, s.dir, append(gdWritten, gdNotWritten...))
+
+ // these exist in the root directory and are preserved
+ preserve := []string{
+ // mix entries with leading / and without
+ "/foo",
+ "/data-copy",
+ "nested-copy/nested",
+ "not-listed", // not present in 'gadget' contents
+ }
+ // these are preserved, but don't exist in the root, so data from gadget
+ // will be written
+ preserveButNotPresent := []string{
+ "/bar-name",
+ "some-dir/data",
+ }
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ for _, en := range preserve {
+ p := filepath.Join(outDir, en)
+ makeSizedFile(c, p, 0, []byte("can't touch this"))
+ }
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "foo",
+ Target: "/foo-dir/",
+ }, {
+ // would overwrite /foo
+ Source: "foo",
+ Target: "/",
+ }, {
+ // preserved, but not present, will be
+ // written
+ Source: "bar",
+ Target: "/bar-name",
+ }, {
+ // some-dir/data is preserved, but not
+ // present, hence will be written
+ Source: "boot-assets/",
+ Target: "/",
+ }, {
+ // would overwrite /data-copy
+ Source: "boot-assets/some-dir/data",
+ Target: "/data-copy",
+ }, {
+ // would overwrite /nested-copy/nested
+ Source: "boot-assets/nested-dir/",
+ Target: "/nested-copy/",
+ }, {
+ Source: "boot-assets",
+ Target: "/boot-assets-copy/",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ Preserve: append(preserve, preserveButNotPresent...),
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Backup()
+ c.Assert(err, IsNil)
+
+ err = rw.Update()
+ c.Assert(err, IsNil)
+
+ // files that existed were preserved
+ for _, en := range preserve {
+ p := filepath.Join(outDir, en)
+ c.Check(p, testutil.FileEquals, "can't touch this")
+ }
+ // everything else was written
+ verifyWrittenGadgetData(c, outDir, gdWritten)
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterUpdateLookupFails(c *C) {
+ makeGadgetData(c, s.dir, []gadgetData{
+ {name: "canary", target: "canary", content: "data"},
+ })
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "/",
+ Target: "/",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return "", errors.New("failed")
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Update()
+ c.Assert(err, ErrorMatches, "cannot find mount location of structure #0: failed")
+}
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterDirContents(c *C) {
+ // some data for the gadget
+ gdWritten := []gadgetData{
+ {name: "bar/foo", target: "/bar-name/foo", content: "data"},
+ {name: "bar/nested/foo", target: "/bar-name/nested/foo", content: "data"},
+ {name: "bar/foo", target: "/bar-copy/bar/foo", content: "data"},
+ {name: "bar/nested/foo", target: "/bar-copy/bar/nested/foo", content: "data"},
+ {name: "deep-nested", target: "/this/is/some/deep/nesting/deep-nested", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gdWritten)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ // contents of bar under /bar-name/
+ Source: "bar/",
+ Target: "/bar-name",
+ }, {
+ // whole bar under /bar-copy/
+ Source: "bar",
+ Target: "/bar-copy/",
+ }, {
+ // deep prefix
+ Source: "deep-nested",
+ Target: "/this/is/some/deep/nesting/",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Backup()
+ c.Assert(err, IsNil)
+
+ err = rw.Update()
+ c.Assert(err, IsNil)
+
+ verifyWrittenGadgetData(c, outDir, gdWritten)
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterExpectsBackup(c *C) {
+ // some data for the gadget
+ gd := []gadgetData{
+ {name: "bar", target: "foo", content: "update"},
+ {name: "bar", target: "some-dir/foo", content: "update"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+ makeExistingData(c, outDir, []gadgetData{
+ {target: "foo", content: "content"},
+ {target: "some-dir/foo", content: "content"},
+ {target: "/preserved", content: "preserve"},
+ })
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/foo",
+ }, {
+ Source: "bar",
+ Target: "/some-dir/foo",
+ }, {
+ Source: "bar",
+ Target: "/preserved",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ // bar not in preserved files
+ Preserve: []string{"preserved"},
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Update()
+ c.Assert(err, ErrorMatches, `cannot update content: missing backup file ".*/struct-0/foo.backup" for /foo`)
+ // create a mock backup of first file
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.backup"), 0, nil)
+ // try again
+ err = rw.Update()
+ c.Assert(err, ErrorMatches, `cannot update content: missing backup file ".*/struct-0/some-dir/foo.backup" for /some-dir/foo`)
+ // create a mock backup of second entry
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/some-dir/foo.backup"), 0, nil)
+ // try again (preserved files need no backup)
+ err = rw.Update()
+ c.Assert(err, IsNil)
+
+ verifyWrittenGadgetData(c, outDir, []gadgetData{
+ {target: "foo", content: "update"},
+ {target: "some-dir/foo", content: "update"},
+ {target: "/preserved", content: "preserve"},
+ })
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterEmptyDir(c *C) {
+ // some data for the gadget
+ err := os.MkdirAll(filepath.Join(s.dir, "empty-dir"), 0755)
+ c.Assert(err, IsNil)
+ err = os.MkdirAll(filepath.Join(s.dir, "non-empty/empty-dir"), 0755)
+ c.Assert(err, IsNil)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "/",
+ Target: "/",
+ }, {
+ Source: "/",
+ Target: "/foo",
+ }, {
+ Source: "/non-empty/empty-dir/",
+ Target: "/contents-of-empty/",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Update()
+ c.Assert(err, IsNil)
+
+ verifyDirContents(c, outDir, map[string]contentType{
+ // / -> /
+ "empty-dir": typeDir,
+ "non-empty/empty-dir": typeDir,
+
+ // / -> /foo
+ "foo/empty-dir": typeDir,
+ "foo/non-empty/empty-dir": typeDir,
+
+ // /non-empty/empty-dir/ -> /contents-of-empty/
+ "contents-of-empty": typeDir,
+ })
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterSameFileSkipped(c *C) {
+ // some data for the gadget
+ gd := []gadgetData{
+ {name: "bar", target: "foo", content: "data"},
+ {name: "bar", target: "some-dir/foo", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+ makeExistingData(c, outDir, []gadgetData{
+ {target: "foo", content: "same"},
+ {target: "some-dir/foo", content: "same"},
+ })
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/foo",
+ }, {
+ Source: "bar",
+ Target: "/some-dir/foo",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ // pretend a backup pass ran and found the files identical
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.same"), 0, nil)
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/some-dir/foo.same"), 0, nil)
+
+ err = rw.Update()
+ c.Assert(err, IsNil)
+ // files were not modified
+ verifyWrittenGadgetData(c, outDir, []gadgetData{
+ {target: "foo", content: "same"},
+ {target: "some-dir/foo", content: "same"},
+ })
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterLonePrefix(c *C) {
+ // some data for the gadget
+ gd := []gadgetData{
+ {name: "bar", target: "1/nested/bar", content: "data"},
+ {name: "bar", target: "2/nested/foo", content: "data"},
+ {name: "bar", target: "3/nested/bar", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/1/nested/",
+ }, {
+ Source: "bar",
+ Target: "/2/nested/foo",
+ }, {
+ Source: "/",
+ Target: "/3/nested/",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Update()
+ c.Assert(err, IsNil)
+ verifyWrittenGadgetData(c, outDir, gd)
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterUpdateErrorOnSymlinkToFile(c *C) {
+ gdWritten := []gadgetData{
+ {name: "data", target: "data", content: "some data"},
+ {name: "foo", symlinkTo: "data"},
+ }
+ makeGadgetData(c, s.dir, gdWritten)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ existing := []gadgetData{
+ {target: "data", content: "some data"},
+ }
+ makeExistingData(c, outDir, existing)
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {Source: "/", Target: "/"},
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ // create a mock backup of first file
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/data.backup"), 0, nil)
+
+ err = rw.Update()
+ c.Assert(err, ErrorMatches, "cannot update content: cannot update file /foo: symbolic links are not supported")
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterBackupErrorOnSymlinkToDir(c *C) {
+ gd := []gadgetData{
+ {name: "bar/data", target: "bar/data", content: "some data"},
+ {name: "baz", symlinkTo: "bar"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ existing := []gadgetData{
+ {target: "bar/data", content: "some data"},
+ }
+ makeExistingData(c, outDir, existing)
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {Source: "/", Target: "/"},
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ // create a mock backup of first file
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/bar/data.backup"), 0, nil)
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/bar.backup"), 0, nil)
+
+ err = rw.Update()
+ c.Assert(err, ErrorMatches, "cannot update content: cannot update file /baz: symbolic links are not supported")
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackFromBackup(c *C) {
+ // some data for the gadget
+ gd := []gadgetData{
+ {name: "bar", target: "foo", content: "data"},
+ {name: "bar", target: "some-dir/foo", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+ makeExistingData(c, outDir, []gadgetData{
+ {target: "foo", content: "written"},
+ {target: "some-dir/foo", content: "written"},
+ })
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/foo",
+ }, {
+ Source: "bar",
+ Target: "/some-dir/foo",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ // pretend a backup pass ran and created a backup
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.backup"), 0, []byte("backup"))
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/some-dir/foo.backup"), 0, []byte("backup"))
+
+ err = rw.Rollback()
+ c.Assert(err, IsNil)
+ // files were restored from backup
+ verifyWrittenGadgetData(c, outDir, []gadgetData{
+ {target: "foo", content: "backup"},
+ {target: "some-dir/foo", content: "backup"},
+ })
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackSkipSame(c *C) {
+ // some data for the gadget
+ gd := []gadgetData{
+ {name: "bar", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+ makeExistingData(c, outDir, []gadgetData{
+ {target: "foo", content: "same"},
+ })
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/foo",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ // pretend a backup pass ran and created a backup
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.same"), 0, nil)
+
+ err = rw.Rollback()
+ c.Assert(err, IsNil)
+ // files were not modified
+ verifyWrittenGadgetData(c, outDir, []gadgetData{
+ {target: "foo", content: "same"},
+ })
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackSkipPreserved(c *C) {
+ // some data for the gadget
+ gd := []gadgetData{
+ {name: "bar", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+ makeExistingData(c, outDir, []gadgetData{
+ {target: "foo", content: "preserved"},
+ })
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/foo",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ Preserve: []string{"foo"},
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ // preserved files get no backup, but gets a stamp instead
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.preserve"), 0, nil)
+
+ err = rw.Rollback()
+ c.Assert(err, IsNil)
+ // files were not modified
+ verifyWrittenGadgetData(c, outDir, []gadgetData{
+ {target: "foo", content: "preserved"},
+ })
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackNewFiles(c *C) {
+ makeGadgetData(c, s.dir, []gadgetData{
+ {name: "bar", content: "data"},
+ })
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+ makeExistingData(c, outDir, []gadgetData{
+ {target: "foo", content: "written"},
+ {target: "some-dir/bar", content: "written"},
+ {target: "this/is/some/deep/nesting/bar", content: "written"},
+ })
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/foo",
+ }, {
+ Source: "bar",
+ Target: "some-dir/",
+ }, {
+ Source: "bar",
+ Target: "/this/is/some/deep/nesting/",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ // none of the marker files exists, files are new, will be removed
+ err = rw.Rollback()
+ c.Assert(err, IsNil)
+ // everything was removed
+ verifyDirContents(c, outDir, map[string]contentType{})
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackRestoreFails(c *C) {
+ makeGadgetData(c, s.dir, []gadgetData{
+ {name: "bar", content: "data"},
+ })
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+ makeExistingData(c, outDir, []gadgetData{
+ {target: "foo", content: "written"},
+ {target: "some-dir/foo", content: "written"},
+ })
+ // make rollback fail when restoring
+ err := os.Chmod(filepath.Join(outDir, "foo"), 0000)
+ c.Assert(err, IsNil)
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/foo",
+ }, {
+ Source: "bar",
+ Target: "/some-dir/foo",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ // one file backed up, the other is new
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.backup"), 0, []byte("backup"))
+
+ err = rw.Rollback()
+ c.Assert(err, ErrorMatches, "cannot rollback content: cannot copy .*: unable to create .*/out-dir/foo: permission denied")
+
+ // remove offending file
+ c.Assert(os.Remove(filepath.Join(outDir, "foo")), IsNil)
+
+ // make destination dir non-writable
+ err = os.Chmod(filepath.Join(outDir, "some-dir"), 0555)
+ c.Assert(err, IsNil)
+ // restore permissions later, otherwise test suite cleanup complains
+ defer os.Chmod(filepath.Join(outDir, "some-dir"), 0755)
+
+ err = rw.Rollback()
+ c.Assert(err, ErrorMatches, "cannot rollback content: cannot remove written update: remove .*/out-dir/some-dir/foo: permission denied")
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackNotWritten(c *C) {
+ makeGadgetData(c, s.dir, []gadgetData{
+ {name: "bar", content: "data"},
+ })
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "bar",
+ Target: "/foo",
+ }, {
+ Source: "bar",
+ Target: "/some-dir/foo",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ // rollback does not error out if files were not written
+ err = rw.Rollback()
+ c.Assert(err, IsNil)
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterRollbackDirectory(c *C) {
+ makeGadgetData(c, s.dir, []gadgetData{
+ {name: "some-dir/bar", content: "data"},
+ {name: "some-dir/foo", content: "data"},
+ {name: "some-dir/nested/nested-foo", content: "data"},
+ {name: "empty-dir/"},
+ })
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+ makeExistingData(c, outDir, []gadgetData{
+ // some-dir/ -> /
+ {target: "foo", content: "written"},
+ {target: "bar", content: "written"},
+ {target: "nested/nested-foo", content: "written"},
+ // some-dir/ -> /other-dir/
+ {target: "other-dir/foo", content: "written"},
+ {target: "other-dir/bar", content: "written"},
+ {target: "other-dir/nested/nested-foo", content: "written"},
+ // some-dir/nested -> /other-dir/nested/
+ {target: "other-dir/nested/nested/nested-foo", content: "written"},
+ // bar -> /this/is/some/deep/nesting/
+ {target: "this/is/some/deep/nesting/bar", content: "written"},
+ {target: "lone-dir/"},
+ })
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "some-dir/",
+ Target: "/",
+ }, {
+ Source: "some-dir/",
+ Target: "/other-dir/",
+ }, {
+ Source: "some-dir/nested",
+ Target: "/other-dir/nested/",
+ }, {
+ Source: "bar",
+ Target: "/this/is/some/deep/nesting/",
+ }, {
+ Source: "empty-dir/",
+ Target: "/lone-dir/",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ // one file backed up
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/foo.backup"), 0, []byte("backup"))
+ // pretend part of the directory structure existed before
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/this/is/some.backup"), 0, nil)
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/this/is.backup"), 0, nil)
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/this.backup"), 0, nil)
+ makeSizedFile(c, filepath.Join(s.backup, "struct-0/lone-dir.backup"), 0, nil)
+
+ // files without a marker are new, will be removed
+ err = rw.Rollback()
+ c.Assert(err, IsNil)
+
+ verifyDirContents(c, outDir, map[string]contentType{
+ "lone-dir": typeDir,
+ "this/is/some": typeDir,
+ "foo": typeFile,
+ })
+ // this one got restored
+ c.Check(filepath.Join(outDir, "foo"), testutil.FileEquals, "backup")
+
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterEndToEndOne(c *C) {
+ // some data for the gadget
+ gdWritten := []gadgetData{
+ {name: "foo", target: "foo-dir/foo", content: "data"},
+ {name: "bar", target: "bar-name", content: "data"},
+ {name: "boot-assets/splash", target: "splash", content: "data"},
+ {name: "boot-assets/some-dir/data", target: "some-dir/data", content: "data"},
+ {name: "boot-assets/some-dir/empty-file", target: "some-dir/empty-file", content: ""},
+ {name: "boot-assets/nested-dir/more-nested/more", target: "/nested-copy/more-nested/more", content: "data"},
+ }
+ gdNotWritten := []gadgetData{
+ {name: "foo", target: "/foo", content: "data"},
+ {name: "boot-assets/some-dir/data", target: "data-copy", content: "data"},
+ {name: "boot-assets/nested-dir/nested", target: "/nested-copy/nested", content: "data"},
+ }
+ makeGadgetData(c, s.dir, append(gdWritten, gdNotWritten...))
+ err := os.MkdirAll(filepath.Join(s.dir, "boot-assets/empty-dir"), 0755)
+ c.Assert(err, IsNil)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+
+ makeExistingData(c, outDir, []gadgetData{
+ {target: "foo", content: "can't touch this"},
+ {target: "data-copy-preserved", content: "can't touch this"},
+ {target: "data-copy", content: "can't touch this"},
+ {target: "nested-copy/nested", content: "can't touch this"},
+ {target: "nested-copy/more-nested/"},
+ {target: "not-listed", content: "can't touch this"},
+ {target: "unrelated/data/here", content: "unrelated"},
+ })
+ // these exist in the root directory and are preserved
+ preserve := []string{
+ // mix entries with leading / and without
+ "/foo",
+ "/data-copy-preserved",
+ "nested-copy/nested",
+ "not-listed", // not present in 'gadget' contents
+ }
+ // these are preserved, but don't exist in the root, so data from gadget
+ // will be written
+ preserveButNotPresent := []string{
+ "/bar-name",
+ "some-dir/data",
+ }
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {
+ Source: "foo",
+ Target: "/foo-dir/",
+ }, {
+ // would overwrite /foo
+ Source: "foo",
+ Target: "/",
+ }, {
+ // preserved, but not present, will be
+ // written
+ Source: "bar",
+ Target: "/bar-name",
+ }, {
+ // some-dir/data is preserved, but not
+ // present, hence will be written
+ Source: "boot-assets/",
+ Target: "/",
+ }, {
+ // would overwrite /data-copy
+ Source: "boot-assets/some-dir/data",
+ Target: "/data-copy-preserved",
+ }, {
+ Source: "boot-assets/some-dir/data",
+ Target: "/data-copy",
+ }, {
+ // would overwrite /nested-copy/nested
+ Source: "boot-assets/nested-dir/",
+ Target: "/nested-copy/",
+ }, {
+ Source: "boot-assets",
+ Target: "/boot-assets-copy/",
+ }, {
+ Source: "/boot-assets/empty-dir/",
+ Target: "/lone-dir/nested/",
+ },
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 1,
+ Preserve: append(preserve, preserveButNotPresent...),
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ originalState := map[string]contentType{
+ "foo": typeFile,
+ "data-copy": typeFile,
+ "not-listed": typeFile,
+ "data-copy-preserved": typeFile,
+ "nested-copy/nested": typeFile,
+ "nested-copy/more-nested": typeDir,
+ "unrelated/data/here": typeFile,
+ }
+ verifyDirContents(c, outDir, originalState)
+
+ // run the backup phase
+ err = rw.Backup()
+ c.Assert(err, IsNil)
+
+ verifyDirContents(c, filepath.Join(s.backup, "struct-0"), map[string]contentType{
+ "nested-copy.backup": typeFile,
+ "nested-copy/nested.preserve": typeFile,
+ "nested-copy/more-nested.backup": typeFile,
+ "foo.preserve": typeFile,
+ "data-copy-preserved.preserve": typeFile,
+ "data-copy.backup": typeFile,
+ })
+
+ // run the update phase
+ err = rw.Update()
+ c.Assert(err, IsNil)
+
+ verifyDirContents(c, outDir, map[string]contentType{
+ "foo": typeFile,
+ "not-listed": typeFile,
+
+ // boot-assets/some-dir/data -> /data-copy
+ "data-copy": typeFile,
+
+ // boot-assets/some-dir/data -> /data-copy-preserved
+ "data-copy-preserved": typeFile,
+
+ // foo -> /foo-dir/
+ "foo-dir/foo": typeFile,
+
+ // bar -> /bar-name
+ "bar-name": typeFile,
+
+ // boot-assets/ -> /
+ "splash": typeFile,
+ "some-dir/data": typeFile,
+ "some-dir/empty-file": typeFile,
+ "nested-dir/nested": typeFile,
+ "nested-dir/more-nested/more": typeFile,
+ "empty-dir": typeDir,
+
+ // boot-assets -> /boot-assets-copy/
+ "boot-assets-copy/boot-assets/splash": typeFile,
+ "boot-assets-copy/boot-assets/some-dir/data": typeFile,
+ "boot-assets-copy/boot-assets/some-dir/empty-file": typeFile,
+ "boot-assets-copy/boot-assets/nested-dir/nested": typeFile,
+ "boot-assets-copy/boot-assets/nested-dir/more-nested/more": typeFile,
+ "boot-assets-copy/boot-assets/empty-dir": typeDir,
+
+ // boot-assets/nested-dir/ -> /nested-copy/
+ "nested-copy/nested": typeFile,
+ "nested-copy/more-nested/more": typeFile,
+
+ // data that was not part of the update
+ "unrelated/data/here": typeFile,
+
+ // boot-assets/empty-dir/ -> /lone-dir/nested/
+ "lone-dir/nested": typeDir,
+ })
+
+ // files that existed were preserved
+ for _, en := range preserve {
+ p := filepath.Join(outDir, en)
+ c.Check(p, testutil.FileEquals, "can't touch this")
+ }
+ // everything else was written
+ verifyWrittenGadgetData(c, outDir, gdWritten)
+
+ err = rw.Rollback()
+ c.Assert(err, IsNil)
+ // back to square one
+ verifyDirContents(c, outDir, originalState)
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterTrivialValidation(c *C) {
+ psNoFs := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ // no filesystem
+ Content: []gadget.VolumeContent{},
+ },
+ }
+
+ lookupFail := func(to *gadget.LaidOutStructure) (string, error) {
+ c.Fatalf("unexpected call")
+ return "", nil
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, psNoFs, s.backup, lookupFail)
+ c.Assert(err, ErrorMatches, "structure #0 has no filesystem")
+ c.Assert(rw, IsNil)
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{},
+ },
+ }
+
+ rw, err = gadget.NewMountedFilesystemUpdater("", ps, s.backup, lookupFail)
+ c.Assert(err, ErrorMatches, `internal error: gadget content directory cannot be unset`)
+ c.Assert(rw, IsNil)
+
+ rw, err = gadget.NewMountedFilesystemUpdater(s.dir, ps, "", lookupFail)
+ c.Assert(err, ErrorMatches, `internal error: backup directory must not be unset`)
+ c.Assert(rw, IsNil)
+
+ rw, err = gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, nil)
+ c.Assert(err, ErrorMatches, `internal error: mount lookup helper must be provided`)
+ c.Assert(rw, IsNil)
+
+ rw, err = gadget.NewMountedFilesystemUpdater(s.dir, nil, s.backup, lookupFail)
+ c.Assert(err, ErrorMatches, `internal error: \*LaidOutStructure.*`)
+ c.Assert(rw, IsNil)
+
+ lookupOk := func(to *gadget.LaidOutStructure) (string, error) {
+ return filepath.Join(s.dir, "foobar"), nil
+ }
+
+ for _, tc := range []struct {
+ content gadget.VolumeContent
+ match string
+ }{
+ {content: gadget.VolumeContent{Source: "", Target: "/"}, match: "internal error: source cannot be unset"},
+ {content: gadget.VolumeContent{Source: "/", Target: ""}, match: "internal error: target cannot be unset"},
+ } {
+ testPs := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{tc.content},
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, testPs, s.backup, lookupOk)
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Update()
+ c.Assert(err, ErrorMatches, "cannot update content: "+tc.match)
+
+ err = rw.Backup()
+ c.Assert(err, ErrorMatches, "cannot backup content: "+tc.match)
+
+ err = rw.Rollback()
+ c.Assert(err, ErrorMatches, "cannot rollback content: "+tc.match)
+ }
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterMountLookupFail(c *C) {
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {Source: "/", Target: "/"},
+ },
+ },
+ }
+
+ lookupFail := func(to *gadget.LaidOutStructure) (string, error) {
+ return "", errors.New("fail fail fail")
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, lookupFail)
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Update()
+ c.Assert(err, ErrorMatches, "cannot find mount location of structure #0: fail fail fail")
+
+ err = rw.Backup()
+ c.Assert(err, ErrorMatches, "cannot find mount location of structure #0: fail fail fail")
+
+ err = rw.Rollback()
+ c.Assert(err, ErrorMatches, "cannot find mount location of structure #0: fail fail fail")
+}
+
+func (s *mountedfilesystemTestSuite) TestMountedUpdaterNonFilePreserveError(c *C) {
+ // some data for the gadget
+ gd := []gadgetData{
+ {name: "foo", content: "data"},
+ }
+ makeGadgetData(c, s.dir, gd)
+
+ outDir := filepath.Join(c.MkDir(), "out-dir")
+ // will conflict with preserve entry
+ err := os.MkdirAll(filepath.Join(outDir, "foo"), 0755)
+ c.Assert(err, IsNil)
+
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Size: 2048,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {Source: "/", Target: "/"},
+ },
+ Update: gadget.VolumeUpdate{
+ Preserve: []string{"foo"},
+ Edition: 1,
+ },
+ },
+ }
+
+ rw, err := gadget.NewMountedFilesystemUpdater(s.dir, ps, s.backup, func(to *gadget.LaidOutStructure) (string, error) {
+ c.Check(to, DeepEquals, ps)
+ return outDir, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(rw, NotNil)
+
+ err = rw.Backup()
+ c.Check(err, ErrorMatches, `cannot map preserve entries for mount location ".*/out-dir": preserved entry "foo" cannot be a directory`)
+ err = rw.Update()
+ c.Check(err, ErrorMatches, `cannot map preserve entries for mount location ".*/out-dir": preserved entry "foo" cannot be a directory`)
+ err = rw.Rollback()
+ c.Check(err, ErrorMatches, `cannot map preserve entries for mount location ".*/out-dir": preserved entry "foo" cannot be a directory`)
+}
diff -Nru snapd-2.40/gadget/offset.go snapd-2.42.1/gadget/offset.go
--- snapd-2.40/gadget/offset.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/offset.go 2019-10-30 12:17:43.000000000 +0000
@@ -28,7 +28,7 @@
// and its content at locations defined by offset-write property. structures and
// their content.
type OffsetWriter struct {
- ps *PositionedStructure
+ ps *LaidOutStructure
sectorSize Size
}
@@ -47,9 +47,9 @@
}
// NewOffsetWriter returns a writer for given structure.
-func NewOffsetWriter(ps *PositionedStructure, sectorSize Size) (*OffsetWriter, error) {
+func NewOffsetWriter(ps *LaidOutStructure, sectorSize Size) (*OffsetWriter, error) {
if ps == nil {
- return nil, fmt.Errorf("internal error: *PositionedStructure is nil")
+ return nil, fmt.Errorf("internal error: *LaidOutStructure is nil")
}
if sectorSize == 0 {
return nil, fmt.Errorf("internal error: sector size cannot be 0")
@@ -65,10 +65,10 @@
// structure, at the locations defined by offset-writer property of respective
// element, in the format of LBA pointer.
func (w *OffsetWriter) Write(out io.WriteSeeker) error {
- // positioning guarantees that start offset is aligned to sector size
+ // layout step guarantees that start offset is aligned to sector size
- if w.ps.PositionedOffsetWrite != nil {
- if err := offsetWrite(out, *w.ps.PositionedOffsetWrite, asLBA(w.ps.StartOffset, w.sectorSize)); err != nil {
+ if w.ps.AbsoluteOffsetWrite != nil {
+ if err := offsetWrite(out, *w.ps.AbsoluteOffsetWrite, asLBA(w.ps.StartOffset, w.sectorSize)); err != nil {
return err
}
}
@@ -78,11 +78,11 @@
return nil
}
- for _, pc := range w.ps.PositionedContent {
- if pc.PositionedOffsetWrite == nil {
+ for _, pc := range w.ps.LaidOutContent {
+ if pc.AbsoluteOffsetWrite == nil {
continue
}
- if err := offsetWrite(out, *pc.PositionedOffsetWrite, asLBA(pc.StartOffset, w.sectorSize)); err != nil {
+ if err := offsetWrite(out, *pc.AbsoluteOffsetWrite, asLBA(pc.StartOffset, w.sectorSize)); err != nil {
return err
}
}
diff -Nru snapd-2.40/gadget/offset_test.go snapd-2.42.1/gadget/offset_test.go
--- snapd-2.40/gadget/offset_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/offset_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -33,16 +33,16 @@
var _ = Suite(&offsetSuite{})
func (m *offsetSuite) TestOffsetWriterOnlyStructure(c *C) {
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 1 * gadget.SizeMiB,
OffsetWrite: &gadget.RelativeOffset{Offset: 512},
},
StartOffset: 1024,
// start offset written at this location
- PositionedOffsetWrite: asSizePtr(512),
+ AbsoluteOffsetWrite: asSizePtr(512),
- PositionedContent: []gadget.PositionedContent{
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{
Image: "foo.img",
@@ -75,13 +75,13 @@
}
func (m *offsetSuite) TestOffsetWriterOnlyRawContent(c *C) {
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 1 * gadget.SizeMiB,
},
StartOffset: gadget.Size(1024),
- PositionedContent: []gadget.PositionedContent{
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{
Image: "foo.img",
@@ -91,7 +91,7 @@
Size: 128,
StartOffset: 2048,
// start offset written here
- PositionedOffsetWrite: asSizePtr(4096),
+ AbsoluteOffsetWrite: asSizePtr(4096),
},
},
}
@@ -118,15 +118,15 @@
}
func (m *offsetSuite) TestOffsetWriterOnlyFsStructure(c *C) {
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 1 * gadget.SizeMiB,
Filesystem: "ext4",
// same as in pc gadget
OffsetWrite: &gadget.RelativeOffset{Offset: 92},
},
- StartOffset: gadget.Size(348 * gadget.SizeKiB),
- PositionedOffsetWrite: asSizePtr(92),
+ StartOffset: gadget.Size(348 * gadget.SizeKiB),
+ AbsoluteOffsetWrite: asSizePtr(92),
}
const sectorSize = 512
@@ -151,15 +151,15 @@
}
func (m *offsetSuite) TestOffsetWriterErrors(c *C) {
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 1 * gadget.SizeMiB,
Filesystem: "ext4",
// same as in pc gadget
OffsetWrite: &gadget.RelativeOffset{Offset: 92},
},
- StartOffset: gadget.Size(348 * gadget.SizeKiB),
- PositionedOffsetWrite: asSizePtr(92),
+ StartOffset: gadget.Size(348 * gadget.SizeKiB),
+ AbsoluteOffsetWrite: asSizePtr(92),
}
const sectorSize = 512
@@ -188,12 +188,12 @@
err = ow.Write(mwBadWriter)
c.Assert(err, ErrorMatches, "cannot write LBA value 0x2b8 at offset 92: bad writer")
- psOnlyContent := &gadget.PositionedStructure{
+ psOnlyContent := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 1 * gadget.SizeMiB,
},
StartOffset: gadget.Size(348 * gadget.SizeKiB),
- PositionedContent: []gadget.PositionedContent{
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{
Image: "foo.img",
@@ -203,7 +203,7 @@
Size: 128,
StartOffset: 2048,
// start offset written here
- PositionedOffsetWrite: asSizePtr(4096),
+ AbsoluteOffsetWrite: asSizePtr(4096),
},
},
}
@@ -217,18 +217,18 @@
func (m *offsetSuite) TestOffsetWriterErrorSimpleValidation(c *C) {
ow, err := gadget.NewOffsetWriter(nil, 512)
- c.Assert(err, ErrorMatches, `internal error: \*PositionedStructure is nil`)
+ c.Assert(err, ErrorMatches, `internal error: \*LaidOutStructure is nil`)
c.Assert(ow, IsNil)
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 1 * gadget.SizeMiB,
Filesystem: "ext4",
// same as in pc gadget
OffsetWrite: &gadget.RelativeOffset{Offset: 92},
},
- StartOffset: gadget.Size(348 * gadget.SizeKiB),
- PositionedOffsetWrite: asSizePtr(92),
+ StartOffset: gadget.Size(348 * gadget.SizeKiB),
+ AbsoluteOffsetWrite: asSizePtr(92),
}
ow, err = gadget.NewOffsetWriter(ps, 0)
diff -Nru snapd-2.40/gadget/partition.go snapd-2.42.1/gadget/partition.go
--- snapd-2.40/gadget/partition.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/partition.go 2019-10-30 12:17:43.000000000 +0000
@@ -45,7 +45,7 @@
return maybeHybridType[:idx], maybeHybridType[idx+1:]
}
-func Partition(image string, pv *PositionedVolume) error {
+func Partition(image string, pv *LaidOutVolume) error {
if image == "" {
return fmt.Errorf("internal error: image path is unset")
}
@@ -73,7 +73,7 @@
}
fmt.Fprintf(script, "\n")
- for _, ps := range pv.PositionedStructure {
+ for _, ps := range pv.LaidOutStructure {
if ps.Type == "bare" || ps.Type == "mbr" {
continue
}
diff -Nru snapd-2.40/gadget/partition_test.go snapd-2.42.1/gadget/partition_test.go
--- snapd-2.40/gadget/partition_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/partition_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -56,14 +56,14 @@
}
func (s *partitionSuite) TestGPTHappy(c *C) {
- pv := &gadget.PositionedVolume{
+ pv := &gadget.LaidOutVolume{
Volume: &gadget.Volume{
Schema: "gpt",
ID: "123-123",
},
Size: 3 * gadget.SizeMiB,
SectorSize: 512,
- PositionedStructure: []gadget.PositionedStructure{
+ LaidOutStructure: []gadget.LaidOutStructure{
{
// does not appear as partition
VolumeStructure: &gadget.VolumeStructure{
@@ -110,14 +110,14 @@
}
func (s *partitionSuite) TestMBRHappy(c *C) {
- pv := &gadget.PositionedVolume{
+ pv := &gadget.LaidOutVolume{
Volume: &gadget.Volume{
Schema: "mbr",
ID: "0x123",
},
Size: 3 * gadget.SizeMiB,
SectorSize: 512,
- PositionedStructure: []gadget.PositionedStructure{
+ LaidOutStructure: []gadget.LaidOutStructure{
{
// does not appear as partition
VolumeStructure: &gadget.VolumeStructure{
@@ -174,20 +174,20 @@
}
func (s *partitionSuite) TestHybridType(c *C) {
- ps := gadget.PositionedStructure{
+ ps := gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2 * gadget.SizeMiB,
Type: "0C,21686148-6449-6E6F-744E-656564454649",
},
StartOffset: 1 * gadget.SizeMiB,
}
- pvGPT := &gadget.PositionedVolume{
+ pvGPT := &gadget.LaidOutVolume{
Volume: &gadget.Volume{
Schema: "gpt",
},
- Size: 3 * gadget.SizeMiB,
- SectorSize: 512,
- PositionedStructure: []gadget.PositionedStructure{ps},
+ Size: 3 * gadget.SizeMiB,
+ SectorSize: 512,
+ LaidOutStructure: []gadget.LaidOutStructure{ps},
}
err := gadget.Partition("foo", pvGPT)
@@ -199,13 +199,13 @@
start=2048, size=4096, type=21686148-6449-6E6F-744E-656564454649
`)
- pvMBR := &gadget.PositionedVolume{
+ pvMBR := &gadget.LaidOutVolume{
Volume: &gadget.Volume{
Schema: "mbr",
},
- Size: 3 * gadget.SizeMiB,
- SectorSize: 512,
- PositionedStructure: []gadget.PositionedStructure{ps},
+ Size: 3 * gadget.SizeMiB,
+ SectorSize: 512,
+ LaidOutStructure: []gadget.LaidOutStructure{ps},
}
err = gadget.Partition("foo", pvMBR)
c.Assert(err, IsNil)
@@ -217,13 +217,13 @@
}
func (s *partitionSuite) TestInputErrors(c *C) {
- pv := &gadget.PositionedVolume{
+ pv := &gadget.LaidOutVolume{
Volume: &gadget.Volume{
Schema: "gpt",
},
Size: 3 * gadget.SizeMiB,
SectorSize: 512,
- PositionedStructure: []gadget.PositionedStructure{
+ LaidOutStructure: []gadget.LaidOutStructure{
{
VolumeStructure: &gadget.VolumeStructure{
Size: 2 * gadget.SizeMiB,
@@ -247,13 +247,13 @@
}
func (s *partitionSuite) TestCommandError(c *C) {
- pv := &gadget.PositionedVolume{
+ pv := &gadget.LaidOutVolume{
Volume: &gadget.Volume{
Schema: "gpt",
},
Size: 3 * gadget.SizeMiB,
SectorSize: 512,
- PositionedStructure: []gadget.PositionedStructure{
+ LaidOutStructure: []gadget.LaidOutStructure{
{
VolumeStructure: &gadget.VolumeStructure{
Size: 2 * gadget.SizeMiB,
diff -Nru snapd-2.40/gadget/position.go snapd-2.42.1/gadget/position.go
--- snapd-2.40/gadget/position.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/position.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,317 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2019 Canonical Ltd
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-package gadget
-
-import (
- "fmt"
- "os"
- "path/filepath"
- "sort"
-)
-
-// PositioningConstraints defines the constraints for positioning structures
-// within a volume
-type PositioningConstraints struct {
- // NonMBRStartOffset is the default start offset of non-MBR structure in
- // the volume.
- NonMBRStartOffset Size
- // SectorSize is the size of the sector to be used for calculations
- SectorSize Size
-}
-
-// PositionedVolume defines the size of a volume and positions of all the
-// structures within it
-type PositionedVolume struct {
- *Volume
- // Size is the total size of the volume
- Size Size
- // SectorSize sector size of the volume
- SectorSize Size
- // PositionedStructure are sorted in order of 'appearance' in the volume
- PositionedStructure []PositionedStructure
- // RootDir is the root directory for volume data
- RootDir string
-}
-
-// PositionedStructure describes a VolumeStructure that has been positioned
-// within the volume
-type PositionedStructure struct {
- *VolumeStructure
- // StartOffset defines the start offset of the structure within the
- // enclosing volume
- StartOffset Size
- // PositionedOffsetWrite is the resolved position of offset-write for
- // this structure element within the enclosing volume
- PositionedOffsetWrite *Size
- // Index of the structure definition in gadget YAML
- Index int
-
- // PositionedContent is a list of raw content included in this structure
- PositionedContent []PositionedContent
-}
-
-func (p PositionedStructure) String() string {
- return fmtIndexAndName(p.Index, p.Name)
-}
-
-type byStartOffset []PositionedStructure
-
-func (b byStartOffset) Len() int { return len(b) }
-func (b byStartOffset) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
-func (b byStartOffset) Less(i, j int) bool { return b[i].StartOffset < b[j].StartOffset }
-
-// PositionedContent describes raw content that has been positioned within the
-// encompassing structure
-type PositionedContent struct {
- *VolumeContent
-
- // StartOffset defines the start offset of this content image
- StartOffset Size
- // PositionedOffsetWrite is the resolved position of offset-write for
- // this content element within the enclosing volume
- PositionedOffsetWrite *Size
- // Size is the maximum size occupied by this image
- Size Size
- // Index of the content in structure declaration inside gadget YAML
- Index int
-}
-
-func (p PositionedContent) String() string {
- if p.Image != "" {
- return fmt.Sprintf("#%v (%q@%#x{%v})", p.Index, p.Image, p.StartOffset, p.Size)
- }
- return fmt.Sprintf("#%v (source:%q)", p.Index, p.Source)
-}
-
-// PositionVolume attempts to lay out the volume using constraints and returns a
-// fully positioned description of the resulting volume
-func PositionVolume(gadgetRootDir string, volume *Volume, constraints PositioningConstraints) (*PositionedVolume, error) {
- previousEnd := Size(0)
- farthestEnd := Size(0)
- fartherstOffsetWrite := Size(0)
- structures := make([]PositionedStructure, len(volume.Structure))
- structuresByName := make(map[string]*PositionedStructure, len(volume.Structure))
-
- if constraints.SectorSize == 0 {
- return nil, fmt.Errorf("cannot position volume, invalid constraints: sector size cannot be 0")
- }
-
- for idx, s := range volume.Structure {
- var start Size
- if s.Offset == nil {
- if s.EffectiveRole() != MBR && previousEnd < constraints.NonMBRStartOffset {
- start = constraints.NonMBRStartOffset
- } else {
- start = previousEnd
- }
- } else {
- start = *s.Offset
- }
-
- end := start + s.Size
- ps := PositionedStructure{
- VolumeStructure: &volume.Structure[idx],
- StartOffset: start,
- Index: idx,
- }
-
- if ps.EffectiveRole() != MBR {
- if s.Size%constraints.SectorSize != 0 {
- return nil, fmt.Errorf("cannot position volume, structure %v size is not a multiple of sector size %v",
- ps, constraints.SectorSize)
- }
- }
-
- if ps.Name != "" {
- structuresByName[ps.Name] = &ps
- }
-
- structures[idx] = ps
-
- if end > farthestEnd {
- farthestEnd = end
- }
- previousEnd = end
- }
-
- // sort by starting offset
- sort.Sort(byStartOffset(structures))
-
- previousEnd = Size(0)
- for idx, ps := range structures {
- if ps.StartOffset < previousEnd {
- return nil, fmt.Errorf("cannot position volume, structure %v overlaps with preceding structure %v", ps, structures[idx-1])
- }
- previousEnd = ps.StartOffset + ps.Size
-
- offsetWrite, err := resolveOffsetWrite(ps.OffsetWrite, structuresByName)
- if err != nil {
- return nil, fmt.Errorf("cannot resolve offset-write of structure %v: %v", ps, err)
- }
- structures[idx].PositionedOffsetWrite = offsetWrite
-
- if offsetWrite != nil && *offsetWrite > fartherstOffsetWrite {
- fartherstOffsetWrite = *offsetWrite
- }
-
- content, err := positionStructureContent(gadgetRootDir, &structures[idx], structuresByName)
- if err != nil {
- return nil, err
- }
-
- for _, c := range content {
- if c.PositionedOffsetWrite != nil && *c.PositionedOffsetWrite > fartherstOffsetWrite {
- fartherstOffsetWrite = *c.PositionedOffsetWrite
- }
- }
-
- structures[idx].PositionedContent = content
- }
-
- volumeSize := farthestEnd
- if fartherstOffsetWrite+SizeLBA48Pointer > farthestEnd {
- volumeSize = fartherstOffsetWrite + SizeLBA48Pointer
- }
-
- vol := &PositionedVolume{
- Volume: volume,
- Size: volumeSize,
- SectorSize: constraints.SectorSize,
- PositionedStructure: structures,
- RootDir: gadgetRootDir,
- }
- return vol, nil
-}
-
-type byContentStartOffset []PositionedContent
-
-func (b byContentStartOffset) Len() int { return len(b) }
-func (b byContentStartOffset) Swap(i, j int) { b[i], b[j] = b[j], b[i] }
-func (b byContentStartOffset) Less(i, j int) bool { return b[i].StartOffset < b[j].StartOffset }
-
-func getImageSize(path string) (Size, error) {
- stat, err := os.Stat(path)
- if err != nil {
- return 0, err
- }
- return Size(stat.Size()), nil
-}
-
-func positionStructureContent(gadgetRootDir string, ps *PositionedStructure, known map[string]*PositionedStructure) ([]PositionedContent, error) {
- if !ps.IsBare() {
- // structures with a filesystem do not need any extra
- // positioning
- return nil, nil
- }
- if len(ps.Content) == 0 {
- return nil, nil
- }
-
- content := make([]PositionedContent, len(ps.Content))
- previousEnd := Size(0)
-
- for idx, c := range ps.Content {
- imageSize, err := getImageSize(filepath.Join(gadgetRootDir, c.Image))
- if err != nil {
- return nil, fmt.Errorf("cannot position structure %v: content %q: %v", ps, c.Image, err)
- }
-
- var start Size
- if c.Offset != nil {
- start = *c.Offset
- } else {
- start = previousEnd
- }
-
- actualSize := imageSize
-
- if c.Size != 0 {
- if c.Size < imageSize {
- return nil, fmt.Errorf("cannot position structure %v: content %q size %v is larger than declared %v", ps, c.Image, actualSize, c.Size)
- }
- actualSize = c.Size
- }
-
- offsetWrite, err := resolveOffsetWrite(c.OffsetWrite, known)
- if err != nil {
- return nil, fmt.Errorf("cannot resolve offset-write of structure %v content %q: %v", ps, c.Image, err)
- }
-
- content[idx] = PositionedContent{
- VolumeContent: &ps.Content[idx],
- Size: actualSize,
- StartOffset: ps.StartOffset + start,
- Index: idx,
- // break for gofmt < 1.11
- PositionedOffsetWrite: offsetWrite,
- }
- previousEnd = start + actualSize
- if previousEnd > ps.Size {
- return nil, fmt.Errorf("cannot position structure %v: content %q does not fit in the structure", ps, c.Image)
- }
- }
-
- sort.Sort(byContentStartOffset(content))
-
- previousEnd = ps.StartOffset
- for idx, pc := range content {
- if pc.StartOffset < previousEnd {
- return nil, fmt.Errorf("cannot position structure %v: content %q overlaps with preceding image %q", ps, pc.Image, content[idx-1].Image)
- }
- previousEnd = pc.StartOffset + pc.Size
- }
-
- return content, nil
-}
-
-func resolveOffsetWrite(offsetWrite *RelativeOffset, knownStructs map[string]*PositionedStructure) (*Size, error) {
- if offsetWrite == nil {
- return nil, nil
- }
-
- var relativeToOffset Size
- if offsetWrite.RelativeTo != "" {
- otherStruct, ok := knownStructs[offsetWrite.RelativeTo]
- if !ok {
- return nil, fmt.Errorf("refers to an unknown structure %q", offsetWrite.RelativeTo)
- }
- relativeToOffset = otherStruct.StartOffset
- }
-
- resolvedOffsetWrite := relativeToOffset + offsetWrite.Offset
- return &resolvedOffsetWrite, nil
-}
-
-// ShiftStructureTo creates a new positioned structure, shifted to start at a
-// given offset. The start offsets of positioned content within the structure is
-// updated.
-func ShiftStructureTo(ps PositionedStructure, offset Size) PositionedStructure {
- change := int64(offset - ps.StartOffset)
-
- newPs := ps
- newPs.StartOffset = Size(int64(ps.StartOffset) + change)
-
- newPs.PositionedContent = make([]PositionedContent, len(ps.PositionedContent))
- for idx, pc := range ps.PositionedContent {
- newPc := pc
- newPc.StartOffset = Size(int64(pc.StartOffset) + change)
- newPs.PositionedContent[idx] = newPc
- }
- return newPs
-}
diff -Nru snapd-2.40/gadget/position_test.go snapd-2.42.1/gadget/position_test.go
--- snapd-2.40/gadget/position_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/position_test.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,1110 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2019 Canonical Ltd
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package gadget_test
-
-import (
- "bytes"
- "fmt"
- "io"
- "os"
- "path/filepath"
-
- . "gopkg.in/check.v1"
- "gopkg.in/yaml.v2"
-
- "github.com/snapcore/snapd/gadget"
-)
-
-type positioningTestSuite struct {
- dir string
-}
-
-var _ = Suite(&positioningTestSuite{})
-
-func (p *positioningTestSuite) SetUpTest(c *C) {
- p.dir = c.MkDir()
-}
-
-var defaultConstraints = gadget.PositioningConstraints{
- NonMBRStartOffset: 1 * gadget.SizeMiB,
- SectorSize: 512,
-}
-
-func (p *positioningTestSuite) TestVolumeSize(c *C) {
- vol := gadget.Volume{
- Structure: []gadget.VolumeStructure{
- {Size: 2 * gadget.SizeMiB},
- },
- }
- v, err := gadget.PositionVolume(p.dir, &vol, defaultConstraints)
- c.Assert(err, IsNil)
-
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: &gadget.Volume{
- Structure: []gadget.VolumeStructure{
- {Size: 2 * gadget.SizeMiB},
- },
- },
- Size: 3 * gadget.SizeMiB,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {VolumeStructure: &gadget.VolumeStructure{Size: 2 * gadget.SizeMiB}, StartOffset: 1 * gadget.SizeMiB},
- },
- })
-}
-
-func mustParseVolume(c *C, gadgetYaml, volume string) *gadget.Volume {
- var gi gadget.Info
- err := yaml.Unmarshal([]byte(gadgetYaml), &gi)
- c.Assert(err, IsNil)
- v, ok := gi.Volumes[volume]
- c.Assert(ok, Equals, true, Commentf("volume %q not found in gadget", volume))
- err = gadget.ValidateVolume("foo", &v)
- c.Assert(err, IsNil)
- return &v
-}
-
-func (p *positioningTestSuite) TestVolumePositionMinimal(c *C) {
- gadgetYaml := `
-volumes:
- first-image:
- bootloader: u-boot
- structure:
- - type: 00000000-0000-0000-0000-0000deadbeef
- size: 400M
- - type: 83,00000000-0000-0000-0000-0000feedface
- role: system-data
- size: 100M
-`
- vol := mustParseVolume(c, gadgetYaml, "first-image")
- c.Assert(vol.Structure, HasLen, 2)
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
-
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 501 * gadget.SizeMiB,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- VolumeStructure: &vol.Structure[0],
- StartOffset: 1 * gadget.SizeMiB,
- Index: 0,
- },
- {
- VolumeStructure: &vol.Structure[1],
- StartOffset: 401 * gadget.SizeMiB,
- Index: 1,
- },
- },
- })
-}
-
-func (p *positioningTestSuite) TestVolumePositionImplicitOrdering(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - type: 00000000-0000-0000-0000-dd00deadbeef
- size: 400M
- - type: 00000000-0000-0000-0000-cc00deadbeef
- role: system-data
- size: 500M
- - type: 00000000-0000-0000-0000-bb00deadbeef
- size: 100M
- - type: 00000000-0000-0000-0000-aa00deadbeef
- size: 100M
-`
- vol := mustParseVolume(c, gadgetYaml, "first")
- c.Assert(vol.Structure, HasLen, 4)
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
-
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 1101 * gadget.SizeMiB,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- VolumeStructure: &vol.Structure[0],
- StartOffset: 1 * gadget.SizeMiB,
- Index: 0,
- },
- {
- VolumeStructure: &vol.Structure[1],
- StartOffset: 401 * gadget.SizeMiB,
- Index: 1,
- },
- {
- VolumeStructure: &vol.Structure[2],
- StartOffset: 901 * gadget.SizeMiB,
- Index: 2,
- },
- {
- VolumeStructure: &vol.Structure[3],
- StartOffset: 1001 * gadget.SizeMiB,
- Index: 3,
- },
- },
- })
-}
-
-func (p *positioningTestSuite) TestVolumePositionExplicitOrdering(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - type: 00000000-0000-0000-0000-dd00deadbeef
- size: 400M
- offset: 800M
- - type: 00000000-0000-0000-0000-cc00deadbeef
- role: system-data
- size: 500M
- offset: 200M
- - type: 00000000-0000-0000-0000-bb00deadbeef
- size: 100M
- offset: 1200M
- - type: 00000000-0000-0000-0000-aa00deadbeef
- size: 100M
- offset: 1M
-`
- vol := mustParseVolume(c, gadgetYaml, "first")
- c.Assert(vol.Structure, HasLen, 4)
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
-
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 1300 * gadget.SizeMiB,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- VolumeStructure: &vol.Structure[3],
- StartOffset: 1 * gadget.SizeMiB,
- Index: 3,
- },
- {
- VolumeStructure: &vol.Structure[1],
- StartOffset: 200 * gadget.SizeMiB,
- Index: 1,
- },
- {
- VolumeStructure: &vol.Structure[0],
- StartOffset: 800 * gadget.SizeMiB,
- Index: 0,
- },
- {
- VolumeStructure: &vol.Structure[2],
- StartOffset: 1200 * gadget.SizeMiB,
- Index: 2,
- },
- },
- })
-}
-
-func (p *positioningTestSuite) TestVolumePositionMixedOrdering(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - type: 00000000-0000-0000-0000-dd00deadbeef
- size: 400M
- offset: 800M
- - type: 00000000-0000-0000-0000-cc00deadbeef
- role: system-data
- size: 500M
- offset: 200M
- - type: 00000000-0000-0000-0000-bb00deadbeef
- size: 100M
- - type: 00000000-0000-0000-0000-aa00deadbeef
- size: 100M
- offset: 1M
-`
- vol := mustParseVolume(c, gadgetYaml, "first")
- c.Assert(vol.Structure, HasLen, 4)
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
-
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 1200 * gadget.SizeMiB,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- VolumeStructure: &vol.Structure[3],
- StartOffset: 1 * gadget.SizeMiB,
- Index: 3,
- },
- {
- VolumeStructure: &vol.Structure[1],
- StartOffset: 200 * gadget.SizeMiB,
- Index: 1,
- },
- {
- VolumeStructure: &vol.Structure[2],
- StartOffset: 700 * gadget.SizeMiB,
- Index: 2,
- },
- {
- VolumeStructure: &vol.Structure[0],
- StartOffset: 800 * gadget.SizeMiB,
- Index: 0,
- },
- },
- })
-}
-
-func (p *positioningTestSuite) TestVolumePositionErrorsContentNoSuchFile(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - type: 00000000-0000-0000-0000-dd00deadbeef
- size: 400M
- offset: 800M
- content:
- - image: foo.img
-`
- vol := mustParseVolume(c, gadgetYaml, "first")
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(v, IsNil)
- c.Assert(err, ErrorMatches, `cannot position structure #0: content "foo.img":.*no such file or directory`)
-}
-
-func makeSizedFile(c *C, path string, size gadget.Size, content []byte) {
- err := os.MkdirAll(filepath.Dir(path), 0755)
- c.Assert(err, IsNil)
-
- f, err := os.Create(path)
- c.Assert(err, IsNil)
- defer f.Close()
- if size != 0 {
- err = f.Truncate(int64(size))
- c.Assert(err, IsNil)
- }
- if content != nil {
- _, err := io.Copy(f, bytes.NewReader(content))
- c.Assert(err, IsNil)
- }
-}
-
-func (p *positioningTestSuite) TestVolumePositionErrorsContentTooLargeSingle(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - type: 00000000-0000-0000-0000-dd00deadbeef
- size: 1M
- content:
- - image: foo.img
-`
- makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB+1, nil)
-
- vol := mustParseVolume(c, gadgetYaml, "first")
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(v, IsNil)
- c.Assert(err, ErrorMatches, `cannot position structure #0: content "foo.img" does not fit in the structure`)
-}
-
-func (p *positioningTestSuite) TestVolumePositionErrorsContentTooLargeMany(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - type: 00000000-0000-0000-0000-dd00deadbeef
- size: 2M
- content:
- - image: foo.img
- - image: bar.img
-`
- makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB+1, nil)
- makeSizedFile(c, filepath.Join(p.dir, "bar.img"), gadget.SizeMiB+1, nil)
-
- vol := mustParseVolume(c, gadgetYaml, "first")
-
- constraints := gadget.PositioningConstraints{
- NonMBRStartOffset: 1 * gadget.SizeMiB,
- SectorSize: 512,
- }
- v, err := gadget.PositionVolume(p.dir, vol, constraints)
- c.Assert(v, IsNil)
- c.Assert(err, ErrorMatches, `cannot position structure #0: content "bar.img" does not fit in the structure`)
-}
-
-func (p *positioningTestSuite) TestVolumePositionErrorsContentTooLargeWithOffset(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - type: 00000000-0000-0000-0000-dd00deadbeef
- size: 1M
- content:
- - image: foo.img
- # 512kB
- offset: 524288
-`
- makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB, nil)
-
- vol := mustParseVolume(c, gadgetYaml, "first")
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(v, IsNil)
- c.Assert(err, ErrorMatches, `cannot position structure #0: content "foo.img" does not fit in the structure`)
-}
-
-func (p *positioningTestSuite) TestVolumePositionErrorsContentLargerThanDeclared(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - type: 00000000-0000-0000-0000-dd00deadbeef
- size: 2M
- content:
- - image: foo.img
- size: 1M
-`
- makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB+1, nil)
-
- vol := mustParseVolume(c, gadgetYaml, "first")
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(v, IsNil)
- c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot position structure #0: content "foo.img" size %v is larger than declared %v`, gadget.SizeMiB+1, gadget.SizeMiB))
-}
-
-func (p *positioningTestSuite) TestVolumePositionErrorsContentOverlap(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - type: 00000000-0000-0000-0000-dd00deadbeef
- size: 2M
- content:
- - image: foo.img
- size: 1M
- # 512kB
- offset: 524288
- - image: bar.img
- size: 1M
- offset: 0
-`
- makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB, nil)
- makeSizedFile(c, filepath.Join(p.dir, "bar.img"), gadget.SizeMiB, nil)
-
- vol := mustParseVolume(c, gadgetYaml, "first")
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(v, IsNil)
- c.Assert(err, ErrorMatches, `cannot position structure #0: content "foo.img" overlaps with preceding image "bar.img"`)
-}
-
-func (p *positioningTestSuite) TestVolumePositionContentExplicitOrder(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - type: 00000000-0000-0000-0000-0000deadbeef
- size: 2M
- content:
- - image: foo.img
- size: 1M
- offset: 1M
- - image: bar.img
- size: 1M
- offset: 0
-`
- makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB, nil)
- makeSizedFile(c, filepath.Join(p.dir, "bar.img"), gadget.SizeMiB, nil)
-
- vol := mustParseVolume(c, gadgetYaml, "first")
- c.Assert(vol.Structure, HasLen, 1)
- c.Assert(vol.Structure[0].Content, HasLen, 2)
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 3 * gadget.SizeMiB,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- VolumeStructure: &vol.Structure[0],
- StartOffset: 1 * gadget.SizeMiB,
- PositionedContent: []gadget.PositionedContent{
- {
- VolumeContent: &vol.Structure[0].Content[1],
- StartOffset: 1 * gadget.SizeMiB,
- Size: gadget.SizeMiB,
- Index: 1,
- },
- {
- VolumeContent: &vol.Structure[0].Content[0],
- StartOffset: 2 * gadget.SizeMiB,
- Size: gadget.SizeMiB,
- Index: 0,
- },
- },
- },
- },
- })
-}
-
-func (p *positioningTestSuite) TestVolumePositionContentImplicitOrder(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - type: 00000000-0000-0000-0000-0000deadbeef
- size: 2M
- content:
- - image: foo.img
- size: 1M
- - image: bar.img
- size: 1M
-`
- makeSizedFile(c, filepath.Join(p.dir, "foo.img"), gadget.SizeMiB, nil)
- makeSizedFile(c, filepath.Join(p.dir, "bar.img"), gadget.SizeMiB, nil)
-
- vol := mustParseVolume(c, gadgetYaml, "first")
- c.Assert(vol.Structure, HasLen, 1)
- c.Assert(vol.Structure[0].Content, HasLen, 2)
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 3 * gadget.SizeMiB,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- VolumeStructure: &vol.Structure[0],
- StartOffset: 1 * gadget.SizeMiB,
- PositionedContent: []gadget.PositionedContent{
- {
- VolumeContent: &vol.Structure[0].Content[0],
- StartOffset: 1 * gadget.SizeMiB,
- Size: gadget.SizeMiB,
- Index: 0,
- },
- {
- VolumeContent: &vol.Structure[0].Content[1],
- StartOffset: 2 * gadget.SizeMiB,
- Size: gadget.SizeMiB,
- Index: 1,
- },
- },
- },
- },
- })
-}
-
-func (p *positioningTestSuite) TestVolumePositionContentImplicitSize(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - type: 00000000-0000-0000-0000-0000deadbeef
- size: 2M
- content:
- - image: foo.img
-`
- size1_5MiB := gadget.SizeMiB + gadget.SizeMiB/2
- makeSizedFile(c, filepath.Join(p.dir, "foo.img"), size1_5MiB, nil)
-
- vol := mustParseVolume(c, gadgetYaml, "first")
- c.Assert(vol.Structure, HasLen, 1)
- c.Assert(vol.Structure[0].Content, HasLen, 1)
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 3 * gadget.SizeMiB,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- VolumeStructure: &vol.Structure[0],
- StartOffset: 1 * gadget.SizeMiB,
- PositionedContent: []gadget.PositionedContent{
- {
- VolumeContent: &vol.Structure[0].Content[0],
- StartOffset: 1 * gadget.SizeMiB,
- Size: size1_5MiB,
- },
- },
- },
- },
- })
-}
-
-func (p *positioningTestSuite) TestVolumePositionContentNonBare(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - type: 00000000-0000-0000-0000-0000deadbeef
- filesystem: ext4
- size: 2M
- content:
- - source: foo.txt
- target: /boot
-`
- makeSizedFile(c, filepath.Join(p.dir, "foo.txt"), 0, []byte("foobar\n"))
-
- vol := mustParseVolume(c, gadgetYaml, "first")
- c.Assert(vol.Structure, HasLen, 1)
- c.Assert(vol.Structure[0].Content, HasLen, 1)
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 3 * gadget.SizeMiB,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- VolumeStructure: &vol.Structure[0],
- StartOffset: 1 * gadget.SizeMiB,
- },
- },
- })
-}
-
-func (p *positioningTestSuite) TestVolumePositionConstraintsChange(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - role: mbr
- type: bare
- size: 446
- offset: 0
- - type: 00000000-0000-0000-0000-0000deadbeef
- filesystem: ext4
- size: 2M
- content:
- - source: foo.txt
- target: /boot
-`
- makeSizedFile(c, filepath.Join(p.dir, "foo.txt"), 0, []byte("foobar\n"))
-
- vol := mustParseVolume(c, gadgetYaml, "first")
- c.Assert(vol.Structure, HasLen, 2)
- c.Assert(vol.Structure[1].Content, HasLen, 1)
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 3 * gadget.SizeMiB,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- VolumeStructure: &vol.Structure[0],
- StartOffset: 0,
- Index: 0,
- },
- {
- VolumeStructure: &vol.Structure[1],
- StartOffset: 1 * gadget.SizeMiB,
- Index: 1,
- },
- },
- })
-
- // still valid
- constraints := gadget.PositioningConstraints{
- // 512kiB
- NonMBRStartOffset: 512 * gadget.SizeKiB,
- SectorSize: 512,
- }
- v, err = gadget.PositionVolume(p.dir, vol, constraints)
- c.Assert(err, IsNil)
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 2*gadget.SizeMiB + 512*gadget.SizeKiB,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- VolumeStructure: &vol.Structure[0],
- StartOffset: 0,
- Index: 0,
- },
- {
- VolumeStructure: &vol.Structure[1],
- StartOffset: 512 * gadget.SizeKiB,
- Index: 1,
- },
- },
- })
-
- // constraints would make a non MBR structure overlap with MBR, but
- // structures start one after another unless offset is specified
- // explicitly
- constraintsBad := gadget.PositioningConstraints{
- NonMBRStartOffset: 400,
- SectorSize: 512,
- }
- v, err = gadget.PositionVolume(p.dir, vol, constraintsBad)
- c.Assert(err, IsNil)
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 2*gadget.SizeMiB + 446,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- VolumeStructure: &vol.Structure[0],
- Index: 0,
- },
- {
- VolumeStructure: &vol.Structure[1],
- StartOffset: 446,
- Index: 1,
- },
- },
- })
-
- // sector size is properly recorded
- constraintsSector := gadget.PositioningConstraints{
- NonMBRStartOffset: 1 * gadget.SizeMiB,
- SectorSize: 1024,
- }
- v, err = gadget.PositionVolume(p.dir, vol, constraintsSector)
- c.Assert(err, IsNil)
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 3 * gadget.SizeMiB,
- SectorSize: 1024,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- VolumeStructure: &vol.Structure[0],
- Index: 0,
- },
- {
- VolumeStructure: &vol.Structure[1],
- StartOffset: 1 * gadget.SizeMiB,
- Index: 1,
- },
- },
- })
-}
-
-func (p *positioningTestSuite) TestVolumePositionConstraintsSectorSize(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - role: mbr
- type: bare
- size: 446
- offset: 0
- - type: 00000000-0000-0000-0000-0000deadbeef
- filesystem: ext4
- size: 2M
- content:
- - source: foo.txt
- target: /boot
-`
- makeSizedFile(c, filepath.Join(p.dir, "foo.txt"), 0, []byte("foobar\n"))
-
- vol := mustParseVolume(c, gadgetYaml, "first")
-
- constraintsBadSectorSize := gadget.PositioningConstraints{
- NonMBRStartOffset: 1 * gadget.SizeMiB,
- SectorSize: 384,
- }
- _, err := gadget.PositionVolume(p.dir, vol, constraintsBadSectorSize)
- c.Assert(err, ErrorMatches, "cannot position volume, structure #1 size is not a multiple of sector size 384")
-}
-
-func (p *positioningTestSuite) TestVolumePositionConstraintsNeedsSectorSize(c *C) {
- constraintsBadSectorSize := gadget.PositioningConstraints{
- NonMBRStartOffset: 1 * gadget.SizeMiB,
- // SectorSize left unspecified
- }
- _, err := gadget.PositionVolume(p.dir, &gadget.Volume{}, constraintsBadSectorSize)
- c.Assert(err, ErrorMatches, "cannot position volume, invalid constraints: sector size cannot be 0")
-}
-
-func (p *positioningTestSuite) TestVolumePositionMBRImplicitConstraints(c *C) {
- gadgetYaml := `
-volumes:
- first:
- schema: gpt
- bootloader: grub
- structure:
- - name: mbr
- type: bare
- role: mbr
- size: 446
- - name: other
- type: 00000000-0000-0000-0000-0000deadbeef
- size: 1M
-`
- vol := mustParseVolume(c, gadgetYaml, "first")
- c.Assert(vol.Structure, HasLen, 2)
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 2 * gadget.SizeMiB,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- // MBR
- VolumeStructure: &vol.Structure[0],
- StartOffset: 0,
- Index: 0,
- }, {
- VolumeStructure: &vol.Structure[1],
- StartOffset: 1 * gadget.SizeMiB,
- Index: 1,
- },
- },
- })
-}
-
-func (p *positioningTestSuite) TestVolumePositionOffsetWriteAll(c *C) {
- var gadgetYaml = `
-volumes:
- pc:
- bootloader: grub
- structure:
- - name: mbr
- type: mbr
- size: 440
- - name: foo
- type: DA,21686148-6449-6E6F-744E-656564454649
- size: 1M
- offset: 1M
- offset-write: mbr+92
- content:
- - image: foo.img
- offset-write: bar+10
- - name: bar
- type: DA,21686148-6449-6E6F-744E-656564454649
- size: 1M
- offset-write: 600
- content:
- - image: bar.img
- offset-write: 450
-`
- makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*gadget.SizeKiB, []byte(""))
- makeSizedFile(c, filepath.Join(p.dir, "bar.img"), 150*gadget.SizeKiB, []byte(""))
-
- vol := mustParseVolume(c, gadgetYaml, "pc")
- c.Assert(vol.Structure, HasLen, 3)
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
- c.Assert(v, DeepEquals, &gadget.PositionedVolume{
- Volume: vol,
- Size: 3 * gadget.SizeMiB,
- SectorSize: 512,
- RootDir: p.dir,
- PositionedStructure: []gadget.PositionedStructure{
- {
- // mbr
- VolumeStructure: &vol.Structure[0],
- StartOffset: 0,
- Index: 0,
- }, {
- // foo
- VolumeStructure: &vol.Structure[1],
- StartOffset: 1 * gadget.SizeMiB,
- Index: 1,
- // break for gofmt < 1.11
- PositionedOffsetWrite: asSizePtr(92),
- PositionedContent: []gadget.PositionedContent{
- {
- VolumeContent: &vol.Structure[1].Content[0],
- Size: 200 * gadget.SizeKiB,
- StartOffset: 1 * gadget.SizeMiB,
- // offset-write: bar+10
- PositionedOffsetWrite: asSizePtr(2*gadget.SizeMiB + 10),
- },
- },
- }, {
- // bar
- VolumeStructure: &vol.Structure[2],
- StartOffset: 2 * gadget.SizeMiB,
- Index: 2,
- // break for gofmt < 1.11
- PositionedOffsetWrite: asSizePtr(600),
- PositionedContent: []gadget.PositionedContent{
- {
- VolumeContent: &vol.Structure[2].Content[0],
- Size: 150 * gadget.SizeKiB,
- StartOffset: 2 * gadget.SizeMiB,
- // offset-write: bar+10
- PositionedOffsetWrite: asSizePtr(450),
- },
- },
- },
- },
- })
-}
-
-func (p *positioningTestSuite) TestVolumePositionOffsetWriteBadRelativeTo(c *C) {
- // define volumes explicitly as those would not pass validation
- volBadStructure := gadget.Volume{
- Structure: []gadget.VolumeStructure{
- {
- Name: "foo",
- Type: "DA,21686148-6449-6E6F-744E-656564454649",
- Size: 1 * gadget.SizeMiB,
- OffsetWrite: &gadget.RelativeOffset{
- RelativeTo: "bar",
- Offset: 10,
- },
- },
- },
- }
- volBadContent := gadget.Volume{
- Structure: []gadget.VolumeStructure{
- {
- Name: "foo",
- Type: "DA,21686148-6449-6E6F-744E-656564454649",
- Size: 1 * gadget.SizeMiB,
- Content: []gadget.VolumeContent{
- {
- Image: "foo.img",
- OffsetWrite: &gadget.RelativeOffset{
- RelativeTo: "bar",
- Offset: 10,
- },
- },
- },
- },
- },
- }
-
- makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*gadget.SizeKiB, []byte(""))
-
- v, err := gadget.PositionVolume(p.dir, &volBadStructure, defaultConstraints)
- c.Check(v, IsNil)
- c.Check(err, ErrorMatches, `cannot resolve offset-write of structure #0 \("foo"\): refers to an unknown structure "bar"`)
-
- v, err = gadget.PositionVolume(p.dir, &volBadContent, defaultConstraints)
- c.Check(v, IsNil)
- c.Check(err, ErrorMatches, `cannot resolve offset-write of structure #0 \("foo"\) content "foo.img": refers to an unknown structure "bar"`)
-}
-
-func (p *positioningTestSuite) TestVolumePositionOffsetWriteEnlargesVolume(c *C) {
- var gadgetYamlStructure = `
-volumes:
- pc:
- bootloader: grub
- structure:
- - name: mbr
- type: mbr
- size: 440
- - name: foo
- type: DA,21686148-6449-6E6F-744E-656564454649
- size: 1M
- offset: 1M
- # 1GB
- offset-write: mbr+1073741824
-
-`
- vol := mustParseVolume(c, gadgetYamlStructure, "pc")
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
- // offset-write is at 1GB
- c.Check(v.Size, Equals, 1*gadget.SizeGiB+gadget.SizeLBA48Pointer)
-
- var gadgetYamlContent = `
-volumes:
- pc:
- bootloader: grub
- structure:
- - name: mbr
- type: mbr
- size: 440
- - name: foo
- type: DA,21686148-6449-6E6F-744E-656564454649
- size: 1M
- offset: 1M
- content:
- - image: foo.img
- # 2GB
- offset-write: mbr+2147483648
- - image: bar.img
- # 1GB
- offset-write: mbr+1073741824
- - image: baz.img
- # 3GB
- offset-write: mbr+3221225472
-
-`
- makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*gadget.SizeKiB, []byte(""))
- makeSizedFile(c, filepath.Join(p.dir, "bar.img"), 150*gadget.SizeKiB, []byte(""))
- makeSizedFile(c, filepath.Join(p.dir, "baz.img"), 100*gadget.SizeKiB, []byte(""))
-
- vol = mustParseVolume(c, gadgetYamlContent, "pc")
-
- v, err = gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
- // foo.img offset-write is at 3GB
- c.Check(v.Size, Equals, 3*gadget.SizeGiB+gadget.SizeLBA48Pointer)
-}
-
-func (p *positioningTestSuite) TestPositionedStructureShift(c *C) {
- var gadgetYamlContent = `
-volumes:
- pc:
- bootloader: grub
- structure:
- - name: foo
- type: DA,21686148-6449-6E6F-744E-656564454649
- size: 1M
- offset: 1M
- content:
- - image: foo.img
- - image: bar.img
- # 300KB
- offset: 307200
-
-`
- makeSizedFile(c, filepath.Join(p.dir, "foo.img"), 200*gadget.SizeKiB, []byte(""))
- makeSizedFile(c, filepath.Join(p.dir, "bar.img"), 150*gadget.SizeKiB, []byte(""))
-
- vol := mustParseVolume(c, gadgetYamlContent, "pc")
-
- v, err := gadget.PositionVolume(p.dir, vol, defaultConstraints)
- c.Assert(err, IsNil)
- c.Assert(v.PositionedStructure, HasLen, 1)
- c.Assert(v.PositionedStructure[0].PositionedContent, HasLen, 2)
-
- ps := v.PositionedStructure[0]
-
- c.Assert(ps, DeepEquals, gadget.PositionedStructure{
- // foo
- VolumeStructure: &vol.Structure[0],
- StartOffset: 1 * gadget.SizeMiB,
- Index: 0,
- PositionedContent: []gadget.PositionedContent{
- {
- VolumeContent: &vol.Structure[0].Content[0],
- Size: 200 * gadget.SizeKiB,
- StartOffset: 1 * gadget.SizeMiB,
- Index: 0,
- }, {
- VolumeContent: &vol.Structure[0].Content[1],
- Size: 150 * gadget.SizeKiB,
- StartOffset: 1*gadget.SizeMiB + 300*gadget.SizeKiB,
- Index: 1,
- },
- },
- })
-
- shiftedTo0 := gadget.ShiftStructureTo(ps, 0)
- c.Assert(shiftedTo0, DeepEquals, gadget.PositionedStructure{
- // foo
- VolumeStructure: &vol.Structure[0],
- StartOffset: 0,
- Index: 0,
- PositionedContent: []gadget.PositionedContent{
- {
- VolumeContent: &vol.Structure[0].Content[0],
- Size: 200 * gadget.SizeKiB,
- StartOffset: 0,
- Index: 0,
- }, {
- VolumeContent: &vol.Structure[0].Content[1],
- Size: 150 * gadget.SizeKiB,
- StartOffset: 300 * gadget.SizeKiB,
- Index: 1,
- },
- },
- })
-
- shiftedTo2M := gadget.ShiftStructureTo(ps, 2*gadget.SizeMiB)
- c.Assert(shiftedTo2M, DeepEquals, gadget.PositionedStructure{
- // foo
- VolumeStructure: &vol.Structure[0],
- StartOffset: 2 * gadget.SizeMiB,
- Index: 0,
- PositionedContent: []gadget.PositionedContent{
- {
- VolumeContent: &vol.Structure[0].Content[0],
- Size: 200 * gadget.SizeKiB,
- StartOffset: 2 * gadget.SizeMiB,
- Index: 0,
- }, {
- VolumeContent: &vol.Structure[0].Content[1],
- Size: 150 * gadget.SizeKiB,
- StartOffset: 2*gadget.SizeMiB + 300*gadget.SizeKiB,
- Index: 1,
- },
- },
- })
-}
diff -Nru snapd-2.40/gadget/raw.go snapd-2.42.1/gadget/raw.go
--- snapd-2.40/gadget/raw.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/raw.go 2019-10-30 12:17:43.000000000 +0000
@@ -33,14 +33,14 @@
// RawStructureWriter implements support for writing raw (bare) structures.
type RawStructureWriter struct {
contentDir string
- ps *PositionedStructure
+ ps *LaidOutStructure
}
// NewRawStructureWriter returns a writer for the given structure, that will load
// the structure content data from the provided gadget content directory.
-func NewRawStructureWriter(contentDir string, ps *PositionedStructure) (*RawStructureWriter, error) {
+func NewRawStructureWriter(contentDir string, ps *LaidOutStructure) (*RawStructureWriter, error) {
if ps == nil {
- return nil, fmt.Errorf("internal error: *PositionedStructure is nil")
+ return nil, fmt.Errorf("internal error: *LaidOutStructure is nil")
}
if !ps.IsBare() {
return nil, fmt.Errorf("internal error: structure %s is not bare", ps)
@@ -56,9 +56,9 @@
}
// writeRawStream writes the input stream in that corresponds to provided
-// positioned content. The number of bytes read from input stream must match
-// exactly the declared size of positioned content entry.
-func writeRawStream(out io.WriteSeeker, pc *PositionedContent, in io.Reader) error {
+// laid out content. The number of bytes read from input stream must match
+// exactly the declared size of the content entry.
+func writeRawStream(out io.WriteSeeker, pc *LaidOutContent, in io.Reader) error {
if _, err := out.Seek(int64(pc.StartOffset), io.SeekStart); err != nil {
return fmt.Errorf("cannot seek to content start offset 0x%x: %v", pc.StartOffset, err)
}
@@ -70,8 +70,8 @@
return nil
}
-// writeRawImage writes a single image described by a positioned content entry.
-func (r *RawStructureWriter) writeRawImage(out io.WriteSeeker, pc *PositionedContent) error {
+// writeRawImage writes a single image described by a laid out content entry.
+func (r *RawStructureWriter) writeRawImage(out io.WriteSeeker, pc *LaidOutContent) error {
if pc.Image == "" {
return fmt.Errorf("internal error: no image defined")
}
@@ -86,7 +86,7 @@
// Write will write whole contents of a structure into the output stream.
func (r *RawStructureWriter) Write(out io.WriteSeeker) error {
- for _, pc := range r.ps.PositionedContent {
+ for _, pc := range r.ps.LaidOutContent {
if err := r.writeRawImage(out, &pc); err != nil {
return fmt.Errorf("failed to write image %v: %v", pc, err)
}
@@ -98,16 +98,16 @@
type RawStructureUpdater struct {
*RawStructureWriter
backupDir string
- deviceLookup locationLookupFunc
+ deviceLookup deviceLookupFunc
}
-type locationLookupFunc func(ps *PositionedStructure) (string, error)
+type deviceLookupFunc func(ps *LaidOutStructure) (device string, offs Size, err error)
// NewRawStructureUpdater returns an updater for the given raw (bare) structure.
// Update data will be loaded from the provided gadget content directory.
// Backups of replaced structures are temporarily kept in the rollback
// directory.
-func NewRawStructureUpdater(contentDir string, ps *PositionedStructure, backupDir string, deviceLookup locationLookupFunc) (*RawStructureUpdater, error) {
+func NewRawStructureUpdater(contentDir string, ps *LaidOutStructure, backupDir string, deviceLookup deviceLookupFunc) (*RawStructureUpdater, error) {
if deviceLookup == nil {
return nil, fmt.Errorf("internal error: device lookup helper must be provided")
}
@@ -127,11 +127,11 @@
return ru, nil
}
-func rawContentBackupPath(backupDir string, ps *PositionedStructure, pc *PositionedContent) string {
+func rawContentBackupPath(backupDir string, ps *LaidOutStructure, pc *LaidOutContent) string {
return filepath.Join(backupDir, fmt.Sprintf("struct-%v-%v", ps.Index, pc.Index))
}
-func (r *RawStructureUpdater) backupOrCheckpointContent(disk io.ReadSeeker, pc *PositionedContent) error {
+func (r *RawStructureUpdater) backupOrCheckpointContent(disk io.ReadSeeker, pc *LaidOutContent) error {
backupPath := rawContentBackupPath(r.backupDir, r.ps, pc)
backupName := backupPath + ".backup"
sameName := backupPath + ".same"
@@ -189,6 +189,23 @@
return nil
}
+// matchDevice identifies the device matching the configured structure, returns
+// device path and a shifted structure should any offset adjustments be needed
+func (r *RawStructureUpdater) matchDevice() (device string, shifted *LaidOutStructure, err error) {
+ device, offs, err := r.deviceLookup(r.ps)
+ if err != nil {
+ return "", nil, fmt.Errorf("cannot find device matching structure %v: %v", r.ps, err)
+ }
+
+ if offs == r.ps.StartOffset {
+ return device, r.ps, nil
+ }
+
+ // Structure starts at different offset, make the necessary adjustment.
+ structForDevice := ShiftStructureTo(*r.ps, offs)
+ return device, &structForDevice, nil
+}
+
// Backup attempts to analyze and prepare a backup copy of data that will be
// replaced during subsequent update. Backups are kept in the backup directory
// passed to NewRawStructureUpdater(). Each region replaced by new content is
@@ -196,17 +213,18 @@
// and backup of each region is checkpointed. Regions that have been backed up
// or determined to be identical will not be analyzed on subsequent calls.
func (r *RawStructureUpdater) Backup() error {
- device, err := r.deviceLookup(r.ps)
+ device, structForDevice, err := r.matchDevice()
if err != nil {
- return fmt.Errorf("cannot find device matching structure %v: %v", r.ps, err)
+ return err
}
+
disk, err := os.OpenFile(device, os.O_RDONLY, 0)
if err != nil {
return fmt.Errorf("cannot open device for reading: %v", err)
}
defer disk.Close()
- for _, pc := range r.ps.PositionedContent {
+ for _, pc := range structForDevice.LaidOutContent {
if err := r.backupOrCheckpointContent(disk, &pc); err != nil {
return fmt.Errorf("cannot backup image %v: %v", pc, err)
}
@@ -215,7 +233,7 @@
return nil
}
-func (r *RawStructureUpdater) rollbackDifferent(out io.WriteSeeker, pc *PositionedContent) error {
+func (r *RawStructureUpdater) rollbackDifferent(out io.WriteSeeker, pc *LaidOutContent) error {
backupPath := rawContentBackupPath(r.backupDir, r.ps, pc)
if osutil.FileExists(backupPath + ".same") {
@@ -237,9 +255,9 @@
// Rollback attempts to restore original content from the backup copies prepared during Backup().
func (r *RawStructureUpdater) Rollback() error {
- device, err := r.deviceLookup(r.ps)
+ device, structForDevice, err := r.matchDevice()
if err != nil {
- return fmt.Errorf("cannot find device matching structure %v: %v", r.ps, err)
+ return err
}
disk, err := os.OpenFile(device, os.O_WRONLY, 0)
@@ -248,7 +266,7 @@
}
defer disk.Close()
- for _, pc := range r.ps.PositionedContent {
+ for _, pc := range structForDevice.LaidOutContent {
if err := r.rollbackDifferent(disk, &pc); err != nil {
return fmt.Errorf("cannot rollback image %v: %v", pc, err)
}
@@ -257,7 +275,7 @@
return nil
}
-func (r *RawStructureUpdater) updateDifferent(disk io.WriteSeeker, pc *PositionedContent) error {
+func (r *RawStructureUpdater) updateDifferent(disk io.WriteSeeker, pc *LaidOutContent) error {
backupPath := rawContentBackupPath(r.backupDir, r.ps, pc)
if osutil.FileExists(backupPath + ".same") {
@@ -281,9 +299,9 @@
// Update attempts to update the structure. The structure must have been
// analyzed and backed up by a prior Backup() call.
func (r *RawStructureUpdater) Update() error {
- device, err := r.deviceLookup(r.ps)
+ device, structForDevice, err := r.matchDevice()
if err != nil {
- return fmt.Errorf("cannot find device matching structure %v: %v", r.ps, err)
+ return err
}
disk, err := os.OpenFile(device, os.O_WRONLY, 0)
@@ -292,7 +310,7 @@
}
defer disk.Close()
- for _, pc := range r.ps.PositionedContent {
+ for _, pc := range structForDevice.LaidOutContent {
if err := r.updateDifferent(disk, &pc); err != nil {
return fmt.Errorf("cannot update image %v: %v", pc, err)
}
diff -Nru snapd-2.40/gadget/raw_test.go snapd-2.42.1/gadget/raw_test.go
--- snapd-2.40/gadget/raw_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/raw_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -75,11 +75,11 @@
makeSizedFile(c, filepath.Join(r.dir, "foo.img"), 128, []byte("foo foo foo"))
makeSizedFile(c, filepath.Join(r.dir, "bar.img"), 128, []byte("bar bar bar"))
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
},
- PositionedContent: []gadget.PositionedContent{
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{
Image: "foo.img",
@@ -124,11 +124,11 @@
func (r *rawTestSuite) TestRawWriterNoFile(c *C) {
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
},
- PositionedContent: []gadget.PositionedContent{
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{
Image: "foo.img",
@@ -176,11 +176,11 @@
}
makeSizedFile(c, filepath.Join(r.dir, "foo.img"), 128, []byte("foo foo foo"))
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
},
- PositionedContent: []gadget.PositionedContent{
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{
Image: "foo.img",
@@ -208,11 +208,11 @@
func (r *rawTestSuite) TestRawWriterNoImage(c *C) {
out := &mockWriteSeeker{}
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
},
- PositionedContent: []gadget.PositionedContent{
+ LaidOutContent: []gadget.LaidOutContent{
{
// invalid content
VolumeContent: &gadget.VolumeContent{
@@ -232,7 +232,7 @@
}
func (r *rawTestSuite) TestRawWriterFailWithNonBare(c *C) {
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
// non-bare
@@ -246,7 +246,7 @@
}
func (r *rawTestSuite) TestRawWriterInternalErrors(c *C) {
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
},
@@ -257,7 +257,7 @@
c.Assert(rw, IsNil)
rw, err = gadget.NewRawStructureWriter(r.dir, nil)
- c.Assert(err, ErrorMatches, `internal error: \*PositionedStructure is nil`)
+ c.Assert(err, ErrorMatches, `internal error: \*LaidOutStructure is nil`)
c.Assert(rw, IsNil)
}
@@ -268,7 +268,7 @@
}
func (r *rawTestSuite) TestRawUpdaterFailWithNonBare(c *C) {
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
// non-bare
@@ -276,9 +276,9 @@
},
}
- ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.PositionedStructure) (string, error) {
+ ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) {
c.Fatalf("unexpected call")
- return "", nil
+ return "", 0, nil
})
c.Assert(err, ErrorMatches, "internal error: structure #0 is not bare")
c.Assert(ru, IsNil)
@@ -286,39 +286,40 @@
func (r *rawTestSuite) TestRawUpdaterBackupUpdateRestoreSame(c *C) {
- diskPath := filepath.Join(r.dir, "disk.img")
- mutateFile(c, diskPath, 2048, []mutateWrite{
+ partitionPath := filepath.Join(r.dir, "partition.img")
+ mutateFile(c, partitionPath, 2048, []mutateWrite{
{[]byte("foo foo foo"), 0},
{[]byte("bar bar bar"), 1024},
})
makeSizedFile(c, filepath.Join(r.dir, "foo.img"), 128, []byte("foo foo foo"))
makeSizedFile(c, filepath.Join(r.dir, "bar.img"), 128, []byte("bar bar bar"))
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
},
- StartOffset: 0,
- PositionedContent: []gadget.PositionedContent{
+ StartOffset: 1 * gadget.SizeMiB,
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{
Image: "foo.img",
},
- StartOffset: 0,
+ StartOffset: 1 * gadget.SizeMiB,
Size: 128,
}, {
VolumeContent: &gadget.VolumeContent{
Image: "bar.img",
},
- StartOffset: 1024,
+ StartOffset: 1*gadget.SizeMiB + 1024,
Size: 128,
Index: 1,
},
},
}
- ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.PositionedStructure) (string, error) {
+ ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) {
c.Check(to, DeepEquals, ps)
- return diskPath, nil
+ // Structure has a partition, thus it starts at 0 offset.
+ return partitionPath, 0, nil
})
c.Assert(err, IsNil)
c.Assert(ru, NotNil)
@@ -326,16 +327,16 @@
err = ru.Backup()
c.Assert(err, IsNil)
- c.Check(osutil.FileExists(gadget.RawContentBackupPath(r.backup, ps, &ps.PositionedContent[0])+".same"), Equals, true)
- c.Check(osutil.FileExists(gadget.RawContentBackupPath(r.backup, ps, &ps.PositionedContent[1])+".same"), Equals, true)
+ c.Check(osutil.FileExists(gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[0])+".same"), Equals, true)
+ c.Check(osutil.FileExists(gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[1])+".same"), Equals, true)
emptyDiskPath := filepath.Join(r.dir, "disk-not-written.img")
err = osutil.AtomicWriteFile(emptyDiskPath, nil, 0644, 0)
c.Assert(err, IsNil)
// update should be a noop now, use the same locations, point to a file
// of 0 size, so that seek fails and write would increase the size
- ru, err = gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.PositionedStructure) (string, error) {
- return emptyDiskPath, nil
+ ru, err = gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) {
+ return emptyDiskPath, 0, nil
})
c.Assert(err, IsNil)
c.Assert(ru, NotNil)
@@ -352,7 +353,7 @@
func (r *rawTestSuite) TestRawUpdaterBackupUpdateRestoreDifferent(c *C) {
- diskPath := filepath.Join(r.dir, "disk.img")
+ diskPath := filepath.Join(r.dir, "partition.img")
mutateFile(c, diskPath, 2048, []mutateWrite{
{[]byte("foo foo foo"), 0},
{[]byte("bar bar bar"), 1024},
@@ -370,31 +371,32 @@
makeSizedFile(c, filepath.Join(r.dir, "foo.img"), 128, []byte("zzz zzz zzz zzz"))
makeSizedFile(c, filepath.Join(r.dir, "bar.img"), 256, []byte("xxx xxx xxx xxx"))
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
},
- StartOffset: 0,
- PositionedContent: []gadget.PositionedContent{
+ StartOffset: 1 * gadget.SizeMiB,
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{
Image: "foo.img",
},
- StartOffset: 0,
+ StartOffset: 1 * gadget.SizeMiB,
Size: 128,
}, {
VolumeContent: &gadget.VolumeContent{
Image: "bar.img",
},
- StartOffset: 1024,
+ StartOffset: 1*gadget.SizeMiB + 1024,
Size: 256,
Index: 1,
},
},
}
- ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.PositionedStructure) (string, error) {
+ ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) {
c.Check(to, DeepEquals, ps)
- return diskPath, nil
+ // Structure has a partition, thus it starts at 0 offset.
+ return diskPath, 0, nil
})
c.Assert(err, IsNil)
c.Assert(ru, NotNil)
@@ -407,10 +409,10 @@
size int64
exists bool
}{
- {gadget.RawContentBackupPath(r.backup, ps, &ps.PositionedContent[0]) + ".backup", 128, true},
- {gadget.RawContentBackupPath(r.backup, ps, &ps.PositionedContent[1]) + ".backup", 256, true},
- {gadget.RawContentBackupPath(r.backup, ps, &ps.PositionedContent[1]) + ".same", 0, false},
- {gadget.RawContentBackupPath(r.backup, ps, &ps.PositionedContent[1]) + ".same", 0, false},
+ {gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[0]) + ".backup", 128, true},
+ {gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[1]) + ".backup", 256, true},
+ {gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[1]) + ".same", 0, false},
+ {gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[1]) + ".same", 0, false},
} {
c.Check(osutil.FileExists(e.path), Equals, e.exists)
if e.exists {
@@ -432,14 +434,94 @@
c.Check(osutil.FilesAreEqual(diskPath, pristinePath), Equals, true)
}
+func (r *rawTestSuite) TestRawUpdaterBackupUpdateRestoreNoPartition(c *C) {
+ diskPath := filepath.Join(r.dir, "disk.img")
+
+ mutateFile(c, diskPath, gadget.SizeMiB+2048, []mutateWrite{
+ {[]byte("baz baz baz"), int64(gadget.SizeMiB)},
+ {[]byte("oof oof oof"), int64(gadget.SizeMiB + 1024)},
+ })
+
+ pristinePath := filepath.Join(r.dir, "pristine.img")
+ err := osutil.CopyFile(diskPath, pristinePath, 0)
+ c.Assert(err, IsNil)
+
+ expectedPath := filepath.Join(r.dir, "expected.img")
+ mutateFile(c, expectedPath, gadget.SizeMiB+2048, []mutateWrite{
+ {[]byte("zzz zzz zzz zzz"), int64(gadget.SizeMiB)},
+ {[]byte("xxx xxx xxx xxx"), int64(gadget.SizeMiB + 1024)},
+ })
+
+ makeSizedFile(c, filepath.Join(r.dir, "foo.img"), 128, []byte("zzz zzz zzz zzz"))
+ makeSizedFile(c, filepath.Join(r.dir, "bar.img"), 256, []byte("xxx xxx xxx xxx"))
+ ps := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ // No partition table entry, would trigger fallback lookup path.
+ Type: "bare",
+ Size: 2048,
+ },
+ StartOffset: 1 * gadget.SizeMiB,
+ LaidOutContent: []gadget.LaidOutContent{
+ {
+ VolumeContent: &gadget.VolumeContent{
+ Image: "foo.img",
+ },
+ StartOffset: 1 * gadget.SizeMiB,
+ Size: 128,
+ }, {
+ VolumeContent: &gadget.VolumeContent{
+ Image: "bar.img",
+ },
+ StartOffset: 1*gadget.SizeMiB + 1024,
+ Size: 256,
+ Index: 1,
+ },
+ },
+ }
+ ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) {
+ c.Check(to, DeepEquals, ps)
+ // No partition table, returned path corresponds to a disk, start offset is non-0.
+ return diskPath, ps.StartOffset, nil
+ })
+ c.Assert(err, IsNil)
+ c.Assert(ru, NotNil)
+
+ err = ru.Backup()
+ c.Assert(err, IsNil)
+
+ for _, e := range []struct {
+ path string
+ size int64
+ }{
+ {gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[0]) + ".backup", 128},
+ {gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[1]) + ".backup", 256},
+ } {
+ c.Check(osutil.FileExists(e.path), Equals, true)
+ c.Check(getFileSize(c, e.path), Equals, e.size)
+ }
+
+ err = ru.Update()
+ c.Assert(err, IsNil)
+
+ // After update, files should be identical.
+ c.Check(osutil.FilesAreEqual(diskPath, expectedPath), Equals, true)
+
+ // Rollback restores the original contents.
+ err = ru.Rollback()
+ c.Assert(err, IsNil)
+
+ // Which should match the pristine copy now.
+ c.Check(osutil.FilesAreEqual(diskPath, pristinePath), Equals, true)
+}
+
func (r *rawTestSuite) TestRawUpdaterBackupErrors(c *C) {
diskPath := filepath.Join(r.dir, "disk.img")
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
},
StartOffset: 0,
- PositionedContent: []gadget.PositionedContent{
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{
Image: "foo.img",
@@ -450,23 +532,23 @@
},
}
- ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.PositionedStructure) (string, error) {
+ ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) {
c.Check(to, DeepEquals, ps)
- return diskPath, nil
+ return diskPath, 0, nil
})
c.Assert(err, IsNil)
c.Assert(ru, NotNil)
err = ru.Backup()
c.Assert(err, ErrorMatches, "cannot open device for reading: .*")
- c.Check(osutil.FileExists(gadget.RawContentBackupPath(r.backup, ps, &ps.PositionedContent[0])+".backup"), Equals, false)
+ c.Check(osutil.FileExists(gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[0])+".backup"), Equals, false)
// 0 sized disk, copying will fail with early EOF
makeSizedFile(c, diskPath, 0, nil)
err = ru.Backup()
c.Assert(err, ErrorMatches, "cannot backup image .*: cannot backup original image: EOF")
- c.Check(osutil.FileExists(gadget.RawContentBackupPath(r.backup, ps, &ps.PositionedContent[0])+".backup"), Equals, false)
+ c.Check(osutil.FileExists(gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[0])+".backup"), Equals, false)
// make proper disk image now
err = os.Remove(diskPath)
@@ -475,7 +557,7 @@
err = ru.Backup()
c.Assert(err, ErrorMatches, "cannot backup image .*: cannot checksum update image: .*")
- c.Check(osutil.FileExists(gadget.RawContentBackupPath(r.backup, ps, &ps.PositionedContent[0])+".backup"), Equals, false)
+ c.Check(osutil.FileExists(gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[0])+".backup"), Equals, false)
}
func (r *rawTestSuite) TestRawUpdaterBackupIdempotent(c *C) {
@@ -483,12 +565,12 @@
// 0 sized disk, copying will fail with early EOF
makeSizedFile(c, diskPath, 0, nil)
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
},
StartOffset: 0,
- PositionedContent: []gadget.PositionedContent{
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{
Image: "foo.img",
@@ -499,14 +581,14 @@
},
}
- ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.PositionedStructure) (string, error) {
+ ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) {
c.Check(to, DeepEquals, ps)
- return diskPath, nil
+ return diskPath, 0, nil
})
c.Assert(err, IsNil)
c.Assert(ru, NotNil)
- contentBackupBasePath := gadget.RawContentBackupPath(r.backup, ps, &ps.PositionedContent[0])
+ contentBackupBasePath := gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[0])
// mock content backed-up marker
makeSizedFile(c, contentBackupBasePath+".backup", 0, nil)
@@ -525,12 +607,12 @@
}
func (r *rawTestSuite) TestRawUpdaterFindDeviceFailed(c *C) {
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
},
StartOffset: 0,
- PositionedContent: []gadget.PositionedContent{
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{
Image: "foo.img",
@@ -545,9 +627,9 @@
c.Assert(err, ErrorMatches, "internal error: device lookup helper must be provided")
c.Assert(ru, IsNil)
- ru, err = gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.PositionedStructure) (string, error) {
+ ru, err = gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) {
c.Check(to, DeepEquals, ps)
- return "", errors.New("failed")
+ return "", 0, errors.New("failed")
})
c.Assert(err, IsNil)
c.Assert(ru, NotNil)
@@ -567,12 +649,12 @@
// 0 sized disk, copying will fail with early EOF
makeSizedFile(c, diskPath, 0, nil)
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
},
StartOffset: 0,
- PositionedContent: []gadget.PositionedContent{
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{
Image: "foo.img",
@@ -583,9 +665,9 @@
},
}
- ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.PositionedStructure) (string, error) {
+ ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) {
c.Check(to, DeepEquals, ps)
- return diskPath, nil
+ return diskPath, 0, nil
})
c.Assert(err, IsNil)
c.Assert(ru, NotNil)
@@ -593,7 +675,7 @@
err = ru.Rollback()
c.Assert(err, ErrorMatches, `cannot rollback image #0 \("foo.img"@0x80\{128\}\): cannot open backup image: .*no such file or directory`)
- contentBackupPath := gadget.RawContentBackupPath(r.backup, ps, &ps.PositionedContent[0]) + ".backup"
+ contentBackupPath := gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[0]) + ".backup"
// trigger short read
makeSizedFile(c, contentBackupPath, 0, nil)
@@ -613,12 +695,12 @@
// 0 sized disk, copying will fail with early EOF
makeSizedFile(c, diskPath, 2048, nil)
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
},
StartOffset: 0,
- PositionedContent: []gadget.PositionedContent{
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{
Image: "foo.img",
@@ -629,9 +711,9 @@
},
}
- ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.PositionedStructure) (string, error) {
+ ru, err := gadget.NewRawStructureUpdater(r.dir, ps, r.backup, func(to *gadget.LaidOutStructure) (string, gadget.Size, error) {
c.Check(to, DeepEquals, ps)
- return diskPath, nil
+ return diskPath, 0, nil
})
c.Assert(err, IsNil)
c.Assert(ru, NotNil)
@@ -641,7 +723,7 @@
c.Assert(err, ErrorMatches, `cannot update image #0 \("foo.img"@0x80\{128\}\): missing backup file`)
// pretend backup was done
- makeSizedFile(c, gadget.RawContentBackupPath(r.backup, ps, &ps.PositionedContent[0])+".backup", 0, nil)
+ makeSizedFile(c, gadget.RawContentBackupPath(r.backup, ps, &ps.LaidOutContent[0])+".backup", 0, nil)
err = ru.Update()
c.Assert(err, ErrorMatches, `cannot update image #0 \("foo.img"@0x80\{128\}\).*: cannot open image file: .*no such file or directory`)
@@ -658,16 +740,16 @@
}
func (r *rawTestSuite) TestRawUpdaterContentBackupPath(c *C) {
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{},
StartOffset: 0,
- PositionedContent: []gadget.PositionedContent{
+ LaidOutContent: []gadget.LaidOutContent{
{
VolumeContent: &gadget.VolumeContent{},
},
},
}
- pc := &ps.PositionedContent[0]
+ pc := &ps.LaidOutContent[0]
p := gadget.RawContentBackupPath(r.backup, ps, pc)
c.Assert(p, Equals, r.backup+"/struct-0-0")
@@ -680,21 +762,21 @@
}
func (r *rawTestSuite) TestRawUpdaterInternalErrors(c *C) {
- ps := &gadget.PositionedStructure{
+ ps := &gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
Size: 2048,
},
}
- f := func(to *gadget.PositionedStructure) (string, error) {
- return "", errors.New("unexpected call")
+ f := func(to *gadget.LaidOutStructure) (string, gadget.Size, error) {
+ return "", 0, errors.New("unexpected call")
}
rw, err := gadget.NewRawStructureUpdater("", ps, r.backup, f)
c.Assert(err, ErrorMatches, "internal error: gadget content directory cannot be unset")
c.Assert(rw, IsNil)
rw, err = gadget.NewRawStructureUpdater(r.dir, nil, r.backup, f)
- c.Assert(err, ErrorMatches, `internal error: \*PositionedStructure is nil`)
+ c.Assert(err, ErrorMatches, `internal error: \*LaidOutStructure is nil`)
c.Assert(rw, IsNil)
rw, err = gadget.NewRawStructureUpdater(r.dir, ps, "", f)
diff -Nru snapd-2.40/gadget/update.go snapd-2.42.1/gadget/update.go
--- snapd-2.40/gadget/update.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/update.go 2019-10-30 12:17:43.000000000 +0000
@@ -21,12 +21,93 @@
import (
"errors"
"fmt"
+
+ "github.com/snapcore/snapd/logger"
+)
+
+var (
+ ErrNoUpdate = errors.New("nothing to update")
)
var (
- ErrNoUpdate = errors.New("no update needed")
+ // default positioning constraints that match ubuntu-image
+ defaultConstraints = LayoutConstraints{
+ NonMBRStartOffset: 1 * SizeMiB,
+ SectorSize: 512,
+ }
)
+// GadgetData holds references to a gadget revision metadata and its data directory.
+type GadgetData struct {
+ // Info is the gadget metadata
+ Info *Info
+ // RootDir is the root directory of gadget snap data
+ RootDir string
+}
+
+// Update applies the gadget update given the gadget information and data from
+// old and new revisions. It errors out when the update is not possible or
+// illegal, or a failure occurs at any of the steps. When there is no update, a
+// special error ErrNoUpdate is returned.
+//
+// Updates are opt-in, and are only applied to structures with a higher value of
+// Edition field in the new gadget definition.
+//
+// Data that would be modified during the update is first backed up inside the
+// rollback directory. Should the apply step fail, the modified data is
+// recovered.
+func Update(old, new GadgetData, rollbackDirPath string) error {
+ // TODO: support multi-volume gadgets. But for now we simply
+ // do not do any gadget updates on those. We cannot error
+ // here because this would break refreshes of gadgets even
+ // when they don't require any updates.
+ if len(new.Info.Volumes) != 1 || len(old.Info.Volumes) != 1 {
+ logger.Noticef("WARNING: gadget assests cannot be updated yet when multiple volumes are used")
+ return nil
+ }
+
+ oldVol, newVol, err := resolveVolume(old.Info, new.Info)
+ if err != nil {
+ return err
+ }
+
+ // layout old partially, without going deep into the layout of structure
+ // content
+ pOld, err := LayoutVolumePartially(oldVol, defaultConstraints)
+ if err != nil {
+ return fmt.Errorf("cannot lay out the old volume: %v", err)
+ }
+
+ // layout new
+ pNew, err := LayoutVolume(new.RootDir, newVol, defaultConstraints)
+ if err != nil {
+ return fmt.Errorf("cannot lay out the new volume: %v", err)
+ }
+
+ if err := canUpdateVolume(pOld, pNew); err != nil {
+ return fmt.Errorf("cannot apply update to volume: %v", err)
+ }
+
+ // now we know which structure is which, find which ones need an update
+ updates, err := resolveUpdate(pOld, pNew)
+ if err != nil {
+ return err
+ }
+ if len(updates) == 0 {
+ // nothing to update
+ return ErrNoUpdate
+ }
+
+ // can update old layout to new layout
+ for _, update := range updates {
+ if err := canUpdateStructure(update.from, update.to); err != nil {
+ return fmt.Errorf("cannot update volume structure %v: %v", update.to, err)
+ }
+ }
+
+ return applyUpdates(new, updates, rollbackDirPath)
+}
+
func resolveVolume(old *Info, new *Info) (oldVol, newVol *Volume, err error) {
// support only one volume
if len(new.Volumes) != 1 || len(old.Volumes) != 1 {
@@ -68,13 +149,13 @@
return false
}
-func isLegacyMBRTransition(from *PositionedStructure, to *PositionedStructure) bool {
+func isLegacyMBRTransition(from *LaidOutStructure, to *LaidOutStructure) bool {
// legacy MBR could have been specified by setting type: mbr, with no
// role
return from.Type == MBR && to.EffectiveRole() == MBR
}
-func canUpdateStructure(from *PositionedStructure, to *PositionedStructure) error {
+func canUpdateStructure(from *LaidOutStructure, to *LaidOutStructure) error {
if from.Size != to.Size {
return fmt.Errorf("cannot change structure size from %v to %v", from.Size, to.Size)
}
@@ -120,15 +201,117 @@
return nil
}
-func canUpdateVolume(from *PositionedVolume, to *PositionedVolume) error {
+func canUpdateVolume(from *PartiallyLaidOutVolume, to *LaidOutVolume) error {
if from.ID != to.ID {
return fmt.Errorf("cannot change volume ID from %q to %q", from.ID, to.ID)
}
if from.EffectiveSchema() != to.EffectiveSchema() {
return fmt.Errorf("cannot change volume schema from %q to %q", from.EffectiveSchema(), to.EffectiveSchema())
}
- if len(from.PositionedStructure) != len(to.PositionedStructure) {
- return fmt.Errorf("cannot change the number of structures within volume from %v to %v", len(from.PositionedStructure), len(to.PositionedStructure))
+ if len(from.LaidOutStructure) != len(to.LaidOutStructure) {
+ return fmt.Errorf("cannot change the number of structures within volume from %v to %v", len(from.LaidOutStructure), len(to.LaidOutStructure))
}
return nil
}
+
+type updatePair struct {
+ from *LaidOutStructure
+ to *LaidOutStructure
+}
+
+func resolveUpdate(oldVol *PartiallyLaidOutVolume, newVol *LaidOutVolume) (updates []updatePair, err error) {
+ if len(oldVol.LaidOutStructure) != len(newVol.LaidOutStructure) {
+ return nil, errors.New("internal error: the number of structures in new and old volume definitions is different")
+ }
+ for j, oldStruct := range oldVol.LaidOutStructure {
+ newStruct := newVol.LaidOutStructure[j]
+ // update only when new edition is higher than the old one; boot
+ // assets are assumed to be backwards compatible, once deployed
+ // are not rolled back or replaced unless a higher edition is
+ // available
+ if newStruct.Update.Edition > oldStruct.Update.Edition {
+ updates = append(updates, updatePair{
+ from: &oldVol.LaidOutStructure[j],
+ to: &newVol.LaidOutStructure[j],
+ })
+ }
+ }
+ return updates, nil
+}
+
+type Updater interface {
+ // Update applies the update or errors out on failures
+ Update() error
+ // Backup prepares a backup copy of data that will be modified by
+ // Update()
+ Backup() error
+ // Rollback restores data modified by update
+ Rollback() error
+}
+
+func applyUpdates(new GadgetData, updates []updatePair, rollbackDir string) error {
+ updaters := make([]Updater, len(updates))
+
+ for i, one := range updates {
+ up, err := updaterForStructure(one.to, new.RootDir, rollbackDir)
+ if err != nil {
+ return fmt.Errorf("cannot prepare update for volume structure %v: %v", one.to, err)
+ }
+ updaters[i] = up
+ }
+
+ for i, one := range updaters {
+ if err := one.Backup(); err != nil {
+ return fmt.Errorf("cannot backup volume structure %v: %v", updates[i].to, err)
+ }
+ }
+
+ var updateErr error
+ var updateLastAttempted int
+ for i, one := range updaters {
+ updateLastAttempted = i
+ if err := one.Update(); err != nil {
+ updateErr = fmt.Errorf("cannot update volume structure %v: %v", updates[i].to, err)
+ break
+ }
+ }
+
+ if updateErr == nil {
+ // all good, updates applied successfully
+ return nil
+ }
+
+ logger.Noticef("cannot update gadget: %v", updateErr)
+ // not so good, rollback ones that got applied
+ for i := 0; i <= updateLastAttempted; i++ {
+ one := updaters[i]
+ if err := one.Rollback(); err != nil {
+ // TODO: log errors to oplog
+ logger.Noticef("cannot rollback volume structure %v update: %v", updates[i].to, err)
+ }
+ }
+
+ return updateErr
+}
+
+var updaterForStructure = updaterForStructureImpl
+
+func updaterForStructureImpl(ps *LaidOutStructure, newRootDir, rollbackDir string) (Updater, error) {
+ var updater Updater
+ var err error
+ if ps.IsBare() {
+ updater, err = NewRawStructureUpdater(newRootDir, ps, rollbackDir, FindDeviceForStructureWithFallback)
+ } else {
+ updater, err = NewMountedFilesystemUpdater(newRootDir, ps, rollbackDir, FindMountPointForStructure)
+ }
+ return updater, err
+}
+
+// MockUpdaterForStructure replace internal call with a mocked one, for use in tests only
+func MockUpdaterForStructure(mock func(ps *LaidOutStructure, rootDir, rollbackDir string) (Updater, error)) (restore func()) {
+ old := updaterForStructure
+ updaterForStructure = mock
+ return func() {
+ updaterForStructure = old
+ }
+}
diff -Nru snapd-2.40/gadget/update_test.go snapd-2.42.1/gadget/update_test.go
--- snapd-2.40/gadget/update_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/gadget/update_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,9 +20,15 @@
package gadget_test
import (
+ "errors"
+ "path/filepath"
+ "strings"
+
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/gadget"
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/testutil"
)
type updateTestSuite struct{}
@@ -82,8 +88,8 @@
}
type canUpdateTestCase struct {
- from gadget.PositionedStructure
- to gadget.PositionedStructure
+ from gadget.LaidOutStructure
+ to gadget.LaidOutStructure
err string
}
@@ -104,19 +110,19 @@
cases := []canUpdateTestCase{
{
// size change
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Size: 1*gadget.SizeMiB + 1*gadget.SizeKiB},
},
err: "cannot change structure size from [0-9]+ to [0-9]+",
}, {
// size change
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB},
},
err: "",
@@ -131,12 +137,12 @@
cases := []canUpdateTestCase{
{
// offset-write change
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: &gadget.RelativeOffset{Offset: 1024},
},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: &gadget.RelativeOffset{Offset: 2048},
},
@@ -144,12 +150,12 @@
err: "cannot change structure offset-write from [0-9]+ to [0-9]+",
}, {
// offset-write, change in relative-to structure name
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: &gadget.RelativeOffset{RelativeTo: "foo", Offset: 1024},
},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: &gadget.RelativeOffset{RelativeTo: "bar", Offset: 1024},
},
@@ -157,12 +163,12 @@
err: `cannot change structure offset-write from foo\+[0-9]+ to bar\+[0-9]+`,
}, {
// offset-write, unspecified in old
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: nil,
},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: &gadget.RelativeOffset{RelativeTo: "bar", Offset: 1024},
},
@@ -170,12 +176,12 @@
err: `cannot change structure offset-write from unspecified to bar\+[0-9]+`,
}, {
// offset-write, unspecified in new
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: &gadget.RelativeOffset{RelativeTo: "foo", Offset: 1024},
},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: nil,
},
@@ -183,12 +189,12 @@
err: `cannot change structure offset-write from foo\+[0-9]+ to unspecified`,
}, {
// all ok, both nils
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: nil,
},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: nil,
},
@@ -196,12 +202,12 @@
err: ``,
}, {
// all ok, both fully specified
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: &gadget.RelativeOffset{RelativeTo: "foo", Offset: 1024},
},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: &gadget.RelativeOffset{RelativeTo: "foo", Offset: 1024},
},
@@ -209,12 +215,12 @@
err: ``,
}, {
// all ok, both fully specified
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: &gadget.RelativeOffset{Offset: 1024},
},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{
OffsetWrite: &gadget.RelativeOffset{Offset: 1024},
},
@@ -230,22 +236,22 @@
cases := []canUpdateTestCase{
{
// explicitly declared start offset change
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB, Offset: asSizePtr(1024)},
StartOffset: 1024,
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB, Offset: asSizePtr(2048)},
StartOffset: 2048,
},
err: "cannot change structure offset from [0-9]+ to [0-9]+",
}, {
// explicitly declared start offset in new structure
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB, Offset: nil},
StartOffset: 1024,
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB, Offset: asSizePtr(2048)},
StartOffset: 2048,
},
@@ -253,22 +259,22 @@
}, {
// explicitly declared start offset in old structure,
// missing from new
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB, Offset: asSizePtr(1024)},
StartOffset: 1024,
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB, Offset: nil},
StartOffset: 2048,
},
err: "cannot change structure offset from [0-9]+ to unspecified",
}, {
- // start offset changed due to positioning
- from: gadget.PositionedStructure{
+ // start offset changed due to layout
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB},
StartOffset: 1 * gadget.SizeMiB,
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Size: 1 * gadget.SizeMiB},
StartOffset: 2 * gadget.SizeMiB,
},
@@ -283,46 +289,46 @@
cases := []canUpdateTestCase{
{
// new role
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Role: ""},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Role: "system-data"},
},
err: `cannot change structure role from "" to "system-data"`,
}, {
// explicitly set tole
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Role: "mbr"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Role: "system-data"},
},
err: `cannot change structure role from "mbr" to "system-data"`,
}, {
// implicit legacy role to proper explicit role
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "mbr"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "bare", Role: "mbr"},
},
err: "",
}, {
// but not in the opposite direction
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "bare", Role: "mbr"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "mbr"},
},
err: `cannot change structure type from "bare" to "mbr"`,
}, {
- // start offset changed due to positioning
- from: gadget.PositionedStructure{
+ // start offset changed due to layout
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Role: ""},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Role: ""},
},
err: "",
@@ -336,75 +342,75 @@
cases := []canUpdateTestCase{
{
// from hybrid type to GUID
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C,00000000-0000-0000-0000-dd00deadbeef"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "00000000-0000-0000-0000-dd00deadbeef"},
},
err: `cannot change structure type from "0C,00000000-0000-0000-0000-dd00deadbeef" to "00000000-0000-0000-0000-dd00deadbeef"`,
}, {
// from MBR type to GUID (would be stopped at volume update checks)
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "00000000-0000-0000-0000-dd00deadbeef"},
},
err: `cannot change structure type from "0C" to "00000000-0000-0000-0000-dd00deadbeef"`,
}, {
// from one MBR type to another
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0A"},
},
err: `cannot change structure type from "0C" to "0A"`,
}, {
// from one MBR type to another
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "bare"},
},
err: `cannot change structure type from "0C" to "bare"`,
}, {
// from one GUID to another
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "00000000-0000-0000-0000-dd00deadcafe"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "00000000-0000-0000-0000-dd00deadbeef"},
},
err: `cannot change structure type from "00000000-0000-0000-0000-dd00deadcafe" to "00000000-0000-0000-0000-dd00deadbeef"`,
}, {
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "bare"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "bare"},
},
}, {
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C"},
},
}, {
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "00000000-0000-0000-0000-dd00deadbeef"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "00000000-0000-0000-0000-dd00deadbeef"},
},
}, {
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C,00000000-0000-0000-0000-dd00deadbeef"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C,00000000-0000-0000-0000-dd00deadbeef"},
},
},
@@ -416,10 +422,10 @@
cases := []canUpdateTestCase{
{
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{ID: "00000000-0000-0000-0000-dd00deadbeef"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{ID: "00000000-0000-0000-0000-dd00deadcafe"},
},
err: `cannot change structure ID from "00000000-0000-0000-0000-dd00deadbeef" to "00000000-0000-0000-0000-dd00deadcafe"`,
@@ -432,52 +438,52 @@
cases := []canUpdateTestCase{
{
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C", Filesystem: "ext4"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C", Filesystem: ""},
},
err: `cannot change a filesystem structure to a bare one`,
}, {
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C", Filesystem: ""},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C", Filesystem: "ext4"},
},
err: `cannot change a bare structure to filesystem one`,
}, {
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C", Filesystem: "ext4"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C", Filesystem: "vfat"},
},
err: `cannot change filesystem from "ext4" to "vfat"`,
}, {
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C", Filesystem: "ext4", Label: "writable"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C", Filesystem: "ext4"},
},
err: `cannot change filesystem label from "writable" to ""`,
}, {
// from implicit filesystem label to explicit one
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C", Filesystem: "ext4", Role: "system-data"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C", Filesystem: "ext4", Role: "system-data", Label: "writable"},
},
err: ``,
}, {
// all ok
- from: gadget.PositionedStructure{
+ from: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C", Filesystem: "ext4", Label: "do-not-touch"},
},
- to: gadget.PositionedStructure{
+ to: gadget.LaidOutStructure{
VolumeStructure: &gadget.VolumeStructure{Type: "0C", Filesystem: "ext4", Label: "do-not-touch"},
},
err: ``,
@@ -489,74 +495,74 @@
func (u *updateTestSuite) TestCanUpdateVolume(c *C) {
for idx, tc := range []struct {
- from gadget.PositionedVolume
- to gadget.PositionedVolume
+ from gadget.PartiallyLaidOutVolume
+ to gadget.LaidOutVolume
err string
}{
{
- from: gadget.PositionedVolume{
+ from: gadget.PartiallyLaidOutVolume{
Volume: &gadget.Volume{Schema: ""},
},
- to: gadget.PositionedVolume{
+ to: gadget.LaidOutVolume{
Volume: &gadget.Volume{Schema: "mbr"},
},
err: `cannot change volume schema from "gpt" to "mbr"`,
}, {
- from: gadget.PositionedVolume{
+ from: gadget.PartiallyLaidOutVolume{
Volume: &gadget.Volume{Schema: "gpt"},
},
- to: gadget.PositionedVolume{
+ to: gadget.LaidOutVolume{
Volume: &gadget.Volume{Schema: "mbr"},
},
err: `cannot change volume schema from "gpt" to "mbr"`,
}, {
- from: gadget.PositionedVolume{
+ from: gadget.PartiallyLaidOutVolume{
Volume: &gadget.Volume{ID: "00000000-0000-0000-0000-0000deadbeef"},
},
- to: gadget.PositionedVolume{
+ to: gadget.LaidOutVolume{
Volume: &gadget.Volume{ID: "00000000-0000-0000-0000-0000deadcafe"},
},
err: `cannot change volume ID from "00000000-0000-0000-0000-0000deadbeef" to "00000000-0000-0000-0000-0000deadcafe"`,
}, {
- from: gadget.PositionedVolume{
+ from: gadget.PartiallyLaidOutVolume{
Volume: &gadget.Volume{},
- PositionedStructure: []gadget.PositionedStructure{
+ LaidOutStructure: []gadget.LaidOutStructure{
{}, {},
},
},
- to: gadget.PositionedVolume{
+ to: gadget.LaidOutVolume{
Volume: &gadget.Volume{},
- PositionedStructure: []gadget.PositionedStructure{
+ LaidOutStructure: []gadget.LaidOutStructure{
{},
},
},
err: `cannot change the number of structures within volume from 2 to 1`,
}, {
// valid, implicit schema
- from: gadget.PositionedVolume{
+ from: gadget.PartiallyLaidOutVolume{
Volume: &gadget.Volume{Schema: ""},
- PositionedStructure: []gadget.PositionedStructure{
+ LaidOutStructure: []gadget.LaidOutStructure{
{}, {},
},
},
- to: gadget.PositionedVolume{
+ to: gadget.LaidOutVolume{
Volume: &gadget.Volume{Schema: "gpt"},
- PositionedStructure: []gadget.PositionedStructure{
+ LaidOutStructure: []gadget.LaidOutStructure{
{}, {},
},
},
err: ``,
}, {
// valid
- from: gadget.PositionedVolume{
+ from: gadget.PartiallyLaidOutVolume{
Volume: &gadget.Volume{Schema: "mbr"},
- PositionedStructure: []gadget.PositionedStructure{
+ LaidOutStructure: []gadget.LaidOutStructure{
{}, {},
},
},
- to: gadget.PositionedVolume{
+ to: gadget.LaidOutVolume{
Volume: &gadget.Volume{Schema: "mbr"},
- PositionedStructure: []gadget.PositionedStructure{
+ LaidOutStructure: []gadget.LaidOutStructure{
{}, {},
},
},
@@ -573,3 +579,687 @@
}
}
+
+type mockUpdater struct {
+ updateCb func() error
+ backupCb func() error
+ rollbackCb func() error
+}
+
+func callOrNil(f func() error) error {
+ if f != nil {
+ return f()
+ }
+ return nil
+}
+
+func (m *mockUpdater) Backup() error {
+ return callOrNil(m.backupCb)
+}
+
+func (m *mockUpdater) Rollback() error {
+ return callOrNil(m.rollbackCb)
+}
+
+func (m *mockUpdater) Update() error {
+ return callOrNil(m.updateCb)
+}
+
+func updateDataSet(c *C) (oldData gadget.GadgetData, newData gadget.GadgetData, rollbackDir string) {
+ // prepare the stage
+ bareStruct := gadget.VolumeStructure{
+ Name: "first",
+ Size: 5 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{
+ {Image: "first.img"},
+ },
+ }
+ fsStruct := gadget.VolumeStructure{
+ Name: "second",
+ Size: 10 * gadget.SizeMiB,
+ Filesystem: "ext4",
+ Content: []gadget.VolumeContent{
+ {Source: "/second-content", Target: "/"},
+ },
+ }
+ lastStruct := gadget.VolumeStructure{
+ Name: "third",
+ Size: 5 * gadget.SizeMiB,
+ Filesystem: "vfat",
+ Content: []gadget.VolumeContent{
+ {Source: "/third-content", Target: "/"},
+ },
+ }
+ // start with identical data for new and old infos, they get updated by
+ // the caller as needed
+ oldInfo := &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "foo": {
+ Bootloader: "grub",
+ Schema: gadget.GPT,
+ Structure: []gadget.VolumeStructure{bareStruct, fsStruct, lastStruct},
+ },
+ },
+ }
+ newInfo := &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "foo": {
+ Bootloader: "grub",
+ Schema: gadget.GPT,
+ Structure: []gadget.VolumeStructure{bareStruct, fsStruct, lastStruct},
+ },
+ },
+ }
+
+ oldRootDir := c.MkDir()
+ makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), gadget.SizeMiB, nil)
+ makeSizedFile(c, filepath.Join(oldRootDir, "/second-content/foo"), 0, nil)
+ makeSizedFile(c, filepath.Join(oldRootDir, "/third-content/bar"), 0, nil)
+ oldData = gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir}
+
+ newRootDir := c.MkDir()
+ makeSizedFile(c, filepath.Join(newRootDir, "first.img"), 900*gadget.SizeKiB, nil)
+ makeSizedFile(c, filepath.Join(newRootDir, "/second-content/foo"), gadget.SizeKiB, nil)
+ makeSizedFile(c, filepath.Join(newRootDir, "/third-content/bar"), gadget.SizeKiB, nil)
+ newData = gadget.GadgetData{Info: newInfo, RootDir: newRootDir}
+
+ rollbackDir = c.MkDir()
+ return oldData, newData, rollbackDir
+}
+
+func (u *updateTestSuite) TestUpdateApplyHappy(c *C) {
+ oldData, newData, rollbackDir := updateDataSet(c)
+ // update two structs
+ newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1
+ newData.Info.Volumes["foo"].Structure[1].Update.Edition = 1
+
+ updaterForStructureCalls := 0
+ updateCalls := make(map[string]bool)
+ backupCalls := make(map[string]bool)
+ restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string) (gadget.Updater, error) {
+ c.Assert(psRootDir, Equals, newData.RootDir)
+ c.Assert(psRollbackDir, Equals, rollbackDir)
+
+ switch updaterForStructureCalls {
+ case 0:
+ c.Check(ps.Name, Equals, "first")
+ c.Check(ps.IsBare(), Equals, true)
+ c.Check(ps.Size, Equals, 5*gadget.SizeMiB)
+ // non MBR start offset defaults to 1MiB
+ c.Check(ps.StartOffset, Equals, 1*gadget.SizeMiB)
+ c.Assert(ps.LaidOutContent, HasLen, 1)
+ c.Check(ps.LaidOutContent[0].Image, Equals, "first.img")
+ c.Check(ps.LaidOutContent[0].Size, Equals, 900*gadget.SizeKiB)
+ case 1:
+ c.Check(ps.Name, Equals, "second")
+ c.Check(ps.IsBare(), Equals, false)
+ c.Check(ps.Filesystem, Equals, "ext4")
+ c.Check(ps.Size, Equals, 10*gadget.SizeMiB)
+ // foo's start offset + foo's size
+ c.Check(ps.StartOffset, Equals, (1+5)*gadget.SizeMiB)
+ c.Assert(ps.LaidOutContent, HasLen, 0)
+ c.Assert(ps.Content, HasLen, 1)
+ c.Check(ps.Content[0].Source, Equals, "/second-content")
+ c.Check(ps.Content[0].Target, Equals, "/")
+ default:
+ c.Fatalf("unexpected call")
+ }
+ updaterForStructureCalls++
+ mu := &mockUpdater{
+ backupCb: func() error {
+ backupCalls[ps.Name] = true
+ return nil
+ },
+ updateCb: func() error {
+ updateCalls[ps.Name] = true
+ return nil
+ },
+ rollbackCb: func() error {
+ c.Fatalf("unexpected call")
+ return errors.New("not called")
+ },
+ }
+ return mu, nil
+ })
+ defer restore()
+
+ // go go go
+ err := gadget.Update(oldData, newData, rollbackDir)
+ c.Assert(err, IsNil)
+ c.Assert(backupCalls, DeepEquals, map[string]bool{
+ "first": true,
+ "second": true,
+ })
+ c.Assert(updateCalls, DeepEquals, map[string]bool{
+ "first": true,
+ "second": true,
+ })
+ c.Assert(updaterForStructureCalls, Equals, 2)
+}
+
+func (u *updateTestSuite) TestUpdateApplyOnlyWhenNeeded(c *C) {
+ oldData, newData, rollbackDir := updateDataSet(c)
+ // first structure is updated
+ oldData.Info.Volumes["foo"].Structure[0].Update.Edition = 0
+ newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1
+ // second one is not, lower edition
+ oldData.Info.Volumes["foo"].Structure[1].Update.Edition = 2
+ newData.Info.Volumes["foo"].Structure[1].Update.Edition = 1
+ // third one is not, same edition
+ oldData.Info.Volumes["foo"].Structure[2].Update.Edition = 3
+ newData.Info.Volumes["foo"].Structure[2].Update.Edition = 3
+
+ updaterForStructureCalls := 0
+ restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string) (gadget.Updater, error) {
+ c.Assert(psRootDir, Equals, newData.RootDir)
+ c.Assert(psRollbackDir, Equals, rollbackDir)
+
+ switch updaterForStructureCalls {
+ case 0:
+ // only called for the first structure
+ c.Assert(ps.Name, Equals, "first")
+ default:
+ c.Fatalf("unexpected call")
+ }
+ updaterForStructureCalls++
+ mu := &mockUpdater{
+ rollbackCb: func() error {
+ c.Fatalf("unexpected call")
+ return errors.New("not called")
+ },
+ }
+ return mu, nil
+ })
+ defer restore()
+
+ // go go go
+ err := gadget.Update(oldData, newData, rollbackDir)
+ c.Assert(err, IsNil)
+}
+
+func (u *updateTestSuite) TestUpdateApplyErrorLayout(c *C) {
+ // prepare the stage
+ bareStruct := gadget.VolumeStructure{
+ Name: "foo",
+ Size: 5 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{
+ {Image: "first.img"},
+ },
+ }
+ bareStructUpdate := bareStruct
+ oldInfo := &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "foo": {
+ Bootloader: "grub",
+ Schema: gadget.GPT,
+ Structure: []gadget.VolumeStructure{bareStruct},
+ },
+ },
+ }
+ newInfo := &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "foo": {
+ Bootloader: "grub",
+ Schema: gadget.GPT,
+ Structure: []gadget.VolumeStructure{bareStructUpdate},
+ },
+ },
+ }
+
+ newRootDir := c.MkDir()
+ newData := gadget.GadgetData{Info: newInfo, RootDir: newRootDir}
+
+ oldRootDir := c.MkDir()
+ oldData := gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir}
+
+ rollbackDir := c.MkDir()
+
+ // both old and new bare struct data is missing
+
+ // cannot lay out the new volume when bare struct data is missing
+ err := gadget.Update(oldData, newData, rollbackDir)
+ c.Assert(err, ErrorMatches, `cannot lay out the new volume: cannot lay out structure #0 \("foo"\): content "first.img": .* no such file or directory`)
+
+ makeSizedFile(c, filepath.Join(newRootDir, "first.img"), gadget.SizeMiB, nil)
+
+ // Update does not error out when when the bare struct data of the old volume is missing
+ err = gadget.Update(oldData, newData, rollbackDir)
+ c.Assert(err, Equals, gadget.ErrNoUpdate)
+}
+
+func (u *updateTestSuite) TestUpdateApplyErrorIllegalVolumeUpdate(c *C) {
+ // prepare the stage
+ bareStruct := gadget.VolumeStructure{
+ Name: "foo",
+ Size: 5 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{
+ {Image: "first.img"},
+ },
+ }
+ bareStructUpdate := bareStruct
+ bareStructUpdate.Name = "foo update"
+ bareStructUpdate.Update.Edition = 1
+ oldInfo := &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "foo": {
+ Bootloader: "grub",
+ Schema: gadget.GPT,
+ Structure: []gadget.VolumeStructure{bareStruct},
+ },
+ },
+ }
+ newInfo := &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "foo": {
+ Bootloader: "grub",
+ Schema: gadget.GPT,
+ // more structures than old
+ Structure: []gadget.VolumeStructure{bareStruct, bareStructUpdate},
+ },
+ },
+ }
+
+ newRootDir := c.MkDir()
+ newData := gadget.GadgetData{Info: newInfo, RootDir: newRootDir}
+
+ oldRootDir := c.MkDir()
+ oldData := gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir}
+
+ rollbackDir := c.MkDir()
+
+ makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), gadget.SizeMiB, nil)
+ makeSizedFile(c, filepath.Join(newRootDir, "first.img"), 900*gadget.SizeKiB, nil)
+
+ err := gadget.Update(oldData, newData, rollbackDir)
+ c.Assert(err, ErrorMatches, `cannot apply update to volume: cannot change the number of structures within volume from 1 to 2`)
+}
+
+func (u *updateTestSuite) TestUpdateApplyErrorIllegalStructureUpdate(c *C) {
+ // prepare the stage
+ bareStruct := gadget.VolumeStructure{
+ Name: "foo",
+ Size: 5 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{
+ {Image: "first.img"},
+ },
+ }
+ fsStruct := gadget.VolumeStructure{
+ Name: "foo",
+ Filesystem: "ext4",
+ Size: 5 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{
+ {Source: "/", Target: "/"},
+ },
+ Update: gadget.VolumeUpdate{Edition: 5},
+ }
+ oldInfo := &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "foo": {
+ Bootloader: "grub",
+ Schema: gadget.GPT,
+ Structure: []gadget.VolumeStructure{bareStruct},
+ },
+ },
+ }
+ newInfo := &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "foo": {
+ Bootloader: "grub",
+ Schema: gadget.GPT,
+ // more structures than old
+ Structure: []gadget.VolumeStructure{fsStruct},
+ },
+ },
+ }
+
+ newRootDir := c.MkDir()
+ newData := gadget.GadgetData{Info: newInfo, RootDir: newRootDir}
+
+ oldRootDir := c.MkDir()
+ oldData := gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir}
+
+ rollbackDir := c.MkDir()
+
+ makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), gadget.SizeMiB, nil)
+
+ err := gadget.Update(oldData, newData, rollbackDir)
+ c.Assert(err, ErrorMatches, `cannot update volume structure #0 \("foo"\): cannot change a bare structure to filesystem one`)
+}
+
+func (u *updateTestSuite) TestUpdateApplyErrorDifferentVolume(c *C) {
+ // prepare the stage
+ bareStruct := gadget.VolumeStructure{
+ Name: "foo",
+ Size: 5 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{
+ {Image: "first.img"},
+ },
+ }
+ oldInfo := &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "foo": {
+ Bootloader: "grub",
+ Schema: gadget.GPT,
+ Structure: []gadget.VolumeStructure{bareStruct},
+ },
+ },
+ }
+ newInfo := &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ // same volume info but using a different name
+ "foo-new": oldInfo.Volumes["foo"],
+ },
+ }
+
+ oldData := gadget.GadgetData{Info: oldInfo, RootDir: c.MkDir()}
+ newData := gadget.GadgetData{Info: newInfo, RootDir: c.MkDir()}
+ rollbackDir := c.MkDir()
+
+ restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string) (gadget.Updater, error) {
+ c.Fatalf("unexpected call")
+ return &mockUpdater{}, nil
+ })
+ defer restore()
+
+ err := gadget.Update(oldData, newData, rollbackDir)
+ c.Assert(err, ErrorMatches, `cannot find entry for volume "foo" in updated gadget info`)
+}
+
+func (u *updateTestSuite) TestUpdateApplyUpdatesAreOptIn(c *C) {
+ // prepare the stage
+ bareStruct := gadget.VolumeStructure{
+ Name: "foo",
+ Size: 5 * gadget.SizeMiB,
+ Content: []gadget.VolumeContent{
+ {Image: "first.img"},
+ },
+ Update: gadget.VolumeUpdate{
+ Edition: 5,
+ },
+ }
+ oldInfo := &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "foo": {
+ Bootloader: "grub",
+ Schema: gadget.GPT,
+ Structure: []gadget.VolumeStructure{bareStruct},
+ },
+ },
+ }
+
+ oldRootDir := c.MkDir()
+ oldData := gadget.GadgetData{Info: oldInfo, RootDir: oldRootDir}
+ makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), gadget.SizeMiB, nil)
+
+ newRootDir := c.MkDir()
+ // same volume description
+ newData := gadget.GadgetData{Info: oldInfo, RootDir: newRootDir}
+ // different content, but updates are opt in
+ makeSizedFile(c, filepath.Join(newRootDir, "first.img"), 900*gadget.SizeKiB, nil)
+
+ rollbackDir := c.MkDir()
+
+ restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string) (gadget.Updater, error) {
+ c.Fatalf("unexpected call")
+ return &mockUpdater{}, nil
+ })
+ defer restore()
+
+ err := gadget.Update(oldData, newData, rollbackDir)
+ c.Assert(err, Equals, gadget.ErrNoUpdate)
+}
+
+func (u *updateTestSuite) TestUpdateApplyBackupFails(c *C) {
+ oldData, newData, rollbackDir := updateDataSet(c)
+ // update both structs
+ newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1
+ newData.Info.Volumes["foo"].Structure[1].Update.Edition = 1
+ newData.Info.Volumes["foo"].Structure[2].Update.Edition = 3
+
+ updaterForStructureCalls := 0
+ restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string) (gadget.Updater, error) {
+ updater := &mockUpdater{
+ updateCb: func() error {
+ c.Fatalf("unexpected update call")
+ return errors.New("not called")
+ },
+ rollbackCb: func() error {
+ c.Fatalf("unexpected rollback call")
+ return errors.New("not called")
+ },
+ }
+ if updaterForStructureCalls == 1 {
+ c.Assert(ps.Name, Equals, "second")
+ updater.backupCb = func() error {
+ return errors.New("failed")
+ }
+ }
+ updaterForStructureCalls++
+ return updater, nil
+ })
+ defer restore()
+
+ // go go go
+ err := gadget.Update(oldData, newData, rollbackDir)
+ c.Assert(err, ErrorMatches, `cannot backup volume structure #1 \("second"\): failed`)
+}
+
+func (u *updateTestSuite) TestUpdateApplyUpdateFailsThenRollback(c *C) {
+ oldData, newData, rollbackDir := updateDataSet(c)
+ // update all structs
+ newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1
+ newData.Info.Volumes["foo"].Structure[1].Update.Edition = 2
+ newData.Info.Volumes["foo"].Structure[2].Update.Edition = 3
+
+ updateCalls := make(map[string]bool)
+ backupCalls := make(map[string]bool)
+ rollbackCalls := make(map[string]bool)
+ updaterForStructureCalls := 0
+ restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string) (gadget.Updater, error) {
+ updater := &mockUpdater{
+ backupCb: func() error {
+ backupCalls[ps.Name] = true
+ return nil
+ },
+ rollbackCb: func() error {
+ rollbackCalls[ps.Name] = true
+ return nil
+ },
+ updateCb: func() error {
+ updateCalls[ps.Name] = true
+ return nil
+ },
+ }
+ if updaterForStructureCalls == 1 {
+ c.Assert(ps.Name, Equals, "second")
+ // fail update of 2nd structure
+ updater.updateCb = func() error {
+ updateCalls[ps.Name] = true
+ return errors.New("failed")
+ }
+ }
+ updaterForStructureCalls++
+ return updater, nil
+ })
+ defer restore()
+
+ // go go go
+ err := gadget.Update(oldData, newData, rollbackDir)
+ c.Assert(err, ErrorMatches, `cannot update volume structure #1 \("second"\): failed`)
+ c.Assert(backupCalls, DeepEquals, map[string]bool{
+ // all were backed up
+ "first": true,
+ "second": true,
+ "third": true,
+ })
+ c.Assert(updateCalls, DeepEquals, map[string]bool{
+ "first": true,
+ "second": true,
+ // third was never updated, as second failed
+ })
+ c.Assert(rollbackCalls, DeepEquals, map[string]bool{
+ "first": true,
+ "second": true,
+ // third does not need as it was not updated
+ })
+}
+
+func (u *updateTestSuite) TestUpdateApplyUpdateErrorRollbackFail(c *C) {
+ logbuf, restore := logger.MockLogger()
+ defer restore()
+
+ oldData, newData, rollbackDir := updateDataSet(c)
+ // update all structs
+ newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1
+ newData.Info.Volumes["foo"].Structure[1].Update.Edition = 2
+ newData.Info.Volumes["foo"].Structure[2].Update.Edition = 3
+
+ updateCalls := make(map[string]bool)
+ backupCalls := make(map[string]bool)
+ rollbackCalls := make(map[string]bool)
+ updaterForStructureCalls := 0
+ restore = gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string) (gadget.Updater, error) {
+ updater := &mockUpdater{
+ backupCb: func() error {
+ backupCalls[ps.Name] = true
+ return nil
+ },
+ rollbackCb: func() error {
+ rollbackCalls[ps.Name] = true
+ return nil
+ },
+ updateCb: func() error {
+ updateCalls[ps.Name] = true
+ return nil
+ },
+ }
+ switch updaterForStructureCalls {
+ case 1:
+ c.Assert(ps.Name, Equals, "second")
+ // rollback fails on 2nd structure
+ updater.rollbackCb = func() error {
+ rollbackCalls[ps.Name] = true
+ return errors.New("rollback failed with different error")
+ }
+ case 2:
+ c.Assert(ps.Name, Equals, "third")
+ // fail update of 3rd structure
+ updater.updateCb = func() error {
+ updateCalls[ps.Name] = true
+ return errors.New("update error")
+ }
+ }
+ updaterForStructureCalls++
+ return updater, nil
+ })
+ defer restore()
+
+ // go go go
+ err := gadget.Update(oldData, newData, rollbackDir)
+ // preserves update error
+ c.Assert(err, ErrorMatches, `cannot update volume structure #2 \("third"\): update error`)
+ c.Assert(backupCalls, DeepEquals, map[string]bool{
+ // all were backed up
+ "first": true,
+ "second": true,
+ "third": true,
+ })
+ c.Assert(updateCalls, DeepEquals, map[string]bool{
+ "first": true,
+ "second": true,
+ "third": true,
+ })
+ c.Assert(rollbackCalls, DeepEquals, map[string]bool{
+ "first": true,
+ "second": true,
+ "third": true,
+ })
+
+ c.Check(logbuf.String(), testutil.Contains, `cannot update gadget: cannot update volume structure #2 ("third"): update error`)
+ c.Check(logbuf.String(), testutil.Contains, `cannot rollback volume structure #1 ("second") update: rollback failed with different error`)
+}
+
+func (u *updateTestSuite) TestUpdateApplyBadUpdater(c *C) {
+ oldData, newData, rollbackDir := updateDataSet(c)
+ // update all structs
+ newData.Info.Volumes["foo"].Structure[0].Update.Edition = 1
+ newData.Info.Volumes["foo"].Structure[1].Update.Edition = 2
+ newData.Info.Volumes["foo"].Structure[2].Update.Edition = 3
+
+ restore := gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, psRootDir, psRollbackDir string) (gadget.Updater, error) {
+ return nil, errors.New("bad updater for structure")
+ })
+ defer restore()
+
+ // go go go
+ err := gadget.Update(oldData, newData, rollbackDir)
+ c.Assert(err, ErrorMatches, `cannot prepare update for volume structure #0 \("first"\): bad updater for structure`)
+}
+
+func (u *updateTestSuite) TestUpdaterForStructure(c *C) {
+ rootDir := c.MkDir()
+ rollbackDir := c.MkDir()
+
+ psBare := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Filesystem: "none",
+ Size: 10 * gadget.SizeMiB,
+ },
+ StartOffset: 1 * gadget.SizeMiB,
+ }
+ updater, err := gadget.UpdaterForStructure(psBare, rootDir, rollbackDir)
+ c.Assert(err, IsNil)
+ c.Assert(updater, FitsTypeOf, &gadget.RawStructureUpdater{})
+
+ psFs := &gadget.LaidOutStructure{
+ VolumeStructure: &gadget.VolumeStructure{
+ Filesystem: "ext4",
+ Size: 10 * gadget.SizeMiB,
+ },
+ StartOffset: 1 * gadget.SizeMiB,
+ }
+ updater, err = gadget.UpdaterForStructure(psFs, rootDir, rollbackDir)
+ c.Assert(err, IsNil)
+ c.Assert(updater, FitsTypeOf, &gadget.MountedFilesystemUpdater{})
+
+ // trigger errors
+ updater, err = gadget.UpdaterForStructure(psBare, rootDir, "")
+ c.Assert(err, ErrorMatches, "internal error: backup directory cannot be unset")
+ c.Assert(updater, IsNil)
+
+ updater, err = gadget.UpdaterForStructure(psFs, "", rollbackDir)
+ c.Assert(err, ErrorMatches, "internal error: gadget content directory cannot be unset")
+ c.Assert(updater, IsNil)
+}
+
+func (u *updateTestSuite) TestUpdaterMultiVolumesDoesNotError(c *C) {
+ logbuf, restore := logger.MockLogger()
+ defer restore()
+
+ multiVolume := gadget.GadgetData{
+ Info: &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "1": {},
+ "2": {},
+ },
+ },
+ }
+ singleVolume := gadget.GadgetData{
+ Info: &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "1": {},
+ },
+ },
+ }
+
+ // a new multi volume gadget update gives no error
+ err := gadget.Update(singleVolume, multiVolume, "some-rollback-dir")
+ c.Assert(err, IsNil)
+ // but it warns that nothing happens either
+ c.Assert(logbuf.String(), testutil.Contains, "WARNING: gadget assests cannot be updated yet when multiple volumes are used")
+
+ // same for old
+ err = gadget.Update(multiVolume, singleVolume, "some-rollback-dir")
+ c.Assert(err, IsNil)
+ c.Assert(strings.Count(logbuf.String(), "WARNING: gadget assests cannot be updated yet when multiple volumes are used"), Equals, 2)
+}
diff -Nru snapd-2.40/HACKING.md snapd-2.42.1/HACKING.md
--- snapd-2.40/HACKING.md 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/HACKING.md 2019-10-30 12:17:43.000000000 +0000
@@ -8,7 +8,8 @@
### Supported Go versions
-snapd is supported on Go 1.6 onwards.
+From snapd 2.38, snapd supports Go 1.9 and onwards. For earlier snapd
+releases, snapd supports Go 1.6.
### Setting up a GOPATH
@@ -108,8 +109,8 @@
Canonical contributor license agreement at
http://www.ubuntu.com/legal/contributors
-Snapd can be found on Github, so in order to fork the source and contribute,
-go to https://github.com/snapcore/snapd. Check out [Github's help
+Snapd can be found on GitHub, so in order to fork the source and contribute,
+go to https://github.com/snapcore/snapd. Check out [GitHub's help
pages](https://help.github.com/) to find out how to set up your local branch,
commit changes and create pull requests.
@@ -126,9 +127,11 @@
This will check if the source format is consistent, that it builds, all tests
work as expected and that "go vet" has nothing to complain.
-The source format follows the `gofmt -s` formating. Please run this on your sources files if `run-checks` complains about the format.
+The source format follows the `gofmt -s` formating. Please run this on your
+source files if `run-checks` complains about the format.
-You can run individual test for a sub-package by changing into that directory and:
+You can run an individual test for a sub-package by changing into that
+directory and:
go test -check.f $testname
@@ -140,33 +143,64 @@
There is more to read about the testing framework on the [website](https://labix.org/gocheck)
-### Running the spread tests
+### Running spread tests
-To run the spread tests locally you need the latest version of spread
-from https://github.com/snapcore/spread. It can be installed via:
+To run the spread tests locally via QEMU, you need the latest version of
+[spread](https://github.com/snapcore/spread). You can get spread, QEMU, and the
+build tools to build QEMU images with:
- $ sudo apt install qemu-kvm autopkgtest
- $ sudo snap install --devmode spread
+ $ sudo apt update && sudo apt install -y qemu-kvm autopkgtest
+ $ curl https://niemeyer.s3.amazonaws.com/spread-amd64.tar.gz | tar -xz -C $GOPATH/bin
-Then setup the environment via:
+#### Building spread VM images
- $ mkdir -p .spread/qemu
- $ cd .spread/qemu
- # For xenial (same works for yakkety/zesty)
- $ adt-buildvm-ubuntu-cloud -r xenial
- $ mv adt-xenial-amd64-cloud.img ubuntu-16.04.img
- # For trusty
- $ adt-buildvm-ubuntu-cloud -r trusty --post-command='sudo apt-get install -y --install-recommends linux-generic-lts-xenial && update-grub'
+To run the spread tests via QEMU you need to create VM images in the
+`~/.spread/qemu` directory:
+
+ $ mkdir -p ~/.spread/qemu
+ $ cd ~/.spread/qemu
+
+Assuming you are building on Ubuntu 18.04 LTS (Bionic Beaver) (or a later
+development release like Ubuntu 19.04 (Disco Dingo)), run the following to
+build a 64-bit Ubuntu 16.04 LTS (Xenial Xerus) VM to run the spread tests on:
+
+ $ autopkgtest-buildvm-ubuntu-cloud -r xenial
+ $ mv autopkgtest-xenial-amd64.img ubuntu-16.04-64.img
+
+To build an Ubuntu 14.04 (Trusty Tahr) based VM, use:
+
+ $ autopkgtest-buildvm-ubuntu-cloud -r trusty --post-command='sudo apt-get install -y --install-recommends linux-generic-lts-xenial && update-grub'
$ mv adt-trusty-amd64-cloud.img ubuntu-14.04-64.img
+This is because we need at least 4.4+ kernel for snapd to run on Ubuntu 14.04
+LTS, which is available through the `linux-generic-lts-xenial` package.
+
+If you are running Ubuntu 16.04 LTS, use
+`adt-buildvm-ubuntu-cloud` instead of `autopkgtest-buildvm-ubuntu-cloud` (the
+latter replaced the former in 18.04):
+
+ $ adt-buildvm-ubuntu-cloud -r xenial
+ $ mv adt-xenial-amd64-cloud.img ubuntu-16.04-64.img
+
+#### Downloading spread VM images
+
+Alternatively, instead of building the QEMU images manually, you can download
+pre-built and somewhat maintained images from
+[spread.zygoon.pl](spread.zygoon.pl). The images will need to be extracted
+with `gunzip` and placed into `~/.spread/qemu` as above.
+
+#### Running spread with QEMU
+
+Finally, you can run the spread tests for Ubuntu 16.04 LTS 64-bit with:
-And you can run the tests via:
+ $ spread -v qemu:ubuntu-16.04-64
- $ spread -v qemu:
+To run for a different system, replace `ubuntu-16.04-64` with a different system
+name.
For quick reuse you can use:
- $ spread -reuse qemu:
+ $ spread -reuse qemu:ubuntu-16.04-64
It will print how to reuse the systems. Make sure to use
`export REUSE_PROJECT=1` in your environment too.
@@ -200,7 +234,7 @@
./mkversion.sh
cd cmd/
autoreconf -i -f
-./configure --prefix=/usr --libexecdir=/usr/lib/snapd --enable-nvidia-ubuntu
+./configure --prefix=/usr --libexecdir=/usr/lib/snapd
```
This will drop makefiles and let you build stuff. You may find the `make hack`
@@ -209,4 +243,5 @@
## Submitting patches
-Please run `make fmt` before sending your patches.
+Please run `(cd cmd; make fmt)` before sending your patches for the "C" part of
+the source code.
diff -Nru snapd-2.40/httputil/retry.go snapd-2.42.1/httputil/retry.go
--- snapd-2.40/httputil/retry.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/httputil/retry.go 2019-10-30 12:17:43.000000000 +0000
@@ -69,6 +69,28 @@
return resp.StatusCode >= 500
}
+// isHttp2ProtocolError returns true if the given error is a http2
+// stream error with code 0x1 (PROTOCOL_ERROR).
+//
+// Unfortunately it seems this is not easy to detect. In e3be142 this
+// code tried to be smart and detect this via http2.StreamError but it
+// seems like with the h2_bundle.go in the go distro this does not
+// work, i.e. in https://travis-ci.org/snapcore/snapd/jobs/575471665
+// we still got protocol errors even with this detection code.
+//
+// So this code falls back to simple and naive detection.
+func isHttp2ProtocolError(err error) bool {
+ if strings.Contains(err.Error(), "PROTOCOL_ERROR") {
+ return true
+ }
+ // here is what a protocol error may look like:
+ // "DEBUG: Not retrying: http.http2StreamError{StreamID:0x1, Code:0x1, Cause:error(nil)}"
+ if strings.Contains(err.Error(), "http2StreamError") && strings.Contains(err.Error(), "Code:0x1,") {
+ return true
+ }
+ return false
+}
+
func ShouldRetryError(attempt *retry.Attempt, err error) bool {
if !attempt.More() {
return false
@@ -127,11 +149,20 @@
logger.Debugf("Encountered non temporary net.OpError: %#v", opErr)
}
- if err == io.ErrUnexpectedEOF || err == io.EOF {
+ // we see this from http2 downloads sometimes - it is unclear what
+ // is causing it but https://github.com/golang/go/issues/29125
+ // indicates a retry might be enough. Note that we get the
+ // PROTOCOL_ERROR *from* the remote side (fastly it seems)
+ if isHttp2ProtocolError(err) {
logger.Debugf("Retrying because of: %s", err)
return true
}
+ if err == io.ErrUnexpectedEOF || err == io.EOF {
+ logger.Debugf("Retrying because of: %s (%s)", err, err)
+ return true
+ }
+
if osutil.GetenvBool("SNAPD_DEBUG") {
logger.Debugf("Not retrying: %#v", err)
}
diff -Nru snapd-2.40/httputil/retry_test.go snapd-2.42.1/httputil/retry_test.go
--- snapd-2.40/httputil/retry_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/httputil/retry_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -26,6 +26,7 @@
"net"
"net/http"
"net/http/httptest"
+ "net/url"
"sync"
"time"
@@ -478,6 +479,24 @@
}
}
readResponseBody := func(resp *http.Response) error {
+ return nil
+ }
+ _, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
+ c.Assert(err, NotNil)
+ c.Assert(n > 1, Equals, true, Commentf("%v not > 1", n))
+}
+
+func (s *retrySuite) TestRetryOnHttp2ProtocolErrors(c *C) {
+ n := 0
+ doRequest := func() (*http.Response, error) {
+ n++
+ return nil, &url.Error{
+ Op: "Get",
+ URL: "http://...",
+ Err: fmt.Errorf("http.http2StreamError{StreamID:0x1, Code:0x1, Cause:error(nil)}"),
+ }
+ }
+ readResponseBody := func(resp *http.Response) error {
return nil
}
_, err := httputil.RetryRequest("endp", doRequest, readResponseBody, testRetryStrategy)
diff -Nru snapd-2.40/httputil/useragent.go snapd-2.42.1/httputil/useragent.go
--- snapd-2.40/httputil/useragent.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/httputil/useragent.go 2019-10-30 12:17:43.000000000 +0000
@@ -64,7 +64,7 @@
// assumption checks out in practice, q.v. https://github.com/zyga/os-release-zoo
userAgent = fmt.Sprintf("snapd/%v (%s)%s %s/%s (%s) linux/%s", version,
strings.Join(extras, "; "), extraProdStr, release.ReleaseInfo.ID,
- release.ReleaseInfo.VersionID, string(arch.UbuntuArchitecture()),
+ release.ReleaseInfo.VersionID, string(arch.DpkgArchitecture()),
sanitizeKernelVersion(osutil.KernelVersion()))
return func() {
userAgent = origUserAgent
diff -Nru snapd-2.40/i18n/i18n.go snapd-2.42.1/i18n/i18n.go
--- snapd-2.40/i18n/i18n.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/i18n/i18n.go 2019-10-30 12:17:43.000000000 +0000
@@ -27,7 +27,7 @@
"path/filepath"
"strings"
- "github.com/ojii/gettext.go"
+ "github.com/snapcore/go-gettext"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
diff -Nru snapd-2.40/image/export_test.go snapd-2.42.1/image/export_test.go
--- snapd-2.40/image/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/image/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -31,7 +31,6 @@
var (
LocalSnaps = localSnaps
DecodeModelAssertion = decodeModelAssertion
- DownloadUnpackGadget = downloadUnpackGadget
SetupSeed = setupSeed
InstallCloudConfig = installCloudConfig
SnapChannel = snapChannel
diff -Nru snapd-2.40/image/helpers.go snapd-2.42.1/image/helpers.go
--- snapd-2.40/image/helpers.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/image/helpers.go 2019-10-30 12:17:43.000000000 +0000
@@ -214,6 +214,8 @@
Channel string
CohortKey string
Basename string
+
+ LeavePartialOnError bool
}
var (
@@ -322,7 +324,8 @@
os.Exit(1)
}()
- if err = sto.Download(context.TODO(), name, targetFn, &snap.DownloadInfo, pb, tsto.user, nil); err != nil {
+ dlOpts := &store.DownloadOptions{LeavePartialOnError: opts.LeavePartialOnError}
+ if err = sto.Download(context.TODO(), name, targetFn, &snap.DownloadInfo, pb, tsto.user, dlOpts); err != nil {
return "", nil, err
}
diff -Nru snapd-2.40/image/helpers_test.go snapd-2.42.1/image/helpers_test.go
--- snapd-2.40/image/helpers_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/image/helpers_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -33,6 +33,7 @@
func (s *imageSuite) TestDownloadOptionsString(c *check.C) {
for opts, str := range map[image.DownloadOptions]string{
+ {LeavePartialOnError: true}: "",
{}: "",
{TargetDir: "/foo"}: `in "/foo"`,
{Basename: "foo"}: `to "foo.snap"`,
@@ -78,6 +79,9 @@
Basename: "/foo",
}: image.ErrPathInBase,
} {
+ opts.LeavePartialOnError = true
+ c.Check(opts.Validate(), check.Equals, err)
+ opts.LeavePartialOnError = false
c.Check(opts.Validate(), check.Equals, err)
}
}
diff -Nru snapd-2.40/image/image.go snapd-2.42.1/image/image.go
--- snapd-2.40/image/image.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/image/image.go 2019-10-30 12:17:43.000000000 +0000
@@ -25,6 +25,7 @@
"io/ioutil"
"os"
"path/filepath"
+ "sort"
"strings"
"syscall"
@@ -32,11 +33,12 @@
"github.com/snapcore/snapd/asserts/snapasserts"
"github.com/snapcore/snapd/asserts/sysdb"
"github.com/snapcore/snapd/boot"
- "github.com/snapcore/snapd/bootloader"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/seed"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/channel"
"github.com/snapcore/snapd/snap/squashfs"
"github.com/snapcore/snapd/strutil"
)
@@ -175,7 +177,7 @@
// classicHasSnaps returns whether the model or options specify any snaps for the classic case
func classicHasSnaps(model *asserts.Model, opts *Options) bool {
- return model.Gadget() != "" || len(model.RequiredSnaps()) != 0 || len(opts.Snaps) != 0
+ return model.Gadget() != "" || len(model.RequiredNoEssentialSnaps()) != 0 || len(opts.Snaps) != 0
}
func Prepare(opts *Options) error {
@@ -207,7 +209,7 @@
if err := validateNonLocalSnaps(opts.Snaps); err != nil {
return err
}
- if _, err := snap.ParseChannel(opts.Channel, ""); err != nil {
+ if _, err := channel.Parse(opts.Channel, ""); err != nil {
return fmt.Errorf("cannot use channel: %v", err)
}
@@ -227,9 +229,9 @@
}
if !opts.Classic {
- // unpacking the gadget for core models
- if err := downloadUnpackGadget(tsto, model, opts, local); err != nil {
- return err
+ // create directory for later unpacking the gadget in
+ if err := os.MkdirAll(opts.GadgetUnpackDir, 0755); err != nil {
+ return fmt.Errorf("cannot create gadget unpack dir %q: %s", opts.GadgetUnpackDir, err)
}
}
@@ -273,6 +275,11 @@
// fallback to default channel
snapChannel = opts.Channel
}
+ if snapChannel != "" {
+ if _, err := channel.ParseVerbatim(snapChannel, "-"); err != nil {
+ return "", fmt.Errorf("cannot use option channel for snap %q: %v", name, err)
+ }
+ }
// consider snap types that can be pinned to a track by the model
var pinnedTrack string
var kind string
@@ -284,58 +291,21 @@
kind = "kernel"
pinnedTrack = model.KernelTrack()
}
- if pinnedTrack != "" {
- ch, err := makeChannelFromTrack(kind, pinnedTrack, snapChannel)
- if err != nil {
- return "", err
- }
- snapChannel = ch
-
+ ch, err := channel.ResolveLocked(pinnedTrack, snapChannel)
+ if err == channel.ErrLockedTrackSwitch {
+ return "", fmt.Errorf("channel %q for %s has a track incompatible with the track from model assertion: %s", snapChannel, kind, pinnedTrack)
}
- return snapChannel, nil
-}
-
-func makeChannelFromTrack(what, track, snapChannel string) (string, error) {
- mch, err := snap.ParseChannel(track, "")
if err != nil {
- return "", fmt.Errorf("cannot use track %q for %s from model assertion: %v", track, what, err)
- }
- if snapChannel != "" {
- ch, err := snap.ParseChannelVerbatim(snapChannel, "")
- if err != nil {
- return "", fmt.Errorf("cannot parse channel %q for %s", snapChannel, what)
- }
- if ch.Track != "" && ch.Track != mch.Track {
- return "", fmt.Errorf("channel %q for %s has a track incompatible with the track from model assertion: %s", snapChannel, what, track)
- }
- mch.Risk = ch.Risk
+ return "", err
}
- return mch.Clean().String(), nil
+ return ch, nil
}
-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)
- }
-
- gadgetName := model.Gadget()
- gadgetChannel, err := snapChannel(gadgetName, model, opts, local)
- if err != nil {
- return err
- }
-
- dlOpts := &DownloadOptions{
- TargetDir: opts.GadgetUnpackDir,
- Channel: gadgetChannel,
- }
- snapFn, _, err := acquireSnap(tsto, gadgetName, dlOpts, local)
- if err != nil {
- return err
- }
+func unpackGadget(gadgetFname, gadgetUnpackDir string) error {
// FIXME: jumping through layers here, we need to make
// unpack part of the container interface (again)
- snap := squashfs.New(snapFn)
- return snap.Unpack("*", opts.GadgetUnpackDir)
+ snap := squashfs.New(gadgetFname)
+ return snap.Unpack("*", gadgetUnpackDir)
}
func acquireSnap(tsto *ToolingStore, name string, dlOpts *DownloadOptions, local *localInfos) (downloadedSnap string, info *snap.Info, err error) {
@@ -394,43 +364,6 @@
}
}
-// neededDefaultProviders returns the names of all default-providers for
-// the content plugs that the given snap.Info needs.
-func neededDefaultProviders(info *snap.Info) (cps []string) {
- for _, plug := range info.Plugs {
- if plug.Interface == "content" {
- var dprovider string
- if err := plug.Attr("default-provider", &dprovider); err == nil && dprovider != "" {
- cps = append(cps, dprovider)
- }
- }
- }
- return cps
-}
-
-// hasBase checks if the given snap has a base in the given localInfos and
-// snaps. If not an error is returned.
-func hasBase(snap *snap.Info, local *localInfos, snaps []string) error {
- // snap needs no base (or it simply needs core which is never listed explicitly): nothing to do
- if snap.Base == "" {
- return nil
- }
-
- // snap explicitly listed as not needing a base snap (e.g. a content-only snap)
- if snap.Base == "none" {
- return nil
- }
-
- // core provides everything that core16 needs
- if snap.Base == "core16" && local.hasName(snaps, "core") {
- return nil
- }
- if local.hasName(snaps, snap.Base) {
- return nil
- }
- return fmt.Errorf("cannot add snap %q without also adding its base %q explicitly", snap.InstanceName(), snap.Base)
-}
-
func setupSeed(tsto *ToolingStore, model *asserts.Model, opts *Options, local *localInfos) error {
if model.Classic() != opts.Classic {
return fmt.Errorf("internal error: classic model but classic mode not set")
@@ -465,7 +398,7 @@
if !osutil.GetenvBool("UBUNTU_IMAGE_SKIP_COPY_UNVERIFIED_MODEL") {
return fmt.Errorf("cannot fetch and check prerequisites for the model assertion: %v", err)
} else {
- fmt.Fprintf(Stderr, "WARNING: Cannot fetch and check prerequisites for the model assertion, it will not be copied into the image making it unusable (unless this is a test): %v\n", err)
+ fmt.Fprintf(Stderr, "WARNING: cannot fetch and check prerequisites for the model assertion, it will not be copied into the image making it unusable (unless this is a test): %v\n", err)
f.addedRefs = nil
}
}
@@ -480,24 +413,50 @@
}
baseName := defaultCore
+ basesAndApps := []string{}
if model.Base() != "" {
baseName = model.Base()
+ basesAndApps = append(basesAndApps, baseName)
+
+ }
+
+ if !opts.Classic {
+ if err := os.MkdirAll(dirs.SnapBlobDir, 0755); err != nil {
+ return err
+ }
+ }
+
+ // TODO|XXX: later use the refs directly and simplify
+ reqSnaps := make([]string, 0, len(model.RequiredNoEssentialSnaps()))
+ for _, reqRef := range model.RequiredNoEssentialSnaps() {
+ reqSnaps = append(reqSnaps, reqRef.SnapName())
+ }
+
+ basesAndApps = append(basesAndApps, reqSnaps...)
+ basesAndApps = append(basesAndApps, opts.Snaps...)
+ // TODO: required snaps should get their base from required
+ // snaps (mentioned in the model); additional snaps could
+ // fetch their base, providers instead if not already present
+ // Be careful about core vs core16
+
+ seed := &imageSeed{
+ model: model,
+ baseName: baseName,
+ opts: opts,
+ local: local,
+ tsto: tsto,
+ basesAndApps: basesAndApps,
+ snapSeedDir: snapSeedDir,
+ f: f,
+ db: db,
+ seen: make(map[string]bool),
+ downloadedSnapsInfoForBootConfig: make(map[string]*snap.Info),
}
snaps := []string{}
// always add an implicit snapd first when a base is used
if model.Base() != "" {
snaps = append(snaps, "snapd")
- // TODO: once we order snaps by what they need this
- // can go aways
- // Here we ensure that "core" is seeded very early
- // when bases are in use. This fixes the issue
- // that when people use model assertions with
- // required snaps like bluez which at this point
- // still requires core will hang forever in seeding.
- if strutil.ListContains(model.RequiredSnaps(), "core") || local.hasName(opts.Snaps, "core") {
- snaps = append(snaps, "core")
- }
}
if !opts.Classic {
@@ -506,140 +465,45 @@
snaps = append(snaps, model.Kernel())
snaps = append(snaps, model.Gadget())
} else {
- // classic image case: first core as needed and gadget
- if classicHasSnaps(model, opts) {
- // TODO: later use snapd+core16 or core18 if specified
- snaps = append(snaps, "core")
- }
+ // classic image case
if model.Gadget() != "" {
snaps = append(snaps, model.Gadget())
}
}
// then required and the user requested stuff
- snaps = append(snaps, model.RequiredSnaps()...)
+ snaps = append(snaps, reqSnaps...)
snaps = append(snaps, opts.Snaps...)
- if !opts.Classic {
- if err := os.MkdirAll(dirs.SnapBlobDir, 0755); err != nil {
- return err
- }
- }
-
- seen := make(map[string]bool)
- var locals []string
- downloadedSnapsInfoForBootConfig := map[string]*snap.Info{}
- var seedYaml snap.Seed
for _, snapName := range snaps {
- name := local.Name(snapName)
- if seen[name] {
- fmt.Fprintf(Stdout, "%s already prepared, skipping\n", name)
- continue
- }
-
- if local.IsLocal(name) {
- fmt.Fprintf(Stdout, "Copying %q (%s)\n", local.Path(name), name)
- } else {
- fmt.Fprintf(Stdout, "Fetching %s\n", name)
- }
-
- snapChannel, err := snapChannel(name, model, opts, local)
- if err != nil {
+ if err := seed.add(snapName); err != nil {
return err
}
+ }
- dlOpts := &DownloadOptions{
- TargetDir: snapSeedDir,
- Channel: snapChannel,
+ // provide snapd or core
+ if len(seed.needsCore) != 0 && !seed.seen["core"] {
+ // one of the snaps requires core as base
+ // which used to be implicit, add it
+ if model.Base() != "" {
+ // TODO: later turn this into an error? for sure for UC20
+ fmt.Fprintf(Stderr, "WARNING: model has base %q but some snaps (%s) require \"core\" as base as well, for compatibility it was added implicitly, adding \"core\" explicitly is recommended\n", model.Base(), strutil.Quoted(seed.needsCore))
}
- fn, info, err := acquireSnap(tsto, name, dlOpts, local)
- if err != nil {
+ if err := seed.add("core"); err != nil {
return err
}
-
- // Sanity check, note that we could support this case
- // if we have a use-case but it requires changes in the
- // devicestate/firstboot.go ordering code.
- if info.GetType() == snap.TypeGadget && info.Base != model.Base() {
- return fmt.Errorf("cannot use gadget snap because its base %q is different from model base %q", info.Base, model.Base())
- }
- if err := hasBase(info, local, snaps); err != nil {
+ } else if opts.Classic && len(seed.seen) != 0 {
+ if err := seed.add("snapd"); err != nil {
return err
}
- // warn about missing default providers
- for _, dp := range neededDefaultProviders(info) {
- if !local.hasName(snaps, dp) {
- // TODO: have a way to ignore this issue on a snap by snap basis?
- return fmt.Errorf("cannot use snap %q without its default content provider %q being added explicitly", info.InstanceName(), dp)
- }
- }
-
- seen[name] = true
- typ := info.GetType()
-
- needsClassic := info.NeedsClassic()
- if needsClassic && !opts.Classic {
- return fmt.Errorf("cannot use classic snap %q in a core system", info.InstanceName())
- }
-
- // if it comes from the store fetch the snap assertions too
- if info.SnapID != "" {
- snapDecl, err := FetchAndCheckSnapAssertions(fn, info, f, db)
- if err != nil {
- return err
- }
- var kind string
- switch typ {
- case snap.TypeKernel:
- kind = "kernel"
- case snap.TypeGadget:
- kind = "gadget"
- }
- if kind != "" { // kernel or gadget
- // TODO: share helpers with devicestate if the policy becomes much more complicated
- publisher := snapDecl.PublisherID()
- if publisher != model.BrandID() && publisher != "canonical" {
- return fmt.Errorf("cannot use %s %q published by %q for model by %q", kind, name, publisher, model.BrandID())
- }
- }
- } else {
- locals = append(locals, name)
- // local snaps have no channel
- snapChannel = ""
- }
-
- // kernel/os/model.base are required for booting on core
- if !opts.Classic && (typ == snap.TypeKernel || name == baseName) {
- dst := filepath.Join(dirs.SnapBlobDir, filepath.Base(fn))
- // construct a relative symlink from the blob dir
- // to the seed file
- relSymlink, err := filepath.Rel(dirs.SnapBlobDir, fn)
- if err != nil {
- return fmt.Errorf("cannot build symlink: %v", err)
- }
- if err := os.Symlink(relSymlink, dst); err != nil {
- return err
- }
- // store the snap.Info for kernel/os/base so
- // that the bootload can DTRT
- downloadedSnapsInfoForBootConfig[dst] = info
- }
+ }
- // set seed.yaml
- seedYaml.Snaps = append(seedYaml.Snaps, &snap.SeedSnap{
- Name: info.InstanceName(),
- SnapID: info.SnapID, // cross-ref
- Channel: snapChannel,
- File: filepath.Base(fn),
- DevMode: info.NeedsDevMode(),
- Classic: needsClassic,
- Contact: info.Contact,
- // no assertions for this snap were put in the seed
- Unasserted: info.SnapID == "",
- })
+ if len(seed.needsCore16) != 0 && !seed.seen["core"] {
+ return fmt.Errorf(`cannot use %s requiring base "core16" without adding "core16" (or "core") explicitly`, strutil.Quoted(seed.needsCore16))
}
- if len(locals) > 0 {
- fmt.Fprintf(Stderr, "WARNING: %s were installed from local snaps disconnected from a store and cannot be refreshed subsequently!\n", strutil.Quoted(locals))
+
+ if len(seed.locals) > 0 {
+ fmt.Fprintf(Stderr, "WARNING: %s installed from local snaps disconnected from a store cannot be refreshed subsequently!\n", strutil.Quoted(seed.locals))
}
// fetch device store assertion (and prereqs) if available
@@ -670,9 +534,8 @@
}
}
- // TODO: add the refs as an assertions list of maps section to seed.yaml
-
seedFn := filepath.Join(dirs.SnapSeedDir, "seed.yaml")
+ seedYaml := seed.seedYaml()
if err := seedYaml.Write(seedFn); err != nil {
return fmt.Errorf("cannot write seed.yaml: %s", err)
}
@@ -691,12 +554,12 @@
}
if !opts.Classic {
- // now do the bootloader stuff
- if err := bootloader.InstallBootConfig(opts.GadgetUnpackDir); err != nil {
+ // unpacking the gadget for core models
+ if err := unpackGadget(seed.gadgetFname, opts.GadgetUnpackDir); err != nil {
return err
}
- if err := setBootvars(downloadedSnapsInfoForBootConfig, model); err != nil {
+ if err := boot.MakeBootable(model, opts.RootDir, seed.downloadedSnapsInfoForBootConfig, opts.GadgetUnpackDir); err != nil {
return err
}
@@ -709,78 +572,198 @@
return nil
}
-func setBootvars(downloadedSnapsInfoForBootConfig map[string]*snap.Info, model *asserts.Model) error {
- if len(downloadedSnapsInfoForBootConfig) != 2 {
- return fmt.Errorf("setBootvars can only be called with exactly one kernel and exactly one core/base boot info: %v", downloadedSnapsInfoForBootConfig)
+type seedEntry struct {
+ snap *seed.Snap16
+ snapType snap.Type
+}
+
+type seedEntriesByType []seedEntry
+
+func (e seedEntriesByType) Len() int { return len(e) }
+func (e seedEntriesByType) Swap(i, j int) { e[i], e[j] = e[j], e[i] }
+func (e seedEntriesByType) Less(i, j int) bool {
+ return e[i].snapType.SortsBefore(e[j].snapType)
+}
+
+type imageSeed struct {
+ model *asserts.Model
+ baseName string
+
+ opts *Options
+ local *localInfos
+
+ tsto *ToolingStore
+
+ basesAndApps []string
+
+ snapSeedDir string
+ f asserts.Fetcher
+ db *asserts.Database
+
+ entries seedEntriesByType
+
+ seen map[string]bool
+ locals []string
+ downloadedSnapsInfoForBootConfig map[string]*snap.Info
+ gadgetFname string
+
+ needsCore []string
+ needsCore16 []string
+}
+
+func (s *imageSeed) add(snapName string) error {
+ model := s.model
+ opts := s.opts
+ local := s.local
+
+ name := local.Name(snapName)
+ if s.seen[name] {
+ fmt.Fprintf(Stdout, "%s already prepared, skipping\n", name)
+ return nil
+ }
+
+ if local.IsLocal(name) {
+ fmt.Fprintf(Stdout, "Copying %q (%s)\n", local.Path(name), name)
+ } else {
+ fmt.Fprintf(Stdout, "Fetching %s\n", name)
}
- // Set bootvars for kernel/core snaps so the system boots and
- // does the first-time initialization. There is also no
- // mounted kernel/core/base snap, but just the blobs.
- loader, err := bootloader.Find()
+ snapChannel, err := snapChannel(name, model, opts, local)
if err != nil {
- return fmt.Errorf("cannot set kernel/core boot variables: %s", err)
+ return err
}
- snaps, err := filepath.Glob(filepath.Join(dirs.SnapBlobDir, "*.snap"))
- if len(snaps) == 0 || err != nil {
- return fmt.Errorf("internal error: cannot find core/kernel snap")
+ dlOpts := &DownloadOptions{
+ TargetDir: s.snapSeedDir,
+ Channel: snapChannel,
+ }
+ fn, info, err := acquireSnap(s.tsto, name, dlOpts, local)
+ if err != nil {
+ return err
}
- m := map[string]string{
- "snap_mode": "",
- "snap_try_core": "",
- "snap_try_kernel": "",
+ if err := s.checkBase(info); err != nil {
+ return err
}
- if model.DisplayName() != "" {
- m["snap_menuentry"] = model.DisplayName()
+ // warn about missing default providers
+ for _, dp := range snap.NeededDefaultProviders(info) {
+ if !local.hasName(s.basesAndApps, dp) {
+ // TODO: have a way to ignore this issue on a snap by snap basis?
+ return fmt.Errorf("cannot use snap %q without its default content provider %q being added explicitly", info.InstanceName(), dp)
+ }
}
- for _, fn := range snaps {
- bootvar := ""
+ s.seen[name] = true
+ typ := info.GetType()
- info := downloadedSnapsInfoForBootConfig[fn]
- if info == nil {
- // this should never happen, if it does print some
- // debug info
- keys := make([]string, 0, len(downloadedSnapsInfoForBootConfig))
- for k := range downloadedSnapsInfoForBootConfig {
- keys = append(keys, k)
- }
- return fmt.Errorf("cannot get download info for snap %s, available infos: %v", fn, keys)
+ needsClassic := info.NeedsClassic()
+ if needsClassic && !opts.Classic {
+ return fmt.Errorf("cannot use classic snap %q in a core system", info.InstanceName())
+ }
+
+ // if it comes from the store fetch the snap assertions too
+ if info.SnapID != "" {
+ snapDecl, err := FetchAndCheckSnapAssertions(fn, info, s.f, s.db)
+ if err != nil {
+ return err
}
- switch info.GetType() {
- case snap.TypeOS, snap.TypeBase:
- bootvar = "snap_core"
+ var kind string
+ switch typ {
case snap.TypeKernel:
- bootvar = "snap_kernel"
- if err := extractKernelAssets(fn, info); err != nil {
- return err
+ kind = "kernel"
+ case snap.TypeGadget:
+ kind = "gadget"
+ }
+ if kind != "" { // kernel or gadget
+ // TODO: share helpers with devicestate if the policy becomes much more complicated
+ publisher := snapDecl.PublisherID()
+ if publisher != model.BrandID() && publisher != "canonical" {
+ return fmt.Errorf("cannot use %s %q published by %q for model by %q", kind, name, publisher, model.BrandID())
}
}
+ } else {
+ s.locals = append(s.locals, name)
+ // local snaps have no channel
+ snapChannel = ""
+ }
- if bootvar != "" {
- name := filepath.Base(fn)
- m[bootvar] = name
- }
+ // kernel/os/model.base are required for booting on core
+ if !opts.Classic && (typ == snap.TypeKernel || name == s.baseName) {
+ // store the snap.Info for kernel/os/base so
+ // that boot.MakeBootable can DTRT
+ s.downloadedSnapsInfoForBootConfig[fn] = info
}
- if err := loader.SetBootVars(m); err != nil {
- return err
+ // remember the gadget for unpacking later
+ if !opts.Classic && typ == snap.TypeGadget {
+ s.gadgetFname = fn
}
+ s.entries = append(s.entries, seedEntry{
+ snap: &seed.Snap16{
+ Name: info.InstanceName(),
+ SnapID: info.SnapID, // cross-ref
+ Channel: snapChannel,
+ File: filepath.Base(fn),
+ DevMode: info.NeedsDevMode(),
+ Classic: needsClassic,
+ Contact: info.Contact,
+ // no assertions for this snap were put in the seed
+ Unasserted: info.SnapID == "",
+ },
+ snapType: typ,
+ })
+
return nil
}
-func extractKernelAssets(snapPath string, info *snap.Info) error {
- snapf, err := snap.Open(snapPath)
- if err != nil {
- return err
+// checkBase checks if the given snap has a base in the given localInfos and
+// snaps. If not an error is returned.
+func (s *imageSeed) checkBase(info *snap.Info) error {
+ // Sanity check, note that we could support this case
+ // if we have a use-case but it requires changes in the
+ // devicestate/firstboot.go ordering code.
+ if info.GetType() == snap.TypeGadget && !s.opts.Classic && info.Base != s.model.Base() {
+ return fmt.Errorf("cannot use gadget snap because its base %q is different from model base %q", info.Base, s.model.Base())
}
- if err := boot.ExtractKernelAssets(info, snapf); err != nil {
- return err
+ // snap needs no base (or it simply needs core which is never listed explicitly): nothing to do
+ if info.Base == "" {
+ if info.GetType() == snap.TypeGadget || info.GetType() == snap.TypeApp {
+ // remember to make sure we have core installed
+ s.needsCore = append(s.needsCore, info.SnapName())
+ }
+ return nil
}
- return nil
+
+ // snap explicitly listed as not needing a base snap (e.g. a content-only snap)
+ if info.Base == "none" {
+ return nil
+ }
+
+ if s.local.hasName(s.basesAndApps, info.Base) {
+ return nil
+ }
+
+ if info.Base == "core16" {
+ // check at the end
+ s.needsCore16 = append(s.needsCore16, info.SnapName())
+ return nil
+ }
+
+ return fmt.Errorf("cannot add snap %q without also adding its base %q explicitly", info.InstanceName(), info.Base)
+}
+
+func (s *imageSeed) seedYaml() *seed.Seed16 {
+ var seedYaml seed.Seed16
+
+ sort.Stable(s.entries)
+
+ seedYaml.Snaps = make([]*seed.Snap16, len(s.entries))
+ for i, e := range s.entries {
+ seedYaml.Snaps[i] = e.snap
+ }
+
+ return &seedYaml
}
func copyLocalSnapFile(snapPath, targetDir string, info *snap.Info) (dstPath string, err error) {
diff -Nru snapd-2.40/image/image_test.go snapd-2.42.1/image/image_test.go
--- snapd-2.40/image/image_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/image/image_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -35,13 +35,15 @@
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/asserts/assertstest"
- "github.com/snapcore/snapd/boot/boottest"
"github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/bootloader/bootloadertest"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/image"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/seed"
+ "github.com/snapcore/snapd/seed/seedtest"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/store"
@@ -67,18 +69,17 @@
type imageSuite struct {
testutil.BaseTest
root string
- bootloader *boottest.MockBootloader
+ bootloader *bootloadertest.MockBootloader
stdout *bytes.Buffer
stderr *bytes.Buffer
- downloadedSnaps map[string]string
- storeSnapInfo map[string]*snap.Info
- storeActions []*store.SnapAction
- tsto *image.ToolingStore
+ storeActions []*store.SnapAction
+ tsto *image.ToolingStore
- storeSigning *assertstest.StoreStack
- brands *assertstest.SigningAccounts
+ // SeedSnaps helps creating and making available seed snaps
+ // (it provides MakeAssertedSnap etc.) for the tests.
+ *seedtest.SeedSnaps
model *asserts.Model
}
@@ -91,7 +92,7 @@
func (s *imageSuite) SetUpTest(c *C) {
s.root = c.MkDir()
- s.bootloader = boottest.NewMockBootloader("grub", c.MkDir())
+ s.bootloader = bootloadertest.Mock("grub", c.MkDir())
bootloader.Force(s.bootloader)
s.BaseTest.SetUpTest(c)
@@ -101,19 +102,17 @@
image.Stdout = s.stdout
s.stderr = &bytes.Buffer{}
image.Stderr = s.stderr
- s.downloadedSnaps = make(map[string]string)
- s.storeSnapInfo = make(map[string]*snap.Info)
s.tsto = image.MockToolingStore(s)
- s.storeSigning = assertstest.NewStoreStack("canonical", nil)
-
- s.brands = assertstest.NewSigningAccounts(s.storeSigning)
- s.brands.Register("my-brand", brandPrivKey, map[string]interface{}{
+ s.SeedSnaps = &seedtest.SeedSnaps{}
+ s.SetupAssertSigning("canonical", s)
+ s.Brands.Register("my-brand", brandPrivKey, map[string]interface{}{
"verification": "verified",
})
- assertstest.AddMany(s.storeSigning, s.brands.AccountsAndKeys("my-brand")...)
+ assertstest.AddMany(s.StoreSigning, s.Brands.AccountsAndKeys("my-brand")...)
+ s.DB = s.StoreSigning.Database
- s.model = s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ s.model = s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"display-name": "my display name",
"architecture": "amd64",
"gadget": "pc",
@@ -121,10 +120,10 @@
"required-snaps": []interface{}{"required-snap1"},
})
- otherAcct := assertstest.NewAccount(s.storeSigning, "other", map[string]interface{}{
+ otherAcct := assertstest.NewAccount(s.StoreSigning, "other", map[string]interface{}{
"account-id": "other",
}, "")
- s.storeSigning.Add(otherAcct)
+ s.StoreSigning.Add(otherAcct)
// mock the mount cmds (for the extract kernel assets stuff)
c1 := testutil.MockCommand(c, "mount", "")
@@ -133,35 +132,6 @@
s.AddCleanup(c2.Restore)
}
-func (s *imageSuite) addSystemSnapAssertions(c *C, snapName string, publisher string) {
- snapID := snapName + "-Id"
- decl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
- "series": "16",
- "snap-id": snapID,
- "snap-name": snapName,
- "publisher-id": publisher,
- "timestamp": time.Now().UTC().Format(time.RFC3339),
- }, nil, "")
- c.Assert(err, IsNil, Commentf("%s %s", snapName, publisher))
- err = s.storeSigning.Add(decl)
- c.Assert(err, IsNil)
-
- snapSHA3_384, snapSize, err := asserts.SnapFileSHA3_384(s.downloadedSnaps[snapName])
- c.Assert(err, IsNil)
-
- snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
- "snap-sha3-384": snapSHA3_384,
- "snap-size": fmt.Sprintf("%d", snapSize),
- "snap-id": snapID,
- "snap-revision": s.storeSnapInfo[snapName].Revision.String(),
- "developer-id": publisher,
- "timestamp": time.Now().UTC().Format(time.RFC3339),
- }, nil, "")
- c.Assert(err, IsNil)
- err = s.storeSigning.Add(snapRev)
- c.Assert(err, IsNil)
-}
-
func (s *imageSuite) TearDownTest(c *C) {
s.BaseTest.TearDownTest(c)
bootloader.Force(nil)
@@ -186,7 +156,7 @@
// record
s.storeActions = append(s.storeActions, actions[0])
- if info, ok := s.storeSnapInfo[actions[0].InstanceName]; ok {
+ if info := s.AssertedSnapInfo(actions[0].InstanceName); info != nil {
info.Channel = actions[0].Channel
return []*snap.Info{info}, nil
}
@@ -194,12 +164,12 @@
}
func (s *imageSuite) Download(ctx context.Context, name, targetFn string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *store.DownloadOptions) error {
- return osutil.CopyFile(s.downloadedSnaps[name], targetFn, 0)
+ return osutil.CopyFile(s.AssertedSnap(name), targetFn, 0)
}
func (s *imageSuite) Assertion(assertType *asserts.AssertionType, primaryKey []string, user *auth.UserState) (asserts.Assertion, error) {
ref := &asserts.Ref{Type: assertType, PrimaryKey: primaryKey}
- return ref.Resolve(s.storeSigning.Find)
+ return ref.Resolve(s.StoreSigning.Find)
}
const packageGadget = `
@@ -220,6 +190,13 @@
type: gadget
`
+const packageClassicGadget18 = `
+name: classic-gadget18
+version: 1.0
+type: gadget
+base: core18
+`
+
const packageKernel = `
name: pc-kernel
version: 4.4-1
@@ -241,7 +218,7 @@
const snapdSnap = `
name: snapd
version: 3.14
-type: application
+type: snapd
`
const otherBase = `
@@ -269,6 +246,12 @@
version: 1.0
`
+const requiredSnap18 = `
+name: required-snap18
+version: 1.0
+base: core18
+`
+
const snapReqOtherBase = `
name: snap-req-other-base
version: 1.0
@@ -480,153 +463,54 @@
c.Check(a.Type(), Equals, asserts.ModelType)
}
-func (s *imageSuite) TestMissingGadgetUnpackDir(c *C) {
- 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`)
-}
-
-func infoFromSnapYaml(c *C, snapYaml string, rev snap.Revision) *snap.Info {
- info, err := snap.InfoFromSnapYaml([]byte(snapYaml))
- c.Assert(err, IsNil)
-
- if !rev.Unset() {
- info.SnapID = info.InstanceName() + "-Id"
- info.Revision = rev
+func (s *imageSuite) setupSnaps(c *C, gadgetUnpackDir string, publishers map[string]string) {
+ if _, ok := publishers["pc"]; ok {
+ s.MakeAssertedSnap(c, packageGadget, [][]string{{"grub.conf", ""}, {"grub.cfg", "I'm a grub.cfg"}}, snap.R(1), publishers["pc"])
}
- return info
-}
-
-func (s *imageSuite) TestDownloadUnpackGadget(c *C) {
- files := [][]string{
- {"subdir/canary.txt", "I'm a canary"},
+ if _, ok := publishers["pc18"]; ok {
+ s.MakeAssertedSnap(c, packageGadgetWithBase, [][]string{{"grub.conf", ""}, {"grub.cfg", "I'm a grub.cfg"}}, snap.R(4), publishers["pc18"])
}
- s.downloadedSnaps["pc"] = snaptest.MakeTestSnapWithFiles(c, packageGadget, files)
- s.storeSnapInfo["pc"] = infoFromSnapYaml(c, packageGadget, snap.R(99))
- gadgetUnpackDir := filepath.Join(c.MkDir(), "gadget-unpack-dir")
- opts := &image.Options{
- GadgetUnpackDir: gadgetUnpackDir,
+ if _, ok := publishers["classic-gadget"]; ok {
+ s.MakeAssertedSnap(c, packageClassicGadget, [][]string{{"some-file", "Some file"}}, snap.R(5), publishers["classic-gadget"])
}
- local, err := image.LocalSnaps(s.tsto, opts)
- c.Assert(err, IsNil)
- err = image.DownloadUnpackGadget(s.tsto, s.model, opts, local)
- c.Assert(err, IsNil)
-
- // verify the right data got unpacked
- for _, t := range []struct{ file, content string }{
- {"meta/snap.yaml", packageGadget},
- {files[0][0], files[0][1]},
- } {
- fn := filepath.Join(gadgetUnpackDir, t.file)
- c.Check(fn, testutil.FileEquals, t.content)
+ if _, ok := publishers["classic-gadget18"]; ok {
+ s.MakeAssertedSnap(c, packageClassicGadget18, [][]string{{"some-file", "Some file"}}, snap.R(5), publishers["classic-gadget18"])
}
-}
-
-func (s *imageSuite) TestDownloadUnpackGadgetFromTrack(c *C) {
- s.downloadedSnaps["pc"] = snaptest.MakeTestSnapWithFiles(c, packageGadget, nil)
- s.storeSnapInfo["pc"] = infoFromSnapYaml(c, packageGadget, snap.R(1818))
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ if _, ok := publishers["pc-kernel"]; ok {
+ s.MakeAssertedSnap(c, packageKernel, nil, snap.R(2), publishers["pc-kernel"])
+ }
- "architecture": "amd64",
- "gadget": "pc=18",
- "kernel": "pc-kernel=18",
- })
+ s.MakeAssertedSnap(c, packageCore, nil, snap.R(3), "canonical")
- gadgetUnpackDir := filepath.Join(c.MkDir(), "gadget-unpack-dir")
- opts := &image.Options{
- GadgetUnpackDir: gadgetUnpackDir,
- }
- local, err := image.LocalSnaps(s.tsto, opts)
- c.Assert(err, IsNil)
+ s.MakeAssertedSnap(c, packageCore18, nil, snap.R(18), "canonical")
+ s.MakeAssertedSnap(c, snapdSnap, nil, snap.R(18), "canonical")
- err = image.DownloadUnpackGadget(s.tsto, model, opts, local)
- c.Assert(err, IsNil)
+ s.MakeAssertedSnap(c, otherBase, nil, snap.R(18), "other")
- c.Check(s.storeActions, HasLen, 1)
- c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
- Action: "download",
- Channel: "18/stable",
- InstanceName: "pc",
- })
+ s.MakeAssertedSnap(c, snapReqCore16Base, nil, snap.R(16), "other")
-}
+ s.MakeAssertedSnap(c, requiredSnap1, nil, snap.R(3), "other")
+ s.AssertedSnapInfo("required-snap1").Contact = "foo@example.com"
-func (s *imageSuite) setupSnaps(c *C, gadgetUnpackDir string, publishers map[string]string) {
- if gadgetUnpackDir != "" {
- err := os.MkdirAll(gadgetUnpackDir, 0755)
- c.Assert(err, IsNil)
- err = ioutil.WriteFile(filepath.Join(gadgetUnpackDir, "grub.conf"), nil, 0644)
- c.Assert(err, IsNil)
- }
+ s.MakeAssertedSnap(c, requiredSnap18, nil, snap.R(6), "other")
+ s.AssertedSnapInfo("required-snap18").Contact = "foo@example.com"
- if _, ok := publishers["pc"]; ok {
- s.downloadedSnaps["pc"] = snaptest.MakeTestSnapWithFiles(c, packageGadget, [][]string{{"grub.cfg", "I'm a grub.cfg"}})
- s.storeSnapInfo["pc"] = infoFromSnapYaml(c, packageGadget, snap.R(1))
- s.addSystemSnapAssertions(c, "pc", publishers["pc"])
- }
- if _, ok := publishers["pc18"]; ok {
- s.downloadedSnaps["pc18"] = snaptest.MakeTestSnapWithFiles(c, packageGadgetWithBase, [][]string{{"grub.cfg", "I'm a grub.cfg"}})
- s.storeSnapInfo["pc18"] = infoFromSnapYaml(c, packageGadgetWithBase, snap.R(4))
- s.addSystemSnapAssertions(c, "pc18", publishers["pc18"])
- }
+ s.MakeAssertedSnap(c, snapReqOtherBase, nil, snap.R(5), "other")
- if _, ok := publishers["classic-gadget"]; ok {
- s.downloadedSnaps["classic-gadget"] = snaptest.MakeTestSnapWithFiles(c, packageClassicGadget, [][]string{{"some-file", "Some file"}})
- s.storeSnapInfo["classic-gadget"] = infoFromSnapYaml(c, packageClassicGadget, snap.R(5))
- s.addSystemSnapAssertions(c, "classic-gadget", publishers["classic-gadget"])
- }
+ s.MakeAssertedSnap(c, snapReqContentProvider, nil, snap.R(5), "other")
- if _, ok := publishers["pc-kernel"]; ok {
- s.downloadedSnaps["pc-kernel"] = snaptest.MakeTestSnapWithFiles(c, packageKernel, nil)
- s.storeSnapInfo["pc-kernel"] = infoFromSnapYaml(c, packageKernel, snap.R(2))
- s.addSystemSnapAssertions(c, "pc-kernel", publishers["pc-kernel"])
- }
-
- s.downloadedSnaps["core"] = snaptest.MakeTestSnapWithFiles(c, packageCore, nil)
- s.storeSnapInfo["core"] = infoFromSnapYaml(c, packageCore, snap.R(3))
- s.addSystemSnapAssertions(c, "core", "canonical")
-
- s.downloadedSnaps["core18"] = snaptest.MakeTestSnapWithFiles(c, packageCore18, nil)
- s.storeSnapInfo["core18"] = infoFromSnapYaml(c, packageCore18, snap.R(18))
- s.addSystemSnapAssertions(c, "core18", "canonical")
-
- s.downloadedSnaps["snapd"] = snaptest.MakeTestSnapWithFiles(c, snapdSnap, nil)
- s.storeSnapInfo["snapd"] = infoFromSnapYaml(c, snapdSnap, snap.R(18))
- s.addSystemSnapAssertions(c, "snapd", "canonical")
-
- s.downloadedSnaps["other-base"] = snaptest.MakeTestSnapWithFiles(c, otherBase, nil)
- s.storeSnapInfo["other-base"] = infoFromSnapYaml(c, otherBase, snap.R(18))
- s.addSystemSnapAssertions(c, "other-base", "other")
-
- s.downloadedSnaps["snap-req-core16-base"] = snaptest.MakeTestSnapWithFiles(c, snapReqCore16Base, nil)
- s.storeSnapInfo["snap-req-core16-base"] = infoFromSnapYaml(c, snapReqCore16Base, snap.R(16))
- s.addSystemSnapAssertions(c, "snap-req-core16-base", "other")
-
- s.downloadedSnaps["required-snap1"] = snaptest.MakeTestSnapWithFiles(c, requiredSnap1, nil)
- s.storeSnapInfo["required-snap1"] = infoFromSnapYaml(c, requiredSnap1, snap.R(3))
- s.storeSnapInfo["required-snap1"].Contact = "foo@example.com"
- s.addSystemSnapAssertions(c, "required-snap1", "other")
-
- s.downloadedSnaps["snap-req-other-base"] = snaptest.MakeTestSnapWithFiles(c, snapReqOtherBase, nil)
- s.storeSnapInfo["snap-req-other-base"] = infoFromSnapYaml(c, snapReqOtherBase, snap.R(5))
- s.addSystemSnapAssertions(c, "snap-req-other-base", "other")
-
- s.downloadedSnaps["snap-req-content-provider"] = snaptest.MakeTestSnapWithFiles(c, snapReqContentProvider, nil)
- s.storeSnapInfo["snap-req-content-provider"] = infoFromSnapYaml(c, snapReqContentProvider, snap.R(5))
- s.addSystemSnapAssertions(c, "snap-req-content-provider", "other")
-
- s.downloadedSnaps["snap-base-none"] = snaptest.MakeTestSnapWithFiles(c, snapBaseNone, nil)
- s.storeSnapInfo["snap-base-none"] = infoFromSnapYaml(c, snapBaseNone, snap.R(1))
- s.addSystemSnapAssertions(c, "snap-base-none", "other")
+ s.MakeAssertedSnap(c, snapBaseNone, nil, snap.R(1), "other")
}
func (s *imageSuite) TestSetupSeed(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
rootdir := filepath.Join(c.MkDir(), "imageroot")
+ blobdir := filepath.Join(rootdir, "var/lib/snapd/snaps")
seeddir := filepath.Join(rootdir, "var/lib/snapd/seed")
seedsnapsdir := filepath.Join(seeddir, "snaps")
seedassertsdir := filepath.Join(seeddir, "assertions")
@@ -648,29 +532,33 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(seeddir, "seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(seeddir, "seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 4)
+ c.Check(seedYaml.Snaps, HasLen, 4)
// check the files are in place
for i, name := range []string{"core", "pc-kernel", "pc"} {
- info := s.storeSnapInfo[name]
+ info := s.AssertedSnapInfo(name)
fn := filepath.Base(info.MountFile())
p := filepath.Join(seedsnapsdir, fn)
c.Check(osutil.FileExists(p), Equals, true)
- c.Check(seed.Snaps[i], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[i], DeepEquals, &seed.Snap16{
Name: name,
- SnapID: name + "-Id",
+ SnapID: s.AssertedSnapID(name),
File: fn,
})
+ // sanity
+ if name == "core" {
+ c.Check(seedYaml.Snaps[i].SnapID, Equals, "coreidididididididididididididid")
+ }
}
- c.Check(seed.Snaps[3].Name, Equals, "required-snap1")
- c.Check(seed.Snaps[3].Contact, Equals, "foo@example.com")
+ c.Check(seedYaml.Snaps[3].Name, Equals, "required-snap1")
+ c.Check(seedYaml.Snaps[3].Contact, Equals, "foo@example.com")
- storeAccountKey := s.storeSigning.StoreAccountKey("")
- brandPubKey := s.brands.PublicKey("my-brand")
+ storeAccountKey := s.StoreSigning.StoreAccountKey("")
+ brandPubKey := s.Brands.PublicKey("my-brand")
// check the assertions are in place
for _, fn := range []string{"model", brandPubKey.ID() + ".account-key", "my-brand.account", storeAccountKey.PublicKeyID() + ".account-key"} {
@@ -687,8 +575,8 @@
c.Check(a.HeaderString("account-id"), Equals, "my-brand")
// check the snap assertions are also in place
- for _, snapId := range []string{"pc-Id", "pc-kernel-Id", "core-Id"} {
- p := filepath.Join(seedassertsdir, fmt.Sprintf("16,%s.snap-declaration", snapId))
+ for _, snapName := range []string{"pc", "pc-kernel", "core"} {
+ p := filepath.Join(seedassertsdir, fmt.Sprintf("16,%s.snap-declaration", s.AssertedSnapID(snapName)))
c.Check(osutil.FileExists(p), Equals, true)
}
@@ -699,11 +587,41 @@
c.Check(m["snap_core"], Equals, "core_3.snap")
c.Check(m["snap_menuentry"], Equals, "my display name")
+ // check symlinks from snap blob dir
+ kernelInfo := s.AssertedSnapInfo("pc-kernel")
+ coreInfo := s.AssertedSnapInfo("core")
+ kernelBlob := filepath.Join(blobdir, filepath.Base(kernelInfo.MountFile()))
+ dst, err := os.Readlink(kernelBlob)
+ c.Assert(err, IsNil)
+ c.Check(dst, Equals, "../seed/snaps/pc-kernel_2.snap")
+ c.Check(kernelBlob, testutil.FilePresent)
+
+ coreBlob := filepath.Join(blobdir, filepath.Base(coreInfo.MountFile()))
+ dst, err = os.Readlink(coreBlob)
+ c.Assert(err, IsNil)
+ c.Check(dst, Equals, "../seed/snaps/core_3.snap")
+ c.Check(coreBlob, testutil.FilePresent)
+
c.Check(s.stderr.String(), Equals, "")
+
+ // check the downloads
+ c.Check(s.storeActions, HasLen, 4)
+ c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
+ Action: "download",
+ InstanceName: "core",
+ })
+ c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
+ Action: "download",
+ InstanceName: "pc-kernel",
+ })
+ c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
+ Action: "download",
+ InstanceName: "pc",
+ })
}
func (s *imageSuite) TestSetupSeedLocalCoreBrandKernel(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
rootdir := filepath.Join(c.MkDir(), "imageroot")
@@ -716,8 +634,8 @@
opts := &image.Options{
Snaps: []string{
- s.downloadedSnaps["core"],
- s.downloadedSnaps["required-snap1"],
+ s.AssertedSnap("core"),
+ s.AssertedSnap("required-snap1"),
},
RootDir: rootdir,
GadgetUnpackDir: gadgetUnpackDir,
@@ -730,15 +648,15 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 4)
+ c.Check(seedYaml.Snaps, HasLen, 4)
// check the files are in place
for i, name := range []string{"core_x1.snap", "pc-kernel", "pc", "required-snap1_x1.snap"} {
unasserted := false
- info := s.storeSnapInfo[name]
+ info := s.AssertedSnapInfo(name)
if info == nil {
switch name {
case "core_x1.snap":
@@ -764,7 +682,7 @@
p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn)
c.Check(osutil.FileExists(p), Equals, true)
- c.Check(seed.Snaps[i], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[i], DeepEquals, &seed.Snap16{
Name: info.InstanceName(),
SnapID: info.SnapID,
File: fn,
@@ -776,8 +694,8 @@
c.Assert(err, IsNil)
c.Check(l, HasLen, 4)
- storeAccountKey := s.storeSigning.StoreAccountKey("")
- brandPubKey := s.brands.PublicKey("my-brand")
+ storeAccountKey := s.StoreSigning.StoreAccountKey("")
+ brandPubKey := s.Brands.PublicKey("my-brand")
// check the assertions are in place
for _, fn := range []string{"model", brandPubKey.ID() + ".account-key", "my-brand.account", storeAccountKey.PublicKeyID() + ".account-key"} {
@@ -805,11 +723,11 @@
c.Assert(err, IsNil)
c.Check(m["snap_core"], Equals, "core_x1.snap")
- c.Check(s.stderr.String(), Equals, "WARNING: \"core\", \"required-snap1\" were installed from local snaps disconnected from a store and cannot be refreshed subsequently!\n")
+ c.Check(s.stderr.String(), Equals, "WARNING: \"core\", \"required-snap1\" installed from local snaps disconnected from a store cannot be refreshed subsequently!\n")
}
func (s *imageSuite) TestSetupSeedDevmodeSnap(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
rootdir := filepath.Join(c.MkDir(), "imageroot")
@@ -819,11 +737,10 @@
"pc-kernel": "canonical",
})
- s.downloadedSnaps["devmode-snap"] = snaptest.MakeTestSnapWithFiles(c, devmodeSnap, nil)
- s.storeSnapInfo["devmode-snap"] = infoFromSnapYaml(c, devmodeSnap, snap.R(0))
+ snapFile := snaptest.MakeTestSnapWithFiles(c, devmodeSnap, nil)
opts := &image.Options{
- Snaps: []string{s.downloadedSnaps["devmode-snap"]},
+ Snaps: []string{snapFile},
RootDir: rootdir,
GadgetUnpackDir: gadgetUnpackDir,
@@ -836,37 +753,37 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 5)
- c.Check(seed.Snaps[0], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps, HasLen, 5)
+ c.Check(seedYaml.Snaps[0], DeepEquals, &seed.Snap16{
Name: "core",
- SnapID: "core-Id",
+ SnapID: s.AssertedSnapID("core"),
File: "core_3.snap",
Channel: "beta",
})
- c.Check(seed.Snaps[1], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[1], DeepEquals, &seed.Snap16{
Name: "pc-kernel",
- SnapID: "pc-kernel-Id",
+ SnapID: s.AssertedSnapID("pc-kernel"),
File: "pc-kernel_2.snap",
Channel: "beta",
})
- c.Check(seed.Snaps[2], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[2], DeepEquals, &seed.Snap16{
Name: "pc",
- SnapID: "pc-Id",
+ SnapID: s.AssertedSnapID("pc"),
File: "pc_1.snap",
Channel: "beta",
})
- c.Check(seed.Snaps[3], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[3], DeepEquals, &seed.Snap16{
Name: "required-snap1",
- SnapID: "required-snap1-Id",
+ SnapID: s.AssertedSnapID("required-snap1"),
File: "required-snap1_3.snap",
Contact: "foo@example.com",
Channel: "beta",
})
// ensure local snaps are put last in seed.yaml
- c.Check(seed.Snaps[4], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[4], DeepEquals, &seed.Snap16{
Name: "devmode-snap",
DevMode: true,
Unasserted: true,
@@ -887,8 +804,8 @@
c.Check(osutil.FileExists(p), Equals, true)
// ensure local snaps are put last in seed.yaml
- last := len(seed.Snaps) - 1
- c.Check(seed.Snaps[last], DeepEquals, &snap.SeedSnap{
+ last := len(seedYaml.Snaps) - 1
+ c.Check(seedYaml.Snaps[last], DeepEquals, &seed.Snap16{
Name: "devmode-snap",
File: fn,
DevMode: true,
@@ -897,7 +814,7 @@
}
func (s *imageSuite) TestSetupSeedWithClassicSnapFails(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
rootdir := filepath.Join(c.MkDir(), "imageroot")
@@ -907,11 +824,10 @@
"pc-kernel": "canonical",
})
- s.downloadedSnaps["classic-snap"] = snaptest.MakeTestSnapWithFiles(c, classicSnap, nil)
- s.storeSnapInfo["classic-snap"] = infoFromSnapYaml(c, classicSnap, snap.R(0))
+ s.MakeAssertedSnap(c, classicSnap, nil, snap.R(1), "other")
opts := &image.Options{
- Snaps: []string{s.downloadedSnaps["classic-snap"]},
+ Snaps: []string{s.AssertedSnap("classic-snap")},
RootDir: rootdir,
GadgetUnpackDir: gadgetUnpackDir,
@@ -925,11 +841,11 @@
}
func (s *imageSuite) TestSetupSeedWithBase(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
// replace model with a model that uses core18
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc18",
"kernel": "pc-kernel",
@@ -938,6 +854,7 @@
})
rootdir := filepath.Join(c.MkDir(), "imageroot")
+ blobdir := filepath.Join(rootdir, "/var/lib/snapd/snaps")
gadgetUnpackDir := c.MkDir()
s.setupSnaps(c, gadgetUnpackDir, map[string]string{
"core18": "canonical",
@@ -958,21 +875,21 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 5)
+ c.Check(seedYaml.Snaps, HasLen, 5)
// check the files are in place
- for i, name := range []string{"snapd", "core18_18.snap", "pc-kernel", "pc18", "other-base"} {
+ for i, name := range []string{"snapd", "pc-kernel", "core18_18.snap", "other-base", "pc18"} {
unasserted := false
- info := s.storeSnapInfo[name]
+ info := s.AssertedSnapInfo(name)
if info == nil {
switch name {
case "core18_18.snap":
info = &snap.Info{
SideInfo: snap.SideInfo{
- SnapID: "core18-Id",
+ SnapID: "core18ididididididididididididid",
RealName: "core18",
Revision: snap.R("18"),
},
@@ -984,7 +901,7 @@
p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn)
c.Check(osutil.FileExists(p), Equals, true)
- c.Check(seed.Snaps[i], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[i], DeepEquals, &seed.Snap16{
Name: info.InstanceName(),
SnapID: info.SnapID,
File: fn,
@@ -1003,11 +920,130 @@
c.Assert(err, IsNil)
c.Check(m["snap_core"], Equals, "core18_18.snap")
+ // check symlinks from snap blob dir
+ kernelInfo := s.AssertedSnapInfo("pc-kernel")
+ baseInfo := s.AssertedSnapInfo("core18")
+ kernelBlob := filepath.Join(blobdir, filepath.Base(kernelInfo.MountFile()))
+ dst, err := os.Readlink(kernelBlob)
+ c.Assert(err, IsNil)
+ c.Check(dst, Equals, "../seed/snaps/pc-kernel_2.snap")
+ c.Check(kernelBlob, testutil.FilePresent)
+
+ baseBlob := filepath.Join(blobdir, filepath.Base(baseInfo.MountFile()))
+ dst, err = os.Readlink(baseBlob)
+ c.Assert(err, IsNil)
+ c.Check(dst, Equals, "../seed/snaps/core18_18.snap")
+ c.Check(baseBlob, testutil.FilePresent)
+
c.Check(s.stderr.String(), Equals, "")
+
+ // check the downloads
+ c.Check(s.storeActions, HasLen, 5)
+ c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
+ Action: "download",
+ InstanceName: "snapd",
+ })
+ c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
+ Action: "download",
+ InstanceName: "core18",
+ })
+ c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
+ Action: "download",
+ InstanceName: "pc-kernel",
+ })
+ c.Check(s.storeActions[3], DeepEquals, &store.SnapAction{
+ Action: "download",
+ InstanceName: "pc18",
+ })
+}
+
+func (s *imageSuite) TestSetupSeedWithBaseLegacySnap(c *C) {
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
+ defer restore()
+
+ // replace model with a model that uses core18
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
+ "architecture": "amd64",
+ "gadget": "pc18",
+ "kernel": "pc-kernel",
+ "base": "core18",
+ "required-snaps": []interface{}{"required-snap1"},
+ })
+
+ // required-snap1 needs core, for backward compatibility
+ // we will add it implicitly but warn about this
+
+ rootdir := filepath.Join(c.MkDir(), "imageroot")
+ gadgetUnpackDir := c.MkDir()
+ s.setupSnaps(c, gadgetUnpackDir, map[string]string{
+ "core18": "canonical",
+ "pc18": "canonical",
+ "pc-kernel": "canonical",
+ "snapd": "canonical",
+ })
+
+ opts := &image.Options{
+ RootDir: rootdir,
+ GadgetUnpackDir: gadgetUnpackDir,
+ }
+ local, err := image.LocalSnaps(s.tsto, opts)
+ c.Assert(err, IsNil)
+
+ err = image.SetupSeed(s.tsto, model, opts, local)
+ c.Assert(err, IsNil)
+
+ // check seed yaml
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ c.Assert(err, IsNil)
+
+ c.Check(seedYaml.Snaps, HasLen, 6)
+
+ // check the files are in place
+ for i, name := range []string{"snapd", "core", "pc-kernel", "core18_18.snap", "pc18"} {
+ unasserted := false
+ info := s.AssertedSnapInfo(name)
+ if info == nil {
+ switch name {
+ case "core18_18.snap":
+ info = &snap.Info{
+ SideInfo: snap.SideInfo{
+ SnapID: "core18ididididididididididididid",
+ RealName: "core18",
+ Revision: snap.R("18"),
+ },
+ }
+ }
+ }
+
+ fn := filepath.Base(info.MountFile())
+ p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn)
+ c.Check(osutil.FileExists(p), Equals, true)
+
+ c.Check(seedYaml.Snaps[i], DeepEquals, &seed.Snap16{
+ Name: info.InstanceName(),
+ SnapID: info.SnapID,
+ File: fn,
+ Unasserted: unasserted,
+ })
+ }
+ c.Check(seedYaml.Snaps[5].Name, Equals, "required-snap1")
+
+ l, err := ioutil.ReadDir(filepath.Join(rootdir, "var/lib/snapd/seed/snaps"))
+ c.Assert(err, IsNil)
+ c.Check(l, HasLen, 6)
+
+ // check the bootloader config
+ m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
+ c.Assert(err, IsNil)
+ c.Check(m["snap_kernel"], Equals, "pc-kernel_2.snap")
+ c.Assert(err, IsNil)
+ c.Check(m["snap_core"], Equals, "core18_18.snap")
+
+ c.Check(s.stderr.String(), Equals, "WARNING: model has base \"core18\" but some snaps (\"required-snap1\") require \"core\" as base as well, for compatibility it was added implicitly, adding \"core\" explicitly is recommended\n")
}
func (s *imageSuite) TestSetupSeedKernelPublisherMismatch(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
rootdir := filepath.Join(c.MkDir(), "imageroot")
@@ -1092,7 +1128,7 @@
}
func (s *imageSuite) TestSetupSeedLocalSnapsWithStoreAsserts(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
rootdir := filepath.Join(c.MkDir(), "imageroot")
@@ -1105,8 +1141,8 @@
opts := &image.Options{
Snaps: []string{
- s.downloadedSnaps["core"],
- s.downloadedSnaps["required-snap1"],
+ s.AssertedSnap("core"),
+ s.AssertedSnap("required-snap1"),
},
RootDir: rootdir,
GadgetUnpackDir: gadgetUnpackDir,
@@ -1118,21 +1154,21 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 4)
+ c.Check(seedYaml.Snaps, HasLen, 4)
// check the files are in place
for i, name := range []string{"core_3.snap", "pc-kernel", "pc", "required-snap1_3.snap"} {
- info := s.storeSnapInfo[name]
+ info := s.AssertedSnapInfo(name)
if info == nil {
switch name {
case "core_3.snap":
info = &snap.Info{
SideInfo: snap.SideInfo{
RealName: "core",
- SnapID: "core-Id",
+ SnapID: s.AssertedSnapID("core"),
Revision: snap.R(3),
},
}
@@ -1140,7 +1176,7 @@
info = &snap.Info{
SideInfo: snap.SideInfo{
RealName: "required-snap1",
- SnapID: "required-snap1-Id",
+ SnapID: s.AssertedSnapID("required-snap1"),
Revision: snap.R(3),
},
}
@@ -1153,7 +1189,7 @@
p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn)
c.Check(osutil.FileExists(p), Equals, true, Commentf("cannot find %s", p))
- c.Check(seed.Snaps[i], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[i], DeepEquals, &seed.Snap16{
Name: info.InstanceName(),
SnapID: info.SnapID,
File: fn,
@@ -1165,8 +1201,8 @@
c.Assert(err, IsNil)
c.Check(l, HasLen, 4)
- storeAccountKey := s.storeSigning.StoreAccountKey("")
- brandPubKey := s.brands.PublicKey("my-brand")
+ storeAccountKey := s.StoreSigning.StoreAccountKey("")
+ brandPubKey := s.Brands.PublicKey("my-brand")
// check the assertions are in place
for _, fn := range []string{"model", brandPubKey.ID() + ".account-key", "my-brand.account", storeAccountKey.PublicKeyID() + ".account-key"} {
@@ -1197,7 +1233,7 @@
}
func (s *imageSuite) TestSetupSeedLocalSnapsWithChannels(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
rootdir := filepath.Join(c.MkDir(), "imageroot")
@@ -1209,13 +1245,13 @@
opts := &image.Options{
Snaps: []string{
- s.downloadedSnaps["required-snap1"],
+ s.AssertedSnap("required-snap1"),
},
RootDir: rootdir,
GadgetUnpackDir: gadgetUnpackDir,
SnapChannels: map[string]string{
"core": "candidate",
- s.downloadedSnaps["required-snap1"]: "edge",
+ s.AssertedSnap("required-snap1"): "edge",
},
}
local, err := image.LocalSnaps(s.tsto, opts)
@@ -1225,21 +1261,21 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 4)
+ c.Check(seedYaml.Snaps, HasLen, 4)
// check the files are in place
for i, name := range []string{"core_3.snap", "pc-kernel", "pc", "required-snap1_3.snap"} {
- info := s.storeSnapInfo[name]
+ info := s.AssertedSnapInfo(name)
if info == nil {
switch name {
case "core_3.snap":
info = &snap.Info{
SideInfo: snap.SideInfo{
RealName: "core",
- SnapID: "core-Id",
+ SnapID: s.AssertedSnapID("core"),
Revision: snap.R(3),
Channel: "candidate",
},
@@ -1248,7 +1284,7 @@
info = &snap.Info{
SideInfo: snap.SideInfo{
RealName: "required-snap1",
- SnapID: "required-snap1-Id",
+ SnapID: s.AssertedSnapID("required-snap1"),
Revision: snap.R(3),
Channel: "edge",
},
@@ -1262,7 +1298,7 @@
p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn)
c.Check(osutil.FileExists(p), Equals, true, Commentf("cannot find %s", p))
- c.Check(seed.Snaps[i], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[i], DeepEquals, &seed.Snap16{
Name: info.InstanceName(),
SnapID: info.SnapID,
Channel: info.Channel,
@@ -1276,6 +1312,18 @@
c.Check(l, HasLen, 4)
}
+func (s *imageSuite) TestMissingGadgetUnpackDir(c *C) {
+ fn := filepath.Join(c.MkDir(), "model.assertion")
+ err := ioutil.WriteFile(fn, asserts.Encode(s.model), 0644)
+ c.Assert(err, IsNil)
+
+ err = image.Prepare(&image.Options{
+ ModelFile: fn,
+ Channel: "stable",
+ })
+ c.Assert(err, ErrorMatches, `cannot create gadget unpack dir "": mkdir : no such file or directory`)
+}
+
func (s *imageSuite) TestNoLocalParallelSnapInstances(c *C) {
fn := filepath.Join(c.MkDir(), "model.assertion")
err := ioutil.WriteFile(fn, asserts.Encode(s.model), 0644)
@@ -1325,10 +1373,10 @@
}
func (s *imageSuite) TestPrepareClassicModelNoClassicMode(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"classic": "true",
})
@@ -1343,10 +1391,10 @@
}
func (s *imageSuite) TestPrepareClassicModelArchOverrideFails(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"classic": "true",
"architecture": "amd64",
})
@@ -1364,10 +1412,10 @@
}
func (s *imageSuite) TestPrepareClassicModelSnapsButNoArchFails(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"classic": "true",
"gadget": "classic-gadget",
})
@@ -1384,11 +1432,11 @@
}
func (s *imageSuite) TestSetupSeedWithKernelAndGadgetTrack(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
// replace model with a model that uses core18
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc=18",
"kernel": "pc-kernel=18",
@@ -1405,6 +1453,7 @@
opts := &image.Options{
RootDir: rootdir,
GadgetUnpackDir: gadgetUnpackDir,
+ Channel: "stable",
}
local, err := image.LocalSnaps(s.tsto, opts)
c.Assert(err, IsNil)
@@ -1413,35 +1462,54 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 3)
- c.Check(seed.Snaps[0], DeepEquals, &snap.SeedSnap{
- Name: "core",
- SnapID: "core-Id",
- File: "core_3.snap",
+ c.Check(seedYaml.Snaps, HasLen, 3)
+ c.Check(seedYaml.Snaps[0], DeepEquals, &seed.Snap16{
+ Name: "core",
+ SnapID: s.AssertedSnapID("core"),
+ File: "core_3.snap",
+ Channel: "stable",
})
- c.Check(seed.Snaps[1], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[1], DeepEquals, &seed.Snap16{
Name: "pc-kernel",
- SnapID: "pc-kernel-Id",
+ SnapID: s.AssertedSnapID("pc-kernel"),
File: "pc-kernel_2.snap",
Channel: "18/stable",
})
- c.Check(seed.Snaps[2], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[2], DeepEquals, &seed.Snap16{
Name: "pc",
- SnapID: "pc-Id",
+ SnapID: s.AssertedSnapID("pc"),
File: "pc_1.snap",
Channel: "18/stable",
})
+
+ // check the downloads
+ c.Check(s.storeActions, HasLen, 3)
+ c.Check(s.storeActions[0], DeepEquals, &store.SnapAction{
+ Action: "download",
+ InstanceName: "core",
+ Channel: "stable",
+ })
+ c.Check(s.storeActions[1], DeepEquals, &store.SnapAction{
+ Action: "download",
+ InstanceName: "pc-kernel",
+ Channel: "18/stable",
+ })
+ c.Check(s.storeActions[2], DeepEquals, &store.SnapAction{
+ Action: "download",
+ InstanceName: "pc",
+ Channel: "18/stable",
+ })
}
func (s *imageSuite) TestSetupSeedWithKernelTrackWithDefaultChannel(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
// replace model with a model that uses core18
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc",
"kernel": "pc-kernel=18",
@@ -1467,36 +1535,36 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 3)
- c.Check(seed.Snaps[0], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps, HasLen, 3)
+ c.Check(seedYaml.Snaps[0], DeepEquals, &seed.Snap16{
Name: "core",
- SnapID: "core-Id",
+ SnapID: s.AssertedSnapID("core"),
File: "core_3.snap",
Channel: "edge",
})
- c.Check(seed.Snaps[1], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[1], DeepEquals, &seed.Snap16{
Name: "pc-kernel",
- SnapID: "pc-kernel-Id",
+ SnapID: s.AssertedSnapID("pc-kernel"),
File: "pc-kernel_2.snap",
Channel: "18/edge",
})
- c.Check(seed.Snaps[2], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[2], DeepEquals, &seed.Snap16{
Name: "pc",
- SnapID: "pc-Id",
+ SnapID: s.AssertedSnapID("pc"),
File: "pc_1.snap",
Channel: "edge",
})
}
func (s *imageSuite) TestSetupSeedWithKernelTrackOnLocalSnap(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
// replace model with a model that uses core18
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc",
"kernel": "pc-kernel=18",
@@ -1511,8 +1579,8 @@
})
// pretend we downloaded the core,kernel already
- cfn := s.downloadedSnaps["core"]
- kfn := s.downloadedSnaps["pc-kernel"]
+ cfn := s.AssertedSnap("core")
+ kfn := s.AssertedSnap("pc-kernel")
opts := &image.Options{
RootDir: rootdir,
GadgetUnpackDir: gadgetUnpackDir,
@@ -1527,30 +1595,30 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 3)
- c.Check(seed.Snaps[0], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps, HasLen, 3)
+ c.Check(seedYaml.Snaps[0], DeepEquals, &seed.Snap16{
Name: "core",
- SnapID: "core-Id",
+ SnapID: s.AssertedSnapID("core"),
File: "core_3.snap",
Channel: "beta",
})
- c.Check(seed.Snaps[1], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[1], DeepEquals, &seed.Snap16{
Name: "pc-kernel",
- SnapID: "pc-kernel-Id",
+ SnapID: s.AssertedSnapID("pc-kernel"),
File: "pc-kernel_2.snap",
Channel: "18/beta",
})
}
func (s *imageSuite) TestSetupSeedWithBaseAndLocalLegacyCoreOrdering(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
// replace model with a model that uses core18
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"base": "core18",
"gadget": "pc18",
@@ -1570,7 +1638,7 @@
RootDir: rootdir,
GadgetUnpackDir: gadgetUnpackDir,
Snaps: []string{
- s.downloadedSnaps["core"],
+ s.AssertedSnap("core"),
},
}
emptyToolingStore := image.MockToolingStore(&emptyStore{})
@@ -1581,49 +1649,49 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 6)
- c.Check(seed.Snaps[0], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps, HasLen, 6)
+ c.Check(seedYaml.Snaps[0], DeepEquals, &seed.Snap16{
Name: "snapd",
- SnapID: "snapd-Id",
+ SnapID: s.AssertedSnapID("snapd"),
File: "snapd_18.snap",
})
- c.Check(seed.Snaps[1], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[1], DeepEquals, &seed.Snap16{
Name: "core",
Unasserted: true,
File: "core_x1.snap",
})
- c.Check(seed.Snaps[2], DeepEquals, &snap.SeedSnap{
- Name: "core18",
- SnapID: "core18-Id",
- File: "core18_18.snap",
- })
- c.Check(seed.Snaps[3], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[2], DeepEquals, &seed.Snap16{
Name: "pc-kernel",
- SnapID: "pc-kernel-Id",
+ SnapID: s.AssertedSnapID("pc-kernel"),
File: "pc-kernel_2.snap",
})
- c.Check(seed.Snaps[4], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[3], DeepEquals, &seed.Snap16{
+ Name: "core18",
+ SnapID: s.AssertedSnapID("core18"),
+ File: "core18_18.snap",
+ })
+ c.Check(seedYaml.Snaps[4], DeepEquals, &seed.Snap16{
Name: "pc18",
- SnapID: "pc18-Id",
+ SnapID: s.AssertedSnapID("pc18"),
File: "pc18_4.snap",
})
- c.Check(seed.Snaps[5], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[5], DeepEquals, &seed.Snap16{
Name: "required-snap1",
- SnapID: "required-snap1-Id",
+ SnapID: s.AssertedSnapID("required-snap1"),
File: "required-snap1_3.snap",
Contact: "foo@example.com",
})
}
func (s *imageSuite) TestSetupSeedWithBaseAndLegacyCoreOrdering(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
// replace model with a model that uses core18
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"base": "core18",
"gadget": "pc18",
@@ -1651,49 +1719,49 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 6)
- c.Check(seed.Snaps[0], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps, HasLen, 6)
+ c.Check(seedYaml.Snaps[0], DeepEquals, &seed.Snap16{
Name: "snapd",
- SnapID: "snapd-Id",
+ SnapID: s.AssertedSnapID("snapd"),
File: "snapd_18.snap",
})
- c.Check(seed.Snaps[1], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[1], DeepEquals, &seed.Snap16{
Name: "core",
- SnapID: "core-Id",
+ SnapID: s.AssertedSnapID("core"),
File: "core_3.snap",
})
- c.Check(seed.Snaps[2], DeepEquals, &snap.SeedSnap{
- Name: "core18",
- SnapID: "core18-Id",
- File: "core18_18.snap",
- })
- c.Check(seed.Snaps[3], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[2], DeepEquals, &seed.Snap16{
Name: "pc-kernel",
- SnapID: "pc-kernel-Id",
+ SnapID: s.AssertedSnapID("pc-kernel"),
File: "pc-kernel_2.snap",
})
- c.Check(seed.Snaps[4], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[3], DeepEquals, &seed.Snap16{
+ Name: "core18",
+ SnapID: s.AssertedSnapID("core18"),
+ File: "core18_18.snap",
+ })
+ c.Check(seedYaml.Snaps[4], DeepEquals, &seed.Snap16{
Name: "pc18",
- SnapID: "pc18-Id",
+ SnapID: s.AssertedSnapID("pc18"),
File: "pc18_4.snap",
})
- c.Check(seed.Snaps[5], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[5], DeepEquals, &seed.Snap16{
Name: "required-snap1",
- SnapID: "required-snap1-Id",
+ SnapID: s.AssertedSnapID("required-snap1"),
File: "required-snap1_3.snap",
Contact: "foo@example.com",
})
}
func (s *imageSuite) TestSetupSeedGadgetBaseModelBaseMismatch(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
// replace model with a model that uses core18 and a gadget
// without a base
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"base": "core18",
"gadget": "pc",
@@ -1719,9 +1787,9 @@
}
func (s *imageSuite) TestSetupSeedSnapReqBase(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc",
"kernel": "pc-kernel",
@@ -1747,9 +1815,9 @@
}
func (s *imageSuite) TestSetupSeedBaseNone(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc",
"kernel": "pc-kernel",
@@ -1774,9 +1842,9 @@
}
func (s *imageSuite) TestSetupSeedSnapCoreSatisfiesCore16(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc",
"kernel": "pc-kernel",
@@ -1802,9 +1870,9 @@
}
func (s *imageSuite) TestSetupSeedStoreAssertionMissing(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc",
"kernel": "pc-kernel",
@@ -1829,20 +1897,20 @@
}
func (s *imageSuite) TestSetupSeedStoreAssertionFetched(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
// add store assertion
- storeAs, err := s.storeSigning.Sign(asserts.StoreType, map[string]interface{}{
+ storeAs, err := s.StoreSigning.Sign(asserts.StoreType, map[string]interface{}{
"store": "my-store",
"operator-id": "canonical",
"timestamp": time.Now().UTC().Format(time.RFC3339),
}, nil, "")
c.Assert(err, IsNil)
- err = s.storeSigning.Add(storeAs)
+ err = s.StoreSigning.Add(storeAs)
c.Assert(err, IsNil)
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc",
"kernel": "pc-kernel",
@@ -1874,9 +1942,9 @@
}
func (s *imageSuite) TestSetupSeedSnapReqBaseFromLocal(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc",
"kernel": "pc-kernel",
@@ -1892,7 +1960,7 @@
"snap-req-other-base": "canonical",
"other-base": "canonical",
})
- bfn := s.downloadedSnaps["other-base"]
+ bfn := s.AssertedSnap("other-base")
opts := &image.Options{
RootDir: rootdir,
GadgetUnpackDir: gadgetUnpackDir,
@@ -1905,9 +1973,9 @@
}
func (s *imageSuite) TestSetupSeedMissingContentProvider(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc",
"kernel": "pc-kernel",
@@ -1942,11 +2010,11 @@
}
func (s *imageSuite) TestSetupSeedClassic(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
// classic model with gadget etc
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"classic": "true",
"architecture": "amd64",
"gadget": "classic-gadget",
@@ -1969,21 +2037,21 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 3)
+ c.Check(seedYaml.Snaps, HasLen, 3)
// check the files are in place
for i, name := range []string{"core", "classic-gadget", "required-snap1"} {
unasserted := false
- info := s.storeSnapInfo[name]
+ info := s.AssertedSnapInfo(name)
fn := filepath.Base(info.MountFile())
p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn)
c.Check(osutil.FileExists(p), Equals, true)
- c.Check(seed.Snaps[i], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[i], DeepEquals, &seed.Snap16{
Name: info.InstanceName(),
SnapID: info.SnapID,
File: fn,
@@ -2011,12 +2079,12 @@
c.Check(osutil.FileExists(blobdir), Equals, false)
}
-func (s *imageSuite) TestSetupSeedClassicWithClassicSnap(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+func (s *imageSuite) TestSetupSeedClassicWithLocalClassicSnap(c *C) {
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
// classic model
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"classic": "true",
"architecture": "amd64",
})
@@ -2024,12 +2092,11 @@
rootdir := filepath.Join(c.MkDir(), "classic-image-root")
s.setupSnaps(c, "", nil)
- s.downloadedSnaps["classic-snap"] = snaptest.MakeTestSnapWithFiles(c, classicSnap, nil)
- s.storeSnapInfo["classic-snap"] = infoFromSnapYaml(c, classicSnap, snap.R(0))
+ snapFile := snaptest.MakeTestSnapWithFiles(c, classicSnap, nil)
opts := &image.Options{
Classic: true,
- Snaps: []string{s.downloadedSnaps["classic-snap"]},
+ Snaps: []string{snapFile},
RootDir: rootdir,
}
local, err := image.LocalSnaps(s.tsto, opts)
@@ -2039,22 +2106,22 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 2)
+ c.Check(seedYaml.Snaps, HasLen, 2)
p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", "core_3.snap")
c.Check(osutil.FileExists(p), Equals, true)
- c.Check(seed.Snaps[0], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[0], DeepEquals, &seed.Snap16{
Name: "core",
- SnapID: "core-Id",
+ SnapID: s.AssertedSnapID("core"),
File: "core_3.snap",
})
p = filepath.Join(rootdir, "var/lib/snapd/seed/snaps", "classic-snap_x1.snap")
c.Check(osutil.FileExists(p), Equals, true)
- c.Check(seed.Snaps[1], DeepEquals, &snap.SeedSnap{
+ c.Check(seedYaml.Snaps[1], DeepEquals, &seed.Snap16{
Name: "classic-snap",
File: "classic-snap_x1.snap",
Classic: true,
@@ -2074,12 +2141,82 @@
})
}
+func (s *imageSuite) TestSetupSeedClassicSnapdOnly(c *C) {
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
+ defer restore()
+
+ // classic model with gadget etc
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
+ "classic": "true",
+ "architecture": "amd64",
+ "gadget": "classic-gadget18",
+ "required-snaps": []interface{}{"core18", "required-snap18"},
+ })
+
+ rootdir := filepath.Join(c.MkDir(), "classic-image-root")
+ s.setupSnaps(c, "", map[string]string{
+ "classic-gadget18": "my-brand",
+ })
+
+ opts := &image.Options{
+ Classic: true,
+ RootDir: rootdir,
+ }
+ local, err := image.LocalSnaps(s.tsto, opts)
+ c.Assert(err, IsNil)
+
+ err = image.SetupSeed(s.tsto, model, opts, local)
+ c.Assert(err, IsNil)
+
+ // check seed yaml
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ c.Assert(err, IsNil)
+
+ c.Check(seedYaml.Snaps, HasLen, 4)
+
+ // check the files are in place
+ for i, name := range []string{"snapd", "core18", "classic-gadget18", "required-snap18"} {
+ unasserted := false
+ info := s.AssertedSnapInfo(name)
+
+ fn := filepath.Base(info.MountFile())
+ p := filepath.Join(rootdir, "var/lib/snapd/seed/snaps", fn)
+ c.Check(osutil.FileExists(p), Equals, true)
+
+ c.Check(seedYaml.Snaps[i], DeepEquals, &seed.Snap16{
+ Name: info.InstanceName(),
+ SnapID: info.SnapID,
+ File: fn,
+ Contact: info.Contact,
+ Unasserted: unasserted,
+ })
+ }
+
+ l, err := ioutil.ReadDir(filepath.Join(rootdir, "var/lib/snapd/seed/snaps"))
+ c.Assert(err, IsNil)
+ c.Check(l, HasLen, 4)
+
+ // check that the bootloader is unset
+ m, err := s.bootloader.GetBootVars("snap_kernel", "snap_core")
+ c.Assert(err, IsNil)
+ c.Check(m, DeepEquals, map[string]string{
+ "snap_core": "",
+ "snap_kernel": "",
+ })
+
+ c.Check(s.stderr.String(), Matches, `WARNING: ensure that the contents under .*/var/lib/snapd/seed are owned by root:root in the \(final\) image`)
+
+ // no blob dir created
+ blobdir := filepath.Join(rootdir, "var/lib/snapd/snaps")
+ c.Check(osutil.FileExists(blobdir), Equals, false)
+}
+
func (s *imageSuite) TestSetupSeedClassicNoSnaps(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
// classic model with gadget etc
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"classic": "true",
})
@@ -2096,10 +2233,10 @@
c.Assert(err, IsNil)
// check seed yaml
- seed, err := snap.ReadSeedYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
+ seedYaml, err := seed.ReadYaml(filepath.Join(rootdir, "var/lib/snapd/seed/seed.yaml"))
c.Assert(err, IsNil)
- c.Check(seed.Snaps, HasLen, 0)
+ c.Check(seedYaml.Snaps, HasLen, 0)
l, err := ioutil.ReadDir(filepath.Join(rootdir, "var/lib/snapd/seed/snaps"))
c.Assert(err, IsNil)
@@ -2118,8 +2255,36 @@
c.Check(osutil.FileExists(blobdir), Equals, false)
}
+func (s *imageSuite) TestSetupSeedClassicSnapdOnlyMissingCore16(c *C) {
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
+ defer restore()
+
+ // classic model with gadget etc
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
+ "classic": "true",
+ "architecture": "amd64",
+ "gadget": "classic-gadget18",
+ "required-snaps": []interface{}{"core18", "snap-req-core16-base"},
+ })
+
+ rootdir := filepath.Join(c.MkDir(), "classic-image-root")
+ s.setupSnaps(c, "", map[string]string{
+ "classic-gadget18": "my-brand",
+ })
+
+ opts := &image.Options{
+ Classic: true,
+ RootDir: rootdir,
+ }
+ local, err := image.LocalSnaps(s.tsto, opts)
+ c.Assert(err, IsNil)
+
+ err = image.SetupSeed(s.tsto, model, opts, local)
+ c.Assert(err, ErrorMatches, `cannot use "snap-req-core16-base" requiring base "core16" without adding "core16" \(or "core"\) explicitly`)
+}
+
func (s *imageSuite) TestSnapChannel(c *C) {
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc=18",
"kernel": "pc-kernel=18",
@@ -2159,14 +2324,19 @@
opts.SnapChannels["pc-kernel"] = "lts/candidate"
_, err = image.SnapChannel("pc-kernel", model, opts, local)
c.Assert(err, ErrorMatches, `channel "lts/candidate" for kernel has a track incompatible with the track from model assertion: 18`)
+
+ opts.SnapChannels["pc-kernel"] = "track/foo"
+ _, err = image.SnapChannel("pc-kernel", model, opts, local)
+ c.Assert(err, ErrorMatches, `cannot use option channel for snap "pc-kernel": invalid risk in channel name: track/foo`)
+
}
func (s *imageSuite) TestSetupSeedLocalSnapd(c *C) {
- restore := image.MockTrusted(s.storeSigning.Trusted)
+ restore := image.MockTrusted(s.StoreSigning.Trusted)
defer restore()
// replace model with a model that uses core18
- model := s.brands.Model("my-brand", "my-model", map[string]interface{}{
+ model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"architecture": "amd64",
"gadget": "pc18",
"kernel": "pc-kernel",
@@ -2182,8 +2352,8 @@
opts := &image.Options{
Snaps: []string{
- s.downloadedSnaps["snapd"],
- s.downloadedSnaps["core18"],
+ s.AssertedSnap("snapd"),
+ s.AssertedSnap("core18"),
},
RootDir: rootdir,
diff -Nru snapd-2.40/include/lk/snappy_boot_v1.h snapd-2.42.1/include/lk/snappy_boot_v1.h
--- snapd-2.40/include/lk/snappy_boot_v1.h 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/include/lk/snappy_boot_v1.h 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,149 @@
+/**
+ * Copyright (C) 2019 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 .
+ *
+ */
+
+#ifndef _BOOTLOADER_SNAP_BOOT_V1_H
+#define _BOOTLOADER_SNAP_BOOT_V1_H
+
+#define SNAP_BOOTSELECT_VERSION 0x00010001
+#define SNAP_BOOTSELECT_SIGNATURE ('S' | ('B' << 8) | ('s' << 16) | ('e' << 24))
+#define SNAP_NAME_MAX_LEN (256)
+#define HASH_LENGTH (32)
+#define SNAP_MODE_TRY "try"
+#define SNAP_MODE_TRYING "trying"
+#define FACTORY_RESET "factory-reset"
+
+/* partition label where boot select structure is stored */
+#define SNAP_BOOTSELECT_PARTITION "snapbootsel"
+
+/* number of available bootimg partitions, min 2 */
+#define SNAP_BOOTIMG_PART_NUM 2
+
+/* snappy bootselect partition format structure */
+typedef struct SNAP_BOOT_SELECTION {
+ /* Contains value BOOTSELECT_SIGNATURE defined above */
+ uint32_t signature;
+ /* snappy boot select version */
+ uint32_t version;
+
+ /* snap_mode, one of: 'empty', "try", "trying" */
+ char snap_mode[SNAP_NAME_MAX_LEN];
+ /* current core snap revision */
+ char snap_core[SNAP_NAME_MAX_LEN];
+ /* try core snap revision */
+ char snap_try_core[SNAP_NAME_MAX_LEN];
+ /* current kernel snap revision */
+ char snap_kernel[SNAP_NAME_MAX_LEN];
+ /* current kernel snap revision */
+ char snap_try_kernel[SNAP_NAME_MAX_LEN];
+
+ /* gadget_mode, one of: 'empty', "try", "trying" */
+ char gadget_mode[SNAP_NAME_MAX_LEN];
+ /* GADGET assets: current gadget assets revision */
+ char snap_gadget[SNAP_NAME_MAX_LEN];
+ /* GADGET assets: try gadget assets revision */
+ char snap_try_gadget [SNAP_NAME_MAX_LEN];
+
+ /**
+ * Reboot reason
+ * optional parameter to signal bootloader alternative reboot reasons
+ * e.g. recovery/factory-reset/boot asset update
+ */
+ char reboot_reason[SNAP_NAME_MAX_LEN];
+
+ /**
+ * Matrix for mapping of boot img partion to installed kernel snap revision
+ *
+ * First column represents boot image partition label (e.g. boot_a,boot_b )
+ * value are static and should be populated at gadget built time
+ * or latest at image build time. Values are not further altered at run time.
+ * Second column represents name currently installed kernel snap
+ * e.g. pi2-kernel_123.snap
+ * initial value representing initial kernel snap revision
+ * is pupulated at image build time by snapd
+ *
+ * There are two rows in the matrix, representing current and previous kernel revision
+ * following describes how this matrix should be modified at different stages:
+ * - at image build time:
+ * - extracted kernel snap revision name should be filled
+ * into free slow (first row, second row)
+ * - snapd:
+ * - when new kernel snap revision is being installed, snapd cycles through
+ * matrix to find unused 'boot slot' to be used for new kernel snap revision
+ * from free slot, first column represents partition label to which kernel
+ * snap boot image should be extracted. Second column is then populated with
+ * kernel snap revision name.
+ * - snap_mode, snap_try_kernel, snap_try_core behaves same way as with u-boot
+ * - bootloader:
+ * - bootloader reads snap_mode to determine if snap_kernel or snap_kernel is used
+ * to get kernel snap revision name
+ * kernel snap revision is then used to search matrix to determine
+ * partition label to be used for current boot
+ * - bootloader NEVER alters this matrix values
+ *
+ * [ ] [ ]
+ * [ ] [ ]
+ */
+ char bootimg_matrix[SNAP_BOOTIMG_PART_NUM][2][SNAP_NAME_MAX_LEN];
+
+ /* name of the boot image from kernel snap to be used for extraction
+ when not defined or empty, default boot.img will be used */
+ char bootimg_file_name[SNAP_NAME_MAX_LEN];
+
+ /**
+ * gadget assets: Matrix for mapping of gadget asset partions
+ * Optional boot asset tracking, based on bootloader support
+ * Some boot chains support A/B boot assets for increased robustness
+ * example being A/B TrustExecutionEnvironment
+ * This matrix can be used to track current and try boot assets for
+ * robust updates
+ * Use of Gadget_asset_matrix matches use of Bootimg_matrix
+ *
+ * [ ] [ ]
+ * [ ] [ ]
+ */
+ char gadget_asset_matrix [SNAP_BOOTIMG_PART_NUM][2][SNAP_NAME_MAX_LEN];
+
+ /* unused placeholders for additional parameters to be used in the future */
+ char unused_key_01 [SNAP_NAME_MAX_LEN];
+ char unused_key_02 [SNAP_NAME_MAX_LEN];
+ char unused_key_03 [SNAP_NAME_MAX_LEN];
+ char unused_key_04 [SNAP_NAME_MAX_LEN];
+ char unused_key_05 [SNAP_NAME_MAX_LEN];
+ char unused_key_06 [SNAP_NAME_MAX_LEN];
+ char unused_key_07 [SNAP_NAME_MAX_LEN];
+ char unused_key_08 [SNAP_NAME_MAX_LEN];
+ char unused_key_09 [SNAP_NAME_MAX_LEN];
+ char unused_key_10 [SNAP_NAME_MAX_LEN];
+ char unused_key_11 [SNAP_NAME_MAX_LEN];
+ char unused_key_12 [SNAP_NAME_MAX_LEN];
+ char unused_key_13 [SNAP_NAME_MAX_LEN];
+ char unused_key_14 [SNAP_NAME_MAX_LEN];
+ char unused_key_15 [SNAP_NAME_MAX_LEN];
+ char unused_key_16 [SNAP_NAME_MAX_LEN];
+ char unused_key_17 [SNAP_NAME_MAX_LEN];
+ char unused_key_18 [SNAP_NAME_MAX_LEN];
+ char unused_key_19 [SNAP_NAME_MAX_LEN];
+ char unused_key_20 [SNAP_NAME_MAX_LEN];
+
+ /* unused array of 10 key - value pairs */
+ char key_value_pairs [10][2][SNAP_NAME_MAX_LEN];
+
+ /* crc32 value for structure */
+ uint32_t crc32;
+} SNAP_BOOT_SELECTION_t;
+
+#endif
diff -Nru snapd-2.40/interfaces/apparmor/backend.go snapd-2.42.1/interfaces/apparmor/backend.go
--- snapd-2.40/interfaces/apparmor/backend.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/apparmor/backend.go 2019-10-30 12:17:43.000000000 +0000
@@ -341,7 +341,7 @@
// Deal with the "snapd" snap - we do the setup slightly differently
// here because this will run both on classic and on Ubuntu Core 18
// systems but /etc/apparmor.d is not writable on core18 systems
- if snapName == "snapd" && release.AppArmorLevel() != release.NoAppArmor {
+ if snapInfo.GetType() == snap.TypeSnapd && release.AppArmorLevel() != release.NoAppArmor {
if err := setupSnapConfineReexec(snapInfo); err != nil {
return fmt.Errorf("cannot create host snap-confine apparmor configuration: %s", err)
}
@@ -352,7 +352,7 @@
// See LP:#1460152 and
// https://forum.snapcraft.io/t/core-snap-revert-issues-on-core-devices/
//
- if (snapName == "core" || snapName == "snapd") && !release.OnClassic {
+ if (snapInfo.GetType() == snap.TypeOS || snapInfo.GetType() == snap.TypeSnapd) && !release.OnClassic {
if li, err := filepath.Glob(filepath.Join(dirs.SystemApparmorCacheDir, "*")); err == nil {
for _, p := range li {
if st, err := os.Stat(p); err == nil && st.Mode().IsRegular() && profileIsRemovableOnCoreSetup(p) {
@@ -606,6 +606,11 @@
repl = ""
}
tagSnippets = strings.Replace(tagSnippets, "###HOME_IX###", repl, -1)
+
+ // Conditionally add privilege dropping policy
+ if len(snapInfo.SystemUsernames) > 0 {
+ tagSnippets += privDropAndChownRules
+ }
}
return tagSnippets
diff -Nru snapd-2.40/interfaces/apparmor/backend_test.go snapd-2.42.1/interfaces/apparmor/backend_test.go
--- snapd-2.40/interfaces/apparmor/backend_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/apparmor/backend_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -723,6 +723,7 @@
const snapdYaml = `name: snapd
version: 1
+type: snapd
`
func (s *backendSuite) writeVanillaSnapConfineProfile(c *C, coreOrSnapdInfo *snap.Info) {
@@ -1785,3 +1786,51 @@
s.RemoveSnap(c, snapInfo)
}
}
+
+func (s *backendSuite) TestSystemUsernamesPolicy(c *C) {
+ restoreTemplate := apparmor.MockTemplate("template\n###SNIPPETS###\n")
+ defer restoreTemplate()
+ restore := release.MockAppArmorLevel(release.FullAppArmor)
+ defer restore()
+
+ snapYaml := `
+name: app
+version: 0.1
+system-usernames:
+ testid: shared
+apps:
+ cmd:
+`
+
+ snapInfo := s.InstallSnap(c, interfaces.ConfinementOptions{}, "", snapYaml, 1)
+ profile := filepath.Join(dirs.SnapAppArmorDir, "snap.app.cmd")
+ data, err := ioutil.ReadFile(profile)
+ c.Assert(err, IsNil)
+ c.Assert(string(data), testutil.Contains, "capability setuid,")
+ c.Assert(string(data), testutil.Contains, "capability setgid,")
+ c.Assert(string(data), testutil.Contains, "capability chown,")
+ s.RemoveSnap(c, snapInfo)
+}
+
+func (s *backendSuite) TestNoSystemUsernamesPolicy(c *C) {
+ restoreTemplate := apparmor.MockTemplate("template\n###SNIPPETS###\n")
+ defer restoreTemplate()
+ restore := release.MockAppArmorLevel(release.FullAppArmor)
+ defer restore()
+
+ snapYaml := `
+name: app
+version: 0.1
+apps:
+ cmd:
+`
+
+ snapInfo := s.InstallSnap(c, interfaces.ConfinementOptions{}, "", snapYaml, 1)
+ profile := filepath.Join(dirs.SnapAppArmorDir, "snap.app.cmd")
+ data, err := ioutil.ReadFile(profile)
+ c.Assert(err, IsNil)
+ c.Assert(string(data), Not(testutil.Contains), "capability setuid,")
+ c.Assert(string(data), Not(testutil.Contains), "capability setgid,")
+ c.Assert(string(data), Not(testutil.Contains), "capability chown,")
+ s.RemoveSnap(c, snapInfo)
+}
diff -Nru snapd-2.40/interfaces/apparmor/spec.go snapd-2.42.1/interfaces/apparmor/spec.go
--- snapd-2.40/interfaces/apparmor/spec.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/apparmor/spec.go 2019-10-30 12:17:43.000000000 +0000
@@ -42,7 +42,7 @@
snippets map[string][]string
// updateNS describe parts of apparmor policy for snap-update-ns executing
// on behalf of a given snap.
- updateNS []string
+ updateNS strutil.OrderedSet
// AppArmor deny rules cannot be undone by allow rules which makes
// deny rules difficult to work with arbitrary combinations of
@@ -92,7 +92,19 @@
// AddUpdateNS adds a new apparmor snippet for the snap-update-ns program.
func (spec *Specification) AddUpdateNS(snippet string) {
- spec.updateNS = append(spec.updateNS, snippet)
+ spec.updateNS.Put(snippet)
+}
+
+// EmitUpdateNSFunc returns a function for emitting update-ns snippets.
+func (spec *Specification) EmitUpdateNSFunc() func(f string, args ...interface{}) {
+ return func(f string, args ...interface{}) {
+ spec.AddUpdateNS(fmt.Sprintf(f, args...))
+ }
+}
+
+// UpdateNSIndexOf returns the index of a previously added snippet.
+func (spec *Specification) UpdateNSIndexOf(snippet string) (idx int, ok bool) {
+ return spec.updateNS.IndexOf(snippet)
}
// AddLayout adds apparmor snippets based on the layout of the snap.
@@ -145,40 +157,44 @@
}
sort.Strings(spec.snippets[tag])
}
+
+ emit := spec.EmitUpdateNSFunc()
+
// Append update-ns snippets that allow constructing the layout.
for _, path := range paths {
- var buf bytes.Buffer
l := si.Layout[path]
- fmt.Fprintf(&buf, " # Layout %s\n", l.String())
+ emit(" # Layout %s\n", l.String())
path := si.ExpandSnapVariables(l.Path)
switch {
case l.Bind != "":
bind := si.ExpandSnapVariables(l.Bind)
// Allow bind mounting the layout element.
- fmt.Fprintf(&buf, " mount options=(rbind, rw) %s/ -> %s/,\n", bind, path)
- fmt.Fprintf(&buf, " umount %s/,\n", path)
+ emit(" mount options=(rbind, rw) %s/ -> %s/,\n", bind, path)
+ emit(" mount options=(rprivate) -> %s/,\n", path)
+ emit(" umount %s/,\n", path)
// Allow constructing writable mimic in both bind-mount source and mount point.
- WritableProfile(&buf, path, 2) // At least / and /some-top-level-directory
- WritableProfile(&buf, bind, 4) // At least /, /snap/, /snap/$SNAP_NAME and /snap/$SNAP_NAME/$SNAP_REVISION
+ GenWritableProfile(emit, path, 2) // At least / and /some-top-level-directory
+ GenWritableProfile(emit, bind, 4) // At least /, /snap/, /snap/$SNAP_NAME and /snap/$SNAP_NAME/$SNAP_REVISION
case l.BindFile != "":
bindFile := si.ExpandSnapVariables(l.BindFile)
// Allow bind mounting the layout element.
- fmt.Fprintf(&buf, " mount options=(bind, rw) %s -> %s,\n", bindFile, path)
- fmt.Fprintf(&buf, " umount %s,\n", path)
+ emit(" mount options=(bind, rw) %s -> %s,\n", bindFile, path)
+ emit(" mount options=(rprivate) -> %s,\n", path)
+ emit(" umount %s,\n", path)
// Allow constructing writable mimic in both bind-mount source and mount point.
- WritableFileProfile(&buf, path, 2) // At least / and /some-top-level-directory
- WritableFileProfile(&buf, bindFile, 4) // At least /, /snap/, /snap/$SNAP_NAME and /snap/$SNAP_NAME/$SNAP_REVISION
+ GenWritableFileProfile(emit, path, 2) // At least / and /some-top-level-directory
+ GenWritableFileProfile(emit, bindFile, 4) // At least /, /snap/, /snap/$SNAP_NAME and /snap/$SNAP_NAME/$SNAP_REVISION
case l.Type == "tmpfs":
- fmt.Fprintf(&buf, " mount fstype=tmpfs tmpfs -> %s/,\n", path)
- fmt.Fprintf(&buf, " umount %s/,\n", path)
+ emit(" mount fstype=tmpfs tmpfs -> %s/,\n", path)
+ emit(" mount options=(rprivate) -> %s/,\n", path)
+ emit(" umount %s/,\n", path)
// Allow constructing writable mimic to mount point.
- WritableProfile(&buf, path, 2) // At least / and /some-top-level-directory
+ GenWritableProfile(emit, path, 2) // At least / and /some-top-level-directory
case l.Symlink != "":
// Allow constructing writable mimic to symlink parent directory.
- fmt.Fprintf(&buf, " %s rw,\n", path)
- WritableProfile(&buf, path, 2) // At least / and /some-top-level-directory
+ emit(" %s rw,\n", path)
+ GenWritableProfile(emit, path, 2) // At least / and /some-top-level-directory
}
- spec.AddUpdateNS(buf.String())
}
}
@@ -217,9 +233,9 @@
return path == "/" || path == "/snap" || path == "/var" || path == "/var/snap" || path == "/tmp" || path == "/usr" || path == "/etc"
}
-// WritableMimicProfile writes apparmor rules for a writable mimic at the given path.
-func WritableMimicProfile(buf *bytes.Buffer, path string, assumedPrefixDepth int) {
- fmt.Fprintf(buf, " # Writable mimic %s\n", path)
+// GenWritableMimicProfile generates apparmor rules for a writable mimic at the given path.
+func GenWritableMimicProfile(emit func(f string, args ...interface{}), path string, assumedPrefixDepth int) {
+ emit(" # Writable mimic %s\n", path)
iter, err := strutil.NewPathIterator(path)
if err != nil {
@@ -227,10 +243,10 @@
}
// Handle the prefix that is assumed to exist first.
- fmt.Fprintf(buf, " # .. permissions for traversing the prefix that is assumed to exist\n")
+ emit(" # .. permissions for traversing the prefix that is assumed to exist\n")
for iter.Next() {
if iter.Depth() < assumedPrefixDepth {
- fmt.Fprintf(buf, " %s r,\n", iter.CurrentPath())
+ emit(" %s r,\n", iter.CurrentPath())
}
}
@@ -246,64 +262,68 @@
// directory path semantics.
mimicPath := filepath.Join(iter.CurrentBase(), iter.CurrentCleanName()) + "/"
mimicAuxPath := filepath.Join("/tmp/.snap", iter.CurrentPath()) + "/"
- fmt.Fprintf(buf, " # .. variant with mimic at %s\n", mimicPath)
- fmt.Fprintf(buf, " # Allow reading the mimic directory, it must exist in the first place.\n")
- fmt.Fprintf(buf, " %s r,\n", mimicPath)
- fmt.Fprintf(buf, " # Allow setting the read-only directory aside via a bind mount.\n")
- fmt.Fprintf(buf, " %s rw,\n", mimicAuxPath)
- fmt.Fprintf(buf, " mount options=(rbind, rw) %s -> %s,\n", mimicPath, mimicAuxPath)
- fmt.Fprintf(buf, " # Allow mounting tmpfs over the read-only directory.\n")
- fmt.Fprintf(buf, " mount fstype=tmpfs options=(rw) tmpfs -> %s,\n", mimicPath)
- fmt.Fprintf(buf, " # Allow creating empty files and directories for bind mounting things\n"+
+ emit(" # .. variant with mimic at %s\n", mimicPath)
+ emit(" # Allow reading the mimic directory, it must exist in the first place.\n")
+ emit(" %s r,\n", mimicPath)
+ emit(" # Allow setting the read-only directory aside via a bind mount.\n")
+ emit(" %s rw,\n", mimicAuxPath)
+ emit(" mount options=(rbind, rw) %s -> %s,\n", mimicPath, mimicAuxPath)
+ emit(" # Allow mounting tmpfs over the read-only directory.\n")
+ emit(" mount fstype=tmpfs options=(rw) tmpfs -> %s,\n", mimicPath)
+ emit(" # Allow creating empty files and directories for bind mounting things\n" +
" # to reconstruct the now-writable parent directory.\n")
- fmt.Fprintf(buf, " %s*/ rw,\n", mimicAuxPath)
- fmt.Fprintf(buf, " %s*/ rw,\n", mimicPath)
- fmt.Fprintf(buf, " mount options=(rbind, rw) %s*/ -> %s*/,\n", mimicAuxPath, mimicPath)
- fmt.Fprintf(buf, " %s* rw,\n", mimicAuxPath)
- fmt.Fprintf(buf, " %s* rw,\n", mimicPath)
- fmt.Fprintf(buf, " mount options=(bind, rw) %s* -> %s*,\n", mimicAuxPath, mimicPath)
- fmt.Fprintf(buf, " # Allow unmounting the auxiliary directory.\n"+
+ emit(" %s*/ rw,\n", mimicAuxPath)
+ emit(" %s*/ rw,\n", mimicPath)
+ emit(" mount options=(rbind, rw) %s*/ -> %s*/,\n", mimicAuxPath, mimicPath)
+ emit(" %s* rw,\n", mimicAuxPath)
+ emit(" %s* rw,\n", mimicPath)
+ emit(" mount options=(bind, rw) %s* -> %s*,\n", mimicAuxPath, mimicPath)
+ emit(" # Allow unmounting the auxiliary directory.\n" +
" # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)\n")
- fmt.Fprintf(buf, " umount %s,\n", mimicAuxPath)
- fmt.Fprintf(buf, " # Allow unmounting the destination directory as well as anything\n"+
- " # inside. This lets us perform the undo plan in case the writable\n"+
+ emit(" mount options=(rprivate) -> %s,\n", mimicAuxPath)
+ emit(" umount %s,\n", mimicAuxPath)
+ emit(" # Allow unmounting the destination directory as well as anything\n" +
+ " # inside. This lets us perform the undo plan in case the writable\n" +
" # mimic fails.\n")
- fmt.Fprintf(buf, " umount %s,\n", mimicPath)
- fmt.Fprintf(buf, " umount %s*,\n", mimicPath)
- fmt.Fprintf(buf, " umount %s*/,\n", mimicPath)
+ emit(" mount options=(rprivate) -> %s,\n", mimicPath)
+ emit(" mount options=(rprivate) -> %s*,\n", mimicPath)
+ emit(" mount options=(rprivate) -> %s*/,\n", mimicPath)
+ emit(" umount %s,\n", mimicPath)
+ emit(" umount %s*,\n", mimicPath)
+ emit(" umount %s*/,\n", mimicPath)
}
}
-// WritableFileProfile writes a profile for snap-update-ns for making given file writable.
-func WritableFileProfile(buf *bytes.Buffer, path string, assumedPrefixDepth int) {
+// GenWritableFileProfile writes a profile for snap-update-ns for making given file writable.
+func GenWritableFileProfile(emit func(f string, args ...interface{}), path string, assumedPrefixDepth int) {
if path == "/" {
return
}
if isProbablyWritable(path) {
- fmt.Fprintf(buf, " # Writable file %s\n", path)
- fmt.Fprintf(buf, " %s rw,\n", path)
+ emit(" # Writable file %s\n", path)
+ emit(" %s rw,\n", path)
for p := parent(path); !isProbablyPresent(p); p = parent(p) {
- fmt.Fprintf(buf, " %s/ rw,\n", p)
+ emit(" %s/ rw,\n", p)
}
} else {
parentPath := parent(path)
- WritableMimicProfile(buf, parentPath, assumedPrefixDepth)
+ GenWritableMimicProfile(emit, parentPath, assumedPrefixDepth)
}
}
-// WritableProfile writes a profile for snap-update-ns for making given directory writable.
-func WritableProfile(buf *bytes.Buffer, path string, assumedPrefixDepth int) {
+// GenWritableProfile generates a profile for snap-update-ns for making given directory writable.
+func GenWritableProfile(emit func(f string, args ...interface{}), path string, assumedPrefixDepth int) {
if path == "/" {
return
}
if isProbablyWritable(path) {
- fmt.Fprintf(buf, " # Writable directory %s\n", path)
+ emit(" # Writable directory %s\n", path)
for p := path; !isProbablyPresent(p); p = parent(p) {
- fmt.Fprintf(buf, " %s/ rw,\n", p)
+ emit(" %s/ rw,\n", p)
}
} else {
parentPath := parent(path)
- WritableMimicProfile(buf, parentPath, assumedPrefixDepth)
+ GenWritableMimicProfile(emit, parentPath, assumedPrefixDepth)
}
}
@@ -337,9 +357,7 @@
// UpdateNS returns a deep copy of all the added snap-update-ns snippets.
func (spec *Specification) UpdateNS() []string {
- cp := make([]string, len(spec.updateNS))
- copy(cp, spec.updateNS)
- return cp
+ return spec.updateNS.Items()
}
func snippetFromLayout(layout *snap.Layout) string {
diff -Nru snapd-2.40/interfaces/apparmor/spec_test.go snapd-2.42.1/interfaces/apparmor/spec_test.go
--- snapd-2.40/interfaces/apparmor/spec_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/apparmor/spec_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,6 +20,8 @@
package apparmor_test
import (
+ "strings"
+
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/interfaces"
@@ -139,6 +141,11 @@
s.spec.AddUpdateNS("s-u-n snippet 1")
s.spec.AddUpdateNS("s-u-n snippet 2")
+ // Check the order of the snippets can be retrieved.
+ idx, ok := s.spec.UpdateNSIndexOf("s-u-n snippet 2")
+ c.Assert(ok, Equals, true)
+ c.Check(idx, Equals, 1)
+
// The snippets were recorded correctly and in the right place.
c.Assert(s.spec.UpdateNS(), DeepEquals, []string{
"s-u-n snippet 1", "s-u-n snippet 2",
@@ -183,6 +190,7 @@
profile0 := ` # Layout /etc/foo.conf: bind-file $SNAP/foo.conf
mount options=(bind, rw) /snap/vanguard/42/foo.conf -> /etc/foo.conf,
+ mount options=(rprivate) -> /etc/foo.conf,
umount /etc/foo.conf,
# Writable mimic /etc
# .. permissions for traversing the prefix that is assumed to exist
@@ -205,227 +213,155 @@
mount options=(bind, rw) /tmp/.snap/etc/* -> /etc/*,
# Allow unmounting the auxiliary directory.
# TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/etc/,
umount /tmp/.snap/etc/,
# Allow unmounting the destination directory as well as anything
# inside. This lets us perform the undo plan in case the writable
# mimic fails.
+ mount options=(rprivate) -> /etc/,
+ mount options=(rprivate) -> /etc/*,
+ mount options=(rprivate) -> /etc/*/,
umount /etc/,
umount /etc/*,
umount /etc/*/,
# Writable mimic /snap/vanguard/42
- # .. permissions for traversing the prefix that is assumed to exist
- / r,
/snap/ r,
/snap/vanguard/ r,
# .. variant with mimic at /snap/vanguard/42/
- # Allow reading the mimic directory, it must exist in the first place.
/snap/vanguard/42/ r,
- # Allow setting the read-only directory aside via a bind mount.
/tmp/.snap/snap/vanguard/42/ rw,
mount options=(rbind, rw) /snap/vanguard/42/ -> /tmp/.snap/snap/vanguard/42/,
- # Allow mounting tmpfs over the read-only directory.
mount fstype=tmpfs options=(rw) tmpfs -> /snap/vanguard/42/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
/tmp/.snap/snap/vanguard/42/*/ rw,
/snap/vanguard/42/*/ rw,
mount options=(rbind, rw) /tmp/.snap/snap/vanguard/42/*/ -> /snap/vanguard/42/*/,
/tmp/.snap/snap/vanguard/42/* rw,
/snap/vanguard/42/* rw,
mount options=(bind, rw) /tmp/.snap/snap/vanguard/42/* -> /snap/vanguard/42/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/snap/vanguard/42/,
umount /tmp/.snap/snap/vanguard/42/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
+ mount options=(rprivate) -> /snap/vanguard/42/,
+ mount options=(rprivate) -> /snap/vanguard/42/*,
+ mount options=(rprivate) -> /snap/vanguard/42/*/,
umount /snap/vanguard/42/,
umount /snap/vanguard/42/*,
umount /snap/vanguard/42/*/,
`
- c.Assert(updateNS[0], Equals, profile0)
+ // Find the slice that describes profile0 by looking for the first unique
+ // line of the next profile.
+ start := 0
+ end, _ := s.spec.UpdateNSIndexOf(" # Layout /usr/foo: bind $SNAP/usr/foo\n")
+ c.Assert(strings.Join(updateNS[start:end], ""), Equals, profile0)
profile1 := ` # Layout /usr/foo: bind $SNAP/usr/foo
mount options=(rbind, rw) /snap/vanguard/42/usr/foo/ -> /usr/foo/,
+ mount options=(rprivate) -> /usr/foo/,
umount /usr/foo/,
# Writable mimic /usr
- # .. permissions for traversing the prefix that is assumed to exist
- / r,
# .. variant with mimic at /usr/
- # Allow reading the mimic directory, it must exist in the first place.
/usr/ r,
- # Allow setting the read-only directory aside via a bind mount.
/tmp/.snap/usr/ rw,
mount options=(rbind, rw) /usr/ -> /tmp/.snap/usr/,
- # Allow mounting tmpfs over the read-only directory.
mount fstype=tmpfs options=(rw) tmpfs -> /usr/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
/tmp/.snap/usr/*/ rw,
/usr/*/ rw,
mount options=(rbind, rw) /tmp/.snap/usr/*/ -> /usr/*/,
/tmp/.snap/usr/* rw,
/usr/* rw,
mount options=(bind, rw) /tmp/.snap/usr/* -> /usr/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/usr/,
umount /tmp/.snap/usr/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
+ mount options=(rprivate) -> /usr/,
+ mount options=(rprivate) -> /usr/*,
+ mount options=(rprivate) -> /usr/*/,
umount /usr/,
umount /usr/*,
umount /usr/*/,
# Writable mimic /snap/vanguard/42/usr
- # .. permissions for traversing the prefix that is assumed to exist
- / r,
- /snap/ r,
- /snap/vanguard/ r,
- # .. variant with mimic at /snap/vanguard/42/
- # Allow reading the mimic directory, it must exist in the first place.
- /snap/vanguard/42/ r,
- # Allow setting the read-only directory aside via a bind mount.
- /tmp/.snap/snap/vanguard/42/ rw,
- mount options=(rbind, rw) /snap/vanguard/42/ -> /tmp/.snap/snap/vanguard/42/,
- # Allow mounting tmpfs over the read-only directory.
- mount fstype=tmpfs options=(rw) tmpfs -> /snap/vanguard/42/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
- /tmp/.snap/snap/vanguard/42/*/ rw,
- /snap/vanguard/42/*/ rw,
- mount options=(rbind, rw) /tmp/.snap/snap/vanguard/42/*/ -> /snap/vanguard/42/*/,
- /tmp/.snap/snap/vanguard/42/* rw,
- /snap/vanguard/42/* rw,
- mount options=(bind, rw) /tmp/.snap/snap/vanguard/42/* -> /snap/vanguard/42/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
- umount /tmp/.snap/snap/vanguard/42/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
- umount /snap/vanguard/42/,
- umount /snap/vanguard/42/*,
- umount /snap/vanguard/42/*/,
# .. variant with mimic at /snap/vanguard/42/usr/
- # Allow reading the mimic directory, it must exist in the first place.
/snap/vanguard/42/usr/ r,
- # Allow setting the read-only directory aside via a bind mount.
/tmp/.snap/snap/vanguard/42/usr/ rw,
mount options=(rbind, rw) /snap/vanguard/42/usr/ -> /tmp/.snap/snap/vanguard/42/usr/,
- # Allow mounting tmpfs over the read-only directory.
mount fstype=tmpfs options=(rw) tmpfs -> /snap/vanguard/42/usr/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
/tmp/.snap/snap/vanguard/42/usr/*/ rw,
/snap/vanguard/42/usr/*/ rw,
mount options=(rbind, rw) /tmp/.snap/snap/vanguard/42/usr/*/ -> /snap/vanguard/42/usr/*/,
/tmp/.snap/snap/vanguard/42/usr/* rw,
/snap/vanguard/42/usr/* rw,
mount options=(bind, rw) /tmp/.snap/snap/vanguard/42/usr/* -> /snap/vanguard/42/usr/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/snap/vanguard/42/usr/,
umount /tmp/.snap/snap/vanguard/42/usr/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
+ mount options=(rprivate) -> /snap/vanguard/42/usr/,
+ mount options=(rprivate) -> /snap/vanguard/42/usr/*,
+ mount options=(rprivate) -> /snap/vanguard/42/usr/*/,
umount /snap/vanguard/42/usr/,
umount /snap/vanguard/42/usr/*,
umount /snap/vanguard/42/usr/*/,
`
- c.Assert(updateNS[1], Equals, profile1)
+ // Find the slice that describes profile1 by looking for the first unique
+ // line of the next profile.
+ start = end
+ end, _ = s.spec.UpdateNSIndexOf(" # Layout /var/cache/mylink: symlink $SNAP_DATA/link/target\n")
+ c.Assert(strings.Join(updateNS[start:end], ""), Equals, profile1)
profile2 := ` # Layout /var/cache/mylink: symlink $SNAP_DATA/link/target
/var/cache/mylink rw,
# Writable mimic /var/cache
- # .. permissions for traversing the prefix that is assumed to exist
- / r,
# .. variant with mimic at /var/
- # Allow reading the mimic directory, it must exist in the first place.
/var/ r,
- # Allow setting the read-only directory aside via a bind mount.
/tmp/.snap/var/ rw,
mount options=(rbind, rw) /var/ -> /tmp/.snap/var/,
- # Allow mounting tmpfs over the read-only directory.
mount fstype=tmpfs options=(rw) tmpfs -> /var/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
/tmp/.snap/var/*/ rw,
/var/*/ rw,
mount options=(rbind, rw) /tmp/.snap/var/*/ -> /var/*/,
/tmp/.snap/var/* rw,
/var/* rw,
mount options=(bind, rw) /tmp/.snap/var/* -> /var/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/var/,
umount /tmp/.snap/var/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
+ mount options=(rprivate) -> /var/,
+ mount options=(rprivate) -> /var/*,
+ mount options=(rprivate) -> /var/*/,
umount /var/,
umount /var/*,
umount /var/*/,
# .. variant with mimic at /var/cache/
- # Allow reading the mimic directory, it must exist in the first place.
/var/cache/ r,
- # Allow setting the read-only directory aside via a bind mount.
/tmp/.snap/var/cache/ rw,
mount options=(rbind, rw) /var/cache/ -> /tmp/.snap/var/cache/,
- # Allow mounting tmpfs over the read-only directory.
mount fstype=tmpfs options=(rw) tmpfs -> /var/cache/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
/tmp/.snap/var/cache/*/ rw,
/var/cache/*/ rw,
mount options=(rbind, rw) /tmp/.snap/var/cache/*/ -> /var/cache/*/,
/tmp/.snap/var/cache/* rw,
/var/cache/* rw,
mount options=(bind, rw) /tmp/.snap/var/cache/* -> /var/cache/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/var/cache/,
umount /tmp/.snap/var/cache/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
+ mount options=(rprivate) -> /var/cache/,
+ mount options=(rprivate) -> /var/cache/*,
+ mount options=(rprivate) -> /var/cache/*/,
umount /var/cache/,
umount /var/cache/*,
umount /var/cache/*/,
`
- c.Assert(updateNS[2], Equals, profile2)
+ // Find the slice that describes profile2 by looking for the first unique
+ // line of the next profile.
+ start = end
+ end, _ = s.spec.UpdateNSIndexOf(" # Layout /var/tmp: type tmpfs, mode: 01777\n")
+ c.Assert(strings.Join(updateNS[start:end], ""), Equals, profile2)
profile3 := ` # Layout /var/tmp: type tmpfs, mode: 01777
mount fstype=tmpfs tmpfs -> /var/tmp/,
+ mount options=(rprivate) -> /var/tmp/,
umount /var/tmp/,
# Writable mimic /var
- # .. permissions for traversing the prefix that is assumed to exist
- / r,
- # .. variant with mimic at /var/
- # Allow reading the mimic directory, it must exist in the first place.
- /var/ r,
- # Allow setting the read-only directory aside via a bind mount.
- /tmp/.snap/var/ rw,
- mount options=(rbind, rw) /var/ -> /tmp/.snap/var/,
- # Allow mounting tmpfs over the read-only directory.
- mount fstype=tmpfs options=(rw) tmpfs -> /var/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
- /tmp/.snap/var/*/ rw,
- /var/*/ rw,
- mount options=(rbind, rw) /tmp/.snap/var/*/ -> /var/*/,
- /tmp/.snap/var/* rw,
- /var/* rw,
- mount options=(bind, rw) /tmp/.snap/var/* -> /var/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
- umount /tmp/.snap/var/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
- umount /var/,
- umount /var/*,
- umount /var/*/,
`
- c.Assert(updateNS[3], Equals, profile3)
- c.Assert(updateNS, DeepEquals, []string{profile0, profile1, profile2, profile3})
+ // Find the slice that describes profile2 by looking till the end of the list.
+ start = end
+ c.Assert(strings.Join(updateNS[start:], ""), Equals, profile3)
+ c.Assert(strings.Join(updateNS, ""), DeepEquals, strings.Join([]string{profile0, profile1, profile2, profile3}, ""))
}
const snapTrivial = `
diff -Nru snapd-2.40/interfaces/apparmor/template.go snapd-2.42.1/interfaces/apparmor/template.go
--- snapd-2.40/interfaces/apparmor/template.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/apparmor/template.go 2019-10-30 12:17:43.000000000 +0000
@@ -132,6 +132,7 @@
/{,usr/}bin/bzgrep ixr,
/{,usr/}bin/bzip2 ixr,
/{,usr/}bin/cat ixr,
+ /{,usr/}bin/chgrp ixr,
/{,usr/}bin/chmod ixr,
/{,usr/}bin/chown ixr,
/{,usr/}bin/clear ixr,
@@ -510,6 +511,65 @@
}
`
+// Template for privilege drop and chown operations. The specific setuid,
+// setgid and chown operations are controlled via seccomp.
+//
+// To expand on the policy comment below: "this is not a problem in practice":
+// access to sockets is mediated by file and unix AppArmor rules. When the
+// access is allowed, the snap is expected to be able to use the socket. Some
+// service listeners will employ additional checks, such as 'is the connecting
+// (snap) process root' or 'is the connecting non-root (snap) process in a
+// particular group', etc. Since snapd daemons start as root and because the
+// service listeners typically let the root process do anything, the snap
+// doesn't gain anything from being able to forge a uid since it has full
+// access to the socket API already. A snap could forge a check to bypass the
+// theoretical case of the service listener wanting to limit root to something
+// less than another user, but in practice service listeners won't do this
+// because it is ineffective against unconfined root processes which can
+// manipulate the service listener in other ways to subvert a check like this.
+//
+// For CAP_KILL, AppArmor mediates signals and the default policy allows
+// sending signals only to processes with a security label that matches the
+// snap, but AppArmor does not currently mediate the uid/gid of the
+// sender/receiver to finely mediate what non-root uid/gids a root process may
+// send to, so we have always required the process-control interface for snaps
+// to send signals to other users (even within the same snap). We want to
+// maintain this with our privilege dropping rules, so we omit 'capability
+// kill' since snaps can work within the system without 'capability kill':
+// - root parent can drop, spawn a child and later (dropped) parent can send a
+// signal
+// - root parent can spawn a child that drops, then later temporarily drop
+// (ie, seteuid/setegid), send the signal, then reraise
+var privDropAndChownRules = `
+ # allow setuid, setgid and chown for privilege dropping (mediation is done
+ # via seccomp). Note: CAP_SETUID allows (and CAP_SETGID is the same, but
+ # for gid operations):
+ # - forging of UIDs when passing passing socket credentials via UNIX domain
+ # sockets and we don't currently mediate socket credentials, between
+ # mediating socket access in general and the execve() boundary that drops
+ # the capability for non-root commands, this is not a problem in practice.
+ # - accessing the persistent keyring via keyctl, but keyctl is mediated via
+ # seccomp.
+ # - writing a user ID mapping in a user namespace, but we mediate access to
+ # /proc/*/uid_map with AppArmor
+ #
+ # CAP_DAC_OVERRIDE and CAP_DAC_READ_SEARCH are intentionally omitted from the
+ # policy since we want traditional DAC to be enforced for root. It is
+ # expected that a program that is dropping privileges, etc will create/modify
+ # files in a way that doesn't require these capabilities.
+ capability setuid,
+ capability setgid,
+ capability chown,
+ #capability dac_override,
+ #capability dac_read_search,
+
+ # Similarly, CAP_KILL is intentionally omitted since we want traditional
+ # DAC to be enforced for root. It is expected that a program that is spawning
+ # processes that ultimately run as non-root will send signals to those
+ # processes as the matching non-root user.
+ #capability kill,
+`
+
// classicTemplate contains apparmor template used for snaps with classic
// confinement. This template was Designed by jdstrand:
// https://github.com/snapcore/snapd/pull/2366#discussion_r90101320
@@ -698,6 +758,7 @@
# Allow the content interface to bind fonts from the host filesystem
mount options=(ro bind) /var/lib/snapd/hostfs/usr/share/fonts/ -> /snap/###SNAP_INSTANCE_NAME###/*/**,
+ mount options=(rw private) -> /snap/###SNAP_INSTANCE_NAME###/*/**,
umount /snap/###SNAP_INSTANCE_NAME###/*/**,
# set up user mount namespace
diff -Nru snapd-2.40/interfaces/builtin/account_control_test.go snapd-2.42.1/interfaces/builtin/account_control_test.go
--- snapd-2.40/interfaces/builtin/account_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/account_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -79,13 +79,6 @@
func (s *AccountControlSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- si := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "account-control",
- Interface: "account-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, si), ErrorMatches,
- "account-control slots are reserved for the core snap")
}
func (s *AccountControlSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/alsa_test.go snapd-2.42.1/interfaces/builtin/alsa_test.go
--- snapd-2.40/interfaces/builtin/alsa_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/alsa_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *AlsaInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "alsa",
- Interface: "alsa",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "alsa slots are reserved for the core snap")
}
func (s *AlsaInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/appstream_metadata.go snapd-2.42.1/interfaces/builtin/appstream_metadata.go
--- snapd-2.40/interfaces/builtin/appstream_metadata.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/appstream_metadata.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,117 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package builtin
+
+import (
+ "path/filepath"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/interfaces/mount"
+ "github.com/snapcore/snapd/osutil"
+)
+
+const appstreamMetadataSummary = `allows access to AppStream metadata`
+
+const appstreamMetadataBaseDeclarationSlots = `
+ appstream-metadata:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+`
+
+// Paths for upstream and collection metadata are defined in the
+// AppStream specification:
+// https://www.freedesktop.org/software/appstream/docs/
+const appstreamMetadataConnectedPlugAppArmor = `
+# Description: Allow access to AppStream metadata from the host system
+
+# Allow access to AppStream upstream metadata files
+/usr/share/metainfo/** r,
+/usr/share/appdata/** r,
+
+# Allow access to AppStream collection metadata
+/usr/share/app-info/** r,
+/var/cache/app-info/** r,
+/var/lib/app-info/** r,
+
+# Apt symlinks the DEP-11 metadata to files in /var/lib/apt/lists
+/var/lib/apt/lists/*.yml.gz r,
+`
+
+var appstreamMetadataDirs = []string{
+ "/usr/share/metainfo",
+ "/usr/share/appdata",
+ "/usr/share/app-info",
+ "/var/cache/app-info",
+ "/var/lib/app-info",
+ "/var/lib/apt/lists",
+}
+
+type appstreamMetadataInterface struct {
+ commonInterface
+}
+
+func (iface *appstreamMetadataInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
+ spec.AddSnippet(appstreamMetadataConnectedPlugAppArmor)
+
+ // Generate rules to allow snap-update-ns to do its thing
+ emit := spec.EmitUpdateNSFunc()
+ for _, target := range appstreamMetadataDirs {
+ source := "/var/lib/snapd/hostfs" + target
+ emit(" # Read-only access to %s\n", target)
+ emit(" mount options=(bind) %s/ -> %s/,\n", source, target)
+ emit(" remount options=(bind, ro) %s/,\n", target)
+ emit(" umount %s/,\n\n", target)
+ // Allow constructing writable mimic to mount point We
+ // expect three components to already exist: /, /usr,
+ // and /usr/share (or equivalents under /var).
+ apparmor.GenWritableProfile(emit, target, 3)
+ }
+ return nil
+}
+
+func (iface *appstreamMetadataInterface) MountConnectedPlug(spec *mount.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
+ for _, dir := range appstreamMetadataDirs {
+ dir = filepath.Join(dirs.GlobalRootDir, dir)
+ if !osutil.IsDirectory(dir) {
+ continue
+ }
+ spec.AddMountEntry(osutil.MountEntry{
+ Name: "/var/lib/snapd/hostfs" + dir,
+ Dir: dirs.StripRootDir(dir),
+ Options: []string{"bind", "ro"},
+ })
+ }
+
+ return nil
+}
+
+func init() {
+ registerIface(&appstreamMetadataInterface{commonInterface{
+ name: "appstream-metadata",
+ summary: appstreamMetadataSummary,
+ implicitOnClassic: true,
+ reservedForOS: true,
+ baseDeclarationSlots: appstreamMetadataBaseDeclarationSlots,
+ }})
+}
diff -Nru snapd-2.40/interfaces/builtin/appstream_metadata_test.go snapd-2.42.1/interfaces/builtin/appstream_metadata_test.go
--- snapd-2.40/interfaces/builtin/appstream_metadata_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/appstream_metadata_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,136 @@
+// -*- 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 .
+ *
+ */
+
+package builtin_test
+
+import (
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/interfaces/mount"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type AppstreamMetadataInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.ConnectedSlot
+ slotInfo *snap.SlotInfo
+ plug *interfaces.ConnectedPlug
+ plugInfo *snap.PlugInfo
+}
+
+var _ = Suite(&AppstreamMetadataInterfaceSuite{
+ iface: builtin.MustInterface("appstream-metadata"),
+})
+
+func (s *AppstreamMetadataInterfaceSuite) SetUpTest(c *C) {
+ const coreYaml = `name: core
+version: 0
+type: os
+slots:
+ appstream-metadata:
+ interface: appstream-metadata
+`
+ s.slot, s.slotInfo = MockConnectedSlot(c, coreYaml, nil, "appstream-metadata")
+
+ const consumerYaml = `name: consumer
+version: 0
+apps:
+ app:
+ plugs: [appstream-metadata]
+`
+ s.plug, s.plugInfo = MockConnectedPlug(c, consumerYaml, nil, "appstream-metadata")
+}
+
+func (s *AppstreamMetadataInterfaceSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("/")
+}
+
+func (s *AppstreamMetadataInterfaceSuite) TestName(c *C) {
+ c.Check(s.iface.Name(), Equals, "appstream-metadata")
+}
+
+func (s *AppstreamMetadataInterfaceSuite) TestSanitize(c *C) {
+ c.Check(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil)
+ c.Check(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
+}
+
+func (s *AppstreamMetadataInterfaceSuite) TestAppArmorConnectedPlug(c *C) {
+ spec := &apparmor.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
+ c.Assert(spec.SecurityTags(), HasLen, 1)
+ c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/var/cache/app-info/** r,`)
+ c.Check(spec.UpdateNS(), testutil.Contains, " # Read-only access to /usr/share/metainfo\n")
+}
+
+func (s *AppstreamMetadataInterfaceSuite) TestMountConnectedPlug(c *C) {
+ tmpdir := c.MkDir()
+ dirs.SetRootDir(tmpdir)
+
+ c.Assert(os.MkdirAll(filepath.Join(tmpdir, "/usr/share/metainfo"), 0777), IsNil)
+ c.Assert(os.MkdirAll(filepath.Join(tmpdir, "/usr/share/appdata"), 0777), IsNil)
+ c.Assert(os.MkdirAll(filepath.Join(tmpdir, "/usr/share/app-info"), 0777), IsNil)
+ c.Assert(os.MkdirAll(filepath.Join(tmpdir, "/var/cache/app-info"), 0777), IsNil)
+ c.Assert(os.MkdirAll(filepath.Join(tmpdir, "/var/lib/app-info"), 0777), IsNil)
+ c.Assert(os.MkdirAll(filepath.Join(tmpdir, "/var/lib/apt/lists"), 0777), IsNil)
+
+ spec := &mount.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
+ entries := spec.MountEntries()
+ c.Assert(entries, HasLen, 6)
+
+ const hostfs = "/var/lib/snapd/hostfs"
+ c.Check(entries[0].Name, Equals, filepath.Join(hostfs, dirs.GlobalRootDir, "/usr/share/metainfo"))
+ c.Check(entries[0].Dir, Equals, "/usr/share/metainfo")
+ c.Check(entries[0].Options, DeepEquals, []string{"bind", "ro"})
+ c.Check(entries[1].Name, Equals, filepath.Join(hostfs, dirs.GlobalRootDir, "/usr/share/appdata"))
+ c.Check(entries[1].Dir, Equals, "/usr/share/appdata")
+ c.Check(entries[1].Options, DeepEquals, []string{"bind", "ro"})
+ c.Check(entries[2].Name, Equals, filepath.Join(hostfs, dirs.GlobalRootDir, "/usr/share/app-info"))
+ c.Check(entries[2].Dir, Equals, "/usr/share/app-info")
+ c.Check(entries[2].Options, DeepEquals, []string{"bind", "ro"})
+ c.Check(entries[3].Name, Equals, filepath.Join(hostfs, dirs.GlobalRootDir, "/var/cache/app-info"))
+ c.Check(entries[3].Dir, Equals, "/var/cache/app-info")
+ c.Check(entries[3].Options, DeepEquals, []string{"bind", "ro"})
+ c.Check(entries[4].Name, Equals, filepath.Join(hostfs, dirs.GlobalRootDir, "/var/lib/app-info"))
+ c.Check(entries[4].Dir, Equals, "/var/lib/app-info")
+ c.Check(entries[4].Options, DeepEquals, []string{"bind", "ro"})
+ c.Check(entries[5].Name, Equals, filepath.Join(hostfs, dirs.GlobalRootDir, "/var/lib/apt/lists"))
+ c.Check(entries[5].Dir, Equals, "/var/lib/apt/lists")
+ c.Check(entries[5].Options, DeepEquals, []string{"bind", "ro"})
+}
+
+func (s *AppstreamMetadataInterfaceSuite) TestStaticInfo(c *C) {
+ si := interfaces.StaticInfoOf(s.iface)
+ c.Check(si.ImplicitOnCore, Equals, false)
+ c.Check(si.ImplicitOnClassic, Equals, true)
+ c.Check(si.Summary, Equals, "allows access to AppStream metadata")
+ c.Check(si.BaseDeclarationSlots, testutil.Contains, "appstream-metadata")
+}
+
+func (s *AppstreamMetadataInterfaceSuite) TestInterfaces(c *C) {
+ c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface)
+}
diff -Nru snapd-2.40/interfaces/builtin/audio_playback.go snapd-2.42.1/interfaces/builtin/audio_playback.go
--- snapd-2.40/interfaces/builtin/audio_playback.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/audio_playback.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,179 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/interfaces/seccomp"
+ "github.com/snapcore/snapd/interfaces/udev"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+)
+
+// The audio-playback interface is the companion interface to the audio-record
+// interface. The design of this interface is based on the idea that the slot
+// implementation (eg pulseaudio) is expected to query snapd on if the
+// audio-record slot is connected or not and the audio service will mediate
+// recording (ie, the rules below allow connecting to the audio service, but do
+// not implement enforcement rules; it is up to the audio service to provide
+// enforcement). If other audio recording servers require different security
+// policy for record and playback (eg, a different socket path), then those
+// accesses will be added to this interface.
+
+const audioPlaybackSummary = `allows audio playback via supporting services`
+
+const audioPlaybackBaseDeclarationSlots = `
+ audio-playback:
+ allow-installation:
+ slot-snap-type:
+ - app
+ - core
+ deny-connection:
+ on-classic: false
+`
+
+const audioPlaybackConnectedPlugAppArmor = `
+# Allow communicating with pulseaudio service
+/{run,dev}/shm/pulse-shm-* mrwk,
+
+owner /{,var/}run/pulse/ r,
+owner /{,var/}run/pulse/native rwk,
+owner /run/user/[0-9]*/ r,
+owner /run/user/[0-9]*/pulse/ rw,
+
+/run/udev/data/c116:[0-9]* r,
+/run/udev/data/+sound:card[0-9]* r,
+`
+
+const audioPlaybackConnectedPlugAppArmorDesktop = `
+# Allow communicating with pulseaudio service on the desktop in classic distro.
+# Only on desktop do we need access to /etc/pulse for any PulseAudio client
+# to read available client side configuration settings. On an Ubuntu Core
+# device those things will be stored inside the snap directory.
+/etc/pulse/ r,
+/etc/pulse/* r,
+owner @{HOME}/.pulse-cookie rk,
+owner @{HOME}/.config/pulse/cookie rk,
+owner /{,var/}run/user/*/pulse/ rwk,
+owner /{,var/}run/user/*/pulse/native rwk,
+`
+
+const audioPlaybackConnectedPlugSecComp = `
+shmctl
+`
+
+const audioPlaybackPermanentSlotAppArmor = `
+# When running PulseAudio in system mode it will switch to the at
+# build time configured user/group on startup.
+capability setuid,
+capability setgid,
+
+capability sys_nice,
+capability sys_resource,
+
+owner @{PROC}/@{pid}/exe r,
+/etc/machine-id r,
+
+# For udev
+network netlink raw,
+/sys/devices/virtual/dmi/id/sys_vendor r,
+/sys/devices/virtual/dmi/id/bios_vendor r,
+/sys/**/sound/** r,
+
+owner /{,var/}run/pulse/ rw,
+owner /{,var/}run/pulse/** rwk,
+
+# Shared memory based communication with clients
+/{run,dev}/shm/pulse-shm-* mrwk,
+
+owner /run/pulse/native/ rwk,
+owner /run/user/[0-9]*/ r,
+owner /run/user/[0-9]*/pulse/ rw,
+`
+
+const audioPlaybackPermanentSlotSecComp = `
+# The following are needed for UNIX sockets
+personality
+setpriority
+bind
+listen
+accept
+accept4
+shmctl
+# Needed to set root as group for different state dirs
+# pulseaudio creates on startup.
+setgroups
+setgroups32
+# libudev
+socket AF_NETLINK - NETLINK_KOBJECT_UEVENT
+`
+
+type audioPlaybackInterface struct{}
+
+func (iface *audioPlaybackInterface) Name() string {
+ return "audio-playback"
+}
+
+func (iface *audioPlaybackInterface) StaticInfo() interfaces.StaticInfo {
+ return interfaces.StaticInfo{
+ Summary: audioPlaybackSummary,
+ ImplicitOnClassic: true,
+ BaseDeclarationSlots: audioPlaybackBaseDeclarationSlots,
+ }
+}
+
+func (iface *audioPlaybackInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
+ spec.AddSnippet(audioPlaybackConnectedPlugAppArmor)
+ if release.OnClassic {
+ spec.AddSnippet(audioPlaybackConnectedPlugAppArmorDesktop)
+ }
+ return nil
+}
+
+func (iface *audioPlaybackInterface) UDevPermanentSlot(spec *udev.Specification, slot *snap.SlotInfo) error {
+ spec.TagDevice(`KERNEL=="controlC[0-9]*"`)
+ spec.TagDevice(`KERNEL=="pcmC[0-9]*D[0-9]*[cp]"`)
+ spec.TagDevice(`KERNEL=="timer"`)
+ return nil
+}
+
+func (iface *audioPlaybackInterface) AppArmorPermanentSlot(spec *apparmor.Specification, slot *snap.SlotInfo) error {
+ spec.AddSnippet(audioPlaybackPermanentSlotAppArmor)
+ return nil
+}
+
+func (iface *audioPlaybackInterface) SecCompConnectedPlug(spec *seccomp.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
+ spec.AddSnippet(audioPlaybackConnectedPlugSecComp)
+ return nil
+}
+
+func (iface *audioPlaybackInterface) SecCompPermanentSlot(spec *seccomp.Specification, slot *snap.SlotInfo) error {
+ spec.AddSnippet(audioPlaybackPermanentSlotSecComp)
+ return nil
+}
+
+func (iface *audioPlaybackInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool {
+ return true
+}
+
+func init() {
+ registerIface(&audioPlaybackInterface{})
+}
diff -Nru snapd-2.40/interfaces/builtin/audio_playback_test.go snapd-2.42.1/interfaces/builtin/audio_playback_test.go
--- snapd-2.40/interfaces/builtin/audio_playback_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/audio_playback_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,206 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/interfaces/seccomp"
+ "github.com/snapcore/snapd/interfaces/udev"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type AudioPlaybackInterfaceSuite struct {
+ iface interfaces.Interface
+ coreSlotInfo *snap.SlotInfo
+ coreSlot *interfaces.ConnectedSlot
+ classicSlotInfo *snap.SlotInfo
+ classicSlot *interfaces.ConnectedSlot
+ plugInfo *snap.PlugInfo
+ plug *interfaces.ConnectedPlug
+}
+
+var _ = Suite(&AudioPlaybackInterfaceSuite{
+ iface: builtin.MustInterface("audio-playback"),
+})
+
+const audioPlaybackMockPlugSnapInfoYaml = `name: consumer
+version: 1.0
+apps:
+ app:
+ command: foo
+ plugs: [audio-playback]
+`
+
+// a audio-playback slot on a audio-playback snap (as installed on a core/all-snap system)
+const audioPlaybackMockCoreSlotSnapInfoYaml = `name: audio-playback
+version: 1.0
+apps:
+ app1:
+ command: foo
+ slots: [audio-playback]
+`
+
+// a audio-playback slot on the core snap (as automatically added on classic)
+const audioPlaybackMockClassicSlotSnapInfoYaml = `name: core
+version: 0
+type: os
+slots:
+ audio-playback:
+ interface: audio-playback
+`
+
+func (s *AudioPlaybackInterfaceSuite) SetUpTest(c *C) {
+ // audio-playback snap with audio-playback slot on an core/all-snap install.
+ snapInfo := snaptest.MockInfo(c, audioPlaybackMockCoreSlotSnapInfoYaml, nil)
+ s.coreSlotInfo = snapInfo.Slots["audio-playback"]
+ s.coreSlot = interfaces.NewConnectedSlot(s.coreSlotInfo, nil, nil)
+ // audio-playback slot on a core snap in a classic install.
+ snapInfo = snaptest.MockInfo(c, audioPlaybackMockClassicSlotSnapInfoYaml, nil)
+ s.classicSlotInfo = snapInfo.Slots["audio-playback"]
+ s.classicSlot = interfaces.NewConnectedSlot(s.classicSlotInfo, nil, nil)
+ // snap with the audio-playback plug
+ snapInfo = snaptest.MockInfo(c, audioPlaybackMockPlugSnapInfoYaml, nil)
+ s.plugInfo = snapInfo.Plugs["audio-playback"]
+ s.plug = interfaces.NewConnectedPlug(s.plugInfo, nil, nil)
+}
+
+func (s *AudioPlaybackInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "audio-playback")
+}
+
+func (s *AudioPlaybackInterfaceSuite) TestSanitizeSlot(c *C) {
+ c.Assert(interfaces.BeforePrepareSlot(s.iface, s.coreSlotInfo), IsNil)
+ c.Assert(interfaces.BeforePrepareSlot(s.iface, s.classicSlotInfo), IsNil)
+}
+
+func (s *AudioPlaybackInterfaceSuite) TestSanitizePlug(c *C) {
+ c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil)
+}
+
+func (s *AudioPlaybackInterfaceSuite) TestSecComp(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ // connected plug to core slot
+ spec := &seccomp.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
+ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "shmctl\n")
+
+ // connected core slot to plug
+ spec = &seccomp.Specification{}
+ c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.coreSlot), IsNil)
+ c.Assert(spec.SecurityTags(), HasLen, 0)
+
+ // permanent core slot
+ spec = &seccomp.Specification{}
+ c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.audio-playback.app1"})
+ c.Assert(spec.SnippetForTag("snap.audio-playback.app1"), testutil.Contains, "listen\n")
+}
+
+func (s *AudioPlaybackInterfaceSuite) TestSecCompOnClassic(c *C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+
+ // connected plug to classic slot
+ spec := &seccomp.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.classicSlot), IsNil)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
+ c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "shmctl\n")
+
+ // connected classic slot to plug
+ spec = &seccomp.Specification{}
+ c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.classicSlot), IsNil)
+ c.Assert(spec.SecurityTags(), HasLen, 0)
+
+ // permanent classic slot
+ spec = &seccomp.Specification{}
+ c.Assert(spec.AddPermanentSlot(s.iface, s.classicSlotInfo), IsNil)
+ c.Assert(spec.SecurityTags(), HasLen, 0)
+}
+
+func (s *AudioPlaybackInterfaceSuite) TestAppArmor(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ // connected plug to core slot
+ spec := &apparmor.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
+ c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{run,dev}/shm/pulse-shm-* mrwk,\n")
+
+ // connected core slot to plug
+ spec = &apparmor.Specification{}
+ c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.coreSlot), IsNil)
+ c.Assert(spec.SecurityTags(), HasLen, 0)
+
+ // permanent core slot
+ spec = &apparmor.Specification{}
+ c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.audio-playback.app1"})
+ c.Check(spec.SnippetForTag("snap.audio-playback.app1"), testutil.Contains, "capability setuid,\n")
+}
+
+func (s *AudioPlaybackInterfaceSuite) TestAppArmorOnClassic(c *C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+
+ // connected plug to classic slot
+ spec := &apparmor.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.classicSlot), IsNil)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
+ c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/{run,dev}/shm/pulse-shm-* mrwk,\n")
+ c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/etc/pulse/ r,\n")
+
+ // connected classic slot to plug
+ spec = &apparmor.Specification{}
+ c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.classicSlot), IsNil)
+ c.Assert(spec.SecurityTags(), HasLen, 0)
+
+ // permanent classic slot
+ spec = &apparmor.Specification{}
+ c.Assert(spec.AddPermanentSlot(s.iface, s.classicSlotInfo), IsNil)
+ c.Assert(spec.SecurityTags(), HasLen, 0)
+}
+
+func (s *AudioPlaybackInterfaceSuite) TestUDev(c *C) {
+ spec := &udev.Specification{}
+ c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil)
+ c.Assert(spec.Snippets(), HasLen, 4)
+ c.Assert(spec.Snippets(), testutil.Contains, `# audio-playback
+KERNEL=="controlC[0-9]*", TAG+="snap_audio-playback_app1"`)
+ c.Assert(spec.Snippets(), testutil.Contains, `# audio-playback
+KERNEL=="pcmC[0-9]*D[0-9]*[cp]", TAG+="snap_audio-playback_app1"`)
+ c.Assert(spec.Snippets(), testutil.Contains, `# audio-playback
+KERNEL=="timer", TAG+="snap_audio-playback_app1"`)
+ c.Assert(spec.Snippets(), testutil.Contains, `TAG=="snap_audio-playback_app1", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_audio-playback_app1 $devpath $major:$minor"`)
+}
+
+func (s *AudioPlaybackInterfaceSuite) TestInterfaces(c *C) {
+ c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface)
+}
diff -Nru snapd-2.40/interfaces/builtin/audio_record.go snapd-2.42.1/interfaces/builtin/audio_record.go
--- snapd-2.40/interfaces/builtin/audio_record.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/audio_record.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,82 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package builtin
+
+import (
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/snap"
+)
+
+// The audio-record interface is the companion interface to the audio-playback
+// interface and is not meant to be used without it. The design of this
+// interface is based on the idea that the slot implementation (eg pulseaudio)
+// is expected to query snapd on if the audio-record slot is connected or not
+// and the audio service will mediate recording (ie, the rules below allow
+// connecting to the audio service, but do not implement enforcement rules; it
+// is up to the audio service to provide enforcement). If other audio recording
+// servers require different security policy for record (eg, a different socket
+// path), then those accesses will be added to this interface.
+
+const audioRecordSummary = `allows audio recording via supporting services`
+
+const audioRecordBaseDeclarationSlots = `
+ audio-record:
+ allow-installation:
+ slot-snap-type:
+ - app
+ - core
+ deny-connection:
+ on-classic: false
+ deny-auto-connection: true
+`
+
+const audioRecordConnectedPlugAppArmor = `
+# Access for communication with audio recording service done via
+# audio-playback interface. The audio service will verify if the audio-record
+# interface is connected.
+`
+
+type audioRecordInterface struct{}
+
+func (iface *audioRecordInterface) Name() string {
+ return "audio-record"
+}
+
+func (iface *audioRecordInterface) StaticInfo() interfaces.StaticInfo {
+ return interfaces.StaticInfo{
+ Summary: audioRecordSummary,
+ ImplicitOnClassic: true,
+ BaseDeclarationSlots: audioRecordBaseDeclarationSlots,
+ }
+}
+
+func (iface *audioRecordInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
+ spec.AddSnippet(audioRecordConnectedPlugAppArmor)
+ return nil
+}
+
+func (iface *audioRecordInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool {
+ return true
+}
+
+func init() {
+ registerIface(&audioRecordInterface{})
+}
diff -Nru snapd-2.40/interfaces/builtin/audio_record_test.go snapd-2.42.1/interfaces/builtin/audio_record_test.go
--- snapd-2.40/interfaces/builtin/audio_record_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/audio_record_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,146 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type AudioRecordInterfaceSuite struct {
+ iface interfaces.Interface
+ coreSlotInfo *snap.SlotInfo
+ coreSlot *interfaces.ConnectedSlot
+ classicSlotInfo *snap.SlotInfo
+ classicSlot *interfaces.ConnectedSlot
+ plugInfo *snap.PlugInfo
+ plug *interfaces.ConnectedPlug
+}
+
+var _ = Suite(&AudioRecordInterfaceSuite{
+ iface: builtin.MustInterface("audio-record"),
+})
+
+const audioRecordMockPlugSnapInfoYaml = `name: consumer
+version: 1.0
+apps:
+ app:
+ command: foo
+ plugs: [audio-record]
+`
+
+// a audio-record slot on a audio-record snap (as installed on a core/all-snap system)
+const audioRecordMockCoreSlotSnapInfoYaml = `name: audio-record
+version: 1.0
+apps:
+ app1:
+ command: foo
+ slots: [audio-record]
+`
+
+// a audio-record slot on the core snap (as automatically added on classic)
+const audioRecordMockClassicSlotSnapInfoYaml = `name: core
+version: 0
+type: os
+slots:
+ audio-record:
+ interface: audio-record
+`
+
+func (s *AudioRecordInterfaceSuite) SetUpTest(c *C) {
+ // audio-record snap with audio-record slot on an core/all-snap install.
+ snapInfo := snaptest.MockInfo(c, audioRecordMockCoreSlotSnapInfoYaml, nil)
+ s.coreSlotInfo = snapInfo.Slots["audio-record"]
+ s.coreSlot = interfaces.NewConnectedSlot(s.coreSlotInfo, nil, nil)
+ // audio-record slot on a core snap in a classic install.
+ snapInfo = snaptest.MockInfo(c, audioRecordMockClassicSlotSnapInfoYaml, nil)
+ s.classicSlotInfo = snapInfo.Slots["audio-record"]
+ s.classicSlot = interfaces.NewConnectedSlot(s.classicSlotInfo, nil, nil)
+ // snap with the audio-record plug
+ snapInfo = snaptest.MockInfo(c, audioRecordMockPlugSnapInfoYaml, nil)
+ s.plugInfo = snapInfo.Plugs["audio-record"]
+ s.plug = interfaces.NewConnectedPlug(s.plugInfo, nil, nil)
+}
+
+func (s *AudioRecordInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "audio-record")
+}
+
+func (s *AudioRecordInterfaceSuite) TestSanitizeSlot(c *C) {
+ c.Assert(interfaces.BeforePrepareSlot(s.iface, s.coreSlotInfo), IsNil)
+ c.Assert(interfaces.BeforePrepareSlot(s.iface, s.classicSlotInfo), IsNil)
+}
+
+func (s *AudioRecordInterfaceSuite) TestSanitizePlug(c *C) {
+ c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil)
+}
+
+func (s *AudioRecordInterfaceSuite) TestAppArmor(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ // connected plug to core slot
+ spec := &apparmor.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
+ c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Access for communication with audio recording service done via\n")
+
+ // connected core slot to plug
+ spec = &apparmor.Specification{}
+ c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.coreSlot), IsNil)
+ c.Assert(spec.SecurityTags(), HasLen, 0)
+
+ // permanent core clot
+ spec = &apparmor.Specification{}
+ c.Assert(spec.AddPermanentSlot(s.iface, s.coreSlotInfo), IsNil)
+ c.Assert(spec.SecurityTags(), HasLen, 0)
+}
+
+func (s *AudioRecordInterfaceSuite) TestAppArmorOnClassic(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ // connected plug to classic slot
+ spec := &apparmor.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.classicSlot), IsNil)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
+ c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Access for communication with audio recording service done via\n")
+
+ // connected classic slot to plug
+ spec = &apparmor.Specification{}
+ c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.classicSlot), IsNil)
+ c.Assert(spec.SecurityTags(), HasLen, 0)
+
+ // permanent classic slot
+ spec = &apparmor.Specification{}
+ c.Assert(spec.AddPermanentSlot(s.iface, s.classicSlotInfo), IsNil)
+ c.Assert(spec.SecurityTags(), HasLen, 0)
+}
+
+func (s *AudioRecordInterfaceSuite) TestInterfaces(c *C) {
+ c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface)
+}
diff -Nru snapd-2.40/interfaces/builtin/autopilot_test.go snapd-2.42.1/interfaces/builtin/autopilot_test.go
--- snapd-2.40/interfaces/builtin/autopilot_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/autopilot_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -69,13 +69,6 @@
func (s *AutopilotInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "autopilot-introspection",
- Interface: "autopilot-introspection",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "autopilot-introspection slots are reserved for the core snap")
}
func (s *AutopilotInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/block_devices_test.go snapd-2.42.1/interfaces/builtin/block_devices_test.go
--- snapd-2.40/interfaces/builtin/block_devices_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/block_devices_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -71,13 +71,6 @@
func (s *blockDevicesInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "block-devices",
- Interface: "block-devices",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "block-devices slots are reserved for the core snap")
}
func (s *blockDevicesInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/bluetooth_control.go snapd-2.42.1/interfaces/builtin/bluetooth_control.go
--- snapd-2.40/interfaces/builtin/bluetooth_control.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/bluetooth_control.go 2019-10-30 12:17:43.000000000 +0000
@@ -61,7 +61,7 @@
bind
`
-var bluetoothControlConnectedPlugUDev = []string{`SUBSYSTEM=="bluetooth"`}
+var bluetoothControlConnectedPlugUDev = []string{`SUBSYSTEM=="bluetooth"`, `SUBSYSTEM=="BT_chrdev"`}
func init() {
registerIface(&commonInterface{
diff -Nru snapd-2.40/interfaces/builtin/bluetooth_control_test.go snapd-2.42.1/interfaces/builtin/bluetooth_control_test.go
--- snapd-2.40/interfaces/builtin/bluetooth_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/bluetooth_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -76,13 +76,6 @@
func (s *BluetoothControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "bluetooth-control",
- Interface: "bluetooth-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "bluetooth-control slots are reserved for the core snap")
}
func (s *BluetoothControlInterfaceSuite) TestSanitizePlug(c *C) {
@@ -106,9 +99,11 @@
func (s *BluetoothControlInterfaceSuite) TestUDevSpec(c *C) {
spec := &udev.Specification{}
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
- c.Assert(spec.Snippets(), HasLen, 2)
+ c.Assert(spec.Snippets(), HasLen, 3)
c.Assert(spec.Snippets(), testutil.Contains, `# bluetooth-control
SUBSYSTEM=="bluetooth", TAG+="snap_other_app2"`)
+ c.Assert(spec.Snippets(), testutil.Contains, `# bluetooth-control
+SUBSYSTEM=="BT_chrdev", TAG+="snap_other_app2"`)
c.Assert(spec.Snippets(), testutil.Contains, `TAG=="snap_other_app2", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_other_app2 $devpath $major:$minor"`)
}
diff -Nru snapd-2.40/interfaces/builtin/bluez.go snapd-2.42.1/interfaces/builtin/bluez.go
--- snapd-2.40/interfaces/builtin/bluez.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/bluez.go 2019-10-30 12:17:43.000000000 +0000
@@ -92,6 +92,11 @@
bus=system
name="org.bluez.obex",
+# Allow binding the service to the requested connection name
+dbus (bind)
+ bus=system
+ name="org.bluez.mesh",
+
# Allow traffic to/from our interface with any method for unconfined clients
# to talk to our bluez services. For the org.bluez interface we don't specify
# an Object Path since according to the bluez specification these can be
@@ -163,6 +168,10 @@
bus=system
peer=(name=org.bluez.obex, label=unconfined),
+dbus (send)
+ bus=system
+ peer=(name=org.bluez.mesh, label=unconfined),
+
dbus (receive)
bus=system
path=/
@@ -191,8 +200,10 @@
+
+
@@ -203,6 +214,14 @@
+
+
+
+
+
+
+
+
diff -Nru snapd-2.40/interfaces/builtin/bluez_test.go snapd-2.42.1/interfaces/builtin/bluez_test.go
--- snapd-2.40/interfaces/builtin/bluez_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/bluez_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -178,6 +178,7 @@
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "peer=(name=org.bluez, label=unconfined)")
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "peer=(name=org.bluez.obex, label=unconfined)")
+ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "peer=(name=org.bluez.mesh, label=unconfined)")
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "peer=(label=unconfined),")
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `interface=org.freedesktop.DBus.ObjectManager`)
c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `interface=org.freedesktop.DBus.*`)
diff -Nru snapd-2.40/interfaces/builtin/broadcom_asic_control_test.go snapd-2.42.1/interfaces/builtin/broadcom_asic_control_test.go
--- snapd-2.40/interfaces/builtin/broadcom_asic_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/broadcom_asic_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -72,13 +72,6 @@
func (s *BroadcomAsicControlSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "broadcom-asic-control",
- Interface: "broadcom-asic-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "broadcom-asic-control slots are reserved for the core snap")
}
func (s *BroadcomAsicControlSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/camera_test.go snapd-2.42.1/interfaces/builtin/camera_test.go
--- snapd-2.40/interfaces/builtin/camera_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/camera_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *CameraInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "camera",
- Interface: "camera",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "camera slots are reserved for the core snap")
}
func (s *CameraInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/can_bus_test.go snapd-2.42.1/interfaces/builtin/can_bus_test.go
--- snapd-2.40/interfaces/builtin/can_bus_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/can_bus_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *CanBusInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "can-bus",
- Interface: "can-bus",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "can-bus slots are reserved for the core snap")
}
func (s *CanBusInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/cifs_mount_test.go snapd-2.42.1/interfaces/builtin/cifs_mount_test.go
--- snapd-2.40/interfaces/builtin/cifs_mount_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/cifs_mount_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *CifsMountInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "cifs-mount",
- Interface: "cifs-mount",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "cifs-mount slots are reserved for the core snap")
}
func (s *CifsMountInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/common.go snapd-2.42.1/interfaces/builtin/common.go
--- snapd-2.40/interfaces/builtin/common.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/common.go 2019-10-30 12:17:43.000000000 +0000
@@ -84,17 +84,6 @@
}
}
-// BeforePrepareSlot checks and possibly modifies a slot.
-//
-// If the reservedForOS flag is set then only slots on core snap
-// are allowed.
-func (iface *commonInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
- if iface.reservedForOS {
- return sanitizeSlotReservedForOS(iface, slot)
- }
- return nil
-}
-
func (iface *commonInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
if iface.usesPtraceTrace {
spec.SetUsesPtraceTrace()
diff -Nru snapd-2.40/interfaces/builtin/content.go snapd-2.42.1/interfaces/builtin/content.go
--- snapd-2.40/interfaces/builtin/content.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/content.go 2019-10-30 12:17:43.000000000 +0000
@@ -214,6 +214,7 @@
func (iface *contentInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
contentSnippet := bytes.NewBuffer(nil)
writePaths := iface.path(slot, "write")
+ emit := spec.EmitUpdateNSFunc()
if len(writePaths) > 0 {
fmt.Fprintf(contentSnippet, `
# In addition to the bind mount, add any AppArmor rules so that
@@ -226,17 +227,16 @@
fmt.Fprintf(contentSnippet, "%s/** mrwklix,\n",
resolveSpecialVariable(w, slot.Snap()))
source, target := sourceTarget(plug, slot, w)
- var buf bytes.Buffer
- fmt.Fprintf(&buf, " # Read-write content sharing %s -> %s (w#%d)\n", plug.Ref(), slot.Ref(), i)
- fmt.Fprintf(&buf, " mount options=(bind, rw) %s/ -> %s/,\n", source, target)
- fmt.Fprintf(&buf, " umount %s/,\n", target)
+ emit(" # Read-write content sharing %s -> %s (w#%d)\n", plug.Ref(), slot.Ref(), i)
+ emit(" mount options=(bind, rw) %s/ -> %s/,\n", source, target)
+ emit(" mount options=(rprivate) -> %s/,\n", target)
+ emit(" umount %s/,\n", target)
// TODO: The assumed prefix depth could be optimized to be more
// precise since content sharing can only take place in a fixed
// list of places with well-known paths (well, constrained set of
// paths). This can be done when the prefix is actually consumed.
- apparmor.WritableProfile(&buf, source, 1)
- apparmor.WritableProfile(&buf, target, 1)
- spec.AddUpdateNS(buf.String())
+ apparmor.GenWritableProfile(emit, source, 1)
+ apparmor.GenWritableProfile(emit, target, 1)
}
}
@@ -252,15 +252,14 @@
resolveSpecialVariable(r, slot.Snap()))
source, target := sourceTarget(plug, slot, r)
- var buf bytes.Buffer
- fmt.Fprintf(&buf, " # Read-only content sharing %s -> %s (r#%d)\n", plug.Ref(), slot.Ref(), i)
- fmt.Fprintf(&buf, " mount options=(bind) %s/ -> %s/,\n", source, target)
- fmt.Fprintf(&buf, " remount options=(bind, ro) %s/,\n", target)
- fmt.Fprintf(&buf, " umount %s/,\n", target)
+ emit(" # Read-only content sharing %s -> %s (r#%d)\n", plug.Ref(), slot.Ref(), i)
+ emit(" mount options=(bind) %s/ -> %s/,\n", source, target)
+ emit(" remount options=(bind, ro) %s/,\n", target)
+ emit(" mount options=(rprivate) -> %s/,\n", target)
+ emit(" umount %s/,\n", target)
// Look at the TODO comment above.
- apparmor.WritableProfile(&buf, source, 1)
- apparmor.WritableProfile(&buf, target, 1)
- spec.AddUpdateNS(buf.String())
+ apparmor.GenWritableProfile(emit, source, 1)
+ apparmor.GenWritableProfile(emit, target, 1)
}
}
diff -Nru snapd-2.40/interfaces/builtin/content_test.go snapd-2.42.1/interfaces/builtin/content_test.go
--- snapd-2.40/interfaces/builtin/content_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/content_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,8 +20,8 @@
package builtin_test
import (
- "fmt"
"path/filepath"
+ "strings"
. "gopkg.in/check.v1"
@@ -318,6 +318,7 @@
profile0 := ` # Read-only content sharing consumer:content -> producer:content (r#0)
mount options=(bind) /snap/producer/5/export/ -> /snap/consumer/7/import/,
remount options=(bind, ro) /snap/consumer/7/import/,
+ mount options=(rprivate) -> /snap/consumer/7/import/,
umount /snap/consumer/7/import/,
# Writable mimic /snap/producer/5
# .. permissions for traversing the prefix that is assumed to exist
@@ -339,193 +340,115 @@
mount options=(bind, rw) /tmp/.snap/* -> /*,
# Allow unmounting the auxiliary directory.
# TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/,
umount /tmp/.snap/,
# Allow unmounting the destination directory as well as anything
# inside. This lets us perform the undo plan in case the writable
# mimic fails.
+ mount options=(rprivate) -> /,
+ mount options=(rprivate) -> /*,
+ mount options=(rprivate) -> /*/,
umount /,
umount /*,
umount /*/,
# .. variant with mimic at /snap/
- # Allow reading the mimic directory, it must exist in the first place.
/snap/ r,
- # Allow setting the read-only directory aside via a bind mount.
/tmp/.snap/snap/ rw,
mount options=(rbind, rw) /snap/ -> /tmp/.snap/snap/,
- # Allow mounting tmpfs over the read-only directory.
mount fstype=tmpfs options=(rw) tmpfs -> /snap/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
/tmp/.snap/snap/*/ rw,
/snap/*/ rw,
mount options=(rbind, rw) /tmp/.snap/snap/*/ -> /snap/*/,
/tmp/.snap/snap/* rw,
/snap/* rw,
mount options=(bind, rw) /tmp/.snap/snap/* -> /snap/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/snap/,
umount /tmp/.snap/snap/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
+ mount options=(rprivate) -> /snap/,
+ mount options=(rprivate) -> /snap/*,
+ mount options=(rprivate) -> /snap/*/,
umount /snap/,
umount /snap/*,
umount /snap/*/,
# .. variant with mimic at /snap/producer/
- # Allow reading the mimic directory, it must exist in the first place.
/snap/producer/ r,
- # Allow setting the read-only directory aside via a bind mount.
/tmp/.snap/snap/producer/ rw,
mount options=(rbind, rw) /snap/producer/ -> /tmp/.snap/snap/producer/,
- # Allow mounting tmpfs over the read-only directory.
mount fstype=tmpfs options=(rw) tmpfs -> /snap/producer/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
/tmp/.snap/snap/producer/*/ rw,
/snap/producer/*/ rw,
mount options=(rbind, rw) /tmp/.snap/snap/producer/*/ -> /snap/producer/*/,
/tmp/.snap/snap/producer/* rw,
/snap/producer/* rw,
mount options=(bind, rw) /tmp/.snap/snap/producer/* -> /snap/producer/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/snap/producer/,
umount /tmp/.snap/snap/producer/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
+ mount options=(rprivate) -> /snap/producer/,
+ mount options=(rprivate) -> /snap/producer/*,
+ mount options=(rprivate) -> /snap/producer/*/,
umount /snap/producer/,
umount /snap/producer/*,
umount /snap/producer/*/,
# .. variant with mimic at /snap/producer/5/
- # Allow reading the mimic directory, it must exist in the first place.
/snap/producer/5/ r,
- # Allow setting the read-only directory aside via a bind mount.
/tmp/.snap/snap/producer/5/ rw,
mount options=(rbind, rw) /snap/producer/5/ -> /tmp/.snap/snap/producer/5/,
- # Allow mounting tmpfs over the read-only directory.
mount fstype=tmpfs options=(rw) tmpfs -> /snap/producer/5/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
/tmp/.snap/snap/producer/5/*/ rw,
/snap/producer/5/*/ rw,
mount options=(rbind, rw) /tmp/.snap/snap/producer/5/*/ -> /snap/producer/5/*/,
/tmp/.snap/snap/producer/5/* rw,
/snap/producer/5/* rw,
mount options=(bind, rw) /tmp/.snap/snap/producer/5/* -> /snap/producer/5/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/snap/producer/5/,
umount /tmp/.snap/snap/producer/5/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
+ mount options=(rprivate) -> /snap/producer/5/,
+ mount options=(rprivate) -> /snap/producer/5/*,
+ mount options=(rprivate) -> /snap/producer/5/*/,
umount /snap/producer/5/,
umount /snap/producer/5/*,
umount /snap/producer/5/*/,
# Writable mimic /snap/consumer/7
- # .. permissions for traversing the prefix that is assumed to exist
- # .. variant with mimic at /
- # Allow reading the mimic directory, it must exist in the first place.
- / r,
- # Allow setting the read-only directory aside via a bind mount.
- /tmp/.snap/ rw,
- mount options=(rbind, rw) / -> /tmp/.snap/,
- # Allow mounting tmpfs over the read-only directory.
- mount fstype=tmpfs options=(rw) tmpfs -> /,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
- /tmp/.snap/*/ rw,
- /*/ rw,
- mount options=(rbind, rw) /tmp/.snap/*/ -> /*/,
- /tmp/.snap/* rw,
- /* rw,
- mount options=(bind, rw) /tmp/.snap/* -> /*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
- umount /tmp/.snap/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
- umount /,
- umount /*,
- umount /*/,
- # .. variant with mimic at /snap/
- # Allow reading the mimic directory, it must exist in the first place.
- /snap/ r,
- # Allow setting the read-only directory aside via a bind mount.
- /tmp/.snap/snap/ rw,
- mount options=(rbind, rw) /snap/ -> /tmp/.snap/snap/,
- # Allow mounting tmpfs over the read-only directory.
- mount fstype=tmpfs options=(rw) tmpfs -> /snap/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
- /tmp/.snap/snap/*/ rw,
- /snap/*/ rw,
- mount options=(rbind, rw) /tmp/.snap/snap/*/ -> /snap/*/,
- /tmp/.snap/snap/* rw,
- /snap/* rw,
- mount options=(bind, rw) /tmp/.snap/snap/* -> /snap/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
- umount /tmp/.snap/snap/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
- umount /snap/,
- umount /snap/*,
- umount /snap/*/,
# .. variant with mimic at /snap/consumer/
- # Allow reading the mimic directory, it must exist in the first place.
/snap/consumer/ r,
- # Allow setting the read-only directory aside via a bind mount.
/tmp/.snap/snap/consumer/ rw,
mount options=(rbind, rw) /snap/consumer/ -> /tmp/.snap/snap/consumer/,
- # Allow mounting tmpfs over the read-only directory.
mount fstype=tmpfs options=(rw) tmpfs -> /snap/consumer/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
/tmp/.snap/snap/consumer/*/ rw,
/snap/consumer/*/ rw,
mount options=(rbind, rw) /tmp/.snap/snap/consumer/*/ -> /snap/consumer/*/,
/tmp/.snap/snap/consumer/* rw,
/snap/consumer/* rw,
mount options=(bind, rw) /tmp/.snap/snap/consumer/* -> /snap/consumer/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/snap/consumer/,
umount /tmp/.snap/snap/consumer/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
+ mount options=(rprivate) -> /snap/consumer/,
+ mount options=(rprivate) -> /snap/consumer/*,
+ mount options=(rprivate) -> /snap/consumer/*/,
umount /snap/consumer/,
umount /snap/consumer/*,
umount /snap/consumer/*/,
# .. variant with mimic at /snap/consumer/7/
- # Allow reading the mimic directory, it must exist in the first place.
/snap/consumer/7/ r,
- # Allow setting the read-only directory aside via a bind mount.
/tmp/.snap/snap/consumer/7/ rw,
mount options=(rbind, rw) /snap/consumer/7/ -> /tmp/.snap/snap/consumer/7/,
- # Allow mounting tmpfs over the read-only directory.
mount fstype=tmpfs options=(rw) tmpfs -> /snap/consumer/7/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
/tmp/.snap/snap/consumer/7/*/ rw,
/snap/consumer/7/*/ rw,
mount options=(rbind, rw) /tmp/.snap/snap/consumer/7/*/ -> /snap/consumer/7/*/,
/tmp/.snap/snap/consumer/7/* rw,
/snap/consumer/7/* rw,
mount options=(bind, rw) /tmp/.snap/snap/consumer/7/* -> /snap/consumer/7/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/snap/consumer/7/,
umount /tmp/.snap/snap/consumer/7/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
+ mount options=(rprivate) -> /snap/consumer/7/,
+ mount options=(rprivate) -> /snap/consumer/7/*,
+ mount options=(rprivate) -> /snap/consumer/7/*/,
umount /snap/consumer/7/,
umount /snap/consumer/7/*,
umount /snap/consumer/7/*/,
`
- c.Assert(updateNS[0], Equals, profile0)
- c.Assert(updateNS, DeepEquals, []string{profile0})
+ c.Assert(strings.Join(updateNS[:], ""), Equals, profile0)
}
// Check that sharing of writable data is possible
@@ -577,6 +500,7 @@
updateNS := apparmorSpec.UpdateNS()
profile0 := ` # Read-write content sharing consumer:content -> producer:content (w#0)
mount options=(bind, rw) /var/snap/producer/5/export/ -> /var/snap/consumer/7/import/,
+ mount options=(rprivate) -> /var/snap/consumer/7/import/,
umount /var/snap/consumer/7/import/,
# Writable directory /var/snap/producer/5/export
/var/snap/producer/5/export/ rw,
@@ -587,8 +511,7 @@
/var/snap/consumer/7/ rw,
/var/snap/consumer/ rw,
`
- c.Assert(updateNS[0], Equals, profile0)
- c.Assert(updateNS, DeepEquals, []string{profile0})
+ c.Assert(strings.Join(updateNS[:], ""), Equals, profile0)
}
// Check that sharing of writable common data is possible
@@ -640,6 +563,7 @@
updateNS := apparmorSpec.UpdateNS()
profile0 := ` # Read-write content sharing consumer:content -> producer:content (w#0)
mount options=(bind, rw) /var/snap/producer/common/export/ -> /var/snap/consumer/common/import/,
+ mount options=(rprivate) -> /var/snap/consumer/common/import/,
umount /var/snap/consumer/common/import/,
# Writable directory /var/snap/producer/common/export
/var/snap/producer/common/export/ rw,
@@ -650,8 +574,7 @@
/var/snap/consumer/common/ rw,
/var/snap/consumer/ rw,
`
- c.Assert(updateNS[0], Equals, profile0)
- c.Assert(updateNS, DeepEquals, []string{profile0})
+ c.Assert(strings.Join(updateNS[:], ""), Equals, profile0)
}
func (s *ContentSuite) TestInterfaces(c *C) {
@@ -734,10 +657,11 @@
/snap/producer/2/read-snap/** mrkix,
`
c.Assert(apparmorSpec.SnippetForTag("snap.consumer.app"), Equals, expected)
- fmt.Printf("")
+
updateNS := apparmorSpec.UpdateNS()
profile0 := ` # Read-write content sharing consumer:content -> producer:content (w#0)
mount options=(bind, rw) /var/snap/producer/common/write-common/ -> /var/snap/consumer/common/import/write-common/,
+ mount options=(rprivate) -> /var/snap/consumer/common/import/write-common/,
umount /var/snap/consumer/common/import/write-common/,
# Writable directory /var/snap/producer/common/write-common
/var/snap/producer/common/write-common/ rw,
@@ -749,58 +673,64 @@
/var/snap/consumer/common/ rw,
/var/snap/consumer/ rw,
`
- c.Assert(updateNS[0], Equals, profile0)
+ // Find the slice that describes profile0 by looking for the first unique
+ // line of the next profile.
+ start := 0
+ end, _ := apparmorSpec.UpdateNSIndexOf(" # Read-write content sharing consumer:content -> producer:content (w#1)\n")
+ c.Assert(strings.Join(updateNS[start:end], ""), Equals, profile0)
profile1 := ` # Read-write content sharing consumer:content -> producer:content (w#1)
mount options=(bind, rw) /var/snap/producer/2/write-data/ -> /var/snap/consumer/common/import/write-data/,
+ mount options=(rprivate) -> /var/snap/consumer/common/import/write-data/,
umount /var/snap/consumer/common/import/write-data/,
# Writable directory /var/snap/producer/2/write-data
/var/snap/producer/2/write-data/ rw,
/var/snap/producer/2/ rw,
- /var/snap/producer/ rw,
# Writable directory /var/snap/consumer/common/import/write-data
/var/snap/consumer/common/import/write-data/ rw,
- /var/snap/consumer/common/import/ rw,
- /var/snap/consumer/common/ rw,
- /var/snap/consumer/ rw,
`
- c.Assert(updateNS[1], Equals, profile1)
+ // Find the slice that describes profile1 by looking for the first unique
+ // line of the next profile.
+ start = end
+ end, _ = apparmorSpec.UpdateNSIndexOf(" # Read-only content sharing consumer:content -> producer:content (r#0)\n")
+ c.Assert(strings.Join(updateNS[start:end], ""), Equals, profile1)
profile2 := ` # Read-only content sharing consumer:content -> producer:content (r#0)
mount options=(bind) /var/snap/producer/common/read-common/ -> /var/snap/consumer/common/import/read-common/,
remount options=(bind, ro) /var/snap/consumer/common/import/read-common/,
+ mount options=(rprivate) -> /var/snap/consumer/common/import/read-common/,
umount /var/snap/consumer/common/import/read-common/,
# Writable directory /var/snap/producer/common/read-common
/var/snap/producer/common/read-common/ rw,
- /var/snap/producer/common/ rw,
- /var/snap/producer/ rw,
# Writable directory /var/snap/consumer/common/import/read-common
/var/snap/consumer/common/import/read-common/ rw,
- /var/snap/consumer/common/import/ rw,
- /var/snap/consumer/common/ rw,
- /var/snap/consumer/ rw,
`
- c.Assert(updateNS[2], Equals, profile2)
+ // Find the slice that describes profile2 by looking for the first unique
+ // line of the next profile.
+ start = end
+ end, _ = apparmorSpec.UpdateNSIndexOf(" # Read-only content sharing consumer:content -> producer:content (r#1)\n")
+ c.Assert(strings.Join(updateNS[start:end], ""), Equals, profile2)
profile3 := ` # Read-only content sharing consumer:content -> producer:content (r#1)
mount options=(bind) /var/snap/producer/2/read-data/ -> /var/snap/consumer/common/import/read-data/,
remount options=(bind, ro) /var/snap/consumer/common/import/read-data/,
+ mount options=(rprivate) -> /var/snap/consumer/common/import/read-data/,
umount /var/snap/consumer/common/import/read-data/,
# Writable directory /var/snap/producer/2/read-data
/var/snap/producer/2/read-data/ rw,
- /var/snap/producer/2/ rw,
- /var/snap/producer/ rw,
# Writable directory /var/snap/consumer/common/import/read-data
/var/snap/consumer/common/import/read-data/ rw,
- /var/snap/consumer/common/import/ rw,
- /var/snap/consumer/common/ rw,
- /var/snap/consumer/ rw,
`
- c.Assert(updateNS[3], Equals, profile3)
+ // Find the slice that describes profile3 by looking for the first unique
+ // line of the next profile.
+ start = end
+ end, _ = apparmorSpec.UpdateNSIndexOf(" # Read-only content sharing consumer:content -> producer:content (r#2)\n")
+ c.Assert(strings.Join(updateNS[start:end], ""), Equals, profile3)
profile4 := ` # Read-only content sharing consumer:content -> producer:content (r#2)
mount options=(bind) /snap/producer/2/read-snap/ -> /var/snap/consumer/common/import/read-snap/,
remount options=(bind, ro) /var/snap/consumer/common/import/read-snap/,
+ mount options=(rprivate) -> /var/snap/consumer/common/import/read-snap/,
umount /var/snap/consumer/common/import/read-snap/,
# Writable mimic /snap/producer/2
# .. permissions for traversing the prefix that is assumed to exist
@@ -822,96 +752,81 @@
mount options=(bind, rw) /tmp/.snap/* -> /*,
# Allow unmounting the auxiliary directory.
# TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/,
umount /tmp/.snap/,
# Allow unmounting the destination directory as well as anything
# inside. This lets us perform the undo plan in case the writable
# mimic fails.
+ mount options=(rprivate) -> /,
+ mount options=(rprivate) -> /*,
+ mount options=(rprivate) -> /*/,
umount /,
umount /*,
umount /*/,
# .. variant with mimic at /snap/
- # Allow reading the mimic directory, it must exist in the first place.
/snap/ r,
- # Allow setting the read-only directory aside via a bind mount.
/tmp/.snap/snap/ rw,
mount options=(rbind, rw) /snap/ -> /tmp/.snap/snap/,
- # Allow mounting tmpfs over the read-only directory.
mount fstype=tmpfs options=(rw) tmpfs -> /snap/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
/tmp/.snap/snap/*/ rw,
/snap/*/ rw,
mount options=(rbind, rw) /tmp/.snap/snap/*/ -> /snap/*/,
/tmp/.snap/snap/* rw,
/snap/* rw,
mount options=(bind, rw) /tmp/.snap/snap/* -> /snap/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/snap/,
umount /tmp/.snap/snap/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
+ mount options=(rprivate) -> /snap/,
+ mount options=(rprivate) -> /snap/*,
+ mount options=(rprivate) -> /snap/*/,
umount /snap/,
umount /snap/*,
umount /snap/*/,
# .. variant with mimic at /snap/producer/
- # Allow reading the mimic directory, it must exist in the first place.
/snap/producer/ r,
- # Allow setting the read-only directory aside via a bind mount.
/tmp/.snap/snap/producer/ rw,
mount options=(rbind, rw) /snap/producer/ -> /tmp/.snap/snap/producer/,
- # Allow mounting tmpfs over the read-only directory.
mount fstype=tmpfs options=(rw) tmpfs -> /snap/producer/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
/tmp/.snap/snap/producer/*/ rw,
/snap/producer/*/ rw,
mount options=(rbind, rw) /tmp/.snap/snap/producer/*/ -> /snap/producer/*/,
/tmp/.snap/snap/producer/* rw,
/snap/producer/* rw,
mount options=(bind, rw) /tmp/.snap/snap/producer/* -> /snap/producer/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/snap/producer/,
umount /tmp/.snap/snap/producer/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
+ mount options=(rprivate) -> /snap/producer/,
+ mount options=(rprivate) -> /snap/producer/*,
+ mount options=(rprivate) -> /snap/producer/*/,
umount /snap/producer/,
umount /snap/producer/*,
umount /snap/producer/*/,
# .. variant with mimic at /snap/producer/2/
- # Allow reading the mimic directory, it must exist in the first place.
/snap/producer/2/ r,
- # Allow setting the read-only directory aside via a bind mount.
/tmp/.snap/snap/producer/2/ rw,
mount options=(rbind, rw) /snap/producer/2/ -> /tmp/.snap/snap/producer/2/,
- # Allow mounting tmpfs over the read-only directory.
mount fstype=tmpfs options=(rw) tmpfs -> /snap/producer/2/,
- # Allow creating empty files and directories for bind mounting things
- # to reconstruct the now-writable parent directory.
/tmp/.snap/snap/producer/2/*/ rw,
/snap/producer/2/*/ rw,
mount options=(rbind, rw) /tmp/.snap/snap/producer/2/*/ -> /snap/producer/2/*/,
/tmp/.snap/snap/producer/2/* rw,
/snap/producer/2/* rw,
mount options=(bind, rw) /tmp/.snap/snap/producer/2/* -> /snap/producer/2/*,
- # Allow unmounting the auxiliary directory.
- # TODO: use fstype=tmpfs here for more strictness (LP: #1613403)
+ mount options=(rprivate) -> /tmp/.snap/snap/producer/2/,
umount /tmp/.snap/snap/producer/2/,
- # Allow unmounting the destination directory as well as anything
- # inside. This lets us perform the undo plan in case the writable
- # mimic fails.
+ mount options=(rprivate) -> /snap/producer/2/,
+ mount options=(rprivate) -> /snap/producer/2/*,
+ mount options=(rprivate) -> /snap/producer/2/*/,
umount /snap/producer/2/,
umount /snap/producer/2/*,
umount /snap/producer/2/*/,
# Writable directory /var/snap/consumer/common/import/read-snap
/var/snap/consumer/common/import/read-snap/ rw,
- /var/snap/consumer/common/import/ rw,
- /var/snap/consumer/common/ rw,
- /var/snap/consumer/ rw,
`
- c.Assert(updateNS[4], Equals, profile4)
- c.Assert(updateNS, DeepEquals, []string{profile0, profile1, profile2, profile3, profile4})
+ // Find the slice that describes profile4 by looking till the end of the list.
+ start = end
+ c.Assert(strings.Join(updateNS[start:], ""), Equals, profile4)
+ c.Assert(strings.Join(updateNS, ""), DeepEquals, strings.Join([]string{profile0, profile1, profile2, profile3, profile4}, ""))
}
func (s *ContentSuite) TestModernContentInterfacePlugins(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/core_support_test.go snapd-2.42.1/interfaces/builtin/core_support_test.go
--- snapd-2.40/interfaces/builtin/core_support_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/core_support_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *CoreSupportInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "core-support",
- Interface: "core-support",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "core-support slots are reserved for the core snap")
}
func (s *CoreSupportInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/cpu_control_test.go snapd-2.42.1/interfaces/builtin/cpu_control_test.go
--- snapd-2.40/interfaces/builtin/cpu_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/cpu_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *CpuControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "cpu-control",
- Interface: "cpu-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "cpu-control slots are reserved for the core snap")
}
func (s *CpuControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/daemon_notify_test.go snapd-2.42.1/interfaces/builtin/daemon_notify_test.go
--- snapd-2.40/interfaces/builtin/daemon_notify_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/daemon_notify_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,15 +67,6 @@
func (s *daemoNotifySuite) TestBeforePrepareSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- nonOsDaemonNotifySlotSnapInfoYaml := `name: non-os-daemon-notify
-version: 1.0
-slots:
- daemon-notify:
- interface: daemon-notify
-`
- si := builtin.MockSlot(c, nonOsDaemonNotifySlotSnapInfoYaml, nil, "daemon-notify")
- c.Assert(interfaces.BeforePrepareSlot(s.iface, si), ErrorMatches,
- "daemon-notify slots are reserved for the core snap")
}
func (s *daemoNotifySuite) TestBeforePreparePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/dcdbas_control_test.go snapd-2.42.1/interfaces/builtin/dcdbas_control_test.go
--- snapd-2.40/interfaces/builtin/dcdbas_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/dcdbas_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *DcdbasControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "dcdbas-control",
- Interface: "dcdbas-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "dcdbas-control slots are reserved for the core snap")
}
func (s *DcdbasControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/desktop.go snapd-2.42.1/interfaces/builtin/desktop.go
--- snapd-2.40/interfaces/builtin/desktop.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/desktop.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,9 +20,6 @@
package builtin
import (
- "bytes"
- "fmt"
-
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/interfaces/apparmor"
@@ -217,10 +214,6 @@
}
}
-func (iface *desktopInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
- return sanitizeSlotReservedForOS(iface, slot)
-}
-
func (iface *desktopInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool {
// allow what declarations allowed
return true
@@ -238,11 +231,10 @@
spec.AddSnippet(desktopConnectedPlugAppArmor)
// Allow mounting document portal
- var buf bytes.Buffer
- fmt.Fprintf(&buf, " # Mount the document portal\n")
- fmt.Fprintf(&buf, " mount options=(bind) /run/user/[0-9]*/doc/by-app/snap.%s/ -> /run/user/[0-9]*/doc/,\n", plug.Snap().InstanceName())
- fmt.Fprintf(&buf, " umount /run/user/[0-9]*/doc/,\n\n")
- spec.AddUpdateNS(buf.String())
+ emit := spec.EmitUpdateNSFunc()
+ emit(" # Mount the document portal\n")
+ emit(" mount options=(bind) /run/user/[0-9]*/doc/by-app/snap.%s/ -> /run/user/[0-9]*/doc/,\n", plug.Snap().InstanceName())
+ emit(" umount /run/user/[0-9]*/doc/,\n\n")
if !release.OnClassic {
// We only need the font mount rules on classic systems
@@ -251,14 +243,12 @@
// Allow mounting fonts
for _, dir := range iface.fontconfigDirs() {
- var buf bytes.Buffer
source := "/var/lib/snapd/hostfs" + dir
target := dirs.StripRootDir(dir)
- fmt.Fprintf(&buf, " # Read-only access to %s\n", target)
- fmt.Fprintf(&buf, " mount options=(bind) %s/ -> %s/,\n", source, target)
- fmt.Fprintf(&buf, " remount options=(bind, ro) %s/,\n", target)
- fmt.Fprintf(&buf, " umount %s/,\n\n", target)
- spec.AddUpdateNS(buf.String())
+ emit(" # Read-only access to %s\n", target)
+ emit(" mount options=(bind) %s/ -> %s/,\n", source, target)
+ emit(" remount options=(bind, ro) %s/,\n", target)
+ emit(" umount %s/,\n\n", target)
}
return nil
diff -Nru snapd-2.40/interfaces/builtin/desktop_legacy_test.go snapd-2.42.1/interfaces/builtin/desktop_legacy_test.go
--- snapd-2.40/interfaces/builtin/desktop_legacy_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/desktop_legacy_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -66,14 +66,6 @@
func (s *DesktopLegacyInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.coreSlotInfo), IsNil)
- // desktop-legacy slot currently only used with core
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "desktop-legacy",
- Interface: "desktop-legacy",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "desktop-legacy slots are reserved for the core snap")
}
func (s *DesktopLegacyInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/desktop_test.go snapd-2.42.1/interfaces/builtin/desktop_test.go
--- snapd-2.40/interfaces/builtin/desktop_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/desktop_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -22,6 +22,7 @@
import (
"os"
"path/filepath"
+ "strings"
. "gopkg.in/check.v1"
@@ -76,15 +77,6 @@
func (s *DesktopInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.coreSlotInfo), IsNil)
-
- // desktop slot currently only used with core
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "desktop",
- Interface: "desktop",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "desktop slots are reserved for the core snap")
}
func (s *DesktopInterfaceSuite) TestSanitizePlug(c *C) {
@@ -112,12 +104,12 @@
// On an all-snaps system, the only UpdateNS rule is for the
// document portal.
updateNS := spec.UpdateNS()
- c.Assert(updateNS, HasLen, 1)
- c.Check(updateNS[0], Equals, ` # Mount the document portal
+ profile0 := ` # Mount the document portal
mount options=(bind) /run/user/[0-9]*/doc/by-app/snap.consumer/ -> /run/user/[0-9]*/doc/,
umount /run/user/[0-9]*/doc/,
-`)
+`
+ c.Assert(strings.Join(updateNS, ""), Equals, profile0)
// On a classic system, there are UpdateNS rules for the host
// system font mounts
@@ -126,11 +118,10 @@
spec = &apparmor.Specification{}
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil)
updateNS = spec.UpdateNS()
- c.Assert(updateNS, HasLen, 4)
- c.Check(updateNS[0], testutil.Contains, "# Mount the document portal")
- c.Check(updateNS[1], testutil.Contains, "# Read-only access to /usr/share/fonts")
- c.Check(updateNS[2], testutil.Contains, "# Read-only access to /usr/local/share/fonts")
- c.Check(updateNS[3], testutil.Contains, "# Read-only access to /var/cache/fontconfig")
+ c.Check(updateNS, testutil.Contains, " # Mount the document portal\n")
+ c.Check(updateNS, testutil.Contains, " # Read-only access to /usr/share/fonts\n")
+ c.Check(updateNS, testutil.Contains, " # Read-only access to /usr/local/share/fonts\n")
+ c.Check(updateNS, testutil.Contains, " # Read-only access to /var/cache/fontconfig\n")
// connected plug to core slot
spec = &apparmor.Specification{}
diff -Nru snapd-2.40/interfaces/builtin/device_buttons_test.go snapd-2.42.1/interfaces/builtin/device_buttons_test.go
--- snapd-2.40/interfaces/builtin/device_buttons_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/device_buttons_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *DeviceButtonsInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "device-buttons",
- Interface: "device-buttons",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "device-buttons slots are reserved for the core snap")
}
func (s *DeviceButtonsInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/display_control_test.go snapd-2.42.1/interfaces/builtin/display_control_test.go
--- snapd-2.40/interfaces/builtin/display_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/display_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -76,13 +76,6 @@
func (s *displayControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "display-control",
- Interface: "display-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "display-control slots are reserved for the core snap")
}
func (s *displayControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/docker_support.go snapd-2.42.1/interfaces/builtin/docker_support.go
--- snapd-2.40/interfaces/builtin/docker_support.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/docker_support.go 2019-10-30 12:17:43.000000000 +0000
@@ -25,6 +25,7 @@
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/interfaces/apparmor"
"github.com/snapcore/snapd/interfaces/seccomp"
+ "github.com/snapcore/snapd/interfaces/udev"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
)
@@ -46,17 +47,16 @@
`
const dockerSupportConnectedPlugAppArmorCore = `
-# these accesses are necessary for Ubuntu Core 16 and 18, likely due to the version
-# of apparmor or the kernel which doesn't resolve the upper layer of an
-# overlayfs mount correctly
-# the accesses show up as runc trying to read from
+# These accesses are necessary for Ubuntu Core 16 and 18, likely due to the
+# version of apparmor or the kernel which doesn't resolve the upper layer of an
+# overlayfs mount correctly the accesses show up as runc trying to read from
# /system-data/var/snap/docker/common/var-lib-docker/overlay2/$SHA/diff/
/system-data/var/snap/{@{SNAP_NAME},@{SNAP_INSTANCE_NAME}}/common/{,**/} rwl,
/system-data/var/snap/{@{SNAP_NAME},@{SNAP_INSTANCE_NAME}}/@{SNAP_REVISION}/{,**/} rwl,
`
const dockerSupportConnectedPlugAppArmor = `
-# Description: allow operating as the Docker daemon. This policy is
+# Description: allow operating as the Docker daemon/containerd. This policy is
# intentionally not restrictive and is here to help guard against programming
# errors and not for security confinement. The Docker daemon by design requires
# extensive access to the system and cannot be effectively confined against
@@ -64,15 +64,26 @@
#include
-# Allow sockets
+# Allow sockets/etc for docker
/{,var/}run/docker.sock rw,
/{,var/}run/docker/ rw,
/{,var/}run/docker/** mrwklix,
/{,var/}run/runc/ rw,
/{,var/}run/runc/** mrwklix,
+# Allow sockets/etc for containerd
+/{,var/}run/containerd/{,runc/,runc/k8s.io/,runc/k8s.io/*/} rw,
+/{,var/}run/containerd/runc/k8s.io/*/** rwk,
+/{,var/}run/containerd/{io.containerd*/,io.containerd*/k8s.io/,io.containerd*/k8s.io/*/} rw,
+/{,var/}run/containerd/io.containerd*/*/** rwk,
+
+# Limit ipam-state to k8s
+/run/ipam-state/k8s-** rw,
+/run/ipam-state/k8s-*/lock k,
+
# Socket for docker-container-shim
unix (bind,listen) type=stream addr="@/containerd-shim/moby/*/shim.sock\x00",
+unix (bind,listen) type=stream addr="@/containerd-shim/k8s.io/*/shim.sock\x00",
/{,var/}run/mount/utab r,
@@ -138,10 +149,17 @@
/sys/kernel/security/apparmor/{,**} r,
# use 'privileged-containers: true' to support --security-opts
+
+# defaults for docker-default
change_profile unsafe /** -> docker-default,
signal (send) peer=docker-default,
ptrace (read, trace) peer=docker-default,
+# defaults for containerd
+change_profile unsafe /** -> cri-containerd.apparmor.d,
+signal (send) peer=cri-containerd.apparmor.d,
+ptrace (read, trace) peer=cri-containerd.apparmor.d,
+
# Graph (storage) driver bits
/{dev,run}/shm/aufs.xino mrw,
/proc/fs/aufs/plink_maint w,
@@ -157,6 +175,23 @@
# needed by runc for mitigation of CVE-2019-5736
# For details see https://bugs.launchpad.net/apparmor/+bug/1820344
/ ix,
+/bin/runc ixr,
+
+/pause ixr,
+/bin/busybox ixr,
+
+# When kubernetes drives containerd, containerd needs access to CNI services,
+# like flanneld's subnet.env for DNS. This would ideally be snap-specific (it
+# could if the control plane was a snap), but in deployments where the control
+# plane is not a snap, it will tell flannel to use this path.
+/run/flannel/{,**} rk,
+
+# When kubernetes drives containerd, containerd needs access to various
+# secrets for the pods which are overlayed at /run/secrets/....
+# This would ideally be snap-specific (it could if the control plane was a
+# snap), but in deployments where the control plane is not a snap, it will tell
+# containerd to use this path for various account information for pods.
+/run/secrets/kubernetes.io/{,**} rk,
`
const dockerSupportConnectedPlugSecComp = `
@@ -594,6 +629,12 @@
parserFeatures = release.AppArmorParserFeatures
)
+func (iface *dockerSupportInterface) UDevConnectedPlug(spec *udev.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
+ spec.SetControlsDeviceCgroup()
+
+ return nil
+}
+
func (iface *dockerSupportInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error {
var privileged bool
_ = plug.Attr("privileged-containers", &privileged)
@@ -608,7 +649,7 @@
if !release.OnClassic {
spec.AddSnippet(dockerSupportConnectedPlugAppArmorCore)
}
- spec.UsesPtraceTrace()
+ spec.SetUsesPtraceTrace()
return nil
}
diff -Nru snapd-2.40/interfaces/builtin/docker_support_test.go snapd-2.42.1/interfaces/builtin/docker_support_test.go
--- snapd-2.40/interfaces/builtin/docker_support_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/docker_support_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -26,6 +26,7 @@
"github.com/snapcore/snapd/interfaces/apparmor"
"github.com/snapcore/snapd/interfaces/builtin"
"github.com/snapcore/snapd/interfaces/seccomp"
+ "github.com/snapcore/snapd/interfaces/udev"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
@@ -33,11 +34,19 @@
)
type DockerSupportInterfaceSuite struct {
- iface interfaces.Interface
- slotInfo *snap.SlotInfo
- slot *interfaces.ConnectedSlot
- plugInfo *snap.PlugInfo
- plug *interfaces.ConnectedPlug
+ iface interfaces.Interface
+ slotInfo *snap.SlotInfo
+ slot *interfaces.ConnectedSlot
+ plugInfo *snap.PlugInfo
+ plug *interfaces.ConnectedPlug
+ networkCtrlSlotInfo *snap.SlotInfo
+ networkCtrlSlot *interfaces.ConnectedSlot
+ networkCtrlPlugInfo *snap.PlugInfo
+ networkCtrlPlug *interfaces.ConnectedPlug
+ privContainersPlugInfo *snap.PlugInfo
+ privContainersPlug *interfaces.ConnectedPlug
+ noPrivContainersPlugInfo *snap.PlugInfo
+ noPrivContainersPlug *interfaces.ConnectedPlug
}
const coreDockerSlotYaml = `name: core
@@ -45,6 +54,7 @@
type: os
slots:
docker-support:
+ network-control:
`
const dockerSupportMockPlugSnapInfoYaml = `name: docker
@@ -52,7 +62,35 @@
apps:
app:
command: foo
- plugs: [docker-support]
+ plugs:
+ - docker-support
+ - network-control
+`
+
+const dockerSupportPrivilegedContainersFalseMockPlugSnapInfoYaml = `name: docker
+version: 1.0
+plugs:
+ privileged:
+ interface: docker-support
+ privileged-containers: false
+apps:
+ app:
+ command: foo
+ plugs:
+ - privileged
+`
+
+const dockerSupportPrivilegedContainersTrueMockPlugSnapInfoYaml = `name: docker
+version: 1.0
+plugs:
+ privileged:
+ interface: docker-support
+ privileged-containers: true
+apps:
+ app:
+ command: foo
+ plugs:
+ - privileged
`
var _ = Suite(&DockerSupportInterfaceSuite{
@@ -62,6 +100,10 @@
func (s *DockerSupportInterfaceSuite) SetUpTest(c *C) {
s.plug, s.plugInfo = MockConnectedPlug(c, dockerSupportMockPlugSnapInfoYaml, nil, "docker-support")
s.slot, s.slotInfo = MockConnectedSlot(c, coreDockerSlotYaml, nil, "docker-support")
+ s.networkCtrlPlug, s.networkCtrlPlugInfo = MockConnectedPlug(c, dockerSupportMockPlugSnapInfoYaml, nil, "network-control")
+ s.networkCtrlSlot, s.networkCtrlSlotInfo = MockConnectedSlot(c, coreDockerSlotYaml, nil, "network-control")
+ s.privContainersPlug, s.privContainersPlugInfo = MockConnectedPlug(c, dockerSupportPrivilegedContainersTrueMockPlugSnapInfoYaml, nil, "privileged")
+ s.noPrivContainersPlug, s.noPrivContainersPlugInfo = MockConnectedPlug(c, dockerSupportPrivilegedContainersFalseMockPlugSnapInfoYaml, nil, "privileged")
}
func (s *DockerSupportInterfaceSuite) TestName(c *C) {
@@ -89,67 +131,25 @@
}
func (s *DockerSupportInterfaceSuite) TestSanitizePlugWithPrivilegedTrue(c *C) {
- var mockSnapYaml = []byte(`name: docker
-version: 1.0
-plugs:
- privileged:
- interface: docker-support
- privileged-containers: true
-apps:
- app:
- command: foo
- plugs:
- - privileged
-`)
-
- info, err := snap.InfoFromSnapYaml(mockSnapYaml)
- c.Assert(err, IsNil)
-
- plug := info.Plugs["privileged"]
- c.Assert(interfaces.BeforePreparePlug(s.iface, plug), IsNil)
-
apparmorSpec := &apparmor.Specification{}
- err = apparmorSpec.AddConnectedPlug(s.iface, interfaces.NewConnectedPlug(plug, nil, nil), s.slot)
- c.Assert(err, IsNil)
+ c.Assert(apparmorSpec.AddConnectedPlug(s.iface, s.privContainersPlug, s.slot), IsNil)
c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.docker.app"})
c.Assert(apparmorSpec.SnippetForTag("snap.docker.app"), testutil.Contains, `change_profile unsafe /**,`)
seccompSpec := &seccomp.Specification{}
- err = seccompSpec.AddConnectedPlug(s.iface, interfaces.NewConnectedPlug(plug, nil, nil), s.slot)
- c.Assert(err, IsNil)
+ c.Assert(seccompSpec.AddConnectedPlug(s.iface, s.privContainersPlug, s.slot), IsNil)
c.Assert(seccompSpec.SecurityTags(), DeepEquals, []string{"snap.docker.app"})
c.Check(seccompSpec.SnippetForTag("snap.docker.app"), testutil.Contains, "@unrestricted")
}
func (s *DockerSupportInterfaceSuite) TestSanitizePlugWithPrivilegedFalse(c *C) {
- var mockSnapYaml = []byte(`name: docker
-version: 1.0
-plugs:
- privileged:
- interface: docker-support
- privileged-containers: false
-apps:
- app:
- command: foo
- plugs:
- - privileged
-`)
-
- info, err := snap.InfoFromSnapYaml(mockSnapYaml)
- c.Assert(err, IsNil)
-
- plug := info.Plugs["privileged"]
- c.Assert(interfaces.BeforePreparePlug(s.iface, plug), IsNil)
-
apparmorSpec := &apparmor.Specification{}
- err = apparmorSpec.AddConnectedPlug(s.iface, interfaces.NewConnectedPlug(plug, nil, nil), s.slot)
- c.Assert(err, IsNil)
+ c.Assert(apparmorSpec.AddConnectedPlug(s.iface, s.noPrivContainersPlug, s.slot), IsNil)
c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.docker.app"})
c.Assert(apparmorSpec.SnippetForTag("snap.docker.app"), Not(testutil.Contains), `change_profile unsafe /**,`)
seccompSpec := &seccomp.Specification{}
- err = seccompSpec.AddConnectedPlug(s.iface, interfaces.NewConnectedPlug(plug, nil, nil), s.slot)
- c.Assert(err, IsNil)
+ c.Assert(seccompSpec.AddConnectedPlug(s.iface, s.noPrivContainersPlug, s.slot), IsNil)
c.Assert(seccompSpec.SecurityTags(), DeepEquals, []string{"snap.docker.app"})
c.Check(seccompSpec.SnippetForTag("snap.docker.app"), Not(testutil.Contains), "@unrestricted")
}
@@ -164,7 +164,6 @@
`
info := snaptest.MockInfo(c, mockSnapYaml, nil)
-
plug := info.Plugs["privileged"]
c.Assert(interfaces.BeforePreparePlug(s.iface, plug), ErrorMatches, "docker-support plug requires bool with 'privileged-containers'")
}
@@ -178,6 +177,7 @@
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.docker.app"})
c.Check(spec.SnippetForTag("snap.docker.app"), testutil.Contains, "/sys/fs/cgroup/*/docker/ rw,\n")
+ c.Check(spec.UsesPtraceTrace(), Equals, true)
}
func (s *DockerSupportInterfaceSuite) TestSecCompSpec(c *C) {
@@ -211,3 +211,26 @@
// verify core rule not present
c.Check(apparmorSpec.SnippetForTag("snap.docker.app"), Not(testutil.Contains), "# /system-data/var/snap/docker/common/var-lib-docker/overlay2/$SHA/diff/\n")
}
+
+func (s *DockerSupportInterfaceSuite) TestUdevTaggingDisablingRemoveLast(c *C) {
+ // make a spec with network-control that has udev tagging
+ spec := &udev.Specification{}
+ c.Assert(spec.AddConnectedPlug(builtin.MustInterface("network-control"), s.networkCtrlPlug, s.networkCtrlSlot), IsNil)
+ c.Assert(spec.Snippets(), HasLen, 3)
+
+ // connect docker-support interface plug and ensure that the udev spec is now nil
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
+ c.Check(spec.Snippets(), HasLen, 0)
+}
+
+func (s *DockerSupportInterfaceSuite) TestUdevTaggingDisablingRemoveFirst(c *C) {
+ spec := &udev.Specification{}
+ // connect docker-support interface plug which specifies
+ // controls-device-cgroup as true and ensure that the udev spec is now nil
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
+ c.Check(spec.Snippets(), HasLen, 0)
+
+ // add network-control and ensure the spec is still nil
+ c.Assert(spec.AddConnectedPlug(builtin.MustInterface("network-control"), s.networkCtrlPlug, s.networkCtrlSlot), IsNil)
+ c.Assert(spec.Snippets(), HasLen, 0)
+}
diff -Nru snapd-2.40/interfaces/builtin/dvb_test.go snapd-2.42.1/interfaces/builtin/dvb_test.go
--- snapd-2.40/interfaces/builtin/dvb_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/dvb_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *DvbInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "dvb",
- Interface: "dvb",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "dvb slots are reserved for the core snap")
}
func (s *DvbInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/export_test.go snapd-2.42.1/interfaces/builtin/export_test.go
--- snapd-2.40/interfaces/builtin/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -30,13 +30,10 @@
)
var (
- RegisterIface = registerIface
- ResolveSpecialVariable = resolveSpecialVariable
- SanitizeSlotReservedForOS = sanitizeSlotReservedForOS
- SanitizeSlotReservedForOSOrGadget = sanitizeSlotReservedForOSOrGadget
- SanitizeSlotReservedForOSOrApp = sanitizeSlotReservedForOSOrApp
- ImplicitSystemPermanentSlot = implicitSystemPermanentSlot
- ImplicitSystemConnectedSlot = implicitSystemConnectedSlot
+ RegisterIface = registerIface
+ ResolveSpecialVariable = resolveSpecialVariable
+ ImplicitSystemPermanentSlot = implicitSystemPermanentSlot
+ ImplicitSystemConnectedSlot = implicitSystemConnectedSlot
)
func MprisGetName(iface interfaces.Interface, attribs map[string]interface{}) (string, error) {
diff -Nru snapd-2.40/interfaces/builtin/firewall_control.go snapd-2.42.1/interfaces/builtin/firewall_control.go
--- snapd-2.40/interfaces/builtin/firewall_control.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/firewall_control.go 2019-10-30 12:17:43.000000000 +0000
@@ -81,6 +81,12 @@
@{PROC}/@{pid}/net/ r,
@{PROC}/@{pid}/net/** r,
+# nft accesses these for routing expressions and device groups
+/etc/iproute2/ r,
+/etc/iproute2/rt_marks r,
+/etc/iproute2/rt_realms r,
+/etc/iproute2/group r,
+
# sysctl
/{,usr/}{,s}bin/sysctl ixr,
@{PROC}/sys/ r,
diff -Nru snapd-2.40/interfaces/builtin/firewall_control_test.go snapd-2.42.1/interfaces/builtin/firewall_control_test.go
--- snapd-2.40/interfaces/builtin/firewall_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/firewall_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *FirewallControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "firewall-control",
- Interface: "firewall-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "firewall-control slots are reserved for the core snap")
}
func (s *FirewallControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/framebuffer_test.go snapd-2.42.1/interfaces/builtin/framebuffer_test.go
--- snapd-2.40/interfaces/builtin/framebuffer_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/framebuffer_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -69,13 +69,6 @@
func (s *FramebufferInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "framebuffer",
- Interface: "framebuffer",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "framebuffer slots are reserved for the core snap")
}
func (s *FramebufferInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/fuse_support_test.go snapd-2.42.1/interfaces/builtin/fuse_support_test.go
--- snapd-2.40/interfaces/builtin/fuse_support_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/fuse_support_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -69,13 +69,6 @@
func (s *FuseSupportInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "fuse-support",
- Interface: "fuse-support",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "fuse-support slots are reserved for the core snap")
}
func (s *FuseSupportInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/gpg_keys_test.go snapd-2.42.1/interfaces/builtin/gpg_keys_test.go
--- snapd-2.40/interfaces/builtin/gpg_keys_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/gpg_keys_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -66,13 +66,6 @@
func (s *GpgKeysInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slotInfo := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "gpg-keys",
- Interface: "gpg-keys",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slotInfo), ErrorMatches,
- "gpg-keys slots are reserved for the core snap")
}
func (s *GpgKeysInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/gpg_public_keys_test.go snapd-2.42.1/interfaces/builtin/gpg_public_keys_test.go
--- snapd-2.40/interfaces/builtin/gpg_public_keys_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/gpg_public_keys_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -66,13 +66,6 @@
func (s *GpgPublicKeysInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slotInfo := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "gpg-public-keys",
- Interface: "gpg-public-keys",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slotInfo), ErrorMatches,
- "gpg-public-keys slots are reserved for the core snap")
}
func (s *GpgPublicKeysInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/gpio_control.go snapd-2.42.1/interfaces/builtin/gpio_control.go
--- snapd-2.40/interfaces/builtin/gpio_control.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/gpio_control.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,60 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package builtin
+
+// https://www.kernel.org/doc/Documentation/gpio/
+const gpioControlSummary = `allows control of all aspects of GPIO pins`
+
+// Controlling all aspects of GPIO pins can potentially impact other snaps and
+// grant wide access to specific hardware and the system, so treat as
+// super-privileged
+const gpioControlBaseDeclarationPlugs = `
+ gpio-control:
+ allow-installation: false
+ deny-auto-connection: true
+`
+const gpioControlBaseDeclarationSlots = `
+ gpio-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+`
+
+const gpioControlConnectedPlugAppArmor = `
+# Description: Allow controlling all aspects of GPIO pins. This can potentially
+# impact the system and other snaps, and allows privileged access to hardware.
+
+/sys/class/gpio/{,un}export rw,
+/sys/class/gpio/gpio[0-9]*/{active_low,direction,value,edge} rw,
+`
+
+func init() {
+ registerIface(&commonInterface{
+ name: "gpio-control",
+ summary: gpioControlSummary,
+ implicitOnCore: true,
+ implicitOnClassic: true,
+ baseDeclarationPlugs: gpioControlBaseDeclarationPlugs,
+ baseDeclarationSlots: gpioControlBaseDeclarationSlots,
+ connectedPlugAppArmor: gpioControlConnectedPlugAppArmor,
+ reservedForOS: true,
+ })
+}
diff -Nru snapd-2.40/interfaces/builtin/gpio_control_test.go snapd-2.42.1/interfaces/builtin/gpio_control_test.go
--- snapd-2.40/interfaces/builtin/gpio_control_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/gpio_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,97 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type GpioControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slotInfo *snap.SlotInfo
+ slot *interfaces.ConnectedSlot
+ plugInfo *snap.PlugInfo
+ plug *interfaces.ConnectedPlug
+}
+
+var _ = Suite(&GpioControlInterfaceSuite{
+ iface: builtin.MustInterface("gpio-control"),
+})
+
+const GpioControlConsumerYaml = `name: consumer
+version: 0
+apps:
+ app:
+ plugs: [gpio-control]
+ `
+
+const GpioControlCoreYaml = `name: core
+version: 0
+type: os
+slots:
+ gpio-control:
+`
+
+func (s *GpioControlInterfaceSuite) SetUpTest(c *C) {
+ s.plug, s.plugInfo = MockConnectedPlug(c, GpioControlConsumerYaml, nil, "gpio-control")
+ s.slot, s.slotInfo = MockConnectedSlot(c, GpioControlCoreYaml, nil, "gpio-control")
+}
+
+func (s *GpioControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "gpio-control")
+}
+
+func (s *GpioControlInterfaceSuite) TestSanitizeSlot(c *C) {
+ c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
+}
+
+func (s *GpioControlInterfaceSuite) TestSanitizePlug(c *C) {
+ c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil)
+}
+
+func (s *GpioControlInterfaceSuite) TestAppArmorSpec(c *C) {
+ spec := &apparmor.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
+ c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"})
+ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/sys/class/gpio/{,un}export rw,`)
+ c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/sys/class/gpio/gpio[0-9]*/{active_low,direction,value,edge} rw,`)
+}
+
+func (s *GpioControlInterfaceSuite) TestStaticInfo(c *C) {
+ si := interfaces.StaticInfoOf(s.iface)
+ c.Assert(si.ImplicitOnCore, Equals, true)
+ c.Assert(si.ImplicitOnClassic, Equals, true)
+ c.Assert(si.Summary, Equals, `allows control of all aspects of GPIO pins`)
+ c.Assert(si.BaseDeclarationPlugs, testutil.Contains, "gpio-control")
+}
+
+func (s *GpioControlInterfaceSuite) TestAutoConnect(c *C) {
+ c.Assert(s.iface.AutoConnect(s.plugInfo, s.slotInfo), Equals, true)
+}
+
+func (s *GpioControlInterfaceSuite) TestInterfaces(c *C) {
+ c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface)
+}
diff -Nru snapd-2.40/interfaces/builtin/gpio.go snapd-2.42.1/interfaces/builtin/gpio.go
--- snapd-2.40/interfaces/builtin/gpio.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/gpio.go 2019-10-30 12:17:43.000000000 +0000
@@ -63,10 +63,6 @@
// BeforePrepareSlot checks the slot definition is valid
func (iface *gpioInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
- if err := sanitizeSlotReservedForOSOrGadget(iface, slot); err != nil {
- return err
- }
-
// Must have a GPIO number
number, ok := slot.Attrs["number"]
if !ok {
diff -Nru snapd-2.40/interfaces/builtin/gpio_memory_control_test.go snapd-2.42.1/interfaces/builtin/gpio_memory_control_test.go
--- snapd-2.40/interfaces/builtin/gpio_memory_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/gpio_memory_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *GpioMemoryControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slotInfo := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "gpio-memory-control",
- Interface: "gpio-memory-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slotInfo), ErrorMatches,
- "gpio-memory-control slots are reserved for the core snap")
}
func (s *GpioMemoryControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/gpio_test.go snapd-2.42.1/interfaces/builtin/gpio_test.go
--- snapd-2.40/interfaces/builtin/gpio_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/gpio_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -46,8 +46,6 @@
gadgetBadInterfacePlug *interfaces.ConnectedPlug
osGpioSlotInfo *snap.SlotInfo
osGpioSlot *interfaces.ConnectedSlot
- appGpioSlotInfo *snap.SlotInfo
- appGpioSlot *interfaces.ConnectedSlot
}
var _ = Suite(&GpioInterfaceSuite{
@@ -98,18 +96,6 @@
`, nil)
s.osGpioSlotInfo = osInfo.Slots["my-pin"]
s.osGpioSlot = interfaces.NewConnectedSlot(s.osGpioSlotInfo, nil, nil)
-
- appInfo := snaptest.MockInfo(c, `
-name: my-app
-version: 0
-slots:
- my-pin:
- interface: gpio
- number: 154
- direction: out
-`, nil)
- s.appGpioSlotInfo = appInfo.Slots["my-pin"]
- s.appGpioSlot = interfaces.NewConnectedSlot(s.appGpioSlotInfo, nil, nil)
}
func (s *GpioInterfaceSuite) TestName(c *C) {
@@ -134,12 +120,6 @@
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.osGpioSlotInfo), IsNil)
}
-func (s *GpioInterfaceSuite) TestSanitizeSlotAppSnap(c *C) {
- // gpio slot not accepted on app snap
- c.Assert(interfaces.BeforePrepareSlot(s.iface, s.appGpioSlotInfo), ErrorMatches,
- "gpio slots are reserved for the core and gadget snaps")
-}
-
func (s *GpioInterfaceSuite) TestSanitizePlug(c *C) {
c.Assert(interfaces.BeforePreparePlug(s.iface, s.gadgetPlugInfo), IsNil)
}
diff -Nru snapd-2.40/interfaces/builtin/greengrass_support_test.go snapd-2.42.1/interfaces/builtin/greengrass_support_test.go
--- snapd-2.40/interfaces/builtin/greengrass_support_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/greengrass_support_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -77,13 +77,6 @@
func (s *GreengrassSupportInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "greengrass-support",
- Interface: "greengrass-support",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "greengrass-support slots are reserved for the core snap")
}
func (s *GreengrassSupportInterfaceSuite) TestSanitizePlug(c *C) {
@@ -95,6 +88,7 @@
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.other.app2"})
c.Check(spec.SnippetForTag("snap.other.app2"), testutil.Contains, "mount options=(rw, bind) /var/snap/{@{SNAP_NAME},@{SNAP_INSTANCE_NAME}}/** -> /var/snap/{@{SNAP_NAME},@{SNAP_INSTANCE_NAME}}/** ,\n")
+ c.Check(spec.UsesPtraceTrace(), Equals, true)
}
func (s *GreengrassSupportInterfaceSuite) TestSecCompSpec(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/gsettings_test.go snapd-2.42.1/interfaces/builtin/gsettings_test.go
--- snapd-2.40/interfaces/builtin/gsettings_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/gsettings_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -69,13 +69,6 @@
func (s *GsettingsInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "gsettings",
- Interface: "gsettings",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "gsettings slots are reserved for the core snap")
}
func (s *GsettingsInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/hardware_observe_test.go snapd-2.42.1/interfaces/builtin/hardware_observe_test.go
--- snapd-2.40/interfaces/builtin/hardware_observe_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/hardware_observe_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -69,13 +69,6 @@
func (s *HardwareObserveInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "hardware-observe",
- Interface: "hardware-observe",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "hardware-observe slots are reserved for the core snap")
}
func (s *HardwareObserveInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/hardware_random_control_test.go snapd-2.42.1/interfaces/builtin/hardware_random_control_test.go
--- snapd-2.40/interfaces/builtin/hardware_random_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/hardware_random_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *HardwareRandomControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "hardware-random-control",
- Interface: "hardware-random-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "hardware-random-control slots are reserved for the core snap")
}
func (s *HardwareRandomControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/hardware_random_observe_test.go snapd-2.42.1/interfaces/builtin/hardware_random_observe_test.go
--- snapd-2.40/interfaces/builtin/hardware_random_observe_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/hardware_random_observe_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *HardwareRandomObserveInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "hardware-random-observe",
- Interface: "hardware-random-observe",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "hardware-random-observe slots are reserved for the core snap")
}
func (s *HardwareRandomObserveInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/hidraw.go snapd-2.42.1/interfaces/builtin/hidraw.go
--- snapd-2.40/interfaces/builtin/hidraw.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/hidraw.go 2019-10-30 12:17:43.000000000 +0000
@@ -72,10 +72,6 @@
// BeforePrepareSlot checks validity of the defined slot
func (iface *hidrawInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
- if err := sanitizeSlotReservedForOSOrGadget(iface, slot); err != nil {
- return err
- }
-
// Check slot has a path attribute identify hidraw device
path, ok := slot.Attrs["path"].(string)
if !ok || path == "" {
diff -Nru snapd-2.40/interfaces/builtin/home_test.go snapd-2.42.1/interfaces/builtin/home_test.go
--- snapd-2.40/interfaces/builtin/home_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/home_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *HomeInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "home",
- Interface: "home",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "home slots are reserved for the core snap")
}
func (s *HomeInterfaceSuite) TestSanitizePlugNoAttrib(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/hostname_control_test.go snapd-2.42.1/interfaces/builtin/hostname_control_test.go
--- snapd-2.40/interfaces/builtin/hostname_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/hostname_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *HostnameControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "hostname-control",
- Interface: "hostname-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "hostname-control slots are reserved for the core snap")
}
func (s *HostnameControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/i2c.go snapd-2.42.1/interfaces/builtin/i2c.go
--- snapd-2.40/interfaces/builtin/i2c.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/i2c.go 2019-10-30 12:17:43.000000000 +0000
@@ -84,10 +84,6 @@
// Check validity of the defined slot
func (iface *i2cInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
- if err := sanitizeSlotReservedForOSOrGadget(iface, slot); err != nil {
- return err
- }
-
sysfsName, ok := slot.Attrs["sysfs-name"].(string)
if ok {
if !i2cValidSysfsName.MatchString(sysfsName) {
diff -Nru snapd-2.40/interfaces/builtin/iio.go snapd-2.42.1/interfaces/builtin/iio.go
--- snapd-2.40/interfaces/builtin/iio.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/iio.go 2019-10-30 12:17:43.000000000 +0000
@@ -78,10 +78,6 @@
// Check validity of the defined slot
func (iface *iioInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
- if err := sanitizeSlotReservedForOSOrGadget(iface, slot); err != nil {
- return err
- }
-
// Validate the path
path, ok := slot.Attrs["path"].(string)
if !ok || path == "" {
diff -Nru snapd-2.40/interfaces/builtin/intel_mei_test.go snapd-2.42.1/interfaces/builtin/intel_mei_test.go
--- snapd-2.40/interfaces/builtin/intel_mei_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/intel_mei_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *IntelMEISuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "intel-mei",
- Interface: "intel-mei",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "intel-mei slots are reserved for the core snap")
}
func (s *IntelMEISuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/io_ports_control_test.go snapd-2.42.1/interfaces/builtin/io_ports_control_test.go
--- snapd-2.40/interfaces/builtin/io_ports_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/io_ports_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *ioPortsControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "io-ports-control",
- Interface: "io-ports-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "io-ports-control slots are reserved for the core snap")
}
func (s *ioPortsControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/jack1_test.go snapd-2.42.1/interfaces/builtin/jack1_test.go
--- snapd-2.40/interfaces/builtin/jack1_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/jack1_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,12 +67,6 @@
func (s *jack1InterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "jack1",
- Interface: "jack1",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, "jack1 slots are reserved for the core snap")
}
func (s *jack1InterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/joystick_test.go snapd-2.42.1/interfaces/builtin/joystick_test.go
--- snapd-2.40/interfaces/builtin/joystick_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/joystick_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *JoystickInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "joystick",
- Interface: "joystick",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "joystick slots are reserved for the core snap")
}
func (s *JoystickInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/juju_client_observe_test.go snapd-2.42.1/interfaces/builtin/juju_client_observe_test.go
--- snapd-2.40/interfaces/builtin/juju_client_observe_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/juju_client_observe_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -66,13 +66,6 @@
func (s *JujuClientObserveInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "juju-client-observe",
- Interface: "juju-client-observe",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "juju-client-observe slots are reserved for the core snap")
}
func (s *JujuClientObserveInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/kernel_module_control_test.go snapd-2.42.1/interfaces/builtin/kernel_module_control_test.go
--- snapd-2.40/interfaces/builtin/kernel_module_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/kernel_module_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *KernelModuleControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "kernel-module-control",
- Interface: "kernel-module-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "kernel-module-control slots are reserved for the core snap")
}
func (s *KernelModuleControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/kernel_module_observe_test.go snapd-2.42.1/interfaces/builtin/kernel_module_observe_test.go
--- snapd-2.40/interfaces/builtin/kernel_module_observe_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/kernel_module_observe_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -64,13 +64,6 @@
func (s *KernelModuleObserveInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "kernel-module-observe",
- Interface: "kernel-module-observe",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "kernel-module-observe slots are reserved for the core snap")
}
func (s *KernelModuleObserveInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/kubernetes_support.go snapd-2.42.1/interfaces/builtin/kubernetes_support.go
--- snapd-2.40/interfaces/builtin/kubernetes_support.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/kubernetes_support.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,17 +67,25 @@
# Common rules for kubernetes use of systemd_run
#include
- /usr/bin/systemd-run r,
+ /{,usr/}bin/systemd-run rm,
owner @{PROC}/@{pid}/stat r,
owner @{PROC}/@{pid}/environ r,
@{PROC}/cmdline r,
+ @{PROC}/sys/kernel/osrelease r,
+ @{PROC}/1/sched r,
# setsockopt()
capability net_admin,
+ # ptrace 'trace' is coarse and not required for using the systemd private
+ # socket, and while the child profile omits 'capability sys_ptrace', skip
+ # for now since it isn't strictly required.
+ ptrace read peer=unconfined,
+ deny ptrace trace peer=unconfined,
/run/systemd/private rw,
- /bin/true ixr,
- ptrace trace peer=unconfined,
+
+ /{,usr/}bin/true ixr,
+ @{INSTALL_DIR}/{@{SNAP_NAME},@{SNAP_INSTANCE_NAME}}/@{SNAP_REVISION}/{,usr/}bin/true ixr,
###KUBERNETES_SUPPORT_SYSTEMD_RUN###
}
`
@@ -88,6 +96,12 @@
# Ideally this would be snap-specific
/run/dockershim.sock rw,
+# Ideally this would be snap-specific (it could if the control plane was a
+# snap), but in deployments where the control plane is not a snap, it will tell
+# flannel to use this path.
+/run/flannel/{,**} rw,
+/run/flannel/** k,
+
# allow managing pods' cgroups
/sys/fs/cgroup/*/kubepods/{,**} rw,
@@ -122,7 +136,8 @@
mount /var/snap/@{SNAP_NAME}/common/{,**} -> /var/snap/@{SNAP_NAME}/common/{,**},
mount options=(rw, rshared) -> /var/snap/@{SNAP_NAME}/common/{,**},
-/bin/umount ixr,
+/{,usr/}bin/mount ixr,
+/{,usr/}bin/umount ixr,
deny /run/mount/utab rw,
umount /var/snap/@{SNAP_INSTANCE_NAME}/common/**,
`
@@ -130,7 +145,7 @@
const kubernetesSupportConnectedPlugAppArmorKubeletSystemdRun = `
# kubelet mount rules
capability sys_admin,
- /bin/mount ixr,
+ /{,usr/}bin/mount ixr,
mount fstype="tmpfs" tmpfs -> /var/snap/@{SNAP_INSTANCE_NAME}/common/**,
deny /run/mount/utab rw,
`
@@ -178,11 +193,6 @@
commonInterface
}
-func (iface *kubernetesSupportInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
- iface.commonInterface.BeforePrepareSlot(slot)
- return sanitizeSlotReservedForOS(iface, slot)
-}
-
func k8sFlavor(plug *interfaces.ConnectedPlug) string {
var flavor string
_ = plug.Attr("flavor", &flavor)
@@ -197,14 +207,14 @@
case "kubelet":
systemd_run_extra = kubernetesSupportConnectedPlugAppArmorKubeletSystemdRun
snippet += kubernetesSupportConnectedPlugAppArmorKubelet
- spec.UsesPtraceTrace()
+ spec.SetUsesPtraceTrace()
case "kubeproxy":
snippet += kubernetesSupportConnectedPlugAppArmorKubeproxy
default:
systemd_run_extra = kubernetesSupportConnectedPlugAppArmorKubeletSystemdRun
snippet += kubernetesSupportConnectedPlugAppArmorKubelet
snippet += kubernetesSupportConnectedPlugAppArmorKubeproxy
- spec.UsesPtraceTrace()
+ spec.SetUsesPtraceTrace()
}
old := "###KUBERNETES_SUPPORT_SYSTEMD_RUN###"
diff -Nru snapd-2.40/interfaces/builtin/kubernetes_support_test.go snapd-2.42.1/interfaces/builtin/kubernetes_support_test.go
--- snapd-2.40/interfaces/builtin/kubernetes_support_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/kubernetes_support_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -102,13 +102,6 @@
func (s *KubernetesSupportInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "kubernetes-support",
- Interface: "kubernetes-support",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "kubernetes-support slots are reserved for the core snap")
}
func (s *KubernetesSupportInterfaceSuite) TestSanitizePlug(c *C) {
@@ -163,6 +156,7 @@
c.Check(spec.SnippetForTag("snap.kubernetes-support.default"), testutil.Contains, "# Allow running as the kubeproxy service\n")
c.Check(spec.SnippetForTag("snap.kubernetes-support.default"), testutil.Contains, "# Common rules for kubernetes use of systemd_run\n")
c.Check(spec.SnippetForTag("snap.kubernetes-support.default"), testutil.Contains, "# kubelet mount rules\n")
+ c.Check(spec.UsesPtraceTrace(), Equals, true)
// kubeproxy should have only its rules
spec = &apparmor.Specification{}
@@ -174,6 +168,7 @@
c.Check(spec.SnippetForTag("snap.kubernetes-support.kubeproxy"), Not(testutil.Contains), "# Allow running as the kubelet service\n")
c.Check(spec.SnippetForTag("snap.kubernetes-support.kubeproxy"), testutil.Contains, "# Common rules for kubernetes use of systemd_run\n")
c.Check(spec.SnippetForTag("snap.kubernetes-support.kubeproxy"), Not(testutil.Contains), "# kubelet mount rules\n")
+ c.Check(spec.UsesPtraceTrace(), Equals, false)
// kubelet should have only its rules
spec = &apparmor.Specification{}
@@ -185,6 +180,7 @@
c.Check(spec.SnippetForTag("snap.kubernetes-support.kubelet"), Not(testutil.Contains), "# Allow running as the kubeproxy service\n")
c.Check(spec.SnippetForTag("snap.kubernetes-support.kubelet"), testutil.Contains, "# Common rules for kubernetes use of systemd_run\n")
c.Check(spec.SnippetForTag("snap.kubernetes-support.kubelet"), testutil.Contains, "# kubelet mount rules\n")
+ c.Check(spec.UsesPtraceTrace(), Equals, true)
}
func (s *KubernetesSupportInterfaceSuite) TestSecCompConnectedPlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/kvm_test.go snapd-2.42.1/interfaces/builtin/kvm_test.go
--- snapd-2.40/interfaces/builtin/kvm_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/kvm_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -93,13 +93,6 @@
func (s *kvmInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "kvm",
- Interface: "kvm",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "kvm slots are reserved for the core snap")
}
func (s *kvmInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/libvirt_test.go snapd-2.42.1/interfaces/builtin/libvirt_test.go
--- snapd-2.40/interfaces/builtin/libvirt_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/libvirt_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -52,10 +52,6 @@
c.Assert(s.iface.Name(), Equals, "libvirt")
}
-func (s *LibvirtInterfaceSuite) TestSanitizeSlot(c *C) {
- c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), ErrorMatches, ".*libvirt slots are reserved for the core snap.*")
-}
-
func (s *LibvirtInterfaceSuite) TestSanitizePlug(c *C) {
c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil)
}
diff -Nru snapd-2.40/interfaces/builtin/locale_control_test.go snapd-2.42.1/interfaces/builtin/locale_control_test.go
--- snapd-2.40/interfaces/builtin/locale_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/locale_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *LocaleControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "locale-control",
- Interface: "locale-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "locale-control slots are reserved for the core snap")
}
func (s *LocaleControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/login_session_control_test.go snapd-2.42.1/interfaces/builtin/login_session_control_test.go
--- snapd-2.40/interfaces/builtin/login_session_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/login_session_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *loginSessionControlSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "login-session-control",
- Interface: "login-session-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "login-session-control slots are reserved for the core snap")
}
func (s *loginSessionControlSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/log_observe_test.go snapd-2.42.1/interfaces/builtin/log_observe_test.go
--- snapd-2.40/interfaces/builtin/log_observe_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/log_observe_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *LogObserveInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "log-observe",
- Interface: "log-observe",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "log-observe slots are reserved for the core snap")
}
func (s *LogObserveInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/lxd_support_test.go snapd-2.42.1/interfaces/builtin/lxd_support_test.go
--- snapd-2.40/interfaces/builtin/lxd_support_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/lxd_support_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,14 +67,6 @@
func (s *LxdSupportInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "lxd-support",
- Interface: "lxd-support",
- }
-
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "lxd-support slots are reserved for the core snap")
}
func (s *LxdSupportInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/mount_observe_test.go snapd-2.42.1/interfaces/builtin/mount_observe_test.go
--- snapd-2.40/interfaces/builtin/mount_observe_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/mount_observe_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,12 +67,6 @@
func (s *MountObserveInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "mount-observe",
- Interface: "mount-observe",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, "mount-observe slots are reserved for the core snap")
}
func (s *MountObserveInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/netlink_audit_test.go snapd-2.42.1/interfaces/builtin/netlink_audit_test.go
--- snapd-2.40/interfaces/builtin/netlink_audit_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/netlink_audit_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -69,13 +69,6 @@
func (s *NetlinkAuditInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "netlink-audit",
- Interface: "netlink-audit",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "netlink-audit slots are reserved for the core snap")
}
func (s *NetlinkAuditInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/netlink_connector_test.go snapd-2.42.1/interfaces/builtin/netlink_connector_test.go
--- snapd-2.40/interfaces/builtin/netlink_connector_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/netlink_connector_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *NetlinkConnectorInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "netlink-connector",
- Interface: "netlink-connector",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "netlink-connector slots are reserved for the core snap")
}
func (s *NetlinkConnectorInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/network_bind_test.go snapd-2.42.1/interfaces/builtin/network_bind_test.go
--- snapd-2.40/interfaces/builtin/network_bind_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/network_bind_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *NetworkBindInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "network-bind",
- Interface: "network-bind",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "network-bind slots are reserved for the core snap")
}
func (s *NetworkBindInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/network_control.go snapd-2.42.1/interfaces/builtin/network_control.go
--- snapd-2.40/interfaces/builtin/network_control.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/network_control.go 2019-10-30 12:17:43.000000000 +0000
@@ -161,7 +161,7 @@
# resolvconf
/sbin/resolvconf ixr,
-/run/resolvconf/{,**} r,
+/run/resolvconf/{,**} rk,
/run/resolvconf/** w,
/etc/resolvconf/{,**} r,
/lib/resolvconf/* ix,
diff -Nru snapd-2.40/interfaces/builtin/network_control_test.go snapd-2.42.1/interfaces/builtin/network_control_test.go
--- snapd-2.40/interfaces/builtin/network_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/network_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *NetworkControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "network-control",
- Interface: "network-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "network-control slots are reserved for the core snap")
}
func (s *NetworkControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/network_manager.go snapd-2.42.1/interfaces/builtin/network_manager.go
--- snapd-2.40/interfaces/builtin/network_manager.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/network_manager.go 2019-10-30 12:17:43.000000000 +0000
@@ -107,7 +107,7 @@
# Needed to use resolvconf from core
/sbin/resolvconf ixr,
-/run/resolvconf/{,**} r,
+/run/resolvconf/{,**} rk,
/run/resolvconf/** w,
/etc/resolvconf/{,**} r,
/lib/resolvconf/* ix,
@@ -257,6 +257,15 @@
path=/org/freedesktop/NetworkManager{,/**}
peer=(label=###PLUG_SECURITY_TAGS###),
+# Later versions of NetworkManager implement org.freedesktop.DBus.ObjectManager
+# for clients to easily obtain all (and be alerted to added/removed) objects
+# from the service.
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop
+ interface=org.freedesktop.DBus.ObjectManager
+ peer=(label=###PLUG_SECURITY_TAGS###),
+
# Explicitly deny ptrace to silence noisy denials. These denials happen when NM
# tries to access /proc//stat. What apparmor prevents is showing
# internal process addresses that live in that file, but that has no adverse
@@ -276,6 +285,31 @@
bus=system
path=/org/freedesktop/NetworkManager{,/**}
peer=(label=###SLOT_SECURITY_TAGS###),
+
+# NM implements org.freedesktop.DBus.ObjectManager too
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop
+ interface=org.freedesktop.DBus.ObjectManager
+ peer=(label=###SLOT_SECURITY_TAGS###),
+`
+
+const networkManagerConnectedPlugIntrospectionSnippet = `
+# Allow us to introspect the network-manager providing snap
+dbus (send)
+ bus=system
+ interface="org.freedesktop.DBus.Introspectable"
+ member="Introspect"
+ peer=(label=###SLOT_SECURITY_TAGS###),
+`
+
+const networkManagerConnectedSlotIntrospectionSnippet = `
+# Allow plugs to introspect us
+dbus (receive)
+ bus=system
+ interface="org.freedesktop.DBus.Introspectable"
+ member="Introspect"
+ peer=(label=###PLUG_SECURITY_TAGS###),
`
const networkManagerConnectedPlugSecComp = `
@@ -465,6 +499,11 @@
}
snippet := strings.Replace(networkManagerConnectedPlugAppArmor, old, new, -1)
spec.AddSnippet(snippet)
+ if !release.OnClassic {
+ // See https://bugs.launchpad.net/snapd/+bug/1849291 for details.
+ snippet := strings.Replace(networkManagerConnectedPlugIntrospectionSnippet, old, new, -1)
+ spec.AddSnippet(snippet)
+ }
return nil
}
@@ -473,6 +512,11 @@
new := plugAppLabelExpr(plug)
snippet := strings.Replace(networkManagerConnectedSlotAppArmor, old, new, -1)
spec.AddSnippet(snippet)
+ if !release.OnClassic {
+ // See https://bugs.launchpad.net/snapd/+bug/1849291 for details.
+ snippet := strings.Replace(networkManagerConnectedSlotIntrospectionSnippet, old, new, -1)
+ spec.AddSnippet(snippet)
+ }
return nil
}
diff -Nru snapd-2.40/interfaces/builtin/network_manager_test.go snapd-2.42.1/interfaces/builtin/network_manager_test.go
--- snapd-2.40/interfaces/builtin/network_manager_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/network_manager_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -158,6 +158,42 @@
c.Assert(apparmorSpec.SnippetForTag("snap.network-manager-client.nmcli"), testutil.Contains, "peer=(label=unconfined),")
}
+func (s *NetworkManagerInterfaceSuite) TestConnectedPlugIntrospectionOnCore(c *C) {
+ release.OnClassic = false
+ apparmorSpec := &apparmor.Specification{}
+ err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot)
+ c.Assert(err, IsNil)
+ c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.network-manager-client.nmcli"})
+ c.Assert(apparmorSpec.SnippetForTag("snap.network-manager-client.nmcli"), testutil.Contains, "Allow us to introspect the network-manager providing snap")
+}
+
+func (s *NetworkManagerInterfaceSuite) TestConnectedSlotIntrospectionOnCore(c *C) {
+ release.OnClassic = false
+ apparmorSpec := &apparmor.Specification{}
+ err := apparmorSpec.AddConnectedSlot(s.iface, s.plug, s.slot)
+ c.Assert(err, IsNil)
+ c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.network-manager.nm"})
+ c.Assert(apparmorSpec.SnippetForTag("snap.network-manager.nm"), testutil.Contains, "# Allow plugs to introspect us")
+}
+
+func (s *NetworkManagerInterfaceSuite) TestConnectedPlugIntrospectionOnClassic(c *C) {
+ release.OnClassic = true
+ apparmorSpec := &apparmor.Specification{}
+ err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot)
+ c.Assert(err, IsNil)
+ c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.network-manager-client.nmcli"})
+ c.Assert(apparmorSpec.SnippetForTag("snap.network-manager-client.nmcli"), Not(testutil.Contains), "Allow us to introspect the network-manager providing snap")
+}
+
+func (s *NetworkManagerInterfaceSuite) TestConnectedSlotIntrospectionOnClassic(c *C) {
+ release.OnClassic = true
+ apparmorSpec := &apparmor.Specification{}
+ err := apparmorSpec.AddConnectedSlot(s.iface, s.plug, s.slot)
+ c.Assert(err, IsNil)
+ c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.network-manager.nm"})
+ c.Assert(apparmorSpec.SnippetForTag("snap.network-manager.nm"), Not(testutil.Contains), "# Allow plugs to introspect us")
+}
+
func (s *NetworkManagerInterfaceSuite) TestConnectedSlotSnippetAppArmor(c *C) {
apparmorSpec := &apparmor.Specification{}
err := apparmorSpec.AddConnectedSlot(s.iface, s.plug, s.slot)
diff -Nru snapd-2.40/interfaces/builtin/network_observe_test.go snapd-2.42.1/interfaces/builtin/network_observe_test.go
--- snapd-2.40/interfaces/builtin/network_observe_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/network_observe_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -69,13 +69,6 @@
func (s *NetworkObserveInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "network-observe",
- Interface: "network-observe",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "network-observe slots are reserved for the core snap")
}
func (s *NetworkObserveInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/network_setup_control.go snapd-2.42.1/interfaces/builtin/network_setup_control.go
--- snapd-2.40/interfaces/builtin/network_setup_control.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/network_setup_control.go 2019-10-30 12:17:43.000000000 +0000
@@ -45,6 +45,17 @@
/run/udev/rules.d/ rw, # needed for cloud-init
/run/udev/rules.d/[0-9]*-netplan-* rw,
+
+#include
+
+# Allow use of NetPlan Apply API, used to apply network configuration
+dbus (send)
+ bus=system
+ interface=io.netplan.Netplan
+ path=/io/netplan/Netplan
+ member=Apply
+ peer=(label=unconfined),
+
`
func init() {
diff -Nru snapd-2.40/interfaces/builtin/network_setup_control_test.go snapd-2.42.1/interfaces/builtin/network_setup_control_test.go
--- snapd-2.40/interfaces/builtin/network_setup_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/network_setup_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *NetworkSetupControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "network-setup-control",
- Interface: "network-setup-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "network-setup-control slots are reserved for the core snap")
}
func (s *NetworkSetupControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/network_setup_observe_test.go snapd-2.42.1/interfaces/builtin/network_setup_observe_test.go
--- snapd-2.40/interfaces/builtin/network_setup_observe_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/network_setup_observe_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *NetworkSetupObserveInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "network-setup-observe",
- Interface: "network-setup-observe",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "network-setup-observe slots are reserved for the core snap")
}
func (s *NetworkSetupObserveInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/network_test.go snapd-2.42.1/interfaces/builtin/network_test.go
--- snapd-2.40/interfaces/builtin/network_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/network_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -69,13 +69,6 @@
func (s *NetworkInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "network",
- Interface: "network",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "network slots are reserved for the core snap")
}
func (s *NetworkInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/opengl.go snapd-2.42.1/interfaces/builtin/opengl.go
--- snapd-2.40/interfaces/builtin/opengl.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/opengl.go 2019-10-30 12:17:43.000000000 +0000
@@ -83,6 +83,10 @@
/dev/nvhost-* rw,
/dev/nvmap rw,
+# Tegra display driver
+/dev/tegra_dc_ctrl rw,
+/dev/tegra_dc_[0-9]* rw,
+
# OpenCL ICD files
/etc/OpenCL/vendors/ r,
/etc/OpenCL/vendors/** r,
@@ -91,10 +95,11 @@
@{PROC}/driver/prl_vtg rw,
# /sys/devices
-/sys/devices/pci[0-9a-f]*/**/config r,
-/sys/devices/pci[0-9a-f]*/**/revision r,
-/sys/devices/pci[0-9a-f]*/**/{,subsystem_}device r,
-/sys/devices/pci[0-9a-f]*/**/{,subsystem_}vendor r,
+/sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/config r,
+/sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/revision r,
+/sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/{,subsystem_}class r,
+/sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/{,subsystem_}device r,
+/sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/{,subsystem_}vendor r,
/sys/devices/**/drm{,_dp_aux_dev}/** r,
# FIXME: this is an information leak and snapd should instead query udev for
@@ -121,6 +126,8 @@
`KERNEL=="renderD[0-9]*"`,
`KERNEL=="nvhost-*"`,
`KERNEL=="nvmap"`,
+ `KERNEL=="tegra_dc_ctrl"`,
+ `KERNEL=="tegra_dc_[0-9]*"`,
}
func init() {
diff -Nru snapd-2.40/interfaces/builtin/opengl_test.go snapd-2.42.1/interfaces/builtin/opengl_test.go
--- snapd-2.40/interfaces/builtin/opengl_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/opengl_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,14 +67,6 @@
func (s *OpenglInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
-
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "opengl",
- Interface: "opengl",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "opengl slots are reserved for the core snap")
}
func (s *OpenglInterfaceSuite) TestSanitizePlug(c *C) {
@@ -92,7 +84,7 @@
func (s *OpenglInterfaceSuite) TestUDevSpec(c *C) {
spec := &udev.Specification{}
c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
- c.Assert(spec.Snippets(), HasLen, 6)
+ c.Assert(spec.Snippets(), HasLen, 8)
c.Assert(spec.Snippets(), testutil.Contains, `# opengl
SUBSYSTEM=="drm", KERNEL=="card[0-9]*", TAG+="snap_consumer_app"`)
c.Assert(spec.Snippets(), testutil.Contains, `# opengl
@@ -101,6 +93,10 @@
KERNEL=="nvhost-*", TAG+="snap_consumer_app"`)
c.Assert(spec.Snippets(), testutil.Contains, `# opengl
KERNEL=="nvmap", TAG+="snap_consumer_app"`)
+ c.Assert(spec.Snippets(), testutil.Contains, `# opengl
+KERNEL=="tegra_dc_ctrl", TAG+="snap_consumer_app"`)
+ c.Assert(spec.Snippets(), testutil.Contains, `# opengl
+KERNEL=="tegra_dc_[0-9]*", TAG+="snap_consumer_app"`)
c.Assert(spec.Snippets(), testutil.Contains, `TAG=="snap_consumer_app", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_consumer_app $devpath $major:$minor"`)
}
diff -Nru snapd-2.40/interfaces/builtin/openvswitch_support_test.go snapd-2.42.1/interfaces/builtin/openvswitch_support_test.go
--- snapd-2.40/interfaces/builtin/openvswitch_support_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/openvswitch_support_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *OpenvSwitchSupportInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "openvswitch-support",
- Interface: "openvswitch-support",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "openvswitch-support slots are reserved for the core snap")
}
func (s *OpenvSwitchSupportInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/openvswitch_test.go snapd-2.42.1/interfaces/builtin/openvswitch_test.go
--- snapd-2.40/interfaces/builtin/openvswitch_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/openvswitch_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *OpenvSwitchInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "openvswitch",
- Interface: "openvswitch",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "openvswitch slots are reserved for the core snap")
}
func (s *OpenvSwitchInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/optical_drive.go snapd-2.42.1/interfaces/builtin/optical_drive.go
--- snapd-2.40/interfaces/builtin/optical_drive.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/optical_drive.go 2019-10-30 12:17:43.000000000 +0000
@@ -46,6 +46,9 @@
# Allow read access to optical drives
/dev/sr[0-9]* r,
/dev/scd[0-9]* r,
+# allow all generic scsi devices here and use the device cgroup to
+# differentiate optical drives
+/dev/sg[0-9]* r,
@{PROC}/sys/dev/cdrom/info r,
/run/udev/data/b11:[0-9]* r,
`
@@ -53,6 +56,10 @@
var opticalDriveConnectedPlugUDev = []string{
`KERNEL=="sr[0-9]*"`,
`KERNEL=="scd[0-9]*"`,
+ // ATTRS{type} below takes scsi peripheral device types.
+ // Type 4 is 'Write-once device'; type 5 is 'CD/DVD-ROM device'
+ // ref: https://en.wikipedia.org/wiki/SCSI_Peripheral_Device_Type
+ `SUBSYSTEM=="scsi_generic", SUBSYSTEMS=="scsi", ATTRS{type}=="4|5"`,
}
// opticalDriveInterface is the type for optical drive interfaces.
@@ -87,6 +94,7 @@
spec.AddSnippet("# Allow write access to optical drives")
spec.AddSnippet("/dev/sr[0-9]* w,")
spec.AddSnippet("/dev/scd[0-9]* w,")
+ spec.AddSnippet("/dev/sg[0-9]* w,")
}
return nil
}
diff -Nru snapd-2.40/interfaces/builtin/optical_drive_test.go snapd-2.42.1/interfaces/builtin/optical_drive_test.go
--- snapd-2.40/interfaces/builtin/optical_drive_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/optical_drive_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -93,13 +93,6 @@
func (s *OpticalDriveInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "optical-drive",
- Interface: "optical-drive",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "optical-drive slots are reserved for the core snap")
}
func (s *OpticalDriveInterfaceSuite) TestSanitizePlug(c *C) {
@@ -152,7 +145,7 @@
c.Assert(spec.AddConnectedPlug(s.iface, s.testPlugDefault, s.slot), IsNil)
c.Assert(spec.AddConnectedPlug(s.iface, s.testPlugReadonly, s.slot), IsNil)
c.Assert(spec.AddConnectedPlug(s.iface, s.testPlugWritable, s.slot), IsNil)
- c.Assert(spec.Snippets(), HasLen, 9) // three rules multiplied by three apps
+ c.Assert(spec.Snippets(), HasLen, 12) // four rules multiplied by three apps
c.Assert(spec.Snippets(), testutil.Contains, `# optical-drive
KERNEL=="sr[0-9]*", TAG+="snap_consumer_app"`)
c.Assert(spec.Snippets(), testutil.Contains, `TAG=="snap_consumer_app", RUN+="/usr/lib/snapd/snap-device-helper $env{ACTION} snap_consumer_app $devpath $major:$minor"`)
diff -Nru snapd-2.40/interfaces/builtin/packagekit_control.go snapd-2.42.1/interfaces/builtin/packagekit_control.go
--- snapd-2.40/interfaces/builtin/packagekit_control.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/packagekit_control.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,113 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package builtin
+
+const packageKitControlSummary = `allows control of the PackageKit service`
+
+const packageKitControlBaseDeclarationPlugs = `
+ packagekit-control:
+ allow-installation: false
+ deny-auto-connection: true
+`
+
+const packageKitControlBaseDeclarationSlots = `
+ packagekit-control:
+ allow-installation:
+ slot-snap-type:
+ - core
+ deny-auto-connection: true
+`
+
+const packageKitControlConnectedPlugAppArmor = `
+# Description: Allow access to PackageKit service which gives
+# privileged access to native package management on the system
+
+#include
+
+# Allow communication with the main PackageKit end point.
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/PackageKit
+ interface=org.freedesktop.PackageKit
+ peer=(label=unconfined),
+dbus (receive, send)
+ bus=system
+ path=/org/freedesktop/PackageKit
+ interface=org.freedesktop.PackageKit.Offline
+ peer=(label=unconfined),
+dbus (send)
+ bus=system
+ path=/org/freedesktop/PackageKit
+ interface=org.freedesktop.DBus.Properties
+ member=Get{,All}
+ peer=(label=unconfined),
+dbus (receive)
+ bus=system
+ path=/org/freedesktop/PackageKit
+ interface=org.freedesktop.DBus.Properties
+ member=PropertiesChanged
+ peer=(label=unconfined),
+dbus (send)
+ bus=system
+ path=/org/freedesktop/PackageKit
+ interface=org.freedesktop.DBus.Introspectable
+ member=Introspect
+ peer=(label=unconfined),
+
+# Allow communication with PackageKit transactions. Transactions are
+# exported with random object paths that currently take the form
+# "/{number}_{hexstring}". If PackageKit (or a reimplementation of
+# packagekitd) changes this, then these rules will need to change too.
+dbus (receive, send)
+ bus=system
+ path=/[0-9]*_[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]
+ interface=org.freedesktop.PackageKit.Transaction
+ peer=(label=unconfined),
+dbus (send)
+ bus=system
+ path=/[0-9]*_[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]
+ interface=org.freedesktop.DBus.Properties
+ member=Get{,All}
+ peer=(label=unconfined),
+dbus (receive)
+ bus=system
+ path=/[0-9]*_[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]
+ interface=org.freedesktop.DBus.Properties
+ member=PropertiesChanged
+ peer=(label=unconfined),
+dbus (send)
+ bus=system
+ path=/[0-9]*_[0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f][0-9a-f]
+ interface=org.freedesktop.DBus.Introspectable
+ member=Introspect
+ peer=(label=unconfined),
+`
+
+func init() {
+ registerIface(&commonInterface{
+ name: "packagekit-control",
+ summary: packageKitControlSummary,
+ implicitOnClassic: true,
+ reservedForOS: true,
+ baseDeclarationPlugs: packageKitControlBaseDeclarationPlugs,
+ baseDeclarationSlots: packageKitControlBaseDeclarationSlots,
+ connectedPlugAppArmor: packageKitControlConnectedPlugAppArmor,
+ })
+}
diff -Nru snapd-2.40/interfaces/builtin/packagekit_control_test.go snapd-2.42.1/interfaces/builtin/packagekit_control_test.go
--- snapd-2.40/interfaces/builtin/packagekit_control_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/packagekit_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,92 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package builtin_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/interfaces"
+ "github.com/snapcore/snapd/interfaces/apparmor"
+ "github.com/snapcore/snapd/interfaces/builtin"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type PackageKitControlInterfaceSuite struct {
+ iface interfaces.Interface
+ slot *interfaces.ConnectedSlot
+ slotInfo *snap.SlotInfo
+ plug *interfaces.ConnectedPlug
+ plugInfo *snap.PlugInfo
+}
+
+var _ = Suite(&PackageKitControlInterfaceSuite{
+ iface: builtin.MustInterface("packagekit-control"),
+})
+
+func (s *PackageKitControlInterfaceSuite) SetUpTest(c *C) {
+ const coreYaml = `name: core
+version: 0
+type: os
+slots:
+ packagekit-control:
+ interface: packagekit-control
+`
+ s.slot, s.slotInfo = MockConnectedSlot(c, coreYaml, nil, "packagekit-control")
+
+ var consumerYaml = `name: consumer
+version: 0
+apps:
+ app:
+ plugs: [packagekit-control]
+`
+ s.plug, s.plugInfo = MockConnectedPlug(c, consumerYaml, nil, "packagekit-control")
+}
+
+func (s *PackageKitControlInterfaceSuite) TestName(c *C) {
+ c.Assert(s.iface.Name(), Equals, "packagekit-control")
+}
+
+func (s *PackageKitControlInterfaceSuite) TestSanitize(c *C) {
+ c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil)
+ c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
+}
+
+func (s *PackageKitControlInterfaceSuite) TestAppArmorConnectedPlug(c *C) {
+ spec := &apparmor.Specification{}
+ c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil)
+ c.Assert(spec.SecurityTags(), HasLen, 1)
+ c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `interface=org.freedesktop.PackageKit`)
+ c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `interface=org.freedesktop.PackageKit.Offline`)
+ c.Check(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `interface=org.freedesktop.PackageKit.Transaction`)
+}
+
+func (s *PackageKitControlInterfaceSuite) TestStaticInfo(c *C) {
+ si := interfaces.StaticInfoOf(s.iface)
+ c.Check(si.ImplicitOnCore, Equals, false)
+ c.Check(si.ImplicitOnClassic, Equals, true)
+ c.Check(si.Summary, Equals, "allows control of the PackageKit service")
+ c.Check(si.BaseDeclarationPlugs, testutil.Contains, "packagekit-control")
+ c.Check(si.BaseDeclarationSlots, testutil.Contains, "packagekit-control")
+}
+
+func (s *PackageKitControlInterfaceSuite) TestInterfaces(c *C) {
+ c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface)
+}
diff -Nru snapd-2.40/interfaces/builtin/password_manager_service_test.go snapd-2.42.1/interfaces/builtin/password_manager_service_test.go
--- snapd-2.40/interfaces/builtin/password_manager_service_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/password_manager_service_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *passwordManagerServiceInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "password-manager-service",
- Interface: "password-manager-service",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "password-manager-service slots are reserved for the core snap")
}
func (s *passwordManagerServiceInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/personal_files_test.go snapd-2.42.1/interfaces/builtin/personal_files_test.go
--- snapd-2.40/interfaces/builtin/personal_files_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/personal_files_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -89,13 +89,6 @@
func (s *personalFilesInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "personal-files",
- Interface: "personal-files",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "personal-files slots are reserved for the core snap")
}
func (s *personalFilesInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/physical_memory_control_test.go snapd-2.42.1/interfaces/builtin/physical_memory_control_test.go
--- snapd-2.40/interfaces/builtin/physical_memory_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/physical_memory_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *PhysicalMemoryControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "physical-memory-control",
- Interface: "physical-memory-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "physical-memory-control slots are reserved for the core snap")
}
func (s *PhysicalMemoryControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/physical_memory_observe_test.go snapd-2.42.1/interfaces/builtin/physical_memory_observe_test.go
--- snapd-2.40/interfaces/builtin/physical_memory_observe_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/physical_memory_observe_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *PhysicalMemoryObserveInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "physical-memory-observe",
- Interface: "physical-memory-observe",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "physical-memory-observe slots are reserved for the core snap")
}
func (s *PhysicalMemoryObserveInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/ppp_test.go snapd-2.42.1/interfaces/builtin/ppp_test.go
--- snapd-2.40/interfaces/builtin/ppp_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/ppp_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,14 +68,6 @@
func (s *PppInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "ppp",
- Interface: "ppp",
- }
-
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "ppp slots are reserved for the core snap")
}
func (s *PppInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/process_control.go snapd-2.42.1/interfaces/builtin/process_control.go
--- snapd-2.40/interfaces/builtin/process_control.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/process_control.go 2019-10-30 12:17:43.000000000 +0000
@@ -42,6 +42,7 @@
capability sys_resource,
capability sys_nice,
+capability kill,
signal (send),
/{,usr/}bin/kill ixr,
/{,usr/}bin/pkill ixr,
diff -Nru snapd-2.40/interfaces/builtin/process_control_test.go snapd-2.42.1/interfaces/builtin/process_control_test.go
--- snapd-2.40/interfaces/builtin/process_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/process_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -69,13 +69,6 @@
func (s *ProcessControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "process-control",
- Interface: "process-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "process-control slots are reserved for the core snap")
}
func (s *ProcessControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/raw_usb_test.go snapd-2.42.1/interfaces/builtin/raw_usb_test.go
--- snapd-2.40/interfaces/builtin/raw_usb_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/raw_usb_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *RawUsbInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "raw-usb",
- Interface: "raw-usb",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "raw-usb slots are reserved for the core snap")
}
func (s *RawUsbInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/removable_media_test.go snapd-2.42.1/interfaces/builtin/removable_media_test.go
--- snapd-2.40/interfaces/builtin/removable_media_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/removable_media_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *RemovableMediaInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "removable-media",
- Interface: "removable-media",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "removable-media slots are reserved for the core snap")
}
func (s *RemovableMediaInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/screencast_legacy_test.go snapd-2.42.1/interfaces/builtin/screencast_legacy_test.go
--- snapd-2.40/interfaces/builtin/screencast_legacy_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/screencast_legacy_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -66,14 +66,6 @@
func (s *ScreencastLegacyInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.coreSlotInfo), IsNil)
- // screencast-legacy slot currently only used with core
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "screencast-legacy",
- Interface: "screencast-legacy",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "screencast-legacy slots are reserved for the core snap")
}
func (s *ScreencastLegacyInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/screen_inhibit_control_test.go snapd-2.42.1/interfaces/builtin/screen_inhibit_control_test.go
--- snapd-2.40/interfaces/builtin/screen_inhibit_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/screen_inhibit_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *ScreenInhibitControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "screen-inhibit-control",
- Interface: "screen-inhibit-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "screen-inhibit-control slots are reserved for the core snap")
}
func (s *ScreenInhibitControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/serial_port.go snapd-2.42.1/interfaces/builtin/serial_port.go
--- snapd-2.40/interfaces/builtin/serial_port.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/serial_port.go 2019-10-30 12:17:43.000000000 +0000
@@ -81,10 +81,6 @@
// BeforePrepareSlot checks validity of the defined slot
func (iface *serialPortInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
- if err := sanitizeSlotReservedForOSOrGadget(iface, slot); err != nil {
- return err
- }
-
// Check slot has a path attribute identify serial device
path, ok := slot.Attrs["path"].(string)
if !ok || path == "" {
diff -Nru snapd-2.40/interfaces/builtin/shutdown_test.go snapd-2.42.1/interfaces/builtin/shutdown_test.go
--- snapd-2.40/interfaces/builtin/shutdown_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/shutdown_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,12 +67,6 @@
func (s *ShutdownInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "shutdown",
- Interface: "shutdown",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, "shutdown slots are reserved for the core snap")
}
func (s *ShutdownInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/snapd_control_test.go snapd-2.42.1/interfaces/builtin/snapd_control_test.go
--- snapd-2.40/interfaces/builtin/snapd_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/snapd_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *SnapdControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "snapd-control",
- Interface: "snapd-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "snapd-control slots are reserved for the core snap")
}
func (s *SnapdControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/spi.go snapd-2.42.1/interfaces/builtin/spi.go
--- snapd-2.40/interfaces/builtin/spi.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/spi.go 2019-10-30 12:17:43.000000000 +0000
@@ -70,9 +70,6 @@
}
func (iface *spiInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
- if err := sanitizeSlotReservedForOSOrGadget(iface, slot); err != nil {
- return err
- }
_, err := iface.path(&interfaces.SlotRef{Snap: slot.Snap.InstanceName(), Name: slot.Name}, slot)
return err
}
diff -Nru snapd-2.40/interfaces/builtin/spi_test.go snapd-2.42.1/interfaces/builtin/spi_test.go
--- snapd-2.40/interfaces/builtin/spi_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/spi_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -196,13 +196,6 @@
c.Assert(err, ErrorMatches, `"/dev/spi-foo" is not a valid SPI device`)
err = interfaces.BeforePrepareSlot(s.iface, s.slotGadgetBad6Info)
c.Assert(err, ErrorMatches, `slot "gadget:bad-spi-6" must have a path attribute`)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "spi",
- Interface: "spi",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "spi slots are reserved for the core and gadget snaps")
}
func (s *spiInterfaceSuite) TestUDevSpec(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/ssh_keys_test.go snapd-2.42.1/interfaces/builtin/ssh_keys_test.go
--- snapd-2.40/interfaces/builtin/ssh_keys_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/ssh_keys_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -66,13 +66,6 @@
func (s *SshKeysInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slotInfo := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "ssh-keys",
- Interface: "ssh-keys",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slotInfo), ErrorMatches,
- "ssh-keys slots are reserved for the core snap")
}
func (s *SshKeysInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/ssh_public_keys_test.go snapd-2.42.1/interfaces/builtin/ssh_public_keys_test.go
--- snapd-2.40/interfaces/builtin/ssh_public_keys_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/ssh_public_keys_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -66,13 +66,6 @@
func (s *SshPublicKeysInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slotInfo := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "ssh-public-keys",
- Interface: "ssh-public-keys",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slotInfo), ErrorMatches,
- "ssh-public-keys slots are reserved for the core snap")
}
func (s *SshPublicKeysInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/system_files_test.go snapd-2.42.1/interfaces/builtin/system_files_test.go
--- snapd-2.40/interfaces/builtin/system_files_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/system_files_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -88,13 +88,6 @@
func (s *systemFilesInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "system-files",
- Interface: "system-files",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "system-files slots are reserved for the core snap")
}
func (s *systemFilesInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/system_observe_test.go snapd-2.42.1/interfaces/builtin/system_observe_test.go
--- snapd-2.40/interfaces/builtin/system_observe_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/system_observe_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -69,13 +69,6 @@
func (s *SystemObserveInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "system-observe",
- Interface: "system-observe",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "system-observe slots are reserved for the core snap")
}
func (s *SystemObserveInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/system_trace_test.go snapd-2.42.1/interfaces/builtin/system_trace_test.go
--- snapd-2.40/interfaces/builtin/system_trace_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/system_trace_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,12 +67,6 @@
func (s *SystemTraceInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "system-trace",
- Interface: "system-trace",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, "system-trace slots are reserved for the core snap")
}
func (s *SystemTraceInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/time_control_test.go snapd-2.42.1/interfaces/builtin/time_control_test.go
--- snapd-2.40/interfaces/builtin/time_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/time_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -68,13 +68,6 @@
func (s *TimeControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "time-control",
- Interface: "time-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "time-control slots are reserved for the core snap")
}
func (s *TimeControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/timeserver_control_test.go snapd-2.42.1/interfaces/builtin/timeserver_control_test.go
--- snapd-2.40/interfaces/builtin/timeserver_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/timeserver_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *TimeserverControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "timeserver-control",
- Interface: "timeserver-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "timeserver-control slots are reserved for the core snap")
}
func (s *TimeserverControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/timezone_control_test.go snapd-2.42.1/interfaces/builtin/timezone_control_test.go
--- snapd-2.40/interfaces/builtin/timezone_control_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/timezone_control_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *TimezoneControlInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "timezone-control",
- Interface: "timezone-control",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "timezone-control slots are reserved for the core snap")
}
func (s *TimezoneControlInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/tpm_test.go snapd-2.42.1/interfaces/builtin/tpm_test.go
--- snapd-2.40/interfaces/builtin/tpm_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/tpm_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,13 +67,6 @@
func (s *TpmInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "tpm",
- Interface: "tpm",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "tpm slots are reserved for the core snap")
}
func (s *TpmInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/u2f_devices_test.go snapd-2.42.1/interfaces/builtin/u2f_devices_test.go
--- snapd-2.40/interfaces/builtin/u2f_devices_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/u2f_devices_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -69,12 +69,6 @@
func (s *u2fDevicesInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "u2f-devices",
- Interface: "u2f-devices",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches, "u2f-devices slots are reserved for the core snap")
}
func (s *u2fDevicesInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/uhid_test.go snapd-2.42.1/interfaces/builtin/uhid_test.go
--- snapd-2.40/interfaces/builtin/uhid_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/uhid_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -66,14 +66,6 @@
func (s *UhidInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "uhid",
- Interface: "uhid",
- }
-
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "uhid slots are reserved for the core snap")
}
func (s *UhidInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/unity7.go snapd-2.42.1/interfaces/builtin/unity7.go
--- snapd-2.40/interfaces/builtin/unity7.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/unity7.go 2019-10-30 12:17:43.000000000 +0000
@@ -661,10 +661,6 @@
return nil
}
-func (iface *unity7Interface) BeforePrepareSlot(slot *snap.SlotInfo) error {
- return sanitizeSlotReservedForOS(iface, slot)
-}
-
func (iface *unity7Interface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool {
// allow what declarations allowed
return true
diff -Nru snapd-2.40/interfaces/builtin/unity7_test.go snapd-2.42.1/interfaces/builtin/unity7_test.go
--- snapd-2.40/interfaces/builtin/unity7_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/unity7_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -69,13 +69,6 @@
func (s *Unity7InterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "unity7",
- Interface: "unity7",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "unity7 slots are reserved for the core snap")
}
func (s *Unity7InterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/upower_observe.go snapd-2.42.1/interfaces/builtin/upower_observe.go
--- snapd-2.40/interfaces/builtin/upower_observe.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/upower_observe.go 2019-10-30 12:17:43.000000000 +0000
@@ -263,10 +263,6 @@
return nil
}
-func (iface *upowerObserveInterface) BeforePrepareSlot(slot *snap.SlotInfo) error {
- return sanitizeSlotReservedForOSOrApp(iface, slot)
-}
-
func (iface *upowerObserveInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool {
// allow what declarations allowed
return true
diff -Nru snapd-2.40/interfaces/builtin/upower_observe_test.go snapd-2.42.1/interfaces/builtin/upower_observe_test.go
--- snapd-2.40/interfaces/builtin/upower_observe_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/upower_observe_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -91,13 +91,6 @@
func (s *UPowerObserveInterfaceSuite) TestSanitizeSlot(c *C) {
c.Assert(interfaces.BeforePrepareSlot(s.iface, s.coreSlotInfo), IsNil)
- slot := &snap.SlotInfo{
- Snap: &snap.Info{SuggestedName: "some-snap"},
- Name: "upower-observe",
- Interface: "upower-observe",
- }
- c.Assert(interfaces.BeforePrepareSlot(s.iface, slot), ErrorMatches,
- "upower-observe slots are reserved for the core and app snaps")
}
func (s *UPowerObserveInterfaceSuite) TestSanitizePlug(c *C) {
diff -Nru snapd-2.40/interfaces/builtin/utils.go snapd-2.42.1/interfaces/builtin/utils.go
--- snapd-2.40/interfaces/builtin/utils.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/utils.go 2019-10-30 12:17:43.000000000 +0000
@@ -74,30 +74,6 @@
return appLabelExpr(plug.Apps(), plug.Snap())
}
-// sanitizeSlotReservedForOS checks if slot is of type os.
-func sanitizeSlotReservedForOS(iface interfaces.Interface, slot *snap.SlotInfo) error {
- if slot.Snap.GetType() != snap.TypeOS && slot.Snap.InstanceName() != "snapd" {
- return fmt.Errorf("%s slots are reserved for the core snap", iface.Name())
- }
- return nil
-}
-
-// sanitizeSlotReservedForOSOrGadget checks if the slot is of type os or gadget.
-func sanitizeSlotReservedForOSOrGadget(iface interfaces.Interface, slot *snap.SlotInfo) error {
- if slot.Snap.GetType() != snap.TypeOS && slot.Snap.GetType() != snap.TypeGadget && slot.Snap.InstanceName() != "snapd" {
- return fmt.Errorf("%s slots are reserved for the core and gadget snaps", iface.Name())
- }
- return nil
-}
-
-// sanitizeSlotReservedForOSOrApp checks if the slot is of type os or app.
-func sanitizeSlotReservedForOSOrApp(iface interfaces.Interface, slot *snap.SlotInfo) error {
- if slot.Snap.GetType() != snap.TypeOS && slot.Snap.GetType() != snap.TypeApp {
- return fmt.Errorf("%s slots are reserved for the core and app snaps", iface.Name())
- }
- return nil
-}
-
// determine if permanent slot side is provided by the system
// on classic system some implicit slots can be provided by system or by
// application snap e.g. avahi (it can be installed as deb or snap)
diff -Nru snapd-2.40/interfaces/builtin/utils_test.go snapd-2.42.1/interfaces/builtin/utils_test.go
--- snapd-2.40/interfaces/builtin/utils_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/utils_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -53,29 +53,6 @@
conSlotApp: interfaces.NewConnectedSlot(&snap.SlotInfo{Snap: &snap.Info{SnapType: snap.TypeApp}}, nil, nil),
})
-func (s *utilsSuite) TestSanitizeSlotReservedForOS(c *C) {
- errmsg := "iface slots are reserved for the core snap"
- c.Assert(builtin.SanitizeSlotReservedForOS(s.iface, s.slotOS), IsNil)
- c.Assert(builtin.SanitizeSlotReservedForOS(s.iface, s.slotSnapd), IsNil)
- c.Assert(builtin.SanitizeSlotReservedForOS(s.iface, s.slotApp), ErrorMatches, errmsg)
- c.Assert(builtin.SanitizeSlotReservedForOS(s.iface, s.slotGadget), ErrorMatches, errmsg)
-}
-
-func (s *utilsSuite) TestSanitizeSlotReservedForOSOrGadget(c *C) {
- errmsg := "iface slots are reserved for the core and gadget snaps"
- c.Assert(builtin.SanitizeSlotReservedForOSOrGadget(s.iface, s.slotOS), IsNil)
- c.Assert(builtin.SanitizeSlotReservedForOSOrGadget(s.iface, s.slotSnapd), IsNil)
- c.Assert(builtin.SanitizeSlotReservedForOSOrGadget(s.iface, s.slotApp), ErrorMatches, errmsg)
- c.Assert(builtin.SanitizeSlotReservedForOSOrGadget(s.iface, s.slotGadget), IsNil)
-}
-
-func (s *utilsSuite) TestSanitizeSlotReservedForOSOrApp(c *C) {
- errmsg := "iface slots are reserved for the core and app snaps"
- c.Assert(builtin.SanitizeSlotReservedForOSOrApp(s.iface, s.slotOS), IsNil)
- c.Assert(builtin.SanitizeSlotReservedForOSOrApp(s.iface, s.slotApp), IsNil)
- c.Assert(builtin.SanitizeSlotReservedForOSOrApp(s.iface, s.slotGadget), ErrorMatches, errmsg)
-}
-
func (s *utilsSuite) TestIsSlotSystemSlot(c *C) {
c.Assert(builtin.ImplicitSystemPermanentSlot(s.slotApp), Equals, false)
c.Assert(builtin.ImplicitSystemPermanentSlot(s.slotOS), Equals, true)
diff -Nru snapd-2.40/interfaces/builtin/wayland.go snapd-2.42.1/interfaces/builtin/wayland.go
--- snapd-2.40/interfaces/builtin/wayland.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/wayland.go 2019-10-30 12:17:43.000000000 +0000
@@ -56,6 +56,11 @@
# Allow access to common client Wayland sockets from non-snap clients
/run/user/[0-9]*/{mesa,mutter,sdl,wayland-cursor,weston,xwayland}-shared-* rw,
+# Allow reading an Xwayland Xauth file
+# (see https://gitlab.gnome.org/GNOME/mutter/merge_requests/626)
+/run/user/[0-9]*/.mutter-Xwaylandauth.* r,
+/run/user/[0-9]*/mutter/Xauthority r,
+
# Allow write access to create /run/user/* to create XDG_RUNTIME_DIR (until
# lp:1738197 is fixed). Note this is not needed if creating a session using
# logind (as provided by the login-session-control snapd interface).
diff -Nru snapd-2.40/interfaces/builtin/x11.go snapd-2.42.1/interfaces/builtin/x11.go
--- snapd-2.40/interfaces/builtin/x11.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/builtin/x11.go 2019-10-30 12:17:43.000000000 +0000
@@ -72,6 +72,12 @@
type=stream
addr="@/tmp/.ICE-unix/[0-9]*",
+# On systems with Tegra drivers, X11 needs to create the socket for clients to
+# use.
+unix (bind, listen, accept)
+ type=dgram
+ addr="@nvidia[0-9a-f]*",
+
# For Xorg to detect screens
/sys/devices/pci**/boot_vga r,
/sys/devices/pci**/resources r,
@@ -120,6 +126,12 @@
# startup.
owner /run/user/[0-9]*/.Xauthority r,
+# Allow reading an Xwayland Xauth file
+# (see https://gitlab.gnome.org/GNOME/mutter/merge_requests/626)
+owner /run/user/[0-9]*/.mutter-Xwaylandauth.* r,
+owner /run/user/[0-9]*/mutter/Xauthority r,
+
+
# Needed by QtSystems on X to detect mouse and keyboard. Note, the 'netlink
# raw' rule is not finely mediated by apparmor so we mediate with seccomp arg
# filtering.
diff -Nru snapd-2.40/interfaces/dbus/backend.go snapd-2.42.1/interfaces/dbus/backend.go
--- snapd-2.40/interfaces/dbus/backend.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/dbus/backend.go 2019-10-30 12:17:43.000000000 +0000
@@ -100,7 +100,7 @@
}
// core/snapd on classic are special
- if (snapName == "core" || snapName == "snapd") && release.OnClassic {
+ if (snapInfo.GetType() == snap.TypeOS || snapInfo.GetType() == snap.TypeSnapd) && release.OnClassic {
if err := setupDbusServiceForUserd(snapInfo); err != nil {
logger.Noticef("cannot create host `snap userd` dbus service file: %s", err)
}
diff -Nru snapd-2.40/interfaces/dbus/backend_test.go snapd-2.42.1/interfaces/dbus/backend_test.go
--- snapd-2.40/interfaces/dbus/backend_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/dbus/backend_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -317,7 +317,7 @@
var (
coreYaml string = "name: core\nversion: 1\ntype: os"
- snapdYaml string = "name: snapd\nversion: 1\n"
+ snapdYaml string = "name: snapd\nversion: 1\ntype: snapd"
)
func (s *backendSuite) TestSetupWritesUsedFilesForCore(c *C) {
diff -Nru snapd-2.40/interfaces/export_test.go snapd-2.42.1/interfaces/export_test.go
--- snapd-2.40/interfaces/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -51,3 +51,11 @@
isHomeUsingNFS = old
}
}
+
+func MockReadBuildID(mock func(p string) (string, error)) (restore func()) {
+ old := readBuildID
+ readBuildID = mock
+ return func() {
+ readBuildID = old
+ }
+}
diff -Nru snapd-2.40/interfaces/mount/backend.go snapd-2.42.1/interfaces/mount/backend.go
--- snapd-2.40/interfaces/mount/backend.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/mount/backend.go 2019-10-30 12:17:43.000000000 +0000
@@ -89,7 +89,7 @@
if err != nil {
return fmt.Errorf("cannot synchronize mount configuration files for snap %q: %s", snapName, err)
}
- return nil
+ return DiscardSnapNamespace(snapName)
}
// addMountProfile adds a mount profile with the given name, based on the given entries.
diff -Nru snapd-2.40/interfaces/mount/backend_test.go snapd-2.42.1/interfaces/mount/backend_test.go
--- snapd-2.40/interfaces/mount/backend_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/mount/backend_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -57,13 +57,12 @@
c.Assert(s.Repo.AddBackend(s.Backend), IsNil)
- err := os.MkdirAll(dirs.SnapMountPolicyDir, 0700)
- c.Assert(err, IsNil)
+ c.Assert(os.MkdirAll(dirs.SnapMountPolicyDir, 0700), IsNil)
+ c.Assert(os.MkdirAll(dirs.SnapRunNsDir, 0700), IsNil)
// add second iface so that we actually test combining snippets
s.iface2 = &ifacetest.TestInterface{InterfaceName: "iface2"}
- err = s.Repo.AddInterface(s.iface2)
- c.Assert(err, IsNil)
+ c.Assert(s.Repo.AddInterface(s.iface2), IsNil)
}
func (s *backendSuite) TearDownTest(c *C) {
@@ -95,6 +94,18 @@
err = ioutil.WriteFile(snapCanaryToStay, []byte("stay!"), 0644)
c.Assert(err, IsNil)
+ // Write the .mnt file, the logic for discarding mount namespaces uses it
+ // as a canary file to look for to even attempt to run the mount discard
+ // tool.
+ mntFile := filepath.Join(dirs.SnapRunNsDir, "hello-world.mnt")
+ err = ioutil.WriteFile(mntFile, []byte(""), 0644)
+ c.Assert(err, IsNil)
+
+ // Mock snap-discard-ns and allow tweak distro libexec dir so that it is used.
+ cmd := testutil.MockCommand(c, "snap-discard-ns", "")
+ defer cmd.Restore()
+ dirs.DistroLibExecDir = cmd.BinDir()
+
err = s.Backend.Remove("hello-world")
c.Assert(err, IsNil)
@@ -103,6 +114,7 @@
c.Assert(osutil.FileExists(hookCanaryToGo), Equals, false)
c.Assert(appCanaryToStay, testutil.FileEquals, "stay!")
c.Assert(snapCanaryToStay, testutil.FileEquals, "stay!")
+ c.Assert(cmd.Calls(), DeepEquals, [][]string{{"snap-discard-ns", "hello-world"}})
}
var mockSnapYaml = `name: snap-name
diff -Nru snapd-2.40/interfaces/policy/basedeclaration_test.go snapd-2.42.1/interfaces/policy/basedeclaration_test.go
--- snapd-2.40/interfaces/policy/basedeclaration_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/policy/basedeclaration_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -140,17 +140,19 @@
// these have more complex or in flux policies and have their
// own separate tests
snowflakes := map[string]bool{
- "content": true,
- "core-support": true,
- "home": true,
- "lxd-support": true,
- "multipass-support": true,
- "snapd-control": true,
- "dummy": true,
+ "content": true,
+ "core-support": true,
+ "home": true,
+ "lxd-support": true,
+ "multipass-support": true,
+ "packagekit-control": true,
+ "snapd-control": true,
+ "dummy": true,
}
// these simply auto-connect, anything else doesn't
autoconnect := map[string]bool{
+ "audio-playback": true,
"browser-support": true,
"desktop": true,
"desktop-legacy": true,
@@ -508,6 +510,24 @@
c.Check(err, IsNil)
}
+func (s *baseDeclSuite) TestAutoConnectionPackagekitControlOverride(c *C) {
+ cand := s.connectCand(c, "packagekit-control", "", "")
+ err := cand.CheckAutoConnect()
+ c.Check(err, NotNil)
+ c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"packagekit-control\"")
+
+ plugsSlots := `
+plugs:
+ packagekit-control:
+ allow-auto-connection: true
+`
+
+ snapDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
+ cand.PlugSnapDeclaration = snapDecl
+ err = cand.CheckAutoConnect()
+ c.Check(err, IsNil)
+}
+
func (s *baseDeclSuite) TestAutoConnectionOverrideMultiple(c *C) {
plugsSlots := `
plugs:
@@ -560,6 +580,8 @@
slotInstallation = map[string][]string{
// other
"adb-support": {"core"},
+ "audio-playback": {"app", "core"},
+ "audio-record": {"app", "core"},
"autopilot-introspection": {"core"},
"avahi-control": {"app", "core"},
"avahi-observe": {"app", "core"},
@@ -572,6 +594,7 @@
"docker-support": {"core"},
"fwupd": {"app"},
"gpio": {"core", "gadget"},
+ "gpio-control": {"core"},
"greengrass-support": {"core"},
"hidraw": {"core", "gadget"},
"i2c": {"core", "gadget"},
@@ -629,12 +652,10 @@
for _, iface := range all {
types, ok := slotInstallation[iface.Name()]
- compareWithSanitize := false
if !ok { // common ones, only core can install them,
- // their plain BeforePrepareSlot checked for that
types = []string{"core"}
- compareWithSanitize = true
}
+
if types == nil {
// snowflake needs to be tested specially
continue
@@ -642,7 +663,6 @@
for name, snapType := range snapTypeMap {
ok := strutil.ListContains(types, name)
ic := s.installSlotCand(c, iface.Name(), snapType, ``)
- slotInfo := ic.Snap.Slots[iface.Name()]
err := ic.Check()
comm := Commentf("%s by %s snap", iface.Name(), name)
if ok {
@@ -650,14 +670,6 @@
} else {
c.Check(err, NotNil, comm)
}
- if compareWithSanitize {
- sanitizeErr := interfaces.BeforePrepareSlot(iface, slotInfo)
- if err == nil {
- c.Check(sanitizeErr, IsNil, comm)
- } else {
- c.Check(sanitizeErr, NotNil, comm)
- }
- }
}
}
@@ -682,10 +694,12 @@
"classic-support": true,
"docker-support": true,
"greengrass-support": true,
+ "gpio-control": true,
"kernel-module-control": true,
"kubernetes-support": true,
"lxd-support": true,
"multipass-support": true,
+ "packagekit-control": true,
"personal-files": true,
"snapd-control": true,
"system-files": true,
@@ -770,6 +784,7 @@
// connecting with these interfaces needs to be allowed on
// case-by-case basis when not on classic
noconnect := map[string]bool{
+ "audio-record": true,
"modem-manager": true,
"network-manager": true,
"ofono": true,
@@ -807,14 +822,17 @@
// listed here to make sure that was a conscious decision
bothSides := map[string]bool{
"block-devices": true,
+ "audio-playback": true,
"classic-support": true,
"core-support": true,
"docker-support": true,
"greengrass-support": true,
+ "gpio-control": true,
"kernel-module-control": true,
"kubernetes-support": true,
"lxd-support": true,
"multipass-support": true,
+ "packagekit-control": true,
"personal-files": true,
"snapd-control": true,
"system-files": true,
diff -Nru snapd-2.40/interfaces/policy/helpers.go snapd-2.42.1/interfaces/policy/helpers.go
--- snapd-2.40/interfaces/policy/helpers.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/policy/helpers.go 2019-10-30 12:17:43.000000000 +0000
@@ -31,24 +31,13 @@
// check helpers
-func snapIDSnapd(snapID string) bool {
- var snapIDsSnapd = []string{
- // production
- "PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4",
- // TODO: when snapd snap is uploaded to staging, replace this with
- // the real snap-id.
- "todo-staging-snapd-id",
- }
- return strutil.ListContains(snapIDsSnapd, snapID)
-}
-
-func checkSnapType(snap *snap.Info, types []string) error {
+func checkSnapType(snapInfo *snap.Info, types []string) error {
if len(types) == 0 {
return nil
}
- snapID := snap.SnapID
- s := string(snap.GetType())
- if s == "os" || snapIDSnapd(snapID) {
+ snapType := snapInfo.GetType()
+ s := string(snapType)
+ if snapType == snap.TypeOS || snapType == snap.TypeSnapd {
// we use "core" in the assertions and we need also to
// allow for the "snapd" snap
s = "core"
@@ -221,6 +210,36 @@
return firstErr
}
+func checkSnapTypeSlotInstallationConstraints1(ic *InstallCandidateMinimalCheck, slot *snap.SlotInfo, cstrs *asserts.SlotInstallationConstraints) error {
+ if err := checkSnapType(slot.Snap, cstrs.SlotSnapTypes); err != nil {
+ return err
+ }
+ if err := checkOnClassic(cstrs.OnClassic); err != nil {
+ return err
+ }
+ return nil
+}
+
+func checkMinimalSlotInstallationConstraints(ic *InstallCandidateMinimalCheck, slot *snap.SlotInfo, cstrs []*asserts.SlotInstallationConstraints) (bool, error) {
+ var firstErr error
+ var hasSnapTypeConstraints bool
+ // OR of constraints
+ for _, cstrs1 := range cstrs {
+ if cstrs1.OnClassic == nil && len(cstrs1.SlotSnapTypes) == 0 {
+ continue
+ }
+ hasSnapTypeConstraints = true
+ err := checkSnapTypeSlotInstallationConstraints1(ic, slot, cstrs1)
+ if err == nil {
+ return true, nil
+ }
+ if firstErr == nil {
+ firstErr = err
+ }
+ }
+ return hasSnapTypeConstraints, firstErr
+}
+
func checkSlotInstallationConstraints1(ic *InstallCandidate, slot *snap.SlotInfo, cstrs *asserts.SlotInstallationConstraints) error {
// TODO: allow evaluated attr constraints here too?
if err := cstrs.SlotAttributes.Check(slot, nil); err != nil {
diff -Nru snapd-2.40/interfaces/policy/policy.go snapd-2.42.1/interfaces/policy/policy.go
--- snapd-2.40/interfaces/policy/policy.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/policy/policy.go 2019-10-30 12:17:43.000000000 +0000
@@ -256,3 +256,48 @@
func (connc *ConnectCandidate) CheckAutoConnect() error {
return connc.check("auto-connection")
}
+
+// InstallCandidateMinimalCheck represents a candidate snap installed with --dangerous flag that should pass minimum checks
+// against snap type (if present). It doesn't check interface attributes.
+type InstallCandidateMinimalCheck struct {
+ Snap *snap.Info
+ BaseDeclaration *asserts.BaseDeclaration
+ Model *asserts.Model
+ Store *asserts.Store
+}
+
+func (ic *InstallCandidateMinimalCheck) checkSlotRule(slot *snap.SlotInfo, rule *asserts.SlotRule) error {
+ if hasConstraints, err := checkMinimalSlotInstallationConstraints(ic, slot, rule.DenyInstallation); hasConstraints && err == nil {
+ return fmt.Errorf("installation denied by %q slot rule of interface %q", slot.Name, slot.Interface)
+ }
+
+ // TODO check that the snap is an app or gadget if allow-installation had no slot-snap-type constraints
+ if _, err := checkMinimalSlotInstallationConstraints(ic, slot, rule.AllowInstallation); err != nil {
+ return fmt.Errorf("installation not allowed by %q slot rule of interface %q", slot.Name, slot.Interface)
+ }
+ return nil
+}
+
+func (ic *InstallCandidateMinimalCheck) checkSlot(slot *snap.SlotInfo) error {
+ iface := slot.Interface
+ if rule := ic.BaseDeclaration.SlotRule(iface); rule != nil {
+ return ic.checkSlotRule(slot, rule)
+ }
+ return nil
+}
+
+// Check checks whether the installation is allowed.
+func (ic *InstallCandidateMinimalCheck) Check() error {
+ if ic.BaseDeclaration == nil {
+ return fmt.Errorf("internal error: improperly initialized InstallCandidateMinimalCheck")
+ }
+
+ for _, slot := range ic.Snap.Slots {
+ err := ic.checkSlot(slot)
+ if err != nil {
+ return err
+ }
+ }
+
+ return nil
+}
diff -Nru snapd-2.40/interfaces/policy/policy_test.go snapd-2.42.1/interfaces/policy/policy_test.go
--- snapd-2.40/interfaces/policy/policy_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/policy/policy_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -1493,6 +1493,106 @@
}
}
+func (s *policySuite) TestBaseDeclAllowDenyInstallationMinimalCheck(c *C) {
+ tests := []struct {
+ installYaml string
+ expected string // "" => no error
+ }{
+ {`name: install-snap
+version: 0
+slots:
+ innocuous:
+ install-slot-coreonly:
+`, `installation not allowed by "install-slot-coreonly" slot rule of interface "install-slot-coreonly"`},
+ {`name: install-gadget
+version: 0
+type: gadget
+slots:
+ install-slot-or:
+`, `installation denied by "install-slot-or" slot rule.*`},
+ {`name: install-snap
+version: 0
+slots:
+ install-slot-or:
+`, ""},
+ {`name: install-snap
+version: 0
+plugs:
+ install-plug-gadget-only:
+`, ``}, // plug is not validated with minimal installation check
+ }
+
+ for _, t := range tests {
+ installSnap := snaptest.MockInfo(c, t.installYaml, nil)
+
+ cand := policy.InstallCandidateMinimalCheck{
+ Snap: installSnap,
+ BaseDeclaration: s.baseDecl,
+ }
+
+ err := cand.Check()
+ if t.expected == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Check(err, ErrorMatches, t.expected)
+ }
+ }
+}
+
+func (s *policySuite) TestOnClassicMinimalInstallationCheck(c *C) {
+ r1 := release.MockOnClassic(false)
+ defer r1()
+ r2 := release.MockReleaseInfo(&release.ReleaseInfo)
+ defer r2()
+
+ tests := []struct {
+ distro string // "" => not classic
+ installYaml string
+ err string // "" => no error
+ }{
+ {"", `name: install-snap
+version: 0
+slots:
+ install-slot-on-classic-distros:`, `installation not allowed by "install-slot-on-classic-distros" slot rule.*`},
+ {"debian", `name: install-snap
+version: 0
+slots:
+ install-slot-on-classic-distros:`, ""},
+ {"", `name: install-snap
+version: 0
+plugs:
+ install-plug-on-classic-distros:`, ""}, // plug is not validated with minimal installation check
+ {"debian", `name: install-snap
+version: 0
+plugs:
+ install-plug-on-classic-distros:`, ""},
+ }
+
+ for _, t := range tests {
+ if t.distro == "" {
+ release.OnClassic = false
+ } else {
+ release.OnClassic = true
+ release.ReleaseInfo = release.OS{
+ ID: t.distro,
+ }
+ }
+
+ installSnap := snaptest.MockInfo(c, t.installYaml, nil)
+
+ cand := policy.InstallCandidateMinimalCheck{
+ Snap: installSnap,
+ BaseDeclaration: s.baseDecl,
+ }
+ err := cand.Check()
+ if t.err == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Check(err, ErrorMatches, t.err)
+ }
+ }
+}
+
func (s *policySuite) TestPlugOnClassicCheckConnection(c *C) {
r1 := release.MockOnClassic(false)
defer r1()
diff -Nru snapd-2.40/interfaces/seccomp/backend.go snapd-2.42.1/interfaces/seccomp/backend.go
--- snapd-2.40/interfaces/seccomp/backend.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/seccomp/backend.go 2019-10-30 12:17:43.000000000 +0000
@@ -53,29 +53,29 @@
)
var (
- kernelFeatures = release.SecCompActions
- ubuntuKernelArchitecture = arch.UbuntuKernelArchitecture
- releaseInfoId = release.ReleaseInfo.ID
- releaseInfoVersionId = release.ReleaseInfo.VersionID
- requiresSocketcall = requiresSocketcallImpl
+ kernelFeatures = release.SecCompActions
+ dpkgKernelArchitecture = arch.DpkgKernelArchitecture
+ releaseInfoId = release.ReleaseInfo.ID
+ releaseInfoVersionId = release.ReleaseInfo.VersionID
+ requiresSocketcall = requiresSocketcallImpl
snapSeccompVersionInfo = snapSeccompVersionInfoImpl
seccompCompilerLookup = cmd.InternalToolPath
)
-func snapSeccompVersionInfoImpl(c Compiler) (string, error) {
+func snapSeccompVersionInfoImpl(c Compiler) (seccomp_compiler.VersionInfo, error) {
return c.VersionInfo()
}
type Compiler interface {
Compile(in, out string) error
- VersionInfo() (string, error)
+ VersionInfo() (seccomp_compiler.VersionInfo, error)
}
// Backend is responsible for maintaining seccomp profiles for snap-confine.
type Backend struct {
snapSeccomp Compiler
- versionInfo string
+ versionInfo seccomp_compiler.VersionInfo
}
var globalProfileLE = []byte{
@@ -235,6 +235,12 @@
return nil
}
+// Obtain the privilege dropping snippet
+func uidGidChownSnippet(name string) (string, error) {
+ tmp := strings.Replace(privDropAndChownSyscalls, "###USERNAME###", name, -1)
+ return strings.Replace(tmp, "###GROUP###", name, -1), nil
+}
+
// deriveContent combines security snippets collected from all the interfaces
// affecting a given snap into a content map applicable to EnsureDirState.
func (b *Backend) deriveContent(spec *Specification, opts interfaces.ConfinementOptions, snapInfo *snap.Info) (content map[string]*osutil.FileState, err error) {
@@ -242,6 +248,20 @@
// template
addSocketcall := requiresSocketcall(snapInfo.Base)
+ var uidGidChownSyscalls bytes.Buffer
+ if len(snapInfo.SystemUsernames) == 0 {
+ uidGidChownSyscalls.WriteString(barePrivDropSyscalls)
+ } else {
+ for _, id := range snapInfo.SystemUsernames {
+ syscalls, err := uidGidChownSnippet(id.Name)
+ if err != nil {
+ return nil, fmt.Errorf(`cannot calculate syscalls for "%s": %s`, id, err)
+ }
+ uidGidChownSyscalls.WriteString(syscalls)
+ }
+ uidGidChownSyscalls.WriteString(rootSetUidGidSyscalls)
+ }
+
for _, hookInfo := range snapInfo.Hooks {
if content == nil {
content = make(map[string]*osutil.FileState)
@@ -250,7 +270,7 @@
path := securityTag + ".src"
content[path] = &osutil.FileState{
- Content: generateContent(opts, spec.SnippetForTag(securityTag), addSocketcall, b.versionInfo),
+ Content: generateContent(opts, spec.SnippetForTag(securityTag), addSocketcall, b.versionInfo, uidGidChownSyscalls.String()),
Mode: 0644,
}
}
@@ -261,7 +281,7 @@
securityTag := appInfo.SecurityTag()
path := securityTag + ".src"
content[path] = &osutil.FileState{
- Content: generateContent(opts, spec.SnippetForTag(securityTag), addSocketcall, b.versionInfo),
+ Content: generateContent(opts, spec.SnippetForTag(securityTag), addSocketcall, b.versionInfo, uidGidChownSyscalls.String()),
Mode: 0644,
}
}
@@ -269,7 +289,7 @@
return content, nil
}
-func generateContent(opts interfaces.ConfinementOptions, snippetForTag string, addSocketcall bool, versionInfo string) []byte {
+func generateContent(opts interfaces.ConfinementOptions, snippetForTag string, addSocketcall bool, versionInfo seccomp_compiler.VersionInfo, uidGidChownSyscalls string) []byte {
var buffer bytes.Buffer
if versionInfo != "" {
@@ -291,6 +311,7 @@
buffer.Write(defaultTemplate)
buffer.WriteString(snippetForTag)
+ buffer.WriteString(uidGidChownSyscalls)
// For systems with force-devmode we need to apply a workaround
// to avoid failing hooks. See description in template.go for
@@ -323,7 +344,7 @@
}
tags = append(tags, "bpf-argument-filtering")
- if res, err := seccomp_compiler.VersionInfo(b.versionInfo).HasFeature("bpf-actlog"); err == nil && res {
+ if res, err := b.versionInfo.HasFeature("bpf-actlog"); err == nil && res {
tags = append(tags, "bpf-actlog")
}
@@ -344,7 +365,7 @@
// - if the kernel architecture is not any of the above, force the use of
// socketcall()
func requiresSocketcallImpl(baseSnap string) bool {
- switch ubuntuKernelArchitecture() {
+ switch dpkgKernelArchitecture() {
case "i386", "s390x":
// glibc sysdeps/unix/sysv/linux/i386/kernel-features.h and
// sysdeps/unix/sysv/linux/s390/kernel-features.h added the
@@ -400,9 +421,11 @@
}
// MockSnapSeccompVersionInfo is for use in tests only.
-func MockSnapSeccompVersionInfo(s func(c Compiler) (string, error)) (restore func()) {
+func MockSnapSeccompVersionInfo(versionInfo string) (restore func()) {
old := snapSeccompVersionInfo
- snapSeccompVersionInfo = s
+ snapSeccompVersionInfo = func(c Compiler) (seccomp_compiler.VersionInfo, error) {
+ return seccomp_compiler.VersionInfo(versionInfo), nil
+ }
return func() {
snapSeccompVersionInfo = old
}
diff -Nru snapd-2.40/interfaces/seccomp/backend_test.go snapd-2.42.1/interfaces/seccomp/backend_test.go
--- snapd-2.40/interfaces/seccomp/backend_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/seccomp/backend_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -35,6 +35,7 @@
"github.com/snapcore/snapd/interfaces/seccomp"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/release"
+ seccomp_compiler "github.com/snapcore/snapd/sandbox/seccomp"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/testutil"
@@ -278,6 +279,7 @@
"# - create_module, init_module, finit_module, delete_module (kernel modules)\n",
"open\n",
"getuid\n",
+ "setresuid\n", // this is not random
} {
c.Assert(string(data), testutil.Contains, line)
}
@@ -489,7 +491,7 @@
func (s *backendSuite) TestRequiresSocketcallByNotNeededArch(c *C) {
testArchs := []string{"amd64", "armhf", "arm64", "powerpc", "ppc64el", "unknownDefault"}
for _, arch := range testArchs {
- restore := seccomp.MockUbuntuKernelArchitecture(func() string { return arch })
+ restore := seccomp.MockDpkgKernelArchitecture(func() string { return arch })
defer restore()
c.Assert(seccomp.RequiresSocketcall(""), Equals, false)
}
@@ -498,7 +500,7 @@
func (s *backendSuite) TestRequiresSocketcallForceByArch(c *C) {
testArchs := []string{"sparc", "sparc64"}
for _, arch := range testArchs {
- restore := seccomp.MockUbuntuKernelArchitecture(func() string { return arch })
+ restore := seccomp.MockDpkgKernelArchitecture(func() string { return arch })
defer restore()
c.Assert(seccomp.RequiresSocketcall(""), Equals, true)
}
@@ -540,7 +542,7 @@
for _, t := range tests {
restore = seccomp.MockReleaseInfoId(t.distro)
defer restore()
- restore = seccomp.MockUbuntuKernelArchitecture(func() string { return t.arch })
+ restore = seccomp.MockDpkgKernelArchitecture(func() string { return t.arch })
defer restore()
restore = seccomp.MockReleaseInfoVersionId(t.release)
defer restore()
@@ -579,7 +581,7 @@
}
for _, t := range tests {
- restore := seccomp.MockUbuntuKernelArchitecture(func() string { return t.arch })
+ restore := seccomp.MockDpkgKernelArchitecture(func() string { return t.arch })
defer restore()
restore = osutil.MockKernelVersion(t.version)
defer restore()
@@ -595,7 +597,7 @@
// check is reached
restore := seccomp.MockReleaseInfoId("other")
defer restore()
- restore = seccomp.MockUbuntuKernelArchitecture(func() string { return "i386" })
+ restore = seccomp.MockDpkgKernelArchitecture(func() string { return "i386" })
defer restore()
restore = osutil.MockKernelVersion("4.3")
defer restore()
@@ -611,7 +613,7 @@
// check is reached
restore := seccomp.MockReleaseInfoId("other")
defer restore()
- restore = seccomp.MockUbuntuKernelArchitecture(func() string { return "i386" })
+ restore = seccomp.MockDpkgKernelArchitecture(func() string { return "i386" })
defer restore()
restore = osutil.MockKernelVersion("4.3")
defer restore()
@@ -724,7 +726,7 @@
sb, ok := s.Backend.(*seccomp.Backend)
c.Assert(ok, Equals, true)
- c.Check(sb.VersionInfo(), Equals, "2345cdef 2.3.4 2345cdef -")
+ c.Check(sb.VersionInfo(), Equals, seccomp_compiler.VersionInfo("2345cdef 2.3.4 2345cdef -"))
}
func (s *backendSuite) TestCompilerInitUnhappy(c *C) {
@@ -736,3 +738,83 @@
err := s.Backend.Initialize()
c.Assert(err, ErrorMatches, "cannot initialize seccomp profile compiler: failed")
}
+
+func (s *backendSuite) TestSystemUsernamesPolicy(c *C) {
+ snapYaml := `
+name: app
+version: 0.1
+system-usernames:
+ testid: shared
+ testid2: shared
+apps:
+ cmd:
+`
+ snapInfo := snaptest.MockInfo(c, snapYaml, nil)
+ // NOTE: we don't call seccomp.MockTemplate()
+ err := s.Backend.Setup(snapInfo, interfaces.ConfinementOptions{}, s.Repo, s.meas)
+ c.Assert(err, IsNil)
+ // NOTE: we don't call seccomp.MockTemplate()
+ profile := filepath.Join(dirs.SnapSeccompDir, "snap.app.cmd")
+ data, err := ioutil.ReadFile(profile + ".src")
+ c.Assert(err, IsNil)
+ for _, line := range []string{
+ // NOTE: a few randomly picked lines from the real
+ // profile. Comments and empty lines are avoided as
+ // those can be discarded in the future.
+ "\n# - create_module, init_module, finit_module, delete_module (kernel modules)\n",
+ "\nopen\n",
+ "\ngetuid\n",
+ "\nsetgroups 0 0\n",
+ // and a few randomly picked lines from root syscalls
+ // with extra \n checks to ensure we have the right
+ // "paragraphs" in the generated output
+ "\n\n# allow setresgid to root\n",
+ "\n# allow setresuid to root\n",
+ "\nsetresuid u:root u:root u:root\n",
+ // and a few randomly picked lines from global id syscalls
+ "\n\n# allow setresgid to testid\n",
+ "\n\n# allow setresuid to testid\n",
+ "\nsetresuid -1 u:testid -1\n",
+ // also for the second user
+ "\n\n# allow setresgid to testid2\n",
+ "\n# allow setresuid to testid2\n",
+ "\nsetresuid -1 u:testid2 -1\n",
+ } {
+ c.Assert(string(data), testutil.Contains, line)
+ }
+
+ // make sure the bare syscalls aren't present
+ c.Assert(string(data), Not(testutil.Contains), "setresuid\n")
+}
+
+func (s *backendSuite) TestNoSystemUsernamesPolicy(c *C) {
+ snapYaml := `
+name: app
+version: 0.1
+apps:
+ cmd:
+`
+ snapInfo := snaptest.MockInfo(c, snapYaml, nil)
+ // NOTE: we don't call seccomp.MockTemplate()
+ err := s.Backend.Setup(snapInfo, interfaces.ConfinementOptions{}, s.Repo, s.meas)
+ c.Assert(err, IsNil)
+ // NOTE: we don't call seccomp.MockTemplate()
+ profile := filepath.Join(dirs.SnapSeccompDir, "snap.app.cmd")
+ data, err := ioutil.ReadFile(profile + ".src")
+ c.Assert(err, IsNil)
+ for _, line := range []string{
+ // and a few randomly picked lines from root syscalls
+ "# allow setresgid to root\n",
+ "# allow setresuid to root\n",
+ "setresuid u:root u:root u:root\n",
+ // and a few randomly picked lines from global id syscalls
+ "# allow setresgid to testid\n",
+ "# allow setresuid to testid\n",
+ "setresuid -1 u:testid -1\n",
+ } {
+ c.Assert(string(data), Not(testutil.Contains), line)
+ }
+
+ // make sure the bare syscalls are present
+ c.Assert(string(data), testutil.Contains, "setresuid\n")
+}
diff -Nru snapd-2.40/interfaces/seccomp/export_test.go snapd-2.42.1/interfaces/seccomp/export_test.go
--- snapd-2.40/interfaces/seccomp/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/seccomp/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -19,14 +19,23 @@
package seccomp
+import (
+ seccomp_compiler "github.com/snapcore/snapd/sandbox/seccomp"
+)
+
// MockTemplate replaces seccomp template.
//
// NOTE: The real seccomp template is long. For testing it is convenient for
// replace it with a shorter snippet.
func MockTemplate(fakeTemplate []byte) (restore func()) {
orig := defaultTemplate
+ origBarePrivDropSyscalls := barePrivDropSyscalls
defaultTemplate = fakeTemplate
- return func() { defaultTemplate = orig }
+ barePrivDropSyscalls = ""
+ return func() {
+ defaultTemplate = orig
+ barePrivDropSyscalls = origBarePrivDropSyscalls
+ }
}
func MockKernelFeatures(f func() []string) (resture func()) {
@@ -45,11 +54,11 @@
}
}
-func MockUbuntuKernelArchitecture(f func() string) (restore func()) {
- old := ubuntuKernelArchitecture
- ubuntuKernelArchitecture = f
+func MockDpkgKernelArchitecture(f func() string) (restore func()) {
+ old := dpkgKernelArchitecture
+ dpkgKernelArchitecture = f
return func() {
- ubuntuKernelArchitecture = old
+ dpkgKernelArchitecture = old
}
}
@@ -77,7 +86,7 @@
}
}
-func (b *Backend) VersionInfo() string {
+func (b *Backend) VersionInfo() seccomp_compiler.VersionInfo {
return b.versionInfo
}
diff -Nru snapd-2.40/interfaces/seccomp/template.go snapd-2.42.1/interfaces/seccomp/template.go
--- snapd-2.40/interfaces/seccomp/template.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/seccomp/template.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,8 +67,9 @@
fchmod
fchmodat
-# snappy doesn't currently support per-app UID/GIDs. All daemons run as 'root'
-# so allow chown to 'root'. DAC will prevent non-root from chowning to root.
+# Daemons typically run as 'root' so allow chown to 'root'. DAC will prevent
+# non-root from chowning to root.
+# (chown root:root)
chown - u:root g:root
chown32 - u:root g:root
fchown - u:root g:root
@@ -76,6 +77,22 @@
fchownat - - u:root g:root
lchown - u:root g:root
lchown32 - u:root g:root
+# (chown root)
+chown - u:root -1
+chown32 - u:root -1
+fchown - u:root -1
+fchown32 - u:root -1
+fchownat - - u:root -1
+lchown - u:root -1
+lchown32 - u:root -1
+# (chgrp root)
+chown - -1 g:root
+chown32 - -1 g:root
+fchown - -1 g:root
+fchown32 - -1 g:root
+fchownat - - -1 g:root
+lchown - -1 g:root
+lchown32 - -1 g:root
clock_getres
clock_gettime
@@ -353,25 +370,6 @@
sendfile
sendfile64
-# While we don't yet have seccomp arg filtering (LP: #1446748), we must allow
-# these because the launcher drops privileges after seccomp_load(). Eventually
-# we will only allow dropping to particular UIDs. For now, we mediate this with
-# AppArmor
-setgid
-setgid32
-setregid
-setregid32
-setresgid
-setresgid32
-setresuid
-setresuid32
-setreuid
-setreuid32
-setuid
-setuid32
-#setgroups
-#setgroups32
-
# These break isolation but are common and can't be mediated at the seccomp
# level with arg filtering
setpgid
@@ -578,3 +576,191 @@
# Add socketcall() for system and/or base that requires it. LP: #1446748
socketcall
`
+
+// Historically snapd has allowed the use of the various setuid, setgid and
+// setgroups syscalls, relying on AppArmor for mediation of the CAP_SETUID and
+// CAP_SETGID. In core20, these can be dropped.
+var barePrivDropSyscalls = `
+# Allow these and rely on AppArmor to mediate CAP_SETUID and CAP_SETGID. When
+# dropping to particular UID/GIDs, we'll use a different set of
+# argument-filtered syscalls.
+setgid
+setgid32
+setregid
+setregid32
+setresgid
+setresgid32
+setresuid
+setresuid32
+setreuid
+setreuid32
+setuid
+setuid32
+`
+
+// Syscalls for setuid/setgid family of syscalls when dealing with only root
+// uid and gid
+var rootSetUidGidSyscalls = `
+# Allow various setuid/setgid/chown family of syscalls with argument
+# filtering. AppArmor has corresponding CAP_SETUID, CAP_SETGID and CAP_CHOWN
+# rules.
+
+# allow use of setgroups(0, NULL)
+setgroups 0 0
+setgroups32 0 0
+
+# allow setgid to root
+setgid g:root
+setgid32 g:root
+
+# allow setuid to root
+setuid u:root
+setuid32 u:root
+
+# allow setregid to root
+setregid g:root g:root
+setregid32 g:root g:root
+setregid -1 g:root
+setregid32 -1 g:root
+setregid g:root -1
+setregid32 g:root -1
+
+# allow setresgid to root
+# (permanent drop)
+setresgid g:root g:root g:root
+setresgid32 g:root g:root g:root
+# (setegid)
+setresgid -1 g:root -1
+setresgid32 -1 g:root -1
+# (setgid equivalent)
+setresgid g:root g:root -1
+setresgid32 g:root g:root -1
+
+# allow setreuid to root
+setreuid u:root u:root
+setreuid32 u:root u:root
+setreuid -1 u:root
+setreuid32 -1 u:root
+setreuid u:root -1
+setreuid32 u:root -1
+
+# allow setresuid to root
+# (permanent drop)
+setresuid u:root u:root u:root
+setresuid32 u:root u:root u:root
+# (seteuid)
+setresuid -1 u:root -1
+setresuid32 -1 u:root -1
+# (setuid equivalent)
+setresuid u:root u:root -1
+setresuid32 u:root u:root -1
+`
+
+// Template for privilege drop and chown operations. This intentionally does
+// not support all combinations of users or obscure combinations (we can add
+// combinations as users dictate). Eg, these are supported:
+// chown foo:foo
+// chown foo
+// chgrp foo
+// but these are not:
+// chown foo:bar
+// chown bar:foo
+// For now, users who want 'foo:bar' can do:
+// chown foo ; chgrp bar
+var privDropAndChownSyscalls = `
+# allow setgid to ###GROUP###
+setgid g:###GROUP###
+setgid32 g:###GROUP###
+
+# allow setregid to ###GROUP###
+setregid g:###GROUP### g:###GROUP###
+setregid32 g:###GROUP### g:###GROUP###
+setregid -1 g:###GROUP###
+setregid32 -1 g:###GROUP###
+setregid g:###GROUP### -1
+setregid32 g:###GROUP### -1
+# (real root)
+setregid g:root g:###GROUP###
+setregid32 g:root g:###GROUP###
+# (euid root)
+setregid g:###GROUP### g:root
+setregid32 g:###GROUP### g:root
+
+# allow setresgid to ###GROUP###
+# (permanent drop)
+setresgid g:###GROUP### g:###GROUP### g:###GROUP###
+setresgid32 g:###GROUP### g:###GROUP### g:###GROUP###
+# (setegid)
+setresgid -1 g:###GROUP### -1
+setresgid32 -1 g:###GROUP### -1
+# (setgid equivalent)
+setresgid g:###GROUP### g:###GROUP### -1
+setresgid32 g:###GROUP### g:###GROUP### -1
+# (saving root)
+setresgid g:###GROUP### g:###GROUP### g:root
+setresgid32 g:###GROUP### g:###GROUP### g:root
+# (euid root and saving root)
+setresgid g:###GROUP### g:root g:root
+setresgid32 g:###GROUP### g:root g:root
+
+# allow setuid to ###USERNAME###
+setuid u:###USERNAME###
+setuid32 u:###USERNAME###
+
+# allow setreuid to ###USERNAME###
+setreuid u:###USERNAME### u:###USERNAME###
+setreuid32 u:###USERNAME### u:###USERNAME###
+setreuid -1 u:###USERNAME###
+setreuid32 -1 u:###USERNAME###
+setreuid u:###USERNAME### -1
+setreuid32 u:###USERNAME### -1
+# (real root)
+setreuid u:root u:###USERNAME###
+setreuid32 u:root u:###USERNAME###
+# (euid root)
+setreuid u:###USERNAME### u:root
+setreuid32 u:###USERNAME### u:root
+
+# allow setresuid to ###USERNAME###
+# (permanent drop)
+setresuid u:###USERNAME### u:###USERNAME### u:###USERNAME###
+setresuid32 u:###USERNAME### u:###USERNAME### u:###USERNAME###
+# (seteuid)
+setresuid -1 u:###USERNAME### -1
+setresuid32 -1 u:###USERNAME### -1
+# (setuid equivalent)
+setresuid u:###USERNAME### u:###USERNAME### -1
+setresuid32 u:###USERNAME### u:###USERNAME### -1
+# (saving root)
+setresuid u:###USERNAME### u:###USERNAME### u:root
+setresuid32 u:###USERNAME### u:###USERNAME### u:root
+# (euid root and saving root)
+setresuid u:###USERNAME### u:root u:root
+setresuid32 u:###USERNAME### u:root u:root
+
+# allow chown to ###USERNAME###:###GROUP###
+# (chown ###USERNAME###:###GROUP###)
+chown - u:###USERNAME### g:###GROUP###
+chown32 - u:###USERNAME### g:###GROUP###
+fchown - u:###USERNAME### g:###GROUP###
+fchown32 - u:###USERNAME### g:###GROUP###
+fchownat - - u:###USERNAME### g:###GROUP###
+lchown - u:###USERNAME### g:###GROUP###
+lchown32 - u:###USERNAME### g:###GROUP###
+# (chown ###USERNAME###)
+chown - u:###USERNAME### -1
+chown32 - u:###USERNAME### -1
+fchown - u:###USERNAME### -1
+fchown32 - u:###USERNAME### -1
+fchownat - - u:###USERNAME### -1
+lchown - u:###USERNAME### -1
+lchown32 - u:###USERNAME### -1
+# (chgrp ###GROUP###)
+chown - -1 g:###GROUP###
+chown32 - -1 g:###GROUP###
+fchown - -1 g:###GROUP###
+fchown32 - -1 g:###GROUP###
+fchownat - - -1 g:###GROUP###
+lchown - -1 g:###GROUP###
+lchown32 - -1 g:###GROUP###
+`
diff -Nru snapd-2.40/interfaces/system_key.go snapd-2.42.1/interfaces/system_key.go
--- snapd-2.40/interfaces/system_key.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/system_key.go 2019-10-30 12:17:43.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2018 Canonical Ltd
+ * Copyright (C) 2018-2019 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
@@ -79,15 +79,11 @@
isHomeUsingNFS = osutil.IsHomeUsingNFS
mockedSystemKey *systemKey
- seccompCompilerVersionInfo = seccompCompilerVersionInfoImpl
+ readBuildID = osutil.ReadBuildID
)
-func seccompCompilerVersionInfoImpl(path string) (string, error) {
- compiler, err := seccomp_compiler.New(func(name string) (string, error) { return path, nil })
- if err != nil {
- return "", err
- }
- return compiler.VersionInfo()
+func seccompCompilerVersionInfo(path string) (seccomp_compiler.VersionInfo, error) {
+ return seccomp_compiler.CompilerVersionInfo(func(name string) (string, error) { return filepath.Join(path, name), nil })
}
func generateSystemKey() (*systemKey, error) {
@@ -103,7 +99,7 @@
if err != nil {
return nil, err
}
- buildID, err := osutil.ReadBuildID(snapdPath)
+ buildID, err := readBuildID(snapdPath)
if err != nil && !os.IsNotExist(err) {
return nil, err
}
@@ -137,12 +133,12 @@
// Add seccomp-features
sk.SecCompActions = release.SecCompActions()
- versionInfo, err := seccompCompilerVersionInfo(filepath.Join(filepath.Dir(snapdPath), "snap-seccomp"))
+ versionInfo, err := seccompCompilerVersionInfo(filepath.Dir(snapdPath))
if err != nil {
logger.Noticef("cannot determine seccomp compiler version in generateSystemKey: %v", err)
return nil, err
}
- sk.SeccompCompilerVersion = versionInfo
+ sk.SeccompCompilerVersion = string(versionInfo)
return sk, nil
}
@@ -254,11 +250,3 @@
mockedSystemKey = &sk
return func() { mockedSystemKey = nil }
}
-
-func MockSeccompCompilerVersionInfo(s func(p string) (string, error)) (restore func()) {
- old := seccompCompilerVersionInfo
- seccompCompilerVersionInfo = s
- return func() {
- seccompCompilerVersionInfo = old
- }
-}
diff -Nru snapd-2.40/interfaces/system_key_test.go snapd-2.42.1/interfaces/system_key_test.go
--- snapd-2.40/interfaces/system_key_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/interfaces/system_key_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -42,7 +42,7 @@
tmp string
apparmorFeatures string
buildID string
- seccompCompilerVersion string
+ seccompCompilerVersion seccomp_compiler.VersionInfo
}
var _ = Suite(&systemKeySuite{})
@@ -60,11 +60,9 @@
c.Assert(err, IsNil)
s.apparmorFeatures = filepath.Join(s.tmp, "/sys/kernel/security/apparmor/features")
- id, err := osutil.MyBuildID()
- c.Assert(err, IsNil)
- s.buildID = id
+ s.buildID = "this-is-my-build-id"
- s.seccompCompilerVersion = "123 2.3.3 abcdef123 -"
+ s.seccompCompilerVersion = seccomp_compiler.VersionInfo("123 2.3.3 abcdef123 -")
testutil.MockCommand(c, filepath.Join(dirs.DistroLibExecDir, "snap-seccomp"), fmt.Sprintf(`
if [ "$1" = "version-info" ]; then echo "%s"; exit 0; fi
exit 1
@@ -83,6 +81,12 @@
restore := interfaces.MockIsHomeUsingNFS(func() (bool, error) { return nfsHome, nil })
defer restore()
+ restore = interfaces.MockReadBuildID(func(p string) (string, error) {
+ c.Assert(p, Equals, filepath.Join(dirs.DistroLibExecDir, "snapd"))
+ return s.buildID, nil
+ })
+ defer restore()
+
err := interfaces.WriteSystemKey()
c.Assert(err, IsNil)
@@ -104,9 +108,6 @@
seccompActionsStr, err := json.Marshal(release.SecCompActions())
c.Assert(err, IsNil)
- buildID, err := osutil.ReadBuildID("/proc/self/exe")
- c.Assert(err, IsNil)
-
compiler, err := seccomp_compiler.New(func(name string) (string, error) {
return filepath.Join(dirs.DistroLibExecDir, "snap-seccomp"), nil
})
@@ -117,7 +118,7 @@
overlayRoot, err := osutil.IsRootWritableOverlay()
c.Assert(err, IsNil)
- c.Check(string(systemKey), Equals, fmt.Sprintf(`{"version":1,"build-id":"%s","apparmor-features":%s,"apparmor-parser-mtime":%s,"apparmor-parser-features":%s,"nfs-home":%v,"overlay-root":%q,"seccomp-features":%s,"seccomp-compiler-version":"%s"}`, buildID, apparmorFeaturesStr, apparmorParserMtime, apparmorParserFeaturesStr, nfsHome, overlayRoot, seccompActionsStr, seccompCompilerVersion))
+ c.Check(string(systemKey), Equals, fmt.Sprintf(`{"version":1,"build-id":"%s","apparmor-features":%s,"apparmor-parser-mtime":%s,"apparmor-parser-features":%s,"nfs-home":%v,"overlay-root":%q,"seccomp-features":%s,"seccomp-compiler-version":"%s"}`, s.buildID, apparmorFeaturesStr, apparmorParserMtime, apparmorParserFeaturesStr, nfsHome, overlayRoot, seccompActionsStr, seccompCompilerVersion))
}
func (s *systemKeySuite) TestInterfaceWriteSystemKeyNoNFS(c *C) {
@@ -128,6 +129,20 @@
s.testInterfaceWriteSystemKey(c, true)
}
+func (s *systemKeySuite) TestInterfaceWriteSystemKeyErrorOnBuildID(c *C) {
+ restore := interfaces.MockIsHomeUsingNFS(func() (bool, error) { return false, nil })
+ defer restore()
+
+ restore = interfaces.MockReadBuildID(func(p string) (string, error) {
+ c.Assert(p, Equals, filepath.Join(dirs.DistroLibExecDir, "snapd"))
+ return "", fmt.Errorf("no build ID for you")
+ })
+ defer restore()
+
+ err := interfaces.WriteSystemKey()
+ c.Assert(err, ErrorMatches, "no build ID for you")
+}
+
func (s *systemKeySuite) TestInterfaceSystemKeyMismatchHappy(c *C) {
s.AddCleanup(interfaces.MockSystemKey(`
{
diff -Nru snapd-2.40/mkversion.sh snapd-2.42.1/mkversion.sh
--- snapd-2.40/mkversion.sh 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/mkversion.sh 2019-10-30 12:17:43.000000000 +0000
@@ -34,32 +34,53 @@
# If the version is passed in as an argument to mkversion.sh, let's use that.
if [ -n "$1" ]; then
- v="$1"
- o=shell
+ version_from_user="$1"
fi
-if [ -z "$v" ]; then
- # Let's try to derive the version from git..
- if command -v git >/dev/null; then
- # not using "--dirty" here until the following bug is fixed:
- # https://bugs.launchpad.net/snapcraft/+bug/1662388
- v="$(git describe --always | sed -e 's/-/+git/;y/-/./' )"
- o=git
- fi
+# Let's try to derive the version from git..
+if command -v git >/dev/null; then
+ # not using "--dirty" here until the following bug is fixed:
+ # https://bugs.launchpad.net/snapcraft/+bug/1662388
+ version_from_git="$(git describe --always | sed -e 's/-/+git/;y/-/./' )"
fi
-if [ -z "$v" ]; then
- # at this point we maybe in _build/src/github etc where we have no
- # debian/changelog (dh-golang only exports the sources here)
- # switch to the real source dir for the changelog parsing
- v="$(cd "$PKG_BUILDDIR"; dpkg-parsechangelog --show-field Version)";
- o=debian/changelog
+# at this point we maybe in _build/src/github etc where we have no
+# debian/changelog (dh-golang only exports the sources here)
+# switch to the real source dir for the changelog parsing
+if command -v dpkg-parsechangelog >/dev/null; then
+ version_from_changelog="$(cd "$PKG_BUILDDIR"; dpkg-parsechangelog --show-field Version)";
fi
-if [ -z "$v" ]; then
+# select version based on priority
+if [ -n "$version_from_user" ]; then
+ # version from user always wins
+ v="$version_from_user"
+ o="user"
+elif [ -n "$version_from_git" ]; then
+ v="$version_from_git"
+ o="git"
+elif [ -n "$version_from_changelog" ]; then
+ v="$version_from_changelog"
+ o="changelog"
+else
+ echo "Cannot generate version"
exit 1
fi
+# if we don't have a user provided versions and if the version is not
+# a release (i.e. the git tag does not match the debian changelog
+# version) then we need to construct the version similar to how we do
+# it in a packaging recipe. We take the debian version from the changelog
+# and append the git revno and commit hash. A simpler approach would be
+# to git tag all pre/rc releases.
+if [ -z "$version_from_user" ] && [ "$version_from_git" != "" ] && [ "$version_from_git" != "$version_from_changelog" ]; then
+ revno=$(git describe --always --abbrev=7|cut -d- -f2)
+ commit=$(git describe --always --abbrev=7|cut -d- -f3)
+ v="${version_from_changelog}+git${revno}.${commit}"
+ o="changelog+git"
+fi
+
+
if [ "$OUTPUT_ONLY" = true ]; then
echo "$v"
exit 0
diff -Nru snapd-2.40/osutil/context.go snapd-2.42.1/osutil/context.go
--- snapd-2.40/osutil/context.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/osutil/context.go 2019-10-30 12:17:43.000000000 +0000
@@ -23,6 +23,7 @@
"context"
"io"
"os/exec"
+ "sync"
"sync/atomic"
"syscall"
)
@@ -58,7 +59,10 @@
}
var ctxDone uint32
+ var wg sync.WaitGroup
waitDone := make(chan struct{})
+
+ wg.Add(1)
go func() {
select {
case <-ctx.Done():
@@ -66,10 +70,13 @@
cmd.Process.Kill()
case <-waitDone:
}
+ wg.Done()
}()
err := cmd.Wait()
close(waitDone)
+ wg.Wait()
+
if atomic.LoadUint32(&ctxDone) != 0 {
// do one last check to make sure the error from Wait is what we expect from Kill
if err, ok := err.(*exec.ExitError); ok {
diff -Nru snapd-2.40/osutil/exitcode.go snapd-2.42.1/osutil/exitcode.go
--- snapd-2.40/osutil/exitcode.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/osutil/exitcode.go 2019-10-30 12:17:43.000000000 +0000
@@ -27,6 +27,8 @@
// ExitCode extract the exit code from the error of a failed cmd.Run() or the
// original error if its not a exec.ExitError
func ExitCode(runErr error) (e int, err error) {
+ // TODO: with golang-1.12 this becomes a bit nicer:
+ // https://github.com/golang/go/issues/26539
// golang, you are kidding me, right?
if exitErr, ok := runErr.(*exec.ExitError); ok {
waitStatus := exitErr.Sys().(syscall.WaitStatus)
diff -Nru snapd-2.40/osutil/export_test.go snapd-2.42.1/osutil/export_test.go
--- snapd-2.40/osutil/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/osutil/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -155,3 +155,35 @@
syscallUname = old
}
}
+
+var (
+ FindUidNoGetentFallback = findUidNoGetentFallback
+ FindGidNoGetentFallback = findGidNoGetentFallback
+
+ FindUidWithGetentFallback = findUidWithGetentFallback
+ FindGidWithGetentFallback = findGidWithGetentFallback
+)
+
+func MockFindUidNoFallback(mock func(name string) (uint64, error)) (restore func()) {
+ old := findUidNoGetentFallback
+ findUidNoGetentFallback = mock
+ return func() { findUidNoGetentFallback = old }
+}
+
+func MockFindGidNoFallback(mock func(name string) (uint64, error)) (restore func()) {
+ old := findGidNoGetentFallback
+ findGidNoGetentFallback = mock
+ return func() { findGidNoGetentFallback = old }
+}
+
+func MockFindUid(mock func(name string) (uint64, error)) (restore func()) {
+ old := findUid
+ findUid = mock
+ return func() { findUid = old }
+}
+
+func MockFindGid(mock func(name string) (uint64, error)) (restore func()) {
+ old := findGid
+ findGid = mock
+ return func() { findGid = old }
+}
diff -Nru snapd-2.40/osutil/flock_test.go snapd-2.42.1/osutil/flock_test.go
--- snapd-2.40/osutil/flock_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/osutil/flock_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -118,27 +118,31 @@
c.Assert(lock.Unlock(), ErrorMatches, "bad file descriptor")
}
-// Test that non-blocking
+// Test that non-blocking locking reports error on pre-acquired lock.
func (s *flockSuite) TestLockUnlockNonblockingWorks(c *C) {
if os.Getenv("TRAVIS_BUILD_NUMBER") != "" {
c.Skip("Cannot use this under travis")
return
}
+ // Use the "flock" command to grab a lock for 9999 seconds in another process.
lockPath := filepath.Join(c.MkDir(), "lock")
cmd := exec.Command("flock", "--exclusive", lockPath, "sleep", "9999")
c.Assert(cmd.Start(), IsNil)
defer cmd.Process.Kill()
+ // Give flock some chance to create the lock file.
for i := 0; i < 10; i++ {
if osutil.FileExists(lockPath) {
break
}
- time.Sleep(time.Millisecond)
+ time.Sleep(time.Millisecond * 300)
}
+ // Try to acquire the same lock file and see that it is busy.
lock, err := osutil.NewFileLock(lockPath)
c.Assert(err, IsNil)
+ c.Assert(lock, NotNil)
defer lock.Close()
c.Assert(lock.TryLock(), Equals, osutil.ErrAlreadyLocked)
diff -Nru snapd-2.40/osutil/group_cgo.go snapd-2.42.1/osutil/group_cgo.go
--- snapd-2.40/osutil/group_cgo.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/osutil/group_cgo.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,29 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+// +build cgo
+
+/*
+ * Copyright (C) 2017-2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package osutil
+
+// The builtin os/user functions use the libc functions when compiled
+// with cgo so we use use them here.
+
+var (
+ findUid = findUidNoGetentFallback
+ findGid = findGidNoGetentFallback
+)
diff -Nru snapd-2.40/osutil/group.go snapd-2.42.1/osutil/group.go
--- snapd-2.40/osutil/group.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/osutil/group.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,31 +20,72 @@
package osutil
import (
+ "bytes"
+ "fmt"
+ "os/exec"
"os/user"
"strconv"
)
-// TODO: the builtin os/user functions only look at /etc/passwd and /etc/group
-// which is fine for our purposes today. In the future we may want to support
-// lookups in extrausers, which is configured via nsswitch.conf. Since snapd
-// does not support being built with cgo itself, when we want to support
-// extrausers here, we can convert these to do the equivalent of:
-//
-// getent passwd | cut -d : -f 3
-// getent group | cut -d : -f 3
-
-// FindUid returns the identifier of the given UNIX user name.
+// FindUid returns the identifier of the given UNIX user name. It will
+// automatically fallback to use "getent" if needed.
func FindUid(username string) (uint64, error) {
- user, err := user.Lookup(username)
+ return findUid(username)
+}
+
+// FindGid returns the identifier of the given UNIX group name. It will
+// automatically fallback to use "getent" if needed.
+func FindGid(groupname string) (uint64, error) {
+ return findGid(groupname)
+}
+
+// getent returns the identifier of the given UNIX user or group name as
+// determined by the specified database
+func getent(database, name string) (uint64, error) {
+ if database != "passwd" && database != "group" {
+ return 0, fmt.Errorf(`unsupported getent database "%q"`, database)
+ }
+
+ cmdStr := []string{
+ "getent",
+ database,
+ name,
+ }
+ cmd := exec.Command(cmdStr[0], cmdStr[1:]...)
+ output, err := cmd.CombinedOutput()
+ if err != nil {
+ // according to getent(1) the exit value of "2" means:
+ // "One or more supplied key could not be found in the
+ // database."
+ exitCode, _ := ExitCode(err)
+ if exitCode == 2 {
+ if database == "passwd" {
+ return 0, user.UnknownUserError(name)
+ }
+ return 0, user.UnknownGroupError(name)
+ }
+ return 0, fmt.Errorf("getent failed with: %v", OutputErr(output, err))
+ }
+
+ // passwd has 7 entries and group 4. In both cases, parts[2] is the id
+ parts := bytes.Split(output, []byte(":"))
+ if len(parts) < 4 {
+ return 0, fmt.Errorf("malformed entry: %q", output)
+ }
+
+ return strconv.ParseUint(string(parts[2]), 10, 64)
+}
+
+var findUidNoGetentFallback = func(username string) (uint64, error) {
+ myuser, err := user.Lookup(username)
if err != nil {
return 0, err
}
- return strconv.ParseUint(user.Uid, 10, 64)
+ return strconv.ParseUint(myuser.Uid, 10, 64)
}
-// FindGid returns the identifier of the given UNIX group name.
-func FindGid(groupname string) (uint64, error) {
+var findGidNoGetentFallback = func(groupname string) (uint64, error) {
group, err := user.LookupGroup(groupname)
if err != nil {
return 0, err
@@ -52,3 +93,49 @@
return strconv.ParseUint(group.Gid, 10, 64)
}
+
+// findUidWithGetentFallback returns the identifier of the given UNIX user name with
+// getent fallback
+func findUidWithGetentFallback(username string) (uint64, error) {
+ // first do the cheap os/user lookup
+ myuser, err := findUidNoGetentFallback(username)
+ switch err.(type) {
+ case nil:
+ // found it!
+ return myuser, nil
+ case user.UnknownUserError:
+ // user unknown, let's try getent
+ return getent("passwd", username)
+ default:
+ // something weird happened with the lookup, just report it
+ return 0, err
+ }
+}
+
+// findGidWithGetentFallback returns the identifier of the given UNIX group name with
+// getent fallback
+func findGidWithGetentFallback(groupname string) (uint64, error) {
+ // first do the cheap os/user lookup
+ group, err := findGidNoGetentFallback(groupname)
+ switch err.(type) {
+ case nil:
+ // found it!
+ return group, nil
+ case user.UnknownGroupError:
+ // group unknown, let's try getent
+ return getent("group", groupname)
+ default:
+ // something weird happened with the lookup, just report it
+ return 0, err
+ }
+}
+
+func IsUnknownUser(err error) bool {
+ _, ok := err.(user.UnknownUserError)
+ return ok
+}
+
+func IsUnknownGroup(err error) bool {
+ _, ok := err.(user.UnknownGroupError)
+ return ok
+}
diff -Nru snapd-2.40/osutil/group_no_cgo.go snapd-2.42.1/osutil/group_no_cgo.go
--- snapd-2.40/osutil/group_no_cgo.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/osutil/group_no_cgo.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,21 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+// +build !cgo
+
+package osutil
+
+// The builtin os/user functions only look at /etc/passwd and
+// /etc/group when building without cgo.
+//
+// So if something extra is configured via nsswitch.conf, like
+// extrausers those are not searched with the standard user.Lookup()
+// which is used in find{Uid,Gid}NoGetenvFallback.
+//
+// To fix this behavior we use find{Uid,Gid}WithGetentFallback() that
+// perform a 'getent ' automatically if no local user
+// is found and getent will use the libc nss functions to search all
+// configured data sources.
+
+var (
+ findUid = findUidWithGetentFallback
+ findGid = findGidWithGetentFallback
+)
diff -Nru snapd-2.40/osutil/group_test.go snapd-2.42.1/osutil/group_test.go
--- snapd-2.40/osutil/group_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/osutil/group_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,227 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package osutil_test
+
+import (
+ "fmt"
+ "os/user"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type findUserGroupSuite struct {
+ testutil.BaseTest
+ mockGetent *testutil.MockCmd
+}
+
+var _ = check.Suite(&findUserGroupSuite{})
+
+func (s *findUserGroupSuite) SetUpTest(c *check.C) {
+ // exit 2 is not found
+ s.mockGetent = testutil.MockCommand(c, "getent", "exit 2")
+}
+
+func (s *findUserGroupSuite) TearDownTest(c *check.C) {
+ s.mockGetent.Restore()
+}
+
+func (s *findUserGroupSuite) TestFindUidNoGetentFallback(c *check.C) {
+ uid, err := osutil.FindUidNoGetentFallback("root")
+ c.Assert(err, check.IsNil)
+ c.Assert(uid, check.Equals, uint64(0))
+ // getent shouldn't have been called with FindUidNoGetentFallback()
+ c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *findUserGroupSuite) TestFindUidNonexistent(c *check.C) {
+ _, err := osutil.FindUidNoGetentFallback("lakatos")
+ c.Assert(err, check.ErrorMatches, "user: unknown user lakatos")
+ _, ok := err.(user.UnknownUserError)
+ c.Assert(ok, check.Equals, true)
+ // getent shouldn't have been called with FindUidNoGetentFallback()
+ c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *findUserGroupSuite) TestFindUidWithGetentFallback(c *check.C) {
+ uid, err := osutil.FindUidWithGetentFallback("root")
+ c.Assert(err, check.IsNil)
+ c.Assert(uid, check.Equals, uint64(0))
+ // getent shouldn't have been called since 'root' is in /etc/passwd
+ c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *findUserGroupSuite) TestFindUidGetentNonexistent(c *check.C) {
+ _, err := osutil.FindUidWithGetentFallback("lakatos")
+ c.Assert(err, check.ErrorMatches, "user: unknown user lakatos")
+ _, ok := err.(user.UnknownUserError)
+ c.Assert(ok, check.Equals, true)
+ // getent should've have been called
+ c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{
+ {"getent", "passwd", "lakatos"},
+ })
+}
+
+func (s *findUserGroupSuite) TestFindUidGetentFoundFromGetent(c *check.C) {
+ restore := osutil.MockFindUidNoFallback(func(string) (uint64, error) {
+ return 1000, nil
+ })
+ defer restore()
+
+ uid, err := osutil.FindUidWithGetentFallback("some-user")
+ c.Assert(err, check.IsNil)
+ c.Assert(uid, check.Equals, uint64(1000))
+ // getent not called, "some-user" was available in the local db
+ c.Check(s.mockGetent.Calls(), check.HasLen, 0)
+}
+
+func (s *findUserGroupSuite) TestFindUidGetentOtherErrFromFindUid(c *check.C) {
+ restore := osutil.MockFindUidNoFallback(func(string) (uint64, error) {
+ return 0, fmt.Errorf("other-error")
+ })
+ defer restore()
+
+ _, err := osutil.FindUidWithGetentFallback("root")
+ c.Assert(err, check.ErrorMatches, "other-error")
+}
+
+func (s *findUserGroupSuite) TestFindUidGetentMockedOtherError(c *check.C) {
+ s.mockGetent = testutil.MockCommand(c, "getent", "exit 3")
+
+ uid, err := osutil.FindUidWithGetentFallback("lakatos")
+ c.Assert(err, check.ErrorMatches, "getent failed with: exit status 3")
+ c.Check(uid, check.Equals, uint64(0))
+ // getent should've have been called
+ c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{
+ {"getent", "passwd", "lakatos"},
+ })
+}
+
+func (s *findUserGroupSuite) TestFindUidGetentMocked(c *check.C) {
+ s.mockGetent = testutil.MockCommand(c, "getent", "echo lakatos:x:1234:5678:::")
+
+ uid, err := osutil.FindUidWithGetentFallback("lakatos")
+ c.Assert(err, check.IsNil)
+ c.Check(uid, check.Equals, uint64(1234))
+ c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{
+ {"getent", "passwd", "lakatos"},
+ })
+}
+
+func (s *findUserGroupSuite) TestFindUidGetentMockedMalformated(c *check.C) {
+ s.mockGetent = testutil.MockCommand(c, "getent", "printf too:few:colons")
+
+ _, err := osutil.FindUidWithGetentFallback("lakatos")
+ c.Assert(err, check.ErrorMatches, `malformed entry: "too:few:colons"`)
+}
+
+func (s *findUserGroupSuite) TestFindGidNoGetentFallback(c *check.C) {
+ gid, err := osutil.FindGidNoGetentFallback("root")
+ c.Assert(err, check.IsNil)
+ c.Assert(gid, check.Equals, uint64(0))
+ // getent shouldn't have been called with FindGidNoGetentFallback()
+ c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *findUserGroupSuite) TestFindGidNonexistent(c *check.C) {
+ _, err := osutil.FindGidNoGetentFallback("lakatos")
+ c.Assert(err, check.ErrorMatches, "group: unknown group lakatos")
+ _, ok := err.(user.UnknownGroupError)
+ c.Assert(ok, check.Equals, true)
+}
+
+func (s *findUserGroupSuite) TestFindGidGetentFoundFromGetent(c *check.C) {
+ restore := osutil.MockFindGidNoFallback(func(string) (uint64, error) {
+ return 1000, nil
+ })
+ defer restore()
+
+ gid, err := osutil.FindGidWithGetentFallback("some-group")
+ c.Assert(err, check.IsNil)
+ c.Assert(gid, check.Equals, uint64(1000))
+ // getent not called, "some-group" was available in the local db
+ c.Check(s.mockGetent.Calls(), check.HasLen, 0)
+}
+
+func (s *findUserGroupSuite) TestFindGidGetentOtherErrFromFindUid(c *check.C) {
+ restore := osutil.MockFindGidNoFallback(func(string) (uint64, error) {
+ return 0, fmt.Errorf("other-error")
+ })
+ defer restore()
+
+ _, err := osutil.FindGidWithGetentFallback("root")
+ c.Assert(err, check.ErrorMatches, "other-error")
+}
+
+func (s *findUserGroupSuite) TestFindGidWithGetentFallback(c *check.C) {
+ gid, err := osutil.FindGidWithGetentFallback("root")
+ c.Assert(err, check.IsNil)
+ c.Assert(gid, check.Equals, uint64(0))
+ // getent shouldn't have been called since 'root' is in /etc/group
+ c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *findUserGroupSuite) TestFindGidGetentNonexistent(c *check.C) {
+ _, err := osutil.FindGidWithGetentFallback("lakatos")
+ c.Assert(err, check.ErrorMatches, "group: unknown group lakatos")
+ _, ok := err.(user.UnknownGroupError)
+ c.Assert(ok, check.Equals, true)
+ // getent should've have been called
+ c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{
+ {"getent", "group", "lakatos"},
+ })
+}
+
+func (s *findUserGroupSuite) TestFindGidGetentMockedOtherError(c *check.C) {
+ s.mockGetent = testutil.MockCommand(c, "getent", "exit 3")
+
+ gid, err := osutil.FindGidWithGetentFallback("lakatos")
+ c.Assert(err, check.ErrorMatches, "getent failed with: exit status 3")
+ c.Check(gid, check.Equals, uint64(0))
+ // getent should've have been called
+ c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{
+ {"getent", "group", "lakatos"},
+ })
+}
+
+func (s *findUserGroupSuite) TestFindGidGetentMocked(c *check.C) {
+ s.mockGetent = testutil.MockCommand(c, "getent", "echo lakatos:x:1234:")
+
+ gid, err := osutil.FindGidWithGetentFallback("lakatos")
+ c.Assert(err, check.IsNil)
+ c.Check(gid, check.Equals, uint64(1234))
+ c.Check(s.mockGetent.Calls(), check.DeepEquals, [][]string{
+ {"getent", "group", "lakatos"},
+ })
+}
+
+func (s *findUserGroupSuite) TestIsUnknownUser(c *check.C) {
+ c.Check(osutil.IsUnknownUser(nil), check.Equals, false)
+ c.Check(osutil.IsUnknownUser(fmt.Errorf("something else")), check.Equals, false)
+ c.Check(osutil.IsUnknownUser(user.UnknownUserError("lakatos")), check.Equals, true)
+}
+
+func (s *findUserGroupSuite) TestIsUnknownGroup(c *check.C) {
+ c.Check(osutil.IsUnknownGroup(nil), check.Equals, false)
+ c.Check(osutil.IsUnknownGroup(fmt.Errorf("something else")), check.Equals, false)
+ c.Check(osutil.IsUnknownGroup(user.UnknownGroupError("lakatos")), check.Equals, true)
+}
diff -Nru snapd-2.40/osutil/squashfs/fstype.go snapd-2.42.1/osutil/squashfs/fstype.go
--- snapd-2.40/osutil/squashfs/fstype.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/osutil/squashfs/fstype.go 2019-10-30 12:17:43.000000000 +0000
@@ -26,10 +26,7 @@
"github.com/snapcore/snapd/osutil"
)
-// useFuse detects if we should be using squashfuse instead
-var useFuse = useFuseImpl
-
-func useFuseImpl() bool {
+var needsFuseImpl = func() bool {
if !osutil.FileExists("/dev/fuse") {
return false
}
@@ -51,13 +48,18 @@
return false
}
-// MockUseFuse is exported so useFuse can be overridden by testing.
-func MockUseFuse(r bool) func() {
- oldUseFuse := useFuse
- useFuse = func() bool {
+// MockNeedsFuse is exported so NeedsFuse can be overridden by testing.
+func MockNeedsFuse(r bool) func() {
+ oldNeedsFuseImpl := needsFuseImpl
+ needsFuseImpl = func() bool {
return r
}
- return func() { useFuse = oldUseFuse }
+ return func() { needsFuseImpl = oldNeedsFuseImpl }
+}
+
+// NeedsFuse returns true if the given system needs fuse to mount snaps
+func NeedsFuse() bool {
+ return needsFuseImpl()
}
// FsType returns what fstype to use for squashfs mounts and what
@@ -66,7 +68,7 @@
fstype = "squashfs"
options = []string{"ro", "x-gdu.hide"}
- if useFuse() {
+ if NeedsFuse() {
options = append(options, "allow_other")
switch {
case osutil.ExecutableExists("squashfuse"):
@@ -74,7 +76,7 @@
case osutil.ExecutableExists("snapfuse"):
fstype = "fuse.snapfuse"
default:
- panic("cannot happen because useFuse() ensures one of the two executables is there")
+ panic("cannot happen because NeedsFuse() ensures one of the two executables is there")
}
}
diff -Nru snapd-2.40/osutil/synctree.go snapd-2.42.1/osutil/synctree.go
--- snapd-2.40/osutil/synctree.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/osutil/synctree.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,191 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package osutil
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "sort"
+ "syscall"
+)
+
+func appendWithPrefix(paths []string, prefix string, filenames []string) []string {
+ for _, filename := range filenames {
+ paths = append(paths, filepath.Join(prefix, filename))
+ }
+ return paths
+}
+
+func removeEmptyDirs(baseDir, relPath string) error {
+ for relPath != "." {
+ if err := os.Remove(filepath.Join(baseDir, relPath)); err != nil {
+ // If the directory doesn't exist, then stop.
+ if os.IsNotExist(err) {
+ return nil
+ }
+ // If the directory is not empty, then stop.
+ if pathErr, ok := err.(*os.PathError); ok && pathErr.Err == syscall.ENOTEMPTY {
+ return nil
+ }
+ return err
+ }
+ relPath = filepath.Dir(relPath)
+ }
+ return nil
+}
+
+func matchAnyComponent(globs []string, path string) (ok bool, index int) {
+ for path != "." {
+ component := filepath.Base(path)
+ if ok, index, _ = matchAny(globs, component); ok {
+ return ok, index
+ }
+ path = filepath.Dir(path)
+ }
+ return false, 0
+}
+
+// EnsureTreeState ensures that a directory tree content matches expectations.
+//
+// EnsureTreeState walks subdirectories of the base directory, and
+// uses EnsureDirStateGlobs to synchronise content with the
+// corresponding entry in the content map. Any non-existent
+// subdirectories in the content map will be created.
+//
+// After synchronising all subdirectories, any subdirectories where
+// files were removed that are now empty will itself be removed, plus
+// its parent directories up to but not including the base directory.
+//
+// While there is a sanity check to prevent creation of directories
+// that match the file glob pattern, it is the caller's responsibility
+// to not create directories that may match globs passed to other
+// invocations.
+//
+// For example, if the glob "snap.$SNAP_NAME.*" is used then the
+// caller should avoid trying to populate any directories matching
+// "snap.*".
+//
+// If an error occurs, all matching files are removed from the tree.
+//
+// A list of changed and removed files is returned, as relative paths
+// to the base directory.
+func EnsureTreeState(baseDir string, globs []string, content map[string]map[string]*FileState) (changed, removed []string, err error) {
+ // Sanity check globs before doing anything
+ if _, index, err := matchAny(globs, "foo"); err != nil {
+ return nil, nil, fmt.Errorf("internal error: EnsureTreeState got invalid pattern %q: %s", globs[index], err)
+ }
+ // Sanity check directory paths and file names in content dict
+ for relPath, dirContent := range content {
+ if filepath.IsAbs(relPath) {
+ return nil, nil, fmt.Errorf("internal error: EnsureTreeState got absolute directory %q", relPath)
+ }
+ if ok, index := matchAnyComponent(globs, relPath); ok {
+ return nil, nil, fmt.Errorf("internal error: EnsureTreeState got path %q that matches glob pattern %q", relPath, globs[index])
+ }
+ for baseName := range dirContent {
+ if filepath.Base(baseName) != baseName {
+ return nil, nil, fmt.Errorf("internal error: EnsureTreeState got filename %q in %q, which has a path component", baseName, relPath)
+ }
+ if ok, _, _ := matchAny(globs, baseName); !ok {
+ return nil, nil, fmt.Errorf("internal error: EnsureTreeState got filename %q in %q, which doesn't match any glob patterns %q", baseName, relPath, globs)
+ }
+ }
+ }
+ // Find all existing subdirectories under the base dir. Don't
+ // perform any modifications here because, as it may confuse
+ // Walk().
+ subdirs := make(map[string]bool)
+ err = filepath.Walk(baseDir, func(path string, fileInfo os.FileInfo, err error) error {
+ if err != nil {
+ return err
+ }
+ if !fileInfo.IsDir() {
+ return nil
+ }
+ relPath, err := filepath.Rel(baseDir, path)
+ if err != nil {
+ return err
+ }
+ subdirs[relPath] = true
+ return nil
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+ // Ensure we process directories listed in content
+ for relPath := range content {
+ subdirs[relPath] = true
+ }
+
+ maybeEmpty := []string{}
+
+ var firstErr error
+ for relPath := range subdirs {
+ dirContent := content[relPath]
+ path := filepath.Join(baseDir, relPath)
+ if err := os.MkdirAll(path, 0755); err != nil {
+ firstErr = err
+ break
+ }
+ dirChanged, dirRemoved, err := EnsureDirStateGlobs(path, globs, dirContent)
+ changed = appendWithPrefix(changed, relPath, dirChanged)
+ removed = appendWithPrefix(removed, relPath, dirRemoved)
+ if err != nil {
+ firstErr = err
+ break
+ }
+ if len(removed) != 0 {
+ maybeEmpty = append(maybeEmpty, relPath)
+ }
+ }
+ // As with EnsureDirState, if an error occurred we want to
+ // delete all matching files under the whole baseDir
+ // hierarchy. This also means emptying subdirectories that
+ // were successfully synchronised.
+ if firstErr != nil {
+ // changed paths will be deleted by this next step
+ changed = nil
+ for relPath := range subdirs {
+ path := filepath.Join(baseDir, relPath)
+ if !IsDirectory(path) {
+ continue
+ }
+ _, dirRemoved, _ := EnsureDirStateGlobs(path, globs, nil)
+ removed = appendWithPrefix(removed, relPath, dirRemoved)
+ if len(removed) != 0 {
+ maybeEmpty = append(maybeEmpty, relPath)
+ }
+ }
+ }
+ sort.Strings(changed)
+ sort.Strings(removed)
+
+ // For directories where files were removed, attempt to remove
+ // empty directories.
+ for _, relPath := range maybeEmpty {
+ if err := removeEmptyDirs(baseDir, relPath); err != nil {
+ if firstErr != nil {
+ firstErr = err
+ }
+ }
+ }
+ return changed, removed, firstErr
+}
diff -Nru snapd-2.40/osutil/synctree_test.go snapd-2.42.1/osutil/synctree_test.go
--- snapd-2.40/osutil/synctree_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/osutil/synctree_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,187 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package osutil_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type EnsureTreeStateSuite struct {
+ dir string
+ globs []string
+}
+
+var _ = Suite(&EnsureTreeStateSuite{globs: []string{"*.snap"}})
+
+func (s *EnsureTreeStateSuite) SetUpTest(c *C) {
+ s.dir = c.MkDir()
+}
+
+func (s *EnsureTreeStateSuite) TestVerifiesExpectedFiles(c *C) {
+ c.Assert(os.MkdirAll(filepath.Join(s.dir, "foo", "bar"), 0755), IsNil)
+ name := filepath.Join(s.dir, "foo", "bar", "expected.snap")
+ c.Assert(ioutil.WriteFile(name, []byte("expected"), 0600), IsNil)
+ changed, removed, err := osutil.EnsureTreeState(s.dir, s.globs, map[string]map[string]*osutil.FileState{
+ "foo/bar": {
+ "expected.snap": {Content: []byte("expected"), Mode: 0600},
+ },
+ })
+ c.Assert(err, IsNil)
+ c.Check(changed, HasLen, 0)
+ c.Check(removed, HasLen, 0)
+
+ // The content and permissions are correct
+ c.Check(name, testutil.FileEquals, "expected")
+ stat, err := os.Stat(name)
+ c.Assert(err, IsNil)
+ c.Check(stat.Mode().Perm(), Equals, os.FileMode(0600))
+}
+
+func (s *EnsureTreeStateSuite) TestCreatesMissingFiles(c *C) {
+ c.Assert(os.MkdirAll(filepath.Join(s.dir, "foo"), 0755), IsNil)
+
+ changed, removed, err := osutil.EnsureTreeState(s.dir, s.globs, map[string]map[string]*osutil.FileState{
+ "foo": {
+ "missing1.snap": {Content: []byte(`content-1`), Mode: 0600},
+ },
+ "bar": {
+ "missing2.snap": {Content: []byte(`content-2`), Mode: 0600},
+ },
+ })
+ c.Assert(err, IsNil)
+ c.Check(changed, DeepEquals, []string{"bar/missing2.snap", "foo/missing1.snap"})
+ c.Check(removed, HasLen, 0)
+}
+
+func (s *EnsureTreeStateSuite) TestRemovesUnexpectedFiles(c *C) {
+ c.Assert(os.MkdirAll(filepath.Join(s.dir, "foo"), 0755), IsNil)
+ c.Assert(os.MkdirAll(filepath.Join(s.dir, "bar"), 0755), IsNil)
+ name1 := filepath.Join(s.dir, "foo", "evil1.snap")
+ name2 := filepath.Join(s.dir, "bar", "evil2.snap")
+ c.Assert(ioutil.WriteFile(name1, []byte(`evil-1`), 0600), IsNil)
+ c.Assert(ioutil.WriteFile(name2, []byte(`evil-2`), 0600), IsNil)
+
+ changed, removed, err := osutil.EnsureTreeState(s.dir, s.globs, map[string]map[string]*osutil.FileState{
+ "foo": {},
+ })
+ c.Assert(err, IsNil)
+ c.Check(changed, HasLen, 0)
+ c.Check(removed, DeepEquals, []string{"bar/evil2.snap", "foo/evil1.snap"})
+ c.Check(name1, testutil.FileAbsent)
+ c.Check(name2, testutil.FileAbsent)
+}
+
+func (s *EnsureTreeStateSuite) TestRemovesEmptyDirectories(c *C) {
+ c.Assert(os.MkdirAll(filepath.Join(s.dir, "foo"), 0755), IsNil)
+ c.Assert(os.MkdirAll(filepath.Join(s.dir, "bar", "baz"), 0755), IsNil)
+ name1 := filepath.Join(s.dir, "foo", "file1.snap")
+ name2 := filepath.Join(s.dir, "foo", "unrelated")
+ name3 := filepath.Join(s.dir, "bar", "baz", "file2.snap")
+ c.Assert(ioutil.WriteFile(name1, []byte(`text`), 0600), IsNil)
+ c.Assert(ioutil.WriteFile(name2, []byte(`text`), 0600), IsNil)
+ c.Assert(ioutil.WriteFile(name3, []byte(`text`), 0600), IsNil)
+
+ _, _, err := osutil.EnsureTreeState(s.dir, s.globs, nil)
+ c.Assert(err, IsNil)
+
+ // The "foo" directory is still present, while the "bar" tree
+ // has been removed.
+ c.Check(filepath.Join(s.dir, "foo"), testutil.FilePresent)
+ c.Check(filepath.Join(s.dir, "bar"), testutil.FileAbsent)
+}
+
+func (s *EnsureTreeStateSuite) TestIgnoresUnrelatedFiles(c *C) {
+ c.Assert(os.MkdirAll(filepath.Join(s.dir, "foo"), 0755), IsNil)
+ name := filepath.Join(s.dir, "foo", "unrelated")
+ err := ioutil.WriteFile(name, []byte(`text`), 0600)
+ c.Assert(err, IsNil)
+ changed, removed, err := osutil.EnsureTreeState(s.dir, s.globs, map[string]map[string]*osutil.FileState{})
+ c.Assert(err, IsNil)
+ // Report says that nothing has changed
+ c.Check(changed, HasLen, 0)
+ c.Check(removed, HasLen, 0)
+ // The file is still there
+ c.Check(name, testutil.FilePresent)
+}
+
+func (s *EnsureTreeStateSuite) TestErrorsOnBadGlob(c *C) {
+ _, _, err := osutil.EnsureTreeState(s.dir, []string{"["}, nil)
+ c.Check(err, ErrorMatches, `internal error: EnsureTreeState got invalid pattern "\[": syntax error in pattern`)
+}
+
+func (s *EnsureTreeStateSuite) TestErrorsOnDirectoryPathsMatchingGlobs(c *C) {
+ _, _, err := osutil.EnsureTreeState(s.dir, s.globs, map[string]map[string]*osutil.FileState{
+ "foo/bar.snap/baz": nil,
+ })
+ c.Check(err, ErrorMatches, `internal error: EnsureTreeState got path "foo/bar.snap/baz" that matches glob pattern "\*.snap"`)
+}
+
+func (s *EnsureTreeStateSuite) TestErrorsOnFilenamesWithSlashes(c *C) {
+ _, _, err := osutil.EnsureTreeState(s.dir, s.globs, map[string]map[string]*osutil.FileState{
+ "foo": {
+ "dir/file1.snap": {Content: []byte(`content-1`), Mode: 0600},
+ },
+ })
+ c.Check(err, ErrorMatches, `internal error: EnsureTreeState got filename "dir/file1.snap" in "foo", which has a path component`)
+}
+
+func (s *EnsureTreeStateSuite) TestErrorsOnFilenamesNotMatchingGlobs(c *C) {
+ _, _, err := osutil.EnsureTreeState(s.dir, s.globs, map[string]map[string]*osutil.FileState{
+ "foo": {
+ "file1.not-snap": {Content: []byte(`content-1`), Mode: 0600},
+ },
+ })
+ c.Check(err, ErrorMatches, `internal error: EnsureTreeState got filename "file1.not-snap" in "foo", which doesn't match any glob patterns \["\*.snap"\]`)
+}
+
+func (s *EnsureTreeStateSuite) TestRemovesFilesOnError(c *C) {
+ c.Assert(os.MkdirAll(filepath.Join(s.dir, "foo"), 0755), IsNil)
+ c.Assert(os.MkdirAll(filepath.Join(s.dir, "bar", "dir.snap"), 0755), IsNil)
+ name1 := filepath.Join(s.dir, "foo", "file1.snap")
+ name2 := filepath.Join(s.dir, "bar", "file2.snap")
+ name3 := filepath.Join(s.dir, "bar", "dir.snap", "sentinel")
+ c.Assert(ioutil.WriteFile(name1, []byte(`text`), 0600), IsNil)
+ c.Assert(ioutil.WriteFile(name2, []byte(`text`), 0600), IsNil)
+ c.Assert(ioutil.WriteFile(name3, []byte(`text`), 0600), IsNil)
+
+ changed, removed, err := osutil.EnsureTreeState(s.dir, s.globs, map[string]map[string]*osutil.FileState{
+ "foo": {
+ "file1.snap": {Content: []byte(`content-1`), Mode: 0600},
+ },
+ })
+ c.Check(err, ErrorMatches, `remove .*/bar/dir.snap: directory not empty`)
+ c.Check(changed, HasLen, 0)
+ c.Check(removed, DeepEquals, []string{"bar/file2.snap", "foo/file1.snap"})
+
+ // Matching files have been removed, along with the empty directory
+ c.Check(name1, testutil.FileAbsent)
+ c.Check(filepath.Dir(name1), testutil.FileAbsent)
+ c.Check(name2, testutil.FileAbsent)
+
+ // But the unmatched file in the bad directory remains
+ c.Check(name3, testutil.FilePresent)
+}
diff -Nru snapd-2.40/osutil/user.go snapd-2.42.1/osutil/user.go
--- snapd-2.40/osutil/user.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/osutil/user.go 2019-10-30 12:17:43.000000000 +0000
@@ -55,6 +55,119 @@
// allows as valid usernames
var IsValidUsername = regexp.MustCompile(`^[a-z0-9][-a-z0-9+._]*$`).MatchString
+// EnsureUserGroup uses the standard shadow utilities' 'useradd' and 'groupadd'
+// commands for creating non-login system users and groups that is portable
+// cross-distro. It will create the group with groupname 'name' and gid 'id' as
+// well as the user with username 'name' and uid 'id'. Importantly, 'useradd'
+// and 'groupadd' will use NSS to determine if a uid/gid is already assigned
+// (so LDAP, etc are consulted), but will themselves only add to local files,
+// which is exactly what we want since we don't want snaps to be blocked on
+// LDAP, etc when performing lookups.
+func EnsureUserGroup(name string, id uint32, extraUsers bool) error {
+ if !IsValidUsername(name) {
+ return fmt.Errorf(`cannot add user/group %q: name contains invalid characters`, name)
+ }
+
+ // Perform uid and gid lookups
+ uid, uidErr := FindUid(name)
+ if uidErr != nil && !IsUnknownUser(uidErr) {
+ return uidErr
+ }
+
+ gid, gidErr := FindGid(name)
+ if gidErr != nil && !IsUnknownGroup(gidErr) {
+ return gidErr
+ }
+
+ if uidErr == nil && gidErr == nil {
+ if uid != uint64(id) {
+ return fmt.Errorf(`found unexpected uid for user %q: %d`, name, uid)
+ } else if gid != uint64(id) {
+ return fmt.Errorf(`found unexpected gid for group %q: %d`, name, gid)
+ }
+ // found the user and group with expected values
+ return nil
+ }
+
+ // If the user and group do not exist, snapd will create both, so if
+ // the admin removed one of them, error and don't assume we can just
+ // add the missing one
+ if uidErr != nil && gidErr == nil {
+ return fmt.Errorf(`cannot add user/group %q: group exists and user does not`, name)
+ } else if uidErr == nil && gidErr != nil {
+ return fmt.Errorf(`cannot add user/group %q: user exists and group does not`, name)
+ }
+
+ // At this point, we know that the user and group don't exist, so
+ // create them.
+
+ // First create the group. useradd --user-group will choose a gid from
+ // the range defined in login.defs, so first call groupadd and use
+ // --gid with useradd.
+ groupCmdStr := []string{
+ "groupadd",
+ "--system",
+ "--gid", strconv.FormatUint(uint64(id), 10),
+ }
+
+ if extraUsers {
+ groupCmdStr = append(groupCmdStr, "--extrausers")
+ }
+ groupCmdStr = append(groupCmdStr, name)
+
+ cmd := exec.Command(groupCmdStr[0], groupCmdStr[1:]...)
+ if output, err := cmd.CombinedOutput(); err != nil {
+ return fmt.Errorf("groupadd failed with: %s", OutputErr(output, err))
+ }
+
+ // Now call useradd with the group we just created. As a non-login
+ // system user, we choose:
+ // - no password or aging (use --system without --password)
+ // - a non-existent home directory (--home-dir /nonexistent and
+ // --no-create-home)
+ // - a non-functional shell (--shell .../nologin)
+ // - use the above group (--gid with --no-user-group)
+ userCmdStr := []string{
+ "useradd",
+ "--system",
+ "--home-dir", "/nonexistent", "--no-create-home",
+ "--shell", LookPathDefault("false", "/bin/false"),
+ "--gid", strconv.FormatUint(uint64(id), 10), "--no-user-group",
+ "--uid", strconv.FormatUint(uint64(id), 10),
+ }
+
+ if extraUsers {
+ userCmdStr = append(userCmdStr, "--extrausers")
+ }
+ userCmdStr = append(userCmdStr, name)
+
+ cmd = exec.Command(userCmdStr[0], userCmdStr[1:]...)
+ if output, err := cmd.CombinedOutput(); err != nil {
+ useraddErrStr := fmt.Sprintf("useradd failed with: %s", OutputErr(output, err))
+
+ delCmdStr := []string{"groupdel"}
+ if extraUsers {
+ delCmdStr = append(delCmdStr, "--extrausers")
+ }
+
+ // TODO: groupdel doesn't currently support --extrausers, so
+ // don't try to clean up when it is specified (LP: #1840375)
+ if !extraUsers {
+ delCmdStr = append(delCmdStr, name)
+ cmd = exec.Command(delCmdStr[0], delCmdStr[1:]...)
+ if output2, err2 := cmd.CombinedOutput(); err2 != nil {
+ return fmt.Errorf("groupdel failed with: %s (after %s)", OutputErr(output2, err2), useraddErrStr)
+ }
+ }
+ return fmt.Errorf(useraddErrStr)
+ }
+
+ return nil
+}
+
+// AddUser uses the Debian/Ubuntu/derivative 'adduser' command for creating
+// regular login users on Ubuntu Core. 'adduser' is not portable cross-distro
+// but is convenient for creating regular login users.
func AddUser(name string, opts *AddUserOptions) error {
if opts == nil {
opts = &AddUserOptions{}
diff -Nru snapd-2.40/osutil/user_test.go snapd-2.42.1/osutil/user_test.go
--- snapd-2.40/osutil/user_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/osutil/user_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,6 +20,7 @@
package osutil_test
import (
+ "fmt"
"os"
"os/user"
"path/filepath"
@@ -277,3 +278,207 @@
c.Check(osutil.IsValidUsername(k), check.Equals, v)
}
}
+
+type ensureUserSuite struct {
+ mockUserAdd *testutil.MockCmd
+ mockGroupAdd *testutil.MockCmd
+ mockGroupDel *testutil.MockCmd
+}
+
+var _ = check.Suite(&ensureUserSuite{})
+
+func (s *ensureUserSuite) SetUpTest(c *check.C) {
+ s.mockUserAdd = testutil.MockCommand(c, "useradd", "")
+ s.mockGroupAdd = testutil.MockCommand(c, "groupadd", "")
+ s.mockGroupDel = testutil.MockCommand(c, "groupdel", "")
+}
+
+func (s *ensureUserSuite) TearDownTest(c *check.C) {
+ s.mockUserAdd.Restore()
+ s.mockGroupAdd.Restore()
+ s.mockGroupDel.Restore()
+}
+
+func (s *ensureUserSuite) TestEnsureUserGroupExtraUsersFalse(c *check.C) {
+ falsePath = osutil.LookPathDefault("false", "/bin/false")
+ err := osutil.EnsureUserGroup("lakatos", 123456, false)
+ c.Assert(err, check.IsNil)
+
+ c.Check(s.mockGroupAdd.Calls(), check.DeepEquals, [][]string{
+ {"groupadd", "--system", "--gid", "123456", "lakatos"},
+ })
+ c.Check(s.mockUserAdd.Calls(), check.DeepEquals, [][]string{
+ {"useradd", "--system", "--home-dir", "/nonexistent", "--no-create-home", "--shell", falsePath, "--gid", "123456", "--no-user-group", "--uid", "123456", "lakatos"},
+ })
+}
+
+func (s *ensureUserSuite) TestEnsureUserGroupExtraUsersTrue(c *check.C) {
+ falsePath = osutil.LookPathDefault("false", "/bin/false")
+ err := osutil.EnsureUserGroup("lakatos", 123456, true)
+ c.Assert(err, check.IsNil)
+
+ c.Check(s.mockGroupAdd.Calls(), check.DeepEquals, [][]string{
+ {"groupadd", "--system", "--gid", "123456", "--extrausers", "lakatos"},
+ })
+ c.Check(s.mockUserAdd.Calls(), check.DeepEquals, [][]string{
+ {"useradd", "--system", "--home-dir", "/nonexistent", "--no-create-home", "--shell", falsePath, "--gid", "123456", "--no-user-group", "--uid", "123456", "--extrausers", "lakatos"},
+ })
+}
+
+func (s *ensureUserSuite) TestEnsureUserGroupBadUser(c *check.C) {
+ err := osutil.EnsureUserGroup("k!", 123456, false)
+ c.Assert(err, check.ErrorMatches, `cannot add user/group "k!": name contains invalid characters`)
+
+ // shouldn't run these on error
+ c.Check(s.mockGroupAdd.Calls(), check.DeepEquals, [][]string(nil))
+ c.Check(s.mockUserAdd.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *ensureUserSuite) TestEnsureUserGroupUnexpectedFindUidError(c *check.C) {
+ restore := osutil.MockFindUid(func(string) (uint64, error) {
+ return 0, fmt.Errorf("some odd FindUid error")
+ })
+ defer restore()
+
+ err := osutil.EnsureUserGroup("lakatos", 1234, false)
+ c.Assert(err, check.ErrorMatches, `some odd FindUid error`)
+
+ // shouldn't run these on error
+ c.Check(s.mockGroupAdd.Calls(), check.DeepEquals, [][]string(nil))
+ c.Check(s.mockUserAdd.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *ensureUserSuite) TestEnsureUserGroupUnexpectedFindGidError(c *check.C) {
+ restore := osutil.MockFindGid(func(string) (uint64, error) {
+ return 0, fmt.Errorf("some odd FindGid error")
+ })
+ defer restore()
+
+ err := osutil.EnsureUserGroup("lakatos", 1234, false)
+ c.Assert(err, check.ErrorMatches, `some odd FindGid error`)
+
+ // shouldn't run these on error
+ c.Check(s.mockGroupAdd.Calls(), check.DeepEquals, [][]string(nil))
+ c.Check(s.mockUserAdd.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *ensureUserSuite) TestEnsureUserGroupUnexpectedUid(c *check.C) {
+ restore := osutil.MockFindUid(func(string) (uint64, error) {
+ return uint64(5432), nil
+ })
+ defer restore()
+ restore = osutil.MockFindGid(func(string) (uint64, error) {
+ return uint64(1234), nil
+ })
+ defer restore()
+
+ err := osutil.EnsureUserGroup("lakatos", 1234, false)
+ c.Assert(err, check.ErrorMatches, `found unexpected uid for user "lakatos": 5432`)
+
+ // shouldn't run these on error
+ c.Check(s.mockGroupAdd.Calls(), check.DeepEquals, [][]string(nil))
+ c.Check(s.mockUserAdd.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *ensureUserSuite) TestEnsureUserGroupUnexpectedGid(c *check.C) {
+ restore := osutil.MockFindUid(func(string) (uint64, error) {
+ return uint64(1234), nil
+ })
+ defer restore()
+ restore = osutil.MockFindGid(func(string) (uint64, error) {
+ return uint64(5432), nil
+ })
+ defer restore()
+
+ err := osutil.EnsureUserGroup("lakatos", 1234, false)
+ c.Assert(err, check.ErrorMatches, `found unexpected gid for group "lakatos": 5432`)
+
+ // shouldn't run these on error
+ c.Check(s.mockGroupAdd.Calls(), check.DeepEquals, [][]string(nil))
+ c.Check(s.mockUserAdd.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *ensureUserSuite) TestEnsureUserGroupFoundBoth(c *check.C) {
+ restore := osutil.MockFindUid(func(string) (uint64, error) {
+ return uint64(1234), nil
+ })
+ defer restore()
+ restore = osutil.MockFindGid(func(string) (uint64, error) {
+ return uint64(1234), nil
+ })
+ defer restore()
+
+ err := osutil.EnsureUserGroup("lakatos", 1234, false)
+ c.Assert(err, check.IsNil)
+
+ // we found both with expected values, shouldn't run these
+ c.Check(s.mockGroupAdd.Calls(), check.DeepEquals, [][]string(nil))
+ c.Check(s.mockUserAdd.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *ensureUserSuite) TestEnsureUserGroupUnexpectedGroupMissing(c *check.C) {
+ restore := osutil.MockFindUid(func(string) (uint64, error) {
+ return uint64(1234), nil
+ })
+ defer restore()
+
+ err := osutil.EnsureUserGroup("lakatos", 1234, false)
+ c.Assert(err, check.ErrorMatches, `cannot add user/group "lakatos": user exists and group does not`)
+
+ // shouldn't run these on error
+ c.Check(s.mockGroupAdd.Calls(), check.DeepEquals, [][]string(nil))
+ c.Check(s.mockUserAdd.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *ensureUserSuite) TestEnsureUserGroupUnexpectedUserMissing(c *check.C) {
+ restore := osutil.MockFindGid(func(string) (uint64, error) {
+ return uint64(1234), nil
+ })
+ defer restore()
+
+ err := osutil.EnsureUserGroup("lakatos", 1234, false)
+ c.Assert(err, check.ErrorMatches, `cannot add user/group "lakatos": group exists and user does not`)
+
+ // shouldn't run these on error
+ c.Check(s.mockGroupAdd.Calls(), check.DeepEquals, [][]string(nil))
+ c.Check(s.mockUserAdd.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *ensureUserSuite) TestEnsureUserGroupFailedGroupadd(c *check.C) {
+ mockGroupAdd := testutil.MockCommand(c, "groupadd", "echo some error; exit 1")
+ defer mockGroupAdd.Restore()
+
+ err := osutil.EnsureUserGroup("lakatos", 123456, false)
+ c.Assert(err, check.ErrorMatches, "groupadd failed with: some error")
+
+ // shouldn't run this on error
+ c.Check(s.mockUserAdd.Calls(), check.DeepEquals, [][]string(nil))
+}
+
+func (s *ensureUserSuite) TestEnsureUserGroupFailedUseraddClassic(c *check.C) {
+ mockUserAdd := testutil.MockCommand(c, "useradd", "echo some error; exit 1")
+ defer mockUserAdd.Restore()
+
+ err := osutil.EnsureUserGroup("lakatos", 123456, false)
+ c.Assert(err, check.ErrorMatches, "useradd failed with: some error")
+
+ c.Check(s.mockGroupDel.Calls(), check.DeepEquals, [][]string{
+ {"groupdel", "lakatos"},
+ })
+}
+
+func (s *ensureUserSuite) TestEnsureUserGroupFailedUseraddCore(c *check.C) {
+ mockUserAdd := testutil.MockCommand(c, "useradd", "echo some error; exit 1")
+ defer mockUserAdd.Restore()
+
+ err := osutil.EnsureUserGroup("lakatos", 123456, true)
+ c.Assert(err, check.ErrorMatches, "useradd failed with: some error")
+
+ // TODO: LP: #1840375
+ /*
+ c.Check(s.mockGroupDel.Calls(), check.DeepEquals, [][]string{
+ {"groupdel", "--extrausers", "lakatos"},
+ })
+ */
+ c.Check(s.mockGroupDel.Calls(), check.DeepEquals, [][]string(nil))
+}
diff -Nru snapd-2.40/overlord/assertstate/assertstate.go snapd-2.42.1/overlord/assertstate/assertstate.go
--- snapd-2.40/overlord/assertstate/assertstate.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/assertstate/assertstate.go 2019-10-30 12:17:43.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016-2017 Canonical Ltd
+ * Copyright (C) 2016-2019 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
@@ -24,7 +24,6 @@
import (
"fmt"
- "io"
"strings"
"github.com/snapcore/snapd/asserts"
@@ -42,91 +41,9 @@
return cachedDB(s).Add(a)
}
-// Batch allows to accumulate a set of assertions possibly out of prerequisite order and then add them in one go to the system assertion database.
-type Batch struct {
- bs asserts.Backstore
- refs []*asserts.Ref
-}
-
-// NewBatch creates a new Batch to accumulate assertions to add in one go to the system assertion database.
-func NewBatch() *Batch {
- return &Batch{
- bs: asserts.NewMemoryBackstore(),
- refs: nil,
- }
-}
-
-// Add one assertion to the batch.
-func (b *Batch) Add(a asserts.Assertion) error {
- if !a.SupportedFormat() {
- return &asserts.UnsupportedFormatError{Ref: a.Ref(), Format: a.Format()}
- }
- if err := b.bs.Put(a.Type(), a); err != nil {
- if revErr, ok := err.(*asserts.RevisionError); ok {
- if revErr.Current >= a.Revision() {
- // we already got something more recent
- return nil
- }
- }
- return err
- }
- b.refs = append(b.refs, a.Ref())
- return nil
-}
-
-// AddStream adds a stream of assertions to the batch.
-// Returns references to to the assertions effectively added.
-func (b *Batch) AddStream(r io.Reader) ([]*asserts.Ref, error) {
- start := len(b.refs)
- dec := asserts.NewDecoder(r)
- for {
- a, err := dec.Decode()
- if err == io.EOF {
- break
- }
- if err != nil {
- return nil, err
- }
- if err := b.Add(a); err != nil {
- return nil, err
- }
- }
- added := b.refs[start:]
- if len(added) == 0 {
- return nil, nil
- }
- refs := make([]*asserts.Ref, len(added))
- copy(refs, added)
- return refs, nil
-}
-
-// Commit adds the batch of assertions to the system assertion database.
-func (b *Batch) Commit(st *state.State) error {
- db := cachedDB(st)
- retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
- a, err := b.bs.Get(ref.Type, ref.PrimaryKey, ref.Type.MaxSupportedFormat())
- if asserts.IsNotFound(err) {
- // fallback to pre-existing assertions
- a, err = ref.Resolve(db.Find)
- }
- if err != nil {
- return nil, findError("cannot find %s", ref, err)
- }
- return a, nil
- }
-
- // linearize using fetcher
- f := newFetcher(st, retrieve)
- for _, ref := range b.refs {
- if err := f.Fetch(ref); err != nil {
- return err
- }
- }
-
- // TODO: trigger w. caller a global sanity check if something is revoked
- // (but try to save as much possible still),
- // or err is a check error
- return f.commit()
+// AddBatch adds the given assertion batch to the system assertion database.
+func AddBatch(s *state.State, batch *asserts.Batch, opts *asserts.CommitOptions) error {
+ return batch.CommitTo(cachedDB(s), opts)
}
func findError(format string, ref *asserts.Ref, err error) error {
diff -Nru snapd-2.40/overlord/assertstate/assertstate_test.go snapd-2.42.1/overlord/assertstate/assertstate_test.go
--- snapd-2.40/overlord/assertstate/assertstate_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/assertstate/assertstate_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2016 Canonical Ltd
+ * Copyright (C) 2016-2019 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
@@ -36,6 +36,7 @@
"github.com/snapcore/snapd/asserts/assertstest"
"github.com/snapcore/snapd/asserts/sysdb"
"github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/overlord"
"github.com/snapcore/snapd/overlord/assertstate"
"github.com/snapcore/snapd/overlord/auth"
@@ -70,8 +71,9 @@
type fakeStore struct {
storetest.Store
- state *state.State
- db asserts.RODatabase
+ state *state.State
+ db asserts.RODatabase
+ maxDeclSupportedFormat int
}
func (sto *fakeStore) pokeStateLock() {
@@ -83,6 +85,10 @@
func (sto *fakeStore) Assertion(assertType *asserts.AssertionType, key []string, _ *auth.UserState) (asserts.Assertion, error) {
sto.pokeStateLock()
+
+ restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, sto.maxDeclSupportedFormat)
+ defer restore()
+
ref := &asserts.Ref{Type: assertType, PrimaryKey: key}
return ref.Resolve(sto.db.Find)
}
@@ -121,6 +127,7 @@
s.fakeStore = &fakeStore{
state: s.state,
db: s.storeSigning,
+ maxDeclSupportedFormat: asserts.SnapDeclarationType.MaxSupportedFormat(),
}
s.trivialDeviceCtx = &snapstatetest.TrivialDeviceContext{
CtxStore: s.fakeStore,
@@ -154,7 +161,7 @@
c.Check(devAcct.(*asserts.Account).Username(), Equals, "developer1")
}
-func (s *assertMgrSuite) TestBatchAddStream(c *C) {
+func (s *assertMgrSuite) TestAddBatch(c *C) {
s.state.Lock()
defer s.state.Unlock()
@@ -166,7 +173,7 @@
enc.Encode(s.storeSigning.StoreAccountKey(""))
c.Assert(err, IsNil)
- batch := assertstate.NewBatch()
+ batch := asserts.NewBatch(nil)
refs, err := batch.AddStream(b)
c.Assert(err, IsNil)
c.Check(refs, DeepEquals, []*asserts.Ref{
@@ -178,7 +185,7 @@
err = batch.Add(s.storeSigning.StoreAccountKey(""))
c.Assert(err, IsNil)
- err = batch.Commit(s.state)
+ err = assertstate.AddBatch(s.state, batch, nil)
c.Assert(err, IsNil)
db := assertstate.DB(s.state)
@@ -189,132 +196,139 @@
c.Check(devAcct.(*asserts.Account).Username(), Equals, "developer1")
}
-func (s *assertMgrSuite) TestBatchConsiderPreexisting(c *C) {
+func (s *assertMgrSuite) TestAddBatchPartial(c *C) {
+ // Commit does add any successful assertion until the first error
s.state.Lock()
defer s.state.Unlock()
- // prereq store key
+ // store key already present
err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
c.Assert(err, IsNil)
- batch := assertstate.NewBatch()
+ batch := asserts.NewBatch(nil)
+
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+
+ err = batch.Add(snapDeclFoo)
+ c.Assert(err, IsNil)
err = batch.Add(s.dev1Acct)
c.Assert(err, IsNil)
- err = batch.Commit(s.state)
+ // too old
+ rev := 1
+ headers := map[string]interface{}{
+ "snap-id": "foo-id",
+ "snap-sha3-384": makeDigest(rev),
+ "snap-size": fmt.Sprintf("%d", len(fakeSnap(rev))),
+ "snap-revision": fmt.Sprintf("%d", rev),
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Time{}.Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
+ c.Assert(err, IsNil)
+
+ err = batch.Add(snapRev)
c.Assert(err, IsNil)
- db := assertstate.DB(s.state)
- devAcct, err := db.Find(asserts.AccountType, map[string]string{
- "account-id": s.dev1Acct.AccountID(),
+ err = assertstate.AddBatch(s.state, batch, nil)
+ c.Check(err, ErrorMatches, `(?ms).*validity.*`)
+
+ // snap-declaration was added anyway
+ _, err = assertstate.DB(s.state).Find(asserts.SnapDeclarationType, map[string]string{
+ "series": "16",
+ "snap-id": "foo-id",
})
c.Assert(err, IsNil)
- c.Check(devAcct.(*asserts.Account).Username(), Equals, "developer1")
}
-func (s *assertMgrSuite) TestBatchAddStreamReturnsEffectivelyAddedRefs(c *C) {
+func (s *assertMgrSuite) TestAddBatchPrecheckPartial(c *C) {
s.state.Lock()
defer s.state.Unlock()
- b := &bytes.Buffer{}
- enc := asserts.NewEncoder(b)
- // wrong order is ok
- err := enc.Encode(s.dev1Acct)
- c.Assert(err, IsNil)
- enc.Encode(s.storeSigning.StoreAccountKey(""))
+ // store key already present
+ err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
c.Assert(err, IsNil)
- batch := assertstate.NewBatch()
+ batch := asserts.NewBatch(nil)
- err = batch.Add(s.storeSigning.StoreAccountKey(""))
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+
+ err = batch.Add(snapDeclFoo)
+ c.Assert(err, IsNil)
+ err = batch.Add(s.dev1Acct)
c.Assert(err, IsNil)
- refs, err := batch.AddStream(b)
+ // too old
+ rev := 1
+ headers := map[string]interface{}{
+ "snap-id": "foo-id",
+ "snap-sha3-384": makeDigest(rev),
+ "snap-size": fmt.Sprintf("%d", len(fakeSnap(rev))),
+ "snap-revision": fmt.Sprintf("%d", rev),
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Time{}.Format(time.RFC3339),
+ }
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
c.Assert(err, IsNil)
- c.Check(refs, DeepEquals, []*asserts.Ref{
- {Type: asserts.AccountType, PrimaryKey: []string{s.dev1Acct.AccountID()}},
- })
- err = batch.Commit(s.state)
+ err = batch.Add(snapRev)
c.Assert(err, IsNil)
- db := assertstate.DB(s.state)
- devAcct, err := db.Find(asserts.AccountType, map[string]string{
- "account-id": s.dev1Acct.AccountID(),
+ err = assertstate.AddBatch(s.state, batch, &asserts.CommitOptions{
+ Precheck: true,
})
- c.Assert(err, IsNil)
- c.Check(devAcct.(*asserts.Account).Username(), Equals, "developer1")
+ c.Check(err, ErrorMatches, `(?ms).*validity.*`)
+
+ // nothing was added
+ _, err = assertstate.DB(s.state).Find(asserts.SnapDeclarationType, map[string]string{
+ "series": "16",
+ "snap-id": "foo-id",
+ })
+ c.Assert(asserts.IsNotFound(err), Equals, true)
}
-func (s *assertMgrSuite) TestBatchCommitRefusesSelfSignedKey(c *C) {
+func (s *assertMgrSuite) TestAddBatchPrecheckHappy(c *C) {
s.state.Lock()
defer s.state.Unlock()
- aKey, _ := assertstest.GenerateKey(752)
- aSignDB := assertstest.NewSigningDB("can0nical", aKey)
-
- aKeyEncoded, err := asserts.EncodePublicKey(aKey.PublicKey())
+ // store key already present
+ err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
c.Assert(err, IsNil)
- headers := map[string]interface{}{
- "authority-id": "can0nical",
- "account-id": "can0nical",
- "public-key-sha3-384": aKey.PublicKey().ID(),
- "name": "default",
- "since": time.Now().UTC().Format(time.RFC3339),
- }
- acctKey, err := aSignDB.Sign(asserts.AccountKeyType, headers, aKeyEncoded, "")
+ batch := asserts.NewBatch(nil)
+
+ snapDeclFoo := s.snapDecl(c, "foo", nil)
+
+ err = batch.Add(snapDeclFoo)
+ c.Assert(err, IsNil)
+ err = batch.Add(s.dev1Acct)
c.Assert(err, IsNil)
- headers = map[string]interface{}{
- "authority-id": "can0nical",
- "brand-id": "can0nical",
- "repair-id": "2",
- "summary": "repair two",
- "timestamp": time.Now().UTC().Format(time.RFC3339),
+ rev := 1
+ revDigest := makeDigest(rev)
+ headers := map[string]interface{}{
+ "snap-id": "foo-id",
+ "snap-sha3-384": revDigest,
+ "snap-size": fmt.Sprintf("%d", len(fakeSnap(rev))),
+ "snap-revision": fmt.Sprintf("%d", rev),
+ "developer-id": s.dev1Acct.AccountID(),
+ "timestamp": time.Now().Format(time.RFC3339),
}
- repair, err := aSignDB.Sign(asserts.RepairType, headers, []byte("#script"), "")
+ snapRev, err := s.storeSigning.Sign(asserts.SnapRevisionType, headers, nil, "")
c.Assert(err, IsNil)
- batch := assertstate.NewBatch()
-
- err = batch.Add(repair)
+ err = batch.Add(snapRev)
c.Assert(err, IsNil)
- err = batch.Add(acctKey)
+ err = assertstate.AddBatch(s.state, batch, &asserts.CommitOptions{
+ Precheck: true,
+ })
c.Assert(err, IsNil)
- // this must fail
- err = batch.Commit(s.state)
- c.Assert(err, ErrorMatches, `circular assertions are not expected:.*`)
-}
-
-func (s *assertMgrSuite) TestBatchAddUnsupported(c *C) {
- restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 111)
- defer restore()
-
- batch := assertstate.NewBatch()
-
- var a asserts.Assertion
- (func() {
- restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 999)
- defer restore()
- headers := map[string]interface{}{
- "format": "999",
- "revision": "1",
- "series": "16",
- "snap-id": "snap-id-1",
- "snap-name": "foo",
- "publisher-id": s.dev1Acct.AccountID(),
- "timestamp": time.Now().Format(time.RFC3339),
- }
- var err error
- a, err = s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
- c.Assert(err, IsNil)
- })()
-
- err := batch.Add(a)
- c.Check(err, ErrorMatches, `proposed "snap-declaration" assertion has format 999 but 111 is latest supported`)
+ _, err = assertstate.DB(s.state).Find(asserts.SnapRevisionType, map[string]string{
+ "snap-sha3-384": revDigest,
+ })
+ c.Check(err, IsNil)
}
func fakeSnap(rev int) []byte {
@@ -420,6 +434,93 @@
c.Assert(err, IsNil)
}
+func (s *assertMgrSuite) TestFetchUnsupportedUpdateIgnored(c *C) {
+ // ATM in principle we ignore updated assertions with unsupported formats
+ // NB: this scenario can only happen if there is a bug
+ // we ask the store to filter what is returned by max supported format!
+ restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 111)
+ defer restore()
+
+ logbuf, restore := logger.MockLogger()
+ defer restore()
+
+ snapDeclFoo0 := s.snapDecl(c, "foo", nil)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ err := assertstate.Add(s.state, s.storeSigning.StoreAccountKey(""))
+ c.Assert(err, IsNil)
+
+ err = assertstate.Add(s.state, s.dev1Acct)
+ c.Assert(err, IsNil)
+ err = assertstate.Add(s.state, snapDeclFoo0)
+ c.Assert(err, IsNil)
+
+ var snapDeclFoo1 *asserts.SnapDeclaration
+ (func() {
+ restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 999)
+ defer restore()
+ snapDeclFoo1 = s.snapDecl(c, "foo", map[string]interface{}{
+ "format": "999",
+ "revision": "1",
+ })
+ })()
+ c.Check(snapDeclFoo1.Revision(), Equals, 1)
+
+ ref := &asserts.Ref{
+ Type: asserts.SnapDeclarationType,
+ PrimaryKey: []string{"16", "foo-id"},
+ }
+ fetching := func(f asserts.Fetcher) error {
+ return f.Fetch(ref)
+ }
+
+ s.fakeStore.(*fakeStore).maxDeclSupportedFormat = 999
+ err = assertstate.DoFetch(s.state, 0, s.trivialDeviceCtx, fetching)
+ // no error and the old one was kept
+ c.Assert(err, IsNil)
+ snapDecl, err := ref.Resolve(assertstate.DB(s.state).Find)
+ c.Assert(err, IsNil)
+ c.Check(snapDecl.Revision(), Equals, 0)
+
+ // we log the issue
+ c.Check(logbuf.String(), testutil.Contains, `Cannot update assertion snap-declaration (foo-id;`)
+}
+
+func (s *assertMgrSuite) TestFetchUnsupportedError(c *C) {
+ // NB: this scenario can only happen if there is a bug
+ // we ask the store to filter what is returned by max supported format!
+
+ restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 111)
+ defer restore()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ var snapDeclFoo1 *asserts.SnapDeclaration
+ (func() {
+ restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 999)
+ defer restore()
+ snapDeclFoo1 = s.snapDecl(c, "foo", map[string]interface{}{
+ "format": "999",
+ "revision": "1",
+ })
+ })()
+ c.Check(snapDeclFoo1.Revision(), Equals, 1)
+
+ ref := &asserts.Ref{
+ Type: asserts.SnapDeclarationType,
+ PrimaryKey: []string{"16", "foo-id"},
+ }
+ fetching := func(f asserts.Fetcher) error {
+ return f.Fetch(ref)
+ }
+
+ s.fakeStore.(*fakeStore).maxDeclSupportedFormat = 999
+ err := assertstate.DoFetch(s.state, 0, s.trivialDeviceCtx, fetching)
+ c.Check(err, ErrorMatches, `(?s).*proposed "snap-declaration" assertion has format 999 but 111 is latest supported.*`)
+}
+
func (s *assertMgrSuite) setModel(model *asserts.Model) {
deviceCtx := &snapstatetest.TrivialDeviceContext{
DeviceModel: model,
@@ -638,60 +739,6 @@
c.Assert(chg.Err(), ErrorMatches, `(?s).*cannot install "f", snap "f" is undergoing a rename to "foo".*`)
}
-func (s *assertMgrSuite) TestValidateSnapSnapDeclIsTooNewFirstInstall(c *C) {
- c.Skip("the assertion service will make this scenario not possible")
-
- s.prereqSnapAssertions(c, 10)
-
- tempdir := c.MkDir()
- snapPath := filepath.Join(tempdir, "foo.snap")
- err := ioutil.WriteFile(snapPath, fakeSnap(10), 0644)
- c.Assert(err, IsNil)
-
- // update snap decl with one that is too new
- (func() {
- restore := asserts.MockMaxSupportedFormat(asserts.SnapDeclarationType, 999)
- defer restore()
- headers := map[string]interface{}{
- "format": "999",
- "revision": "1",
- "series": "16",
- "snap-id": "snap-id-1",
- "snap-name": "foo",
- "publisher-id": s.dev1Acct.AccountID(),
- "timestamp": time.Now().Format(time.RFC3339),
- }
- snapDecl, err := s.storeSigning.Sign(asserts.SnapDeclarationType, headers, nil, "")
- c.Assert(err, IsNil)
- err = s.storeSigning.Add(snapDecl)
- c.Assert(err, IsNil)
- })()
-
- s.state.Lock()
- defer s.state.Unlock()
-
- chg := s.state.NewChange("install", "...")
- t := s.state.NewTask("validate-snap", "Fetch and check snap assertions")
- snapsup := snapstate.SnapSetup{
- SnapPath: snapPath,
- UserID: 0,
- SideInfo: &snap.SideInfo{
- RealName: "foo",
- SnapID: "snap-id-1",
- Revision: snap.R(10),
- },
- }
- t.Set("snap-setup", snapsup)
- chg.AddTask(t)
-
- s.state.Unlock()
- defer s.se.Stop()
- s.settle(c)
- s.state.Lock()
-
- c.Assert(chg.Err(), ErrorMatches, `(?s).*proposed "snap-declaration" assertion has format 999 but 0 is latest supported.*`)
-}
-
func (s *assertMgrSuite) snapDecl(c *C, name string, extraHeaders map[string]interface{}) *asserts.SnapDeclaration {
headers := map[string]interface{}{
"series": "16",
diff -Nru snapd-2.40/overlord/assertstate/helpers.go snapd-2.42.1/overlord/assertstate/helpers.go
--- snapd-2.40/overlord/assertstate/helpers.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/assertstate/helpers.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,9 +20,6 @@
package assertstate
import (
- "fmt"
- "strings"
-
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/overlord/auth"
@@ -38,66 +35,24 @@
return auth.User(st, userID)
}
-type fetcher struct {
- db *asserts.Database
- asserts.Fetcher
- fetched []asserts.Assertion
-}
+func doFetch(s *state.State, userID int, deviceCtx snapstate.DeviceContext, fetching func(asserts.Fetcher) error) error {
+ // TODO: once we have a bulk assertion retrieval endpoint this approach will change
-// newFetches creates a fetcher used to retrieve assertions and later commit them to the system database in one go.
-func newFetcher(s *state.State, retrieve func(*asserts.Ref) (asserts.Assertion, error)) *fetcher {
db := cachedDB(s)
- f := &fetcher{db: db}
-
- save := func(a asserts.Assertion) error {
- f.fetched = append(f.fetched, a)
- return nil
- }
-
- f.Fetcher = asserts.NewFetcher(db, retrieve, save)
-
- return f
-}
-
-type commitError struct {
- errs []error
-}
-
-func (e *commitError) Error() string {
- l := []string{""}
- for _, e := range e.errs {
- l = append(l, e.Error())
- }
- return fmt.Sprintf("cannot add some assertions to the system database:%s", strings.Join(l, "\n - "))
-}
-
-// commit does a best effort of adding all the fetched assertions to the system database.
-func (f *fetcher) commit() error {
- var errs []error
- for _, a := range f.fetched {
- err := f.db.Add(a)
- if asserts.IsUnaccceptedUpdate(err) {
- if _, ok := err.(*asserts.UnsupportedFormatError); ok {
- // we kept the old one, but log the issue
- logger.Noticef("Cannot update assertion: %v", err)
- }
- // be idempotent
- // system db has already the same or newer
- continue
- }
- if err != nil {
- errs = append(errs, err)
+ // this is a fallback in case of bugs, we ask the store
+ // to filter unsupported formats!
+ unsupported := func(ref *asserts.Ref, unsupportedErr error) error {
+ if _, err := ref.Resolve(db.Find); err != nil {
+ // nothing there yet or any other error
+ return unsupportedErr
}
+ // we keep the old one, but log the issue
+ logger.Noticef("Cannot update assertion %v: %v", ref, unsupportedErr)
+ return nil
}
- if len(errs) != 0 {
- return &commitError{errs: errs}
- }
- return nil
-}
-func doFetch(s *state.State, userID int, deviceCtx snapstate.DeviceContext, fetching func(asserts.Fetcher) error) error {
- // TODO: once we have a bulk assertion retrieval endpoint this approach will change
+ b := asserts.NewBatch(unsupported)
user, err := userFromUserID(s, userID)
if err != nil {
@@ -111,10 +66,8 @@
return sto.Assertion(ref.Type, ref.PrimaryKey, user)
}
- f := newFetcher(s, retrieve)
-
s.Unlock()
- err = fetching(f)
+ err = b.Fetch(db, retrieve, fetching)
s.Lock()
if err != nil {
return err
@@ -123,5 +76,5 @@
// TODO: trigger w. caller a global sanity check if a is revoked
// (but try to save as much possible still),
// or err is a check error
- return f.commit()
+ return b.CommitTo(db, nil)
}
diff -Nru snapd-2.40/overlord/cmdstate/cmdstate_test.go snapd-2.42.1/overlord/cmdstate/cmdstate_test.go
--- snapd-2.40/overlord/cmdstate/cmdstate_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/cmdstate/cmdstate_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -75,6 +75,7 @@
s.manager = cmdstate.Manager(s.state, runner)
s.se.AddManager(s.manager)
s.se.AddManager(runner)
+ c.Assert(s.se.StartUp(), check.IsNil)
s.restore = cmdstate.MockDefaultExecTimeout(time.Second / 10)
}
diff -Nru snapd-2.40/overlord/configstate/config/export_test.go snapd-2.42.1/overlord/configstate/config/export_test.go
--- snapd-2.40/overlord/configstate/config/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/config/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -23,6 +23,8 @@
"encoding/json"
)
+var PurgeNulls = purgeNulls
+
func (t *Transaction) PristineConfig() map[string]map[string]*json.RawMessage {
return t.pristine
}
diff -Nru snapd-2.40/overlord/configstate/config/helpers.go snapd-2.42.1/overlord/configstate/config/helpers.go
--- snapd-2.40/overlord/configstate/config/helpers.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/config/helpers.go 2019-10-30 12:17:43.000000000 +0000
@@ -47,6 +47,41 @@
return subkeys, nil
}
+func purgeNulls(config interface{}) interface{} {
+ switch config := config.(type) {
+ // map of json raw messages is the starting point for purgeNulls, this is the configuration we receive
+ case map[string]*json.RawMessage:
+ for k, v := range config {
+ if cfg := purgeNulls(v); cfg != nil {
+ config[k] = cfg.(*json.RawMessage)
+ } else {
+ delete(config, k)
+ }
+ }
+ case map[string]interface{}:
+ for k, v := range config {
+ if cfg := purgeNulls(v); cfg != nil {
+ config[k] = cfg
+ } else {
+ delete(config, k)
+ }
+ }
+ case *json.RawMessage:
+ if config == nil {
+ return nil
+ }
+ var configm interface{}
+ if err := jsonutil.DecodeWithNumber(bytes.NewReader(*config), &configm); err != nil {
+ panic(fmt.Errorf("internal error: cannot unmarshal configuration: %v", err))
+ }
+ if cfg := purgeNulls(configm); cfg != nil {
+ return jsonRaw(cfg)
+ }
+ return nil
+ }
+ return config
+}
+
func PatchConfig(snapName string, subkeys []string, pos int, config interface{}, value *json.RawMessage) (interface{}, error) {
switch config := config.(type) {
diff -Nru snapd-2.40/overlord/configstate/config/helpers_test.go snapd-2.42.1/overlord/configstate/config/helpers_test.go
--- snapd-2.40/overlord/configstate/config/helpers_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/config/helpers_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,11 +20,13 @@
package config_test
import (
+ "bytes"
"encoding/json"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/features"
+ "github.com/snapcore/snapd/jsonutil"
"github.com/snapcore/snapd/overlord/configstate/config"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
@@ -211,3 +213,103 @@
_, err := config.PatchConfig("snap1", []string{"foo"}, 0, invalid, &value)
c.Assert(err, ErrorMatches, `internal error: unexpected configuration type \[\]string`)
}
+
+func (s *configHelpersSuite) TestPurgeNulls(c *C) {
+ cfg1 := map[string]interface{}{
+ "foo": nil,
+ "bar": map[string]interface{}{
+ "one": 1,
+ "two": nil,
+ },
+ "baz": map[string]interface{}{
+ "three": nil,
+ },
+ }
+ config.PurgeNulls(cfg1)
+ c.Check(cfg1, DeepEquals, map[string]interface{}{
+ "bar": map[string]interface{}{
+ "one": 1,
+ },
+ "baz": map[string]interface{}{},
+ })
+
+ cfg2 := map[string]interface{}{"foo": nil}
+ c.Check(config.PurgeNulls(cfg2), DeepEquals, map[string]interface{}{})
+ c.Check(cfg2, DeepEquals, map[string]interface{}{})
+
+ jsonData, err := json.Marshal(map[string]interface{}{
+ "foo": nil,
+ "bar": map[string]interface{}{
+ "one": 2,
+ "two": nil,
+ },
+ "baz": map[string]interface{}{
+ "three": nil,
+ },
+ })
+ c.Assert(err, IsNil)
+ raw := json.RawMessage(jsonData)
+ cfg4 := map[string]*json.RawMessage{
+ "root": &raw,
+ }
+ config.PurgeNulls(cfg4)
+
+ val, ok := cfg4["root"]
+ c.Assert(ok, Equals, true)
+
+ var out interface{}
+ jsonutil.DecodeWithNumber(bytes.NewReader(*val), &out)
+ c.Check(out, DeepEquals, map[string]interface{}{
+ "bar": map[string]interface{}{
+ "one": json.Number("2"),
+ },
+ "baz": map[string]interface{}{},
+ })
+
+ sub := json.RawMessage(`{"foo":"bar"}`)
+ cfg5 := map[string]interface{}{
+ "core": map[string]*json.RawMessage{
+ "proxy": nil,
+ "sub": &sub,
+ },
+ }
+ config.PurgeNulls(cfg5)
+ c.Check(cfg5, DeepEquals, map[string]interface{}{
+ "core": map[string]*json.RawMessage{
+ "sub": &sub,
+ },
+ })
+}
+
+func (s *configHelpersSuite) TestPurgeNullsTopLevelNull(c *C) {
+ cfgJSON := `{
+ "experimental": {
+ "parallel-instances": true,
+ "snapd-snap": true
+ },
+ "proxy": null,
+ "seed": {
+ "loaded": true
+ }
+}`
+ var cfg map[string]*json.RawMessage
+ err := jsonutil.DecodeWithNumber(bytes.NewReader([]byte(cfgJSON)), &cfg)
+ c.Assert(err, IsNil)
+
+ config.PurgeNulls(cfg)
+
+ cfgJSON2, err := json.Marshal(cfg)
+ c.Assert(err, IsNil)
+
+ var out interface{}
+ jsonutil.DecodeWithNumber(bytes.NewReader(cfgJSON2), &out)
+ c.Check(out, DeepEquals, map[string]interface{}{
+ "experimental": map[string]interface{}{
+ "parallel-instances": true,
+ "snapd-snap": true,
+ },
+ "seed": map[string]interface{}{
+ "loaded": true,
+ },
+ })
+}
diff -Nru snapd-2.40/overlord/configstate/config/transaction.go snapd-2.42.1/overlord/configstate/config/transaction.go
--- snapd-2.40/overlord/configstate/config/transaction.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/config/transaction.go 2019-10-30 12:17:43.000000000 +0000
@@ -77,8 +77,13 @@
// check if we need to dive into a sub-config
var configm map[string]interface{}
if err := jsonutil.DecodeWithNumber(bytes.NewReader(*subCfg), &configm); err == nil {
- out = append(out, changes(cfgStr+"."+k, configm)...)
- continue
+ // curiously, json decoder decodes json.RawMessage("null") into a nil map, so no change is
+ // reported when we recurse into it. This happens when unsetting a key and the underlying
+ // config path doesn't exist.
+ if len(configm) > 0 {
+ out = append(out, changes(cfgStr+"."+k, configm)...)
+ continue
+ }
}
out = append(out, []string{cfgStr + "." + k}...)
default:
@@ -172,6 +177,8 @@
// commit changes onto a copy of pristine configuration, so that get has a complete view of the config.
config := t.copyPristine(snapName)
applyChanges(config, t.changes[snapName])
+
+ purgeNulls(config)
return getFromConfig(snapName, subkeys, 0, config, result)
}
@@ -254,6 +261,7 @@
config = make(map[string]*json.RawMessage)
}
applyChanges(config, snapChanges)
+ purgeNulls(config)
t.pristine[instanceName] = config
}
diff -Nru snapd-2.40/overlord/configstate/config/transaction_test.go snapd-2.42.1/overlord/configstate/config/transaction_test.go
--- snapd-2.40/overlord/configstate/config/transaction_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/config/transaction_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -90,8 +90,8 @@
var setGetTests = [][]setGetOp{{
// Basics.
- `set n=null`,
- `get n=null`,
+ `get foo=-`,
+ `getroot => snap "core" has no configuration`,
`set one=1 two=2`,
`set big=1234567890`,
`setunder three=3 big=9876543210`,
@@ -99,7 +99,6 @@
`getunder one=- two=- three=3 big=9876543210`,
`changes core.big core.one core.two`,
`commit`,
- `get n=null`,
`getunder one=1 two=2 three=3`,
`get one=1 two=2 three=3`,
`set two=22 four=4 big=1234567890`,
@@ -114,6 +113,105 @@
`get doc={"one":1,"two":2}`,
`changes core.doc.one core.doc.two`,
}, {
+ // Nulls via dotted path
+ `set doc={"one":1,"two":2}`,
+ `commit`,
+ `set doc.one=null`,
+ `changes core.doc.one`,
+ `get doc={"two":2}`,
+ `getunder doc={"one":1,"two":2}`,
+ `commit`,
+ `get doc={"two":2}`,
+ `getroot ={"doc":{"two":2}}`,
+ `getunder doc={"two":2}`, // nils are not committed to state
+}, {
+ // Nulls via dotted path, resuling in empty map
+ `set doc={"one":{"three":3},"two":2}`,
+ `set doc.one.three=null`,
+ `changes core.doc.one.three core.doc.two`,
+ `get doc={"one":{},"two":2}`,
+ `commit`,
+ `get doc={"one":{},"two":2}`,
+ `getunder doc={"one":{},"two":2}`, // nils are not committed to state
+}, {
+ // Nulls via dotted path in a doc
+ `set doc={"one":1,"two":2}`,
+ `set doc.three={"four":4}`,
+ `get doc={"one":1,"two":2,"three":{"four":4}}`,
+ `set doc.three={"four":null}`,
+ `changes core.doc.one core.doc.three.four core.doc.two`,
+ `get doc={"one":1,"two":2,"three":{}}`,
+ `commit`,
+ `get doc={"one":1,"two":2,"three":{}}`,
+ `getunder doc={"one":1,"two":2,"three":{}}`, // nils are not committed to state
+}, {
+ // Nulls nested in a document
+ `set doc={"one":{"three":3,"two":2}}`,
+ `changes core.doc.one.three core.doc.one.two`,
+ `set doc={"one":{"three":null,"two":2}}`,
+ `changes core.doc.one.three core.doc.one.two`,
+ `get doc={"one":{"two":2}}`,
+ `commit`,
+ `get doc={"one":{"two":2}}`,
+ `getunder doc={"one":{"two":2}}`, // nils are not committed to state
+}, {
+ // Nulls with mutating
+ `set doc={"one":{"two":2}}`,
+ `set doc.one.two=null`,
+ `changes core.doc.one.two`,
+ `set doc.one="foo"`,
+ `get doc.one="foo"`,
+ `commit`,
+ `get doc={"one":"foo"}`,
+ `getunder doc={"one":"foo"}`, // nils are not committed to state
+}, {
+ // Nulls, intermediate temporary maps
+ `set doc={"one":{"two":2}}`,
+ `commit`,
+ `set doc.one.three.four.five=null`,
+ `get doc={"one":{"two":2,"three":{"four":{}}}}`,
+ `commit`,
+ `get doc={"one":{"two":2,"three":{"four":{}}}}`,
+ `getrootunder ={"doc":{"one":{"two":2,"three":{"four":{}}}}}`, // nils are not committed to state
+}, {
+ // Nulls, same transaction
+ `set doc={"one":{"two":2}}`,
+ `set doc.one.three.four.five=null`,
+ `changes core.doc.one.three.four.five core.doc.one.two`,
+ `get doc={"one":{"two":2,"three":{"four":{}}}}`,
+ `commit`,
+ `get doc={"one":{"two":2,"three":{"four":{}}}}`,
+ `getrootunder ={"doc":{"one":{"two":2,"three":{"four":{}}}}}`, // nils are not committed to state
+}, {
+ // Null leading to empty doc
+ `set doc={"one":1}`,
+ `set doc.one=null`,
+ `changes core.doc.one`,
+ `commit`,
+ `get doc={}`,
+}, {
+ // Nulls leading to no snap configuration
+ `set doc="foo"`,
+ `set doc=null`,
+ `changes core.doc`,
+ `commit`,
+ `get doc=-`,
+ `getroot => snap "core" has no configuration`,
+}, {
+ // set null over non-existing path
+ `set x.y.z=null`,
+ `changes core.x.y.z`,
+ `commit`,
+ `get x.y.z=-`,
+}, {
+ // set null over non-existing path with initial config
+ `set foo=bar`,
+ `commit`,
+ `set x=null`,
+ `changes core.x`,
+ `commit`,
+ `get x=-`,
+}, {
// Root doc
`set doc={"one":1,"two":2}`,
`changes core.doc.one core.doc.two`,
@@ -297,7 +395,12 @@
}
case "getroot":
var obtained interface{}
- c.Assert(t.Get(snap, "", &obtained), IsNil)
+ err := t.Get(snap, "", &obtained)
+ if op.fails() {
+ c.Assert(err, ErrorMatches, op.error())
+ continue
+ }
+ c.Assert(err, IsNil)
c.Assert(obtained, DeepEquals, op.args()[""])
case "getrootunder":
var config map[string]*json.RawMessage
diff -Nru snapd-2.40/overlord/configstate/configcore/corecfg.go snapd-2.42.1/overlord/configstate/configcore/corecfg.go
--- snapd-2.40/overlord/configstate/configcore/corecfg.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/configcore/corecfg.go 2019-10-30 12:17:43.000000000 +0000
@@ -99,7 +99,7 @@
}
// Export experimental.* flags to a place easily accessible from snapd helpers.
- if err := handleExperimentalFlags(tr); err != nil {
+ if err := ExportExperimentalFlags(tr); err != nil {
return err
}
diff -Nru snapd-2.40/overlord/configstate/configcore/experimental.go snapd-2.42.1/overlord/configstate/configcore/experimental.go
--- snapd-2.40/overlord/configstate/configcore/experimental.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/configcore/experimental.go 2019-10-30 12:17:43.000000000 +0000
@@ -48,7 +48,7 @@
return nil
}
-func handleExperimentalFlags(tr config.Conf) error {
+func ExportExperimentalFlags(tr config.Conf) error {
dir := dirs.FeaturesDir
if err := os.MkdirAll(dir, 0755); err != nil {
return err
diff -Nru snapd-2.40/overlord/configstate/configcore/picfg.go snapd-2.42.1/overlord/configstate/configcore/picfg.go
--- snapd-2.40/overlord/configstate/configcore/picfg.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/configcore/picfg.go 2019-10-30 12:17:43.000000000 +0000
@@ -54,6 +54,7 @@
"sdtv_aspect": true,
"config_hdmi_boost": true,
"hdmi_force_hotplug": true,
+ "start_x": true,
}
func init() {
diff -Nru snapd-2.40/overlord/configstate/configmgr.go snapd-2.42.1/overlord/configstate/configmgr.go
--- snapd-2.40/overlord/configstate/configmgr.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/configmgr.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,14 +20,17 @@
package configstate
import (
+ "fmt"
"regexp"
"github.com/snapcore/snapd/overlord/configstate/config"
"github.com/snapcore/snapd/overlord/configstate/configcore"
"github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/state"
)
var configcoreRun = configcore.Run
+var configcoreExportExperimentalFlags = configcore.ExportExperimentalFlags
func MockConfigcoreRun(f func(config.Conf) error) (restore func()) {
origConfigcoreRun := configcoreRun
@@ -37,7 +40,15 @@
}
}
-func Init(hookManager *hookstate.HookManager) {
+func MockConfigcoreExportExperimentalFlags(mock func(tr config.Conf) error) (restore func()) {
+ old := configcoreExportExperimentalFlags
+ configcoreExportExperimentalFlags = mock
+ return func() {
+ configcoreExportExperimentalFlags = old
+ }
+}
+
+func Init(st *state.State, hookManager *hookstate.HookManager) error {
// Most configuration is handled via the "configure" hook of the
// snaps. However some configuration is internally handled
hookManager.Register(regexp.MustCompile("^configure$"), newConfigureHandler)
@@ -50,4 +61,12 @@
ctx.Unlock()
return configcoreRun(tr)
})
+
+ st.Lock()
+ defer st.Unlock()
+ tr := config.NewTransaction(st)
+ if err := configcoreExportExperimentalFlags(tr); err != nil {
+ return fmt.Errorf("cannot export experimental config flags: %v", err)
+ }
+ return nil
}
diff -Nru snapd-2.40/overlord/configstate/configstate.go snapd-2.42.1/overlord/configstate/configstate.go
--- snapd-2.40/overlord/configstate/configstate.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/configstate.go 2019-10-30 12:17:43.000000000 +0000
@@ -65,12 +65,16 @@
}
// the "snapd" snap cannot be configured yet
- if snapName == "snapd" {
+ typ, err := snapst.Type()
+ if err != nil {
+ return err
+ }
+ if typ == snap.TypeSnapd {
return fmt.Errorf(`cannot configure the "snapd" snap, please use "system" instead`)
}
// bases cannot be configured for now
- typ, err := snapst.Type()
+ typ, err = snapst.Type()
if err != nil {
return err
}
diff -Nru snapd-2.40/overlord/configstate/configstate_test.go snapd-2.42.1/overlord/configstate/configstate_test.go
--- snapd-2.40/overlord/configstate/configstate_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/configstate_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,6 +20,7 @@
package configstate_test
import (
+ "fmt"
"time"
. "gopkg.in/check.v1"
@@ -31,6 +32,7 @@
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
)
type tasksetsSuite struct {
@@ -206,7 +208,7 @@
},
Current: snap.R(1),
Active: true,
- SnapType: "app",
+ SnapType: "snapd",
})
_, err := configstate.ConfigureInstalled(s.state, "snapd", patch, 0)
@@ -214,17 +216,26 @@
}
type configcoreHijackSuite struct {
+ testutil.BaseTest
+
o *overlord.Overlord
state *state.State
}
func (s *configcoreHijackSuite) SetUpTest(c *C) {
+ s.BaseTest.SetUpTest(c)
s.o = overlord.Mock()
s.state = s.o.State()
hookMgr, err := hookstate.Manager(s.state, s.o.TaskRunner())
c.Assert(err, IsNil)
s.o.AddManager(hookMgr)
- configstate.Init(hookMgr)
+ r := configstate.MockConfigcoreExportExperimentalFlags(func(_ config.Conf) error {
+ return nil
+ })
+ s.AddCleanup(r)
+
+ err = configstate.Init(s.state, hookMgr)
+ c.Assert(err, IsNil)
s.o.AddManager(s.o.TaskRunner())
}
@@ -309,3 +320,52 @@
c.Assert(configstate.RemapSnapFromRequest("system"), Equals, "core")
c.Assert(configstate.RemapSnapToResponse("core"), Equals, "system")
}
+
+type configcoreExportSuite struct {
+ o *overlord.Overlord
+ state *state.State
+ hookMgr *hookstate.HookManager
+}
+
+func (s *configcoreExportSuite) SetUpTest(c *C) {
+ s.o = overlord.Mock()
+ s.state = s.o.State()
+ hookMgr, err := hookstate.Manager(s.state, s.o.TaskRunner())
+ c.Assert(err, IsNil)
+ s.o.AddManager(hookMgr)
+ s.hookMgr = hookMgr
+}
+
+func (s *configcoreExportSuite) TestExportHappy(c *C) {
+ var calls int
+ var val string
+
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "experimental.key", "foobar")
+ tr.Commit()
+
+ r := configstate.MockConfigcoreExportExperimentalFlags(func(conf config.Conf) error {
+ calls++
+ err := conf.Get("core", "experimental.keys", &val)
+ c.Assert(err, IsNil)
+ return nil
+ })
+ defer r()
+ err := configstate.Init(s.state, s.hookMgr)
+ c.Assert(err, IsNil)
+ c.Assert(calls, Equals, 1)
+ c.Assert(val, Equals, "foobar")
+}
+
+func (s *configcoreExportSuite) TestExportErr(c *C) {
+ var calls int
+
+ r := configstate.MockConfigcoreExportExperimentalFlags(func(conf config.Conf) error {
+ calls++
+ return fmt.Errorf("bad bad")
+ })
+ defer r()
+ err := configstate.Init(s.state, s.hookMgr)
+ c.Assert(err, ErrorMatches, "cannot export experimental config flags: bad bad")
+ c.Assert(calls, Equals, 1)
+}
diff -Nru snapd-2.40/overlord/configstate/export_test.go snapd-2.42.1/overlord/configstate/export_test.go
--- snapd-2.40/overlord/configstate/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,3 +20,4 @@
package configstate
var NewConfigureHandler = newConfigureHandler
+var SortPatchKeysByDepth = sortPatchKeysByDepth
diff -Nru snapd-2.40/overlord/configstate/handler_test.go snapd-2.42.1/overlord/configstate/handler_test.go
--- snapd-2.40/overlord/configstate/handler_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/handler_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -26,11 +26,14 @@
. "gopkg.in/check.v1"
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/overlord/configstate"
"github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/hookstate/hooktest"
"github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
@@ -108,6 +111,21 @@
c.Check(value, Equals, "bar")
}
+func makeModel(override map[string]interface{}) *asserts.Model {
+ model := map[string]interface{}{
+ "type": "model",
+ "authority-id": "brand",
+ "series": "16",
+ "brand-id": "brand",
+ "model": "baz-3000",
+ "architecture": "armhf",
+ "gadget": "brand-gadget",
+ "kernel": "kernel",
+ "timestamp": "2018-01-01T08:00:00+00:00",
+ }
+ return assertstest.FakeAssertion(model, override).(*asserts.Model)
+}
+
func (s *configureHandlerSuite) TestBeforeInitializesTransactionUseDefaults(c *C) {
r := release.MockOnClassic(false)
defer r()
@@ -141,6 +159,11 @@
SnapType: "gadget",
})
+ r = snapstatetest.MockDeviceModel(makeModel(map[string]interface{}{
+ "gadget": "canonical-pc",
+ }))
+ defer r()
+
const mockTestSnapYaml = `
name: test-snap
hooks:
@@ -210,6 +233,11 @@
SnapType: "gadget",
})
+ r = snapstatetest.MockDeviceModel(makeModel(map[string]interface{}{
+ "gadget": "canonical-pc",
+ }))
+ defer r()
+
snapstate.Set(s.state, "test-snap", &snapstate.SnapState{
Active: true,
Sequence: []*snap.SideInfo{
diff -Nru snapd-2.40/overlord/configstate/helpers.go snapd-2.42.1/overlord/configstate/helpers.go
--- snapd-2.40/overlord/configstate/helpers.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/helpers.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,42 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package configstate
+
+import (
+ "sort"
+ "strings"
+)
+
+func sortPatchKeysByDepth(patch map[string]interface{}) []string {
+ if len(patch) == 0 {
+ return nil
+ }
+ depths := make(map[string]int, len(patch))
+ keys := make([]string, 0, len(patch))
+ for k := range patch {
+ depths[k] = strings.Count(k, ".")
+ keys = append(keys, k)
+ }
+
+ sort.Slice(keys, func(i, j int) bool {
+ return depths[keys[i]] < depths[keys[j]]
+ })
+ return keys
+}
diff -Nru snapd-2.40/overlord/configstate/helpers_test.go snapd-2.42.1/overlord/configstate/helpers_test.go
--- snapd-2.40/overlord/configstate/helpers_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/helpers_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,45 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package configstate_test
+
+import (
+ "github.com/snapcore/snapd/overlord/configstate"
+
+ . "gopkg.in/check.v1"
+)
+
+func (s *miscSuite) TestSortPatchKeysEmpty(c *C) {
+ patch := map[string]interface{}{}
+ keys := configstate.SortPatchKeysByDepth(patch)
+ c.Assert(keys, IsNil)
+}
+
+func (s *miscSuite) TestSortPatchKeys(c *C) {
+ patch := map[string]interface{}{
+ "a.b.c": 0,
+ "a": 0,
+ "a.b.c.d": 0,
+ "q.w.e.r.t.y.u": 0,
+ "f.g": 0,
+ }
+
+ keys := configstate.SortPatchKeysByDepth(patch)
+ c.Assert(keys, DeepEquals, []string{"a", "f.g", "a.b.c", "a.b.c.d", "q.w.e.r.t.y.u"})
+}
diff -Nru snapd-2.40/overlord/configstate/hooks.go snapd-2.42.1/overlord/configstate/hooks.go
--- snapd-2.40/overlord/configstate/hooks.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/configstate/hooks.go 2019-10-30 12:17:43.000000000 +0000
@@ -86,8 +86,13 @@
instanceName := h.context.InstanceName()
st := h.context.State()
if useDefaults {
- var err error
- patch, err = snapstate.ConfigDefaults(st, instanceName)
+ task, _ := h.context.Task()
+ deviceCtx, err := snapstate.DeviceCtx(st, task, nil)
+ if err != nil {
+ return err
+ }
+
+ patch, err = snapstate.ConfigDefaults(st, deviceCtx, instanceName)
if err != nil && err != state.ErrNoState {
return err
}
@@ -107,8 +112,9 @@
}
}
- for key, value := range patch {
- if err := tr.Set(instanceName, key, value); err != nil {
+ patchKeys := sortPatchKeysByDepth(patch)
+ for _, key := range patchKeys {
+ if err := tr.Set(instanceName, key, patch[key]); err != nil {
return err
}
}
diff -Nru snapd-2.40/overlord/devicestate/devicectx.go snapd-2.42.1/overlord/devicestate/devicectx.go
--- snapd-2.40/overlord/devicestate/devicectx.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/devicestate/devicectx.go 2019-10-30 12:17:43.000000000 +0000
@@ -25,11 +25,23 @@
"github.com/snapcore/snapd/overlord/state"
)
-// DeviceCtx picks a device context from state, optional task or an optionally pre-provided one. Returns ErrNoState if a model assertion is not yet known.
+// DeviceCtx picks a device context from state, optional task or an
+// optionally pre-provided one. Returns ErrNoState if a model
+// assertion is not yet known.
+// In particular if task belongs to a remodeling change this will find
+// the appropriate remodel context.
func DeviceCtx(st *state.State, task *state.Task, providedDeviceCtx snapstate.DeviceContext) (snapstate.DeviceContext, error) {
if providedDeviceCtx != nil {
return providedDeviceCtx, nil
}
+ // use the remodelContext if the task is part of a remodel change
+ remodCtx, err := remodelCtxFromTask(task)
+ if err == nil {
+ return remodCtx, nil
+ }
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
modelAs, err := findModel(st)
if err != nil {
return nil, err
diff -Nru snapd-2.40/overlord/devicestate/devicemgr.go snapd-2.42.1/overlord/devicestate/devicemgr.go
--- snapd-2.40/overlord/devicestate/devicemgr.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/devicestate/devicemgr.go 2019-10-30 12:17:43.000000000 +0000
@@ -27,7 +27,7 @@
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/asserts/sysdb"
- "github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/overlord/assertstate"
@@ -92,8 +92,11 @@
runner.AddHandler("generate-device-key", m.doGenerateDeviceKey, nil)
runner.AddHandler("request-serial", m.doRequestSerial, nil)
runner.AddHandler("mark-seeded", m.doMarkSeeded, nil)
+ runner.AddHandler("prepare-remodeling", m.doPrepareRemodeling, nil)
+ runner.AddCleanup("prepare-remodeling", m.cleanupRemodel)
// this *must* always run last and finalizes a remodel
runner.AddHandler("set-model", m.doSetModel, nil)
+ runner.AddCleanup("set-model", m.cleanupRemodel)
// There is no undo for successful gadget updates. The system is
// rebooted during update, if it boots up to the point where snapd runs
// we deem the new assets (be it bootloader or firmware) functional. The
@@ -435,11 +438,7 @@
}
if !m.bootOkRan {
- loader, err := bootloader.Find()
- if err != nil {
- return fmt.Errorf(i18n.G("cannot mark boot successful: %s"), err)
- }
- if err := bootloader.MarkBootSuccessful(loader); err != nil {
+ if err := boot.MarkBootSuccessful(); err != nil {
return err
}
m.bootOkRan = true
diff -Nru snapd-2.40/overlord/devicestate/devicestate.go snapd-2.42.1/overlord/devicestate/devicestate.go
--- snapd-2.40/overlord/devicestate/devicestate.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/devicestate/devicestate.go 2019-10-30 12:17:43.000000000 +0000
@@ -22,6 +22,7 @@
package devicestate
import (
+ "context"
"fmt"
"sync"
@@ -38,6 +39,7 @@
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/naming"
)
var (
@@ -288,26 +290,9 @@
return false
}
-func getAllRequiredSnapsForModel(model *asserts.Model) map[string]bool {
- reqSnaps := model.RequiredSnaps()
- // +4 for (snapd, base, gadget, kernel)
- required := make(map[string]bool, len(reqSnaps)+4)
- for _, snap := range reqSnaps {
- required[snap] = true
- }
- if model.Base() != "" {
- required["snapd"] = true
- required[model.Base()] = true
- } else {
- required["core"] = true
- }
- if model.Kernel() != "" {
- required[model.Kernel()] = true
- }
- if model.Gadget() != "" {
- required[model.Gadget()] = true
- }
- return required
+func getAllRequiredSnapsForModel(model *asserts.Model) *naming.SnapSet {
+ reqSnaps := model.RequiredWithEssentialSnaps()
+ return naming.NewSnapSet(reqSnaps)
}
// extractDownloadInstallEdgesFromTs extracts the first, last download
@@ -333,25 +318,42 @@
return firstDl, tasks[edgeTaskIndex], tasks[edgeTaskIndex+1], lastInst, nil
}
-func remodelTasks(st *state.State, current, new *asserts.Model, deviceCtx snapstate.DeviceContext) ([]*state.TaskSet, error) {
+func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Model, deviceCtx snapstate.DeviceContext, fromChange string) ([]*state.TaskSet, error) {
userID := 0
+ var tss []*state.TaskSet
// adjust kernel track
- var tss []*state.TaskSet
- if current.KernelTrack() != new.KernelTrack() {
- ts, err := snapstateUpdateWithDeviceContext(st, new.Kernel(), &snapstate.RevisionOptions{Channel: new.KernelTrack()}, userID, snapstate.Flags{NoReRefresh: true}, deviceCtx)
+ if current.Kernel() == new.Kernel() && current.KernelTrack() != new.KernelTrack() {
+ ts, err := snapstateUpdateWithDeviceContext(st, new.Kernel(), &snapstate.RevisionOptions{Channel: new.KernelTrack()}, userID, snapstate.Flags{NoReRefresh: true}, deviceCtx, fromChange)
if err != nil {
return nil, err
}
tss = append(tss, ts)
}
+ // add new kernel
+ if current.Kernel() != new.Kernel() {
+ // TODO: we need to support corner cases here like:
+ // 0. start with "old-kernel"
+ // 1. remodel to "new-kernel"
+ // 2. remodel back to "old-kernel"
+ // In step (2) we will get a "already-installed" error
+ // here right now (workaround: remove "old-kernel")
+ ts, err := snapstateInstallWithDeviceContext(ctx, st, new.Kernel(), &snapstate.RevisionOptions{Channel: new.KernelTrack()}, userID, snapstate.Flags{}, deviceCtx, fromChange)
+ if err != nil {
+ return nil, err
+ }
+ tss = append(tss, ts)
+ }
+
// add new required-snaps, no longer required snaps will be cleaned
// in "set-model"
- for _, snapName := range new.RequiredSnaps() {
- _, err := snapstate.CurrentInfo(st, snapName)
+ for _, snapRef := range new.RequiredNoEssentialSnaps() {
+ // TODO|XXX: have methods that take refs directly
+ // to respect the snap ids
+ _, err := snapstate.CurrentInfo(st, snapRef.SnapName())
// If the snap is not installed we need to install it now.
if _, ok := err.(*snap.NotInstalledError); ok {
- ts, err := snapstateInstallWithDeviceContext(st, snapName, nil, userID, snapstate.Flags{Required: true}, deviceCtx)
+ ts, err := snapstateInstallWithDeviceContext(ctx, st, snapRef.SnapName(), nil, userID, snapstate.Flags{Required: true}, deviceCtx, fromChange)
if err != nil {
return nil, err
}
@@ -437,7 +439,6 @@
// TODO:
// - Check estimated disk size delta
// - Reapply gadget connections as needed
-// - Need new session/serial if changing store or model
// - Check all relevant snaps exist in new store
// (need to check that even unchanged snaps are accessible)
func Remodel(st *state.State, new *asserts.Model) (*state.Change, error) {
@@ -459,18 +460,11 @@
}
// TODO: we need dedicated assertion language to permit for
- // model transitions before we allow that cross vault
+ // model transitions before we allow cross vault
// transitions.
- //
- // Right now we only allow "remodel" to a different revision of
- // the same model.
remodelKind := ClassifyRemodel(current, new)
- if remodelKind == ReregRemodel {
- return nil, fmt.Errorf("cannot remodel to different brand/model yet")
- }
-
// TODO: should we restrict remodel from one arch to another?
// There are valid use-cases here though, i.e. amd64 machine that
// remodels itself to/from i386 (if the HW can do both 32/64 bit)
@@ -483,11 +477,6 @@
if current.Base() != new.Base() {
return nil, fmt.Errorf("cannot remodel to different bases yet")
}
- // FIXME: we need to support this soon but right now only a single
- // snap of type "gadget/kernel" is allowed so this needs work
- if current.Kernel() != new.Kernel() {
- return nil, fmt.Errorf("cannot remodel to different kernels yet")
- }
if current.Gadget() != new.Gadget() {
return nil, fmt.Errorf("cannot remodel to different gadgets yet")
}
@@ -502,7 +491,23 @@
return nil, err
}
- if remodelKind == StoreSwitchRemodel {
+ var tss []*state.TaskSet
+ switch remodelKind {
+ case ReregRemodel:
+ // nothing else can be in-flight
+ for _, chg := range st.Changes() {
+ if !chg.IsReady() {
+ return nil, &snapstate.ChangeConflictError{Message: "cannot start complete remodel, other changes are in progress"}
+ }
+ }
+
+ requestSerial := st.NewTask("request-serial", i18n.G("Request new device serial"))
+
+ prepare := st.NewTask("prepare-remodeling", i18n.G("Prepare remodeling"))
+ prepare.WaitFor(requestSerial)
+ ts := state.NewTaskSet(requestSerial, prepare)
+ tss = []*state.TaskSet{ts}
+ case StoreSwitchRemodel:
sto := remodCtx.Store()
if sto == nil {
return nil, fmt.Errorf("internal error: a store switch remodeling should have built a store")
@@ -514,22 +519,34 @@
if err != nil {
return nil, fmt.Errorf("cannot get a store session based on the new model assertion: %v", err)
}
+ fallthrough
+ case UpdateRemodel:
+ var err error
+ tss, err = remodelTasks(context.TODO(), st, current, new, remodCtx, "")
+ if err != nil {
+ return nil, err
+ }
}
- tss, err := remodelTasks(st, current, new, remodCtx)
+ // we potentially released the lock a couple of times here:
+ // make sure the current model is essentially the same as when
+ // we started
+ current1, err := findModel(st)
if err != nil {
return nil, err
}
-
- // TODO: we released the lock a couple of times here:
- // make sure the current model is essentially the same
- // make sure no remodeling is in progress
+ if current.BrandID() != current1.BrandID() || current.Model() != current1.Model() || current.Revision() != current1.Revision() {
+ return nil, &snapstate.ChangeConflictError{Message: fmt.Sprintf("cannot start remodel, clashing with concurrent remodel to %v/%v (%v)", current1.BrandID(), current1.Model(), current1.Revision())}
+ }
+ // make sure another unfinished remodel wasn't already setup either
+ if Remodeling(st) {
+ return nil, &snapstate.ChangeConflictError{Message: "cannot start remodel, clashing with concurrent one"}
+ }
var msg string
if current.BrandID() == new.BrandID() && current.Model() == new.Model() {
msg = fmt.Sprintf(i18n.G("Refresh model assertion from revision %v to %v"), current.Revision(), new.Revision())
} else {
- // TODO: add test once we support this kind of remodel
msg = fmt.Sprintf(i18n.G("Remodel device to %v/%v (%v)"), new.BrandID(), new.Model(), new.Revision())
}
diff -Nru snapd-2.40/overlord/devicestate/devicestatetest/devicesvc.go snapd-2.42.1/overlord/devicestate/devicestatetest/devicesvc.go
--- snapd-2.40/overlord/devicestate/devicestatetest/devicesvc.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/devicestate/devicestatetest/devicesvc.go 2019-10-30 12:17:43.000000000 +0000
@@ -23,7 +23,6 @@
"bytes"
"fmt"
"io"
- "io/ioutil"
"net/http"
"net/http/httptest"
"sync"
@@ -38,13 +37,14 @@
type DeviceServiceBehavior struct {
ReqID string
- RequestIDURLPath string
- SerialURLPath string
+ RequestIDURLPath string
+ SerialURLPath string
+ ExpectedCapabilities string
Head func(c *C, bhv *DeviceServiceBehavior, w http.ResponseWriter, r *http.Request)
PostPreflight func(c *C, bhv *DeviceServiceBehavior, w http.ResponseWriter, r *http.Request)
- SignSerial func(c *C, bhv *DeviceServiceBehavior, headers map[string]interface{}, body []byte) (asserts.Assertion, error)
+ SignSerial func(c *C, bhv *DeviceServiceBehavior, headers map[string]interface{}, body []byte) (serial asserts.Assertion, ancillary []asserts.Assertion, err error)
}
// Request IDs for hard-coded behaviors.
@@ -68,6 +68,10 @@
bhv.RequestIDURLPath = requestIDURLPath
bhv.SerialURLPath = serialURLPath
}
+ // currently supported
+ if bhv.ExpectedCapabilities == "" {
+ bhv.ExpectedCapabilities = "serial-stream"
+ }
var mu sync.Mutex
count := 0
@@ -105,18 +109,28 @@
io.WriteString(w, fmt.Sprintf(`{"request-id": "%s"}`, bhv.ReqID))
case bhv.SerialURLPath:
c.Check(r.Header.Get("User-Agent"), Equals, expectedUserAgent)
+ c.Check(r.Header.Get("Snap-Device-Capabilities"), Equals, bhv.ExpectedCapabilities)
mu.Lock()
serialNum := 9999 + count
count++
mu.Unlock()
- b, err := ioutil.ReadAll(r.Body)
- c.Assert(err, IsNil)
- a, err := asserts.Decode(b)
+ dec := asserts.NewDecoder(r.Body)
+
+ a, err := dec.Decode()
c.Assert(err, IsNil)
serialReq, ok := a.(*asserts.SerialRequest)
c.Assert(ok, Equals, true)
+ extra := []asserts.Assertion{}
+ for {
+ a1, err := dec.Decode()
+ if err == io.EOF {
+ break
+ }
+ c.Assert(err, IsNil)
+ extra = append(extra, a1)
+ }
err = asserts.SignatureCheck(serialReq, serialReq.DeviceKey())
c.Assert(err, IsNil)
brandID := serialReq.BrandID()
@@ -139,7 +153,19 @@
// use proposed serial
serialStr = serialReq.Serial()
}
- serial, err := bhv.SignSerial(c, bhv, map[string]interface{}{
+ if serialReq.HeaderString("original-model") != "" {
+ // re-registration
+ c.Check(extra, HasLen, 2)
+ _, ok := extra[0].(*asserts.Model)
+ c.Check(ok, Equals, true)
+ origSerial, ok := extra[1].(*asserts.Serial)
+ c.Check(ok, Equals, true)
+ c.Check(origSerial.DeviceKey(), DeepEquals, serialReq.DeviceKey())
+ // TODO: more checks once we have Original* accessors
+ } else {
+ c.Check(extra, HasLen, 0)
+ }
+ serial, ancillary, err := bhv.SignSerial(c, bhv, map[string]interface{}{
"authority-id": "canonical",
"brand-id": brandID,
"model": model,
@@ -151,11 +177,18 @@
c.Assert(err, IsNil)
w.Header().Set("Content-Type", asserts.MediaType)
w.WriteHeader(200)
- encoded := asserts.Encode(serial)
if reqID == ReqIDSerialWithBadModel {
+ encoded := asserts.Encode(serial)
+
encoded = bytes.Replace(encoded, []byte("model: pc"), []byte("model: bad-model-foo"), 1)
+ w.Write(encoded)
+ return
+ }
+ enc := asserts.NewEncoder(w)
+ enc.Encode(serial)
+ for _, a := range ancillary {
+ enc.Encode(a)
}
- w.Write(encoded)
}
}))
}
diff -Nru snapd-2.40/overlord/devicestate/devicestate_test.go snapd-2.42.1/overlord/devicestate/devicestate_test.go
--- snapd-2.40/overlord/devicestate/devicestate_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/devicestate/devicestate_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,6 +20,7 @@
package devicestate_test
import (
+ "context"
"errors"
"fmt"
"io/ioutil"
@@ -29,6 +30,7 @@
"net/url"
"os"
"path/filepath"
+ "strings"
"testing"
"time"
@@ -39,8 +41,8 @@
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/asserts/assertstest"
"github.com/snapcore/snapd/asserts/sysdb"
- "github.com/snapcore/snapd/boot/boottest"
"github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/bootloader/bootloadertest"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/gadget"
"github.com/snapcore/snapd/httputil"
@@ -66,6 +68,7 @@
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/store/storetest"
"github.com/snapcore/snapd/strutil"
+ "github.com/snapcore/snapd/testutil"
"github.com/snapcore/snapd/timings"
)
@@ -79,13 +82,15 @@
mgr *devicestate.DeviceManager
db *asserts.Database
- bootloader *boottest.MockBootloader
+ bootloader *bootloadertest.MockBootloader
storeSigning *assertstest.StoreStack
brands *assertstest.SigningAccounts
reqID string
+ ancillary []asserts.Assertion
+
restartRequests []state.RestartType
restoreOnClassic func()
@@ -119,7 +124,8 @@
}
var (
- brandPrivKey, _ = assertstest.GenerateKey(752)
+ brandPrivKey, _ = assertstest.GenerateKey(752)
+ brandPrivKey2, _ = assertstest.GenerateKey(752)
)
func (s *deviceMgrSuite) SetUpTest(c *C) {
@@ -130,23 +136,26 @@
s.restoreSanitize = snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})
- s.bootloader = boottest.NewMockBootloader("mock", c.MkDir())
+ s.bootloader = bootloadertest.Mock("mock", c.MkDir())
bootloader.Force(s.bootloader)
s.restoreOnClassic = release.MockOnClassic(false)
s.storeSigning = assertstest.NewStoreStack("canonical", nil)
- s.o = overlord.Mock()
- s.o.SetRestartHandler(func(req state.RestartType) {
+ s.o = overlord.MockWithRestartHandler(func(req state.RestartType) {
s.restartRequests = append(s.restartRequests, req)
})
s.state = s.o.State()
+ s.state.Lock()
+ s.state.VerifyReboot("boot-id-0")
+ s.state.Unlock()
s.se = s.o.StateEngine()
s.restoreGenericClassicMod = sysdb.MockGenericClassicModel(s.storeSigning.GenericClassicModel)
s.brands = assertstest.NewSigningAccounts(s.storeSigning)
s.brands.Register("my-brand", brandPrivKey, nil)
+ s.brands.Register("rereg-brand", brandPrivKey2, nil)
db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
Backstore: asserts.NewMemoryBackstore(),
@@ -180,6 +189,8 @@
}
s.o.TaskRunner().AddHandler("error-trigger", erroringHandler, nil)
+ c.Assert(s.o.StartUp(), IsNil)
+
s.state.Lock()
snapstate.ReplaceStore(s.state, &fakeStore{
state: s.state,
@@ -193,6 +204,7 @@
}
func (s *deviceMgrSuite) TearDownTest(c *C) {
+ s.ancillary = nil
s.state.Lock()
assertstate.ReplaceDB(s.state, nil)
s.state.Unlock()
@@ -216,11 +228,13 @@
chg.SetStatus(state.DoingStatus)
}
-func (s *deviceMgrSuite) signSerial(c *C, bhv *devicestatetest.DeviceServiceBehavior, headers map[string]interface{}, body []byte) (asserts.Assertion, error) {
+func (s *deviceMgrSuite) signSerial(c *C, bhv *devicestatetest.DeviceServiceBehavior, headers map[string]interface{}, body []byte) (serial asserts.Assertion, ancillary []asserts.Assertion, err error) {
brandID := headers["brand-id"].(string)
model := headers["model"].(string)
keyID := ""
+ var signing assertstest.SignerDB = s.storeSigning
+
switch model {
case "pc", "pc2":
case "classic-alt-store":
@@ -229,10 +243,14 @@
c.Check(brandID, Equals, "generic")
headers["authority-id"] = "generic"
keyID = s.storeSigning.GenericKey.PublicKeyID()
+ case "rereg-model":
+ headers["authority-id"] = "rereg-brand"
+ signing = s.brands.Signing("rereg-brand")
default:
- c.Fatal("unknown model")
+ c.Fatalf("unknown model: %s", model)
}
- return s.storeSigning.Sign(asserts.SerialType, headers, body, keyID)
+ a, err := signing.Sign(asserts.SerialType, headers, body, keyID)
+ return a, s.ancillary, err
}
func (s *deviceMgrSuite) mockServer(c *C, reqID string, bhv *devicestatetest.DeviceServiceBehavior) *httptest.Server {
@@ -242,6 +260,7 @@
bhv.ReqID = reqID
bhv.SignSerial = s.signSerial
+ bhv.ExpectedCapabilities = "serial-stream"
return devicestatetest.MockDeviceService(c, bhv)
}
@@ -1466,6 +1485,10 @@
c.Check(ser.Serial(), Equals, "8989")
}
+var (
+ devKey, _ = assertstest.GenerateKey(testKeyLength)
+)
+
func (s *deviceMgrSuite) TestStoreContextBackendDeviceSessionRequestParams(c *C) {
s.state.Lock()
defer s.state.Unlock()
@@ -1477,7 +1500,6 @@
c.Check(err, ErrorMatches, "internal error: cannot sign a session request without a serial")
// setup state as done by device initialisation
- devKey, _ := assertstest.GenerateKey(testKeyLength)
encDevKey, err := asserts.EncodePublicKey(devKey.PublicKey())
c.Check(err, IsNil)
seriala, err := s.storeSigning.Sign(asserts.SerialType, map[string]interface{}{
@@ -1704,6 +1726,7 @@
// simulate that we have a new core_2, tried to boot it but that failed
s.bootloader.SetBootVars(map[string]string{
"snap_mode": "",
+ "snap_kernel": "kernel_1.snap",
"snap_try_core": "core_2.snap",
"snap_core": "core_1.snap",
})
@@ -1995,8 +2018,7 @@
assertstatetest.AddMany(s.state, modelAs)
}
-func makeSerialAssertionInState(c *C, brands *assertstest.SigningAccounts, st *state.State, brandID, model, serialN string) {
- devKey, _ := assertstest.GenerateKey(752)
+func makeSerialAssertionInState(c *C, brands *assertstest.SigningAccounts, st *state.State, brandID, model, serialN string) *asserts.Serial {
encDevKey, err := asserts.EncodePublicKey(devKey.PublicKey())
c.Assert(err, IsNil)
serial, err := brands.Signing(brandID).Sign(asserts.SerialType, map[string]interface{}{
@@ -2010,10 +2032,11 @@
c.Assert(err, IsNil)
err = assertstate.Add(st, serial)
c.Assert(err, IsNil)
+ return serial.(*asserts.Serial)
}
-func (s *deviceMgrSuite) makeSerialAssertionInState(c *C, brandID, model, serialN string) {
- makeSerialAssertionInState(c, s.brands, s.state, brandID, model, serialN)
+func (s *deviceMgrSuite) makeSerialAssertionInState(c *C, brandID, model, serialN string) *asserts.Serial {
+ return makeSerialAssertionInState(c, s.brands, s.state, brandID, model, serialN)
}
func (s *deviceMgrSuite) TestCanAutoRefreshOnCore(c *C) {
@@ -2478,11 +2501,8 @@
new map[string]string
errStr string
}{
- {map[string]string{"brand": "my-brand"}, "cannot remodel to different brand/model yet"},
- {map[string]string{"model": "other-model"}, "cannot remodel to different brand/model yet"},
{map[string]string{"architecture": "pdp-7"}, "cannot remodel to different architectures yet"},
{map[string]string{"base": "core20"}, "cannot remodel to different bases yet"},
- {map[string]string{"kernel": "other-kernel"}, "cannot remodel to different kernels yet"},
{map[string]string{"gadget": "other-gadget"}, "cannot remodel to different gadgets yet"},
} {
// copy current model unless new model test data is different
@@ -2504,7 +2524,7 @@
}
}
-func (s *deviceMgrSuite) TestRemodelTasksSmoke(c *C) {
+func (s *deviceMgrSuite) TestRemodelTasksSwitchKernelTrack(c *C) {
s.state.Lock()
defer s.state.Unlock()
s.state.Set("seeded", true)
@@ -2512,10 +2532,10 @@
var testDeviceCtx snapstate.DeviceContext
- restore := devicestate.MockSnapstateInstallWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext) (*state.TaskSet, error) {
-
+ restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) {
c.Check(flags.Required, Equals, true)
c.Check(deviceCtx, Equals, testDeviceCtx)
+ c.Check(fromChange, Equals, "99")
tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name))
tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name))
@@ -2528,6 +2548,23 @@
})
defer restore()
+ restore = devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) {
+ c.Check(flags.Required, Equals, false)
+ c.Check(flags.NoReRefresh, Equals, true)
+ c.Check(deviceCtx, Equals, testDeviceCtx)
+ c.Check(fromChange, Equals, "99")
+
+ tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s to track %s", name, opts.Channel))
+ tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name))
+ tValidate.WaitFor(tDownload)
+ tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel))
+ tUpdate.WaitFor(tValidate)
+ ts := state.NewTaskSet(tDownload, tValidate, tUpdate)
+ ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge)
+ return ts, nil
+ })
+ defer restore()
+
// set a model assertion
current := s.brands.Model("canonical", "pc-model", map[string]interface{}{
"architecture": "amd64",
@@ -2544,30 +2581,83 @@
new := s.brands.Model("canonical", "pc-model", map[string]interface{}{
"architecture": "amd64",
- "kernel": "pc-kernel",
+ "kernel": "pc-kernel=18",
"gadget": "pc",
"base": "core18",
"required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"},
"revision": "1",
})
- testDeviceCtx = &snapstatetest.TrivialDeviceContext{}
+ testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true}
- tss, err := devicestate.RemodelTasks(s.state, current, new, testDeviceCtx)
+ tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99")
c.Assert(err, IsNil)
- // 2 snaps plus the remodel task, the wait chain is tested in
- // TestRemodel*
- c.Assert(tss, HasLen, 3)
+ // 2 snaps, plus one track switch plus the remodel task, the
+ // wait chain is tested in TestRemodel*
+ c.Assert(tss, HasLen, 4)
}
-func (s *deviceMgrSuite) TestRemodelRequiredSnaps(c *C) {
+func (s *deviceMgrSuite) TestRemodelTasksSwitchKernel(c *C) {
s.state.Lock()
defer s.state.Unlock()
s.state.Set("seeded", true)
s.state.Set("refresh-privacy-key", "some-privacy-key")
- restore := devicestate.MockSnapstateInstallWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext) (*state.TaskSet, error) {
+ var testDeviceCtx snapstate.DeviceContext
+
+ restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) {
+ c.Check(deviceCtx, Equals, testDeviceCtx)
+ c.Check(name, Equals, "other-kernel")
+ c.Check(opts.Channel, Equals, "18")
+
+ tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name))
+ tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name))
+ tValidate.WaitFor(tDownload)
+ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name))
+ tInstall.WaitFor(tValidate)
+ ts := state.NewTaskSet(tDownload, tValidate, tInstall)
+ ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge)
+ return ts, nil
+ })
+ defer restore()
+
+ // set a model assertion
+ current := s.brands.Model("canonical", "pc-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core18",
+ })
+ err := assertstate.Add(s.state, current)
+ c.Assert(err, IsNil)
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc-model",
+ })
+
+ new := s.brands.Model("canonical", "pc-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "other-kernel=18",
+ "gadget": "pc",
+ "base": "core18",
+ "revision": "1",
+ })
+
+ testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true}
+ tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99")
+ c.Assert(err, IsNil)
+ // 1 new kernel plus the remodel task
+ c.Assert(tss, HasLen, 2)
+}
+
+func (s *deviceMgrSuite) TestRemodelRequiredSnaps(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ s.state.Set("seeded", true)
+ s.state.Set("refresh-privacy-key", "some-privacy-key")
+
+ restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) {
c.Check(flags.Required, Equals, true)
c.Check(deviceCtx, NotNil)
c.Check(deviceCtx.ForRemodeling(), Equals, true)
@@ -2611,8 +2701,11 @@
// 2 snaps,
c.Assert(tl, HasLen, 2*3+1)
- remodCtx, err := devicestate.RemodelCtxFromTask(tl[0])
+ deviceCtx, err := devicestate.DeviceCtx(s.state, tl[0], nil)
c.Assert(err, IsNil)
+ // deviceCtx is actually a remodelContext here
+ remodCtx, ok := deviceCtx.(devicestate.RemodelContext)
+ c.Assert(ok, Equals, true)
c.Check(remodCtx.ForRemodeling(), Equals, true)
c.Check(remodCtx.Kind(), Equals, devicestate.UpdateRemodel)
c.Check(remodCtx.Model(), DeepEquals, new)
@@ -2674,7 +2767,7 @@
s.state.Set("seeded", true)
s.state.Set("refresh-privacy-key", "some-privacy-key")
- restore := devicestate.MockSnapstateInstallWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext) (*state.TaskSet, error) {
+ restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) {
c.Check(flags.Required, Equals, true)
c.Check(deviceCtx, NotNil)
c.Check(deviceCtx.ForRemodeling(), Equals, true)
@@ -2690,7 +2783,7 @@
})
defer restore()
- restore = devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext) (*state.TaskSet, error) {
+ restore = devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) {
c.Check(flags.Required, Equals, false)
c.Check(flags.NoReRefresh, Equals, true)
c.Check(deviceCtx, NotNil)
@@ -2833,7 +2926,7 @@
var testStore snapstate.StoreService
- restore := devicestate.MockSnapstateInstallWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext) (*state.TaskSet, error) {
+ restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) {
c.Check(flags.Required, Equals, true)
c.Check(deviceCtx, NotNil)
c.Check(deviceCtx.ForRemodeling(), Equals, true)
@@ -2896,14 +2989,235 @@
// 1 "set-model" task at the end
c.Assert(tl, HasLen, 2*3+1)
- remodCtx, err := devicestate.RemodelCtxFromTask(tl[0])
+ deviceCtx, err := devicestate.DeviceCtx(s.state, tl[0], nil)
c.Assert(err, IsNil)
+ // deviceCtx is actually a remodelContext here
+ remodCtx, ok := deviceCtx.(devicestate.RemodelContext)
+ c.Assert(ok, Equals, true)
c.Check(remodCtx.ForRemodeling(), Equals, true)
c.Check(remodCtx.Kind(), Equals, devicestate.StoreSwitchRemodel)
c.Check(remodCtx.Model(), DeepEquals, new)
c.Check(remodCtx.Store(), Equals, testStore)
}
+func (s *deviceMgrSuite) TestRemodelRereg(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ s.state.Set("seeded", true)
+
+ // set a model assertion
+ s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core18",
+ })
+ s.makeSerialAssertionInState(c, "canonical", "pc-model", "orig-serial")
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc-model",
+ Serial: "orig-serial",
+ SessionMacaroon: "old-session",
+ })
+
+ new := s.brands.Model("canonical", "rereg-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core18",
+ "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"},
+ })
+
+ s.newFakeStore = func(devBE storecontext.DeviceBackend) snapstate.StoreService {
+ mod, err := devBE.Model()
+ c.Check(err, IsNil)
+ if err == nil {
+ c.Check(mod, DeepEquals, new)
+ }
+ return nil
+ }
+
+ chg, err := devicestate.Remodel(s.state, new)
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Summary(), Equals, "Remodel device to canonical/rereg-model (0)")
+
+ tl := chg.Tasks()
+ c.Assert(tl, HasLen, 2)
+
+ // check the tasks
+ tRequestSerial := tl[0]
+ tPrepareRemodeling := tl[1]
+
+ // check the tasks
+ c.Assert(tRequestSerial.Kind(), Equals, "request-serial")
+ c.Assert(tRequestSerial.Summary(), Equals, "Request new device serial")
+ c.Assert(tRequestSerial.WaitTasks(), HasLen, 0)
+
+ c.Assert(tPrepareRemodeling.Kind(), Equals, "prepare-remodeling")
+ c.Assert(tPrepareRemodeling.Summary(), Equals, "Prepare remodeling")
+ c.Assert(tPrepareRemodeling.WaitTasks(), DeepEquals, []*state.Task{tRequestSerial})
+}
+
+func (s *deviceMgrSuite) TestRemodelClash(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ s.state.Set("seeded", true)
+ s.state.Set("refresh-privacy-key", "some-privacy-key")
+
+ var clashing *asserts.Model
+
+ restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) {
+ // simulate things changing under our feet
+ assertstatetest.AddMany(st, clashing)
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: clashing.Model(),
+ })
+
+ tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name))
+ tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name))
+ tValidate.WaitFor(tDownload)
+ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name))
+ tInstall.WaitFor(tValidate)
+ ts := state.NewTaskSet(tDownload, tValidate, tInstall)
+ ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge)
+ return ts, nil
+ })
+ defer restore()
+
+ // set a model assertion
+ s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core18",
+ })
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc-model",
+ })
+
+ new := s.brands.Model("canonical", "pc-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core18",
+ "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"},
+ "revision": "1",
+ })
+ other := s.brands.Model("canonical", "pc-model-other", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core18",
+ "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"},
+ })
+
+ clashing = other
+ _, err := devicestate.Remodel(s.state, new)
+ c.Check(err, DeepEquals, &snapstate.ChangeConflictError{
+ Message: "cannot start remodel, clashing with concurrent remodel to canonical/pc-model-other (0)",
+ })
+
+ // reset
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc-model",
+ })
+ clashing = new
+ _, err = devicestate.Remodel(s.state, new)
+ c.Check(err, DeepEquals, &snapstate.ChangeConflictError{
+ Message: "cannot start remodel, clashing with concurrent remodel to canonical/pc-model (1)",
+ })
+}
+
+func (s *deviceMgrSuite) TestRemodelClashInProgress(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ s.state.Set("seeded", true)
+ s.state.Set("refresh-privacy-key", "some-privacy-key")
+
+ restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) {
+ // simulate another started remodeling
+ st.NewChange("remodel", "other remodel")
+
+ tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name))
+ tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name))
+ tValidate.WaitFor(tDownload)
+ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name))
+ tInstall.WaitFor(tValidate)
+ ts := state.NewTaskSet(tDownload, tValidate, tInstall)
+ ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge)
+ return ts, nil
+ })
+ defer restore()
+
+ // set a model assertion
+ s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core18",
+ })
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc-model",
+ })
+
+ new := s.brands.Model("canonical", "pc-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core18",
+ "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"},
+ "revision": "1",
+ })
+
+ _, err := devicestate.Remodel(s.state, new)
+ c.Check(err, DeepEquals, &snapstate.ChangeConflictError{
+ Message: "cannot start remodel, clashing with concurrent one",
+ })
+}
+
+func (s *deviceMgrSuite) TestReregRemodelClashAnyChange(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ s.state.Set("seeded", true)
+
+ // set a model assertion
+ s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core18",
+ })
+ s.makeSerialAssertionInState(c, "canonical", "pc-model", "orig-serial")
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc-model",
+ Serial: "orig-serial",
+ SessionMacaroon: "old-session",
+ })
+
+ new := s.brands.Model("canonical", "pc-model-2", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core18",
+ "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"},
+ "revision": "1",
+ })
+
+ // simulate any other change
+ s.state.NewChange("chg", "other change")
+
+ _, err := devicestate.Remodel(s.state, new)
+ c.Check(err, DeepEquals, &snapstate.ChangeConflictError{
+ Message: "cannot start complete remodel, other changes are in progress",
+ })
+}
+
func (s *deviceMgrSuite) TestRemodeling(c *C) {
s.state.Lock()
defer s.state.Unlock()
@@ -2924,6 +3238,166 @@
c.Check(devicestate.Remodeling(s.state), Equals, false)
}
+func (s *deviceMgrSuite) testDoRequestSerialReregistration(c *C, setAncillary func(origSerial *asserts.Serial)) *state.Task {
+ mockServer := s.mockServer(c, "REQID-1", nil)
+ defer mockServer.Close()
+
+ restore := devicestate.MockBaseStoreURL(mockServer.URL)
+ defer restore()
+
+ // setup state as after initial registration
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ s.makeModelAssertionInState(c, "my-brand", "my-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ })
+
+ devicestatetest.MockGadget(c, s.state, "pc", snap.R(2), nil)
+
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "my-brand",
+ Model: "my-model",
+ KeyID: devKey.PublicKey().ID(),
+ Serial: "9999",
+ })
+ devicestate.KeypairManager(s.mgr).Put(devKey)
+
+ // have a serial assertion
+ serial0 := s.makeSerialAssertionInState(c, "my-brand", "my-model", "9999")
+ // give a chance to the test to setup returning a stream vs
+ // just the serial assertion
+ if setAncillary != nil {
+ setAncillary(serial0)
+ }
+
+ new := s.brands.Model("rereg-brand", "rereg-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ })
+ cur, err := s.mgr.Model()
+ c.Assert(err, IsNil)
+
+ s.newFakeStore = func(devBE storecontext.DeviceBackend) snapstate.StoreService {
+ mod, err := devBE.Model()
+ c.Check(err, IsNil)
+ if err == nil {
+ c.Check(mod, DeepEquals, new)
+ }
+ return nil
+ }
+
+ remodCtx, err := devicestate.RemodelCtx(s.state, cur, new)
+ c.Assert(err, IsNil)
+ c.Check(remodCtx.Kind(), Equals, devicestate.ReregRemodel)
+
+ t := s.state.NewTask("request-serial", "test")
+ chg := s.state.NewChange("remodel", "...")
+ // associate with context
+ remodCtx.Init(chg)
+ chg.AddTask(t)
+
+ // sanity
+ regCtx, err := devicestate.RegistrationCtx(s.mgr, t)
+ c.Assert(err, IsNil)
+ c.Check(regCtx, Equals, remodCtx.(devicestate.RegistrationContext))
+
+ // avoid full seeding
+ s.seeding()
+
+ s.state.Unlock()
+ s.se.Ensure()
+ s.se.Wait()
+ s.state.Lock()
+
+ return t
+}
+
+func (s *deviceMgrSuite) TestDoRequestSerialReregistration(c *C) {
+ assertstest.AddMany(s.storeSigning, s.brands.AccountsAndKeys("rereg-brand")...)
+
+ t := s.testDoRequestSerialReregistration(c, nil)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ chg := t.Change()
+
+ c.Check(chg.Status(), Equals, state.DoneStatus, Commentf("%s", t.Log()))
+ c.Check(chg.Err(), IsNil)
+ device, err := devicestatetest.Device(s.state)
+ c.Check(err, IsNil)
+ c.Check(device.Serial, Equals, "9999")
+ _, err = s.db.Find(asserts.SerialType, map[string]string{
+ "brand-id": "rereg-brand",
+ "model": "rereg-model",
+ "serial": "9999",
+ })
+ c.Assert(err, IsNil)
+}
+
+func (s *deviceMgrSuite) TestDoRequestSerialReregistrationStreamFromService(c *C) {
+ setAncillary := func(_ *asserts.Serial) {
+ // sets up such that re-registration returns a stream
+ // of assertions
+ s.ancillary = s.brands.AccountsAndKeys("rereg-brand")
+ }
+
+ t := s.testDoRequestSerialReregistration(c, setAncillary)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ chg := t.Change()
+
+ c.Check(chg.Status(), Equals, state.DoneStatus, Commentf("%s", t.Log()))
+ c.Check(chg.Err(), IsNil)
+ device, err := devicestatetest.Device(s.state)
+ c.Check(err, IsNil)
+ c.Check(device.Serial, Equals, "9999")
+ _, err = s.db.Find(asserts.SerialType, map[string]string{
+ "brand-id": "rereg-brand",
+ "model": "rereg-model",
+ "serial": "9999",
+ })
+ c.Assert(err, IsNil)
+}
+
+func (s *deviceMgrSuite) TestDoRequestSerialReregistrationIncompleteStreamFromService(c *C) {
+ setAncillary := func(_ *asserts.Serial) {
+ // will produce an incomplete stream!
+ s.ancillary = s.brands.AccountsAndKeys("rereg-brand")[:1]
+ }
+
+ t := s.testDoRequestSerialReregistration(c, setAncillary)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ chg := t.Change()
+
+ c.Check(chg.Status(), Equals, state.ErrorStatus, Commentf("%s", t.Log()))
+ c.Check(chg.Err(), ErrorMatches, `(?ms).*cannot accept stream of assertions from device service:.*`)
+}
+
+func (s *deviceMgrSuite) TestDoRequestSerialReregistrationDoubleSerialStreamFromService(c *C) {
+ setAncillary := func(serial0 *asserts.Serial) {
+ // will produce a stream with confusingly two serial
+ // assertions
+ s.ancillary = s.brands.AccountsAndKeys("rereg-brand")
+ s.ancillary = append(s.ancillary, serial0)
+ }
+
+ t := s.testDoRequestSerialReregistration(c, setAncillary)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ chg := t.Change()
+
+ c.Check(chg.Status(), Equals, state.ErrorStatus, Commentf("%s", t.Log()))
+ c.Check(chg.Err(), ErrorMatches, `(?ms).*cannot accept more than a single device serial assertion from the device service.*`)
+}
+
func (s *deviceMgrSuite) TestDeviceCtxNoTask(c *C) {
s.state.Lock()
defer s.state.Unlock()
@@ -3025,7 +3499,7 @@
func (s *deviceMgrSuite) TestUpdateGadgetOnCoreSimple(c *C) {
var updateCalled bool
var passedRollbackDir string
- restore := devicestate.MockGadgetUpdate(func(current, update *gadget.Info, path string) error {
+ restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string) error {
updateCalled = true
passedRollbackDir = path
st, err := os.Stat(path)
@@ -3059,7 +3533,7 @@
func (s *deviceMgrSuite) TestUpdateGadgetOnCoreNoUpdateNeeded(c *C) {
var called bool
- restore := devicestate.MockGadgetUpdate(func(current, update *gadget.Info, path string) error {
+ restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string) error {
called = true
return gadget.ErrNoUpdate
})
@@ -3086,7 +3560,7 @@
c.Skip("this test cannot run as root (permissions are not honored)")
}
- restore := devicestate.MockGadgetUpdate(func(current, update *gadget.Info, path string) error {
+ restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string) error {
return errors.New("unexpected call")
})
defer restore()
@@ -3112,7 +3586,7 @@
}
func (s *deviceMgrSuite) TestUpdateGadgetOnCoreUpdateFailed(c *C) {
- restore := devicestate.MockGadgetUpdate(func(current, update *gadget.Info, path string) error {
+ restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string) error {
return errors.New("gadget exploded")
})
defer restore()
@@ -3135,7 +3609,7 @@
}
func (s *deviceMgrSuite) TestUpdateGadgetOnCoreNotDuringFirstboot(c *C) {
- restore := devicestate.MockGadgetUpdate(func(current, update *gadget.Info, path string) error {
+ restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string) error {
return errors.New("unexpected call")
})
defer restore()
@@ -3179,7 +3653,7 @@
}
func (s *deviceMgrSuite) TestUpdateGadgetOnCoreBadGadgetYaml(c *C) {
- restore := devicestate.MockGadgetUpdate(func(current, update *gadget.Info, path string) error {
+ restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string) error {
return errors.New("unexpected call")
})
defer restore()
@@ -3228,7 +3702,7 @@
s.state.Lock()
defer s.state.Unlock()
c.Assert(chg.IsReady(), Equals, true)
- c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(cannot read gadget snap details: .*\).*`)
+ c.Check(chg.Err(), ErrorMatches, `(?s).*update gadget \(cannot read candidate gadget snap details: .*\).*`)
c.Check(t.Status(), Equals, state.ErrorStatus)
rollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget")
c.Check(osutil.IsDirectory(rollbackDir), Equals, false)
@@ -3239,7 +3713,7 @@
restore := release.MockOnClassic(true)
defer restore()
- restore = devicestate.MockGadgetUpdate(func(current, update *gadget.Info, path string) error {
+ restore = devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string) error {
return errors.New("unexpected call")
})
defer restore()
@@ -3273,6 +3747,103 @@
c.Check(t.Status(), Equals, state.ErrorStatus)
}
+type mockUpdater struct{}
+
+func (m *mockUpdater) Backup() error { return nil }
+
+func (m *mockUpdater) Rollback() error { return nil }
+
+func (m *mockUpdater) Update() error { return nil }
+
+func (s *deviceMgrSuite) TestUpdateGadgetCallsToGadget(c *C) {
+ siCurrent := &snap.SideInfo{
+ RealName: "foo-gadget",
+ Revision: snap.R(33),
+ SnapID: "foo-id",
+ }
+ si := &snap.SideInfo{
+ RealName: "foo-gadget",
+ Revision: snap.R(34),
+ SnapID: "foo-id",
+ }
+ var gadgetCurrentYaml = `
+volumes:
+ pc:
+ bootloader: grub
+ structure:
+ - name: foo
+ size: 10M
+ type: bare
+ content:
+ - image: content.img
+`
+ var gadgetUpdateYaml = `
+volumes:
+ pc:
+ bootloader: grub
+ structure:
+ - name: foo
+ size: 10M
+ type: bare
+ content:
+ - image: content.img
+ update:
+ edition: 2
+`
+ snaptest.MockSnapWithFiles(c, snapYaml, siCurrent, [][]string{
+ {"meta/gadget.yaml", gadgetCurrentYaml},
+ {"content.img", "some content"},
+ })
+ updateInfo := snaptest.MockSnapWithFiles(c, snapYaml, si, [][]string{
+ {"meta/gadget.yaml", gadgetUpdateYaml},
+ {"content.img", "updated content"},
+ })
+
+ expectedRollbackDir := filepath.Join(dirs.SnapRollbackDir, "foo-gadget_34")
+ updaterForStructureCalls := 0
+ gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, rootDir, rollbackDir string) (gadget.Updater, error) {
+ updaterForStructureCalls++
+
+ c.Assert(ps.Name, Equals, "foo")
+ c.Assert(rootDir, Equals, updateInfo.MountDir())
+ c.Assert(filepath.Join(rootDir, "content.img"), testutil.FileEquals, "updated content")
+ c.Assert(strings.HasPrefix(rollbackDir, expectedRollbackDir), Equals, true)
+ c.Assert(osutil.IsDirectory(rollbackDir), Equals, true)
+ return &mockUpdater{}, nil
+ })
+
+ s.state.Lock()
+
+ snapstate.Set(s.state, "foo-gadget", &snapstate.SnapState{
+ SnapType: "gadget",
+ Sequence: []*snap.SideInfo{siCurrent},
+ Current: siCurrent.Revision,
+ Active: true,
+ })
+
+ t := s.state.NewTask("update-gadget-assets", "update gadget")
+ t.Set("snap-setup", &snapstate.SnapSetup{
+ SideInfo: si,
+ Type: snap.TypeGadget,
+ })
+ chg := s.state.NewChange("dummy", "...")
+ chg.AddTask(t)
+
+ s.state.Unlock()
+
+ for i := 0; i < 6; i++ {
+ s.se.Ensure()
+ s.se.Wait()
+ }
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ c.Assert(chg.IsReady(), Equals, true)
+ c.Check(t.Status(), Equals, state.DoneStatus)
+ c.Check(s.restartRequests, HasLen, 1)
+ c.Check(updaterForStructureCalls, Equals, 1)
+}
+
func (s *deviceMgrSuite) TestCurrentAndUpdateInfo(c *C) {
siCurrent := &snap.SideInfo{
RealName: "foo-gadget",
@@ -3311,22 +3882,23 @@
current, update, err = devicestate.GadgetCurrentAndUpdate(s.state, snapsup)
c.Assert(current, IsNil)
c.Assert(update, IsNil)
- c.Assert(err, ErrorMatches, "cannot read gadget snap details: .*/meta/gadget.yaml: no such file or directory")
+ c.Assert(err, ErrorMatches, "cannot read current gadget snap details: .*/33/meta/gadget.yaml: no such file or directory")
// drop gadget.yaml for current snap
ioutil.WriteFile(filepath.Join(ci.MountDir(), "meta/gadget.yaml"), []byte(gadgetYaml), 0644)
+ // update missing snap.yaml
current, update, err = devicestate.GadgetCurrentAndUpdate(s.state, snapsup)
c.Assert(current, IsNil)
c.Assert(update, IsNil)
- c.Assert(err, ErrorMatches, "cannot find installed snap \"foo-gadget\" at revision 34: .*")
+ c.Assert(err, ErrorMatches, "cannot read candidate gadget snap details: cannot find installed snap .* .*/34/meta/snap.yaml")
ui := snaptest.MockSnapWithFiles(c, snapYaml, si, nil)
current, update, err = devicestate.GadgetCurrentAndUpdate(s.state, snapsup)
c.Assert(current, IsNil)
c.Assert(update, IsNil)
- c.Assert(err, ErrorMatches, "cannot read gadget snap details: .*")
+ c.Assert(err, ErrorMatches, "cannot read candidate gadget snap details: .*/34/meta/gadget.yaml: no such file or directory")
var updateGadgetYaml = `
volumes:
@@ -3340,20 +3912,26 @@
current, update, err = devicestate.GadgetCurrentAndUpdate(s.state, snapsup)
c.Assert(err, IsNil)
- c.Assert(current, DeepEquals, &gadget.Info{
- Volumes: map[string]gadget.Volume{
- "pc": {
- Bootloader: "grub",
+ c.Assert(current, DeepEquals, &gadget.GadgetData{
+ Info: &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "pc": {
+ Bootloader: "grub",
+ },
},
},
+ RootDir: ci.MountDir(),
})
- c.Assert(update, DeepEquals, &gadget.Info{
- Volumes: map[string]gadget.Volume{
- "pc": {
- Bootloader: "grub",
- ID: "123",
+ c.Assert(update, DeepEquals, &gadget.GadgetData{
+ Info: &gadget.Info{
+ Volumes: map[string]gadget.Volume{
+ "pc": {
+ Bootloader: "grub",
+ ID: "123",
+ },
},
},
+ RootDir: ui.MountDir(),
})
}
diff -Nru snapd-2.40/overlord/devicestate/export_test.go snapd-2.42.1/overlord/devicestate/export_test.go
--- snapd-2.40/overlord/devicestate/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/devicestate/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,6 +20,7 @@
package devicestate
import (
+ "context"
"time"
"github.com/snapcore/snapd/asserts"
@@ -90,7 +91,7 @@
}
}
-func MockSnapstateInstallWithDeviceContext(f func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext) (*state.TaskSet, error)) (restore func()) {
+func MockSnapstateInstallWithDeviceContext(f func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error)) (restore func()) {
old := snapstateInstallWithDeviceContext
snapstateInstallWithDeviceContext = f
return func() {
@@ -98,7 +99,7 @@
}
}
-func MockSnapstateUpdateWithDeviceContext(f func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext) (*state.TaskSet, error)) (restore func()) {
+func MockSnapstateUpdateWithDeviceContext(f func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error)) (restore func()) {
old := snapstateUpdateWithDeviceContext
snapstateUpdateWithDeviceContext = f
return func() {
@@ -128,6 +129,11 @@
m.bootOkRan = b
}
+type (
+ RegistrationContext = registrationContext
+ RemodelContext = remodelContext
+)
+
func RegistrationCtx(m *DeviceManager, t *state.Task) (registrationContext, error) {
return m.registrationCtx(t)
}
@@ -149,15 +155,15 @@
RemodelTasks = remodelTasks
- RemodelCtx = remodelCtx
- RemodelCtxFromTask = remodelCtxFromTask
- CleanupRemodelCtx = cleanupRemodelCtx
+ RemodelCtx = remodelCtx
+ CleanupRemodelCtx = cleanupRemodelCtx
+ CachedRemodelCtx = cachedRemodelCtx
GadgetUpdateBlocked = gadgetUpdateBlocked
GadgetCurrentAndUpdate = gadgetCurrentAndUpdate
)
-func MockGadgetUpdate(mock func(current, update *gadget.Info, path string) error) (restore func()) {
+func MockGadgetUpdate(mock func(current, update gadget.GadgetData, path string) error) (restore func()) {
old := gadgetUpdate
gadgetUpdate = mock
return func() {
diff -Nru snapd-2.40/overlord/devicestate/firstboot.go snapd-2.42.1/overlord/devicestate/firstboot.go
--- snapd-2.40/overlord/devicestate/firstboot.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/devicestate/firstboot.go 2019-10-30 12:17:43.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2014-2015 Canonical Ltd
+ * Copyright (C) 2014-2019 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,28 +22,27 @@
import (
"errors"
"fmt"
- "io/ioutil"
- "os"
- "path/filepath"
"sort"
"github.com/snapcore/snapd/asserts"
- "github.com/snapcore/snapd/asserts/snapasserts"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/i18n"
- "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/assertstate"
"github.com/snapcore/snapd/overlord/devicestate/internal"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/seed"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/timings"
)
var errNothingToDo = errors.New("nothing to do")
-func installSeedSnap(st *state.State, sn *snap.SeedSnap, flags snapstate.Flags, tm timings.Measurer) (*state.TaskSet, *snap.Info, error) {
+func installSeedSnap(st *state.State, sn *seed.Snap, flags snapstate.Flags) (*state.TaskSet, *snap.Info, error) {
+ if sn.Required {
+ flags.Required = true
+ }
if sn.Classic {
flags.Classic = true
}
@@ -51,29 +50,7 @@
flags.DevMode = true
}
- path := filepath.Join(dirs.SnapSeedDir, "snaps", sn.File)
-
- var sideInfo snap.SideInfo
- if sn.Unasserted {
- sideInfo.RealName = sn.Name
- } else {
- var si *snap.SideInfo
- var err error
- timings.Run(tm, "derive-side-info", fmt.Sprintf("hash and derive side info for snap %q", sn.Name), func(nested timings.Measurer) {
- si, err = snapasserts.DeriveSideInfo(path, assertstate.DB(st))
- })
- if asserts.IsNotFound(err) {
- return nil, nil, fmt.Errorf("cannot find signatures with metadata for snap %q (%q)", sn.Name, path)
- }
- if err != nil {
- return nil, nil, err
- }
- sideInfo = *si
- sideInfo.Private = sn.Private
- sideInfo.Contact = sn.Contact
- }
-
- return snapstate.InstallPath(st, &sideInfo, path, "", sn.Channel, flags)
+ return snapstate.InstallPath(st, sn.SideInfo, sn.Path, "", sn.Channel, flags)
}
func trivialSeeding(st *state.State, markSeeded *state.Task) []*state.TaskSet {
@@ -97,10 +74,15 @@
markSeeded := st.NewTask("mark-seeded", i18n.G("Mark system seeded"))
+ deviceSeed, err := seed.Open(dirs.SnapSeedDir)
+ if err != nil {
+ return nil, err
+ }
+
// ack all initial assertions
var model *asserts.Model
timings.Run(tm, "import-assertions", "import assertions from seed", func(nested timings.Measurer) {
- model, err = importAssertionsFromSeed(st)
+ model, err = importAssertionsFromSeed(st, deviceSeed)
})
if err == errNothingToDo {
return trivialSeeding(st, markSeeded), nil
@@ -109,146 +91,100 @@
return nil, err
}
- seedYamlFile := filepath.Join(dirs.SnapSeedDir, "seed.yaml")
- if release.OnClassic && !osutil.FileExists(seedYamlFile) {
+ err = deviceSeed.LoadMeta(tm)
+ if release.OnClassic && err == seed.ErrNoMeta {
// on classic it is ok to not seed any snaps
return trivialSeeding(st, markSeeded), nil
}
-
- seed, err := snap.ReadSeedYaml(seedYamlFile)
if err != nil {
return nil, err
}
- required := getAllRequiredSnapsForModel(model)
- seeding := make(map[string]*snap.SeedSnap, len(seed.Snaps))
- for _, sn := range seed.Snaps {
- seeding[sn.Name] = sn
+ essentialSeedSnaps := deviceSeed.EssentialSnaps()
+ seedSnaps, err := deviceSeed.ModeSnaps("run") // XXX mode should be passed in
+ if err != nil {
+ return nil, err
}
- alreadySeeded := make(map[string]bool, 3)
+
+ // allSnapInfos are collected for cross-check validation of bases
+ allSnapInfos := make(map[string]*snap.Info, len(essentialSeedSnaps)+len(seedSnaps))
tsAll := []*state.TaskSet{}
configTss := []*state.TaskSet{}
-
- baseSnap := "core"
- if model.Base() != "" {
- baseSnap = model.Base()
- }
-
- installSeedEssential := func(snapName string, last int) (*snap.Info, error) {
- seedSnap := seeding[snapName]
- if seedSnap == nil {
- return nil, fmt.Errorf("cannot proceed without seeding %q", snapName)
- }
- ts, info, err := installSeedSnap(st, seedSnap, snapstate.Flags{SkipConfigure: true, Required: true}, tm)
- if err != nil {
- return nil, err
+ chainTs := func(all []*state.TaskSet, ts *state.TaskSet) []*state.TaskSet {
+ n := len(all)
+ if n != 0 {
+ ts.WaitAll(all[n-1])
}
- if last >= 0 {
- ts.WaitAll(tsAll[last])
+ return append(all, ts)
+ }
+ chainSorted := func(infos []*snap.Info, infoToTs map[*snap.Info]*state.TaskSet) {
+ sort.Stable(snap.ByType(infos))
+ for _, info := range infos {
+ ts := infoToTs[info]
+ tsAll = chainTs(tsAll, ts)
}
- tsAll = append(tsAll, ts)
- alreadySeeded[snapName] = true
- return info, nil
}
- last := -1
- // if there are snaps to seed, core/base needs to be seeded too
- if len(seed.Snaps) != 0 {
- // ensure "snapd" snap is installed first
- if model.Base() != "" {
- if _, err := installSeedEssential("snapd", last); err != nil {
- return nil, err
- }
- last++
- }
- if _, err := installSeedEssential(baseSnap, last); err != nil {
- return nil, err
- }
+ essInfoToTs := make(map[*snap.Info]*state.TaskSet, len(essentialSeedSnaps))
+ essInfos := make([]*snap.Info, 0, len(essentialSeedSnaps))
+
+ if len(essentialSeedSnaps) != 0 {
// we *always* configure "core" here even if bases are used
// for booting. "core" if where the system config lives.
- configTss = append(configTss, snapstate.ConfigureSnap(st, "core", snapstate.UseConfigDefaults))
- last++
- }
-
- lastConf := 0
- if kernelName := model.Kernel(); kernelName != "" {
- if _, err := installSeedEssential(kernelName, last); err != nil {
- return nil, err
- }
- configTs := snapstate.ConfigureSnap(st, kernelName, snapstate.UseConfigDefaults)
- // wait for the previous configTss
- configTs.WaitAll(configTss[lastConf])
- configTss = append(configTss, configTs)
- last++
- lastConf++
+ configTss = chainTs(configTss, snapstate.ConfigureSnap(st, "core", snapstate.UseConfigDefaults))
}
- // FIXME: ensure that any base is ordered before the gadget so that
- // the gadget can use bases that are not the model base
- if gadgetName := model.Gadget(); gadgetName != "" {
- info, err := installSeedEssential(gadgetName, last)
+ for _, seedSnap := range essentialSeedSnaps {
+ ts, info, err := installSeedSnap(st, seedSnap, snapstate.Flags{SkipConfigure: true})
if err != nil {
return nil, err
}
- // Sanity check, note that we could support this if we have
- // a use-case. However this requires that we do the sorting
- // different, i.e. other bases will have to be sorted before
- // the gadget.
- if info.Base != model.Base() {
- return nil, fmt.Errorf("cannot use gadget snap because its base %q is different from model base %q", info.Base, model.Base())
+ if info.GetType() == snap.TypeKernel || info.GetType() == snap.TypeGadget {
+ configTs := snapstate.ConfigureSnap(st, info.SnapName(), snapstate.UseConfigDefaults)
+ // wait for the previous configTss
+ configTss = chainTs(configTss, configTs)
}
-
- configTs := snapstate.ConfigureSnap(st, gadgetName, snapstate.UseConfigDefaults)
- // wait for the previous configTss
- configTs.WaitAll(configTss[lastConf])
- configTss = append(configTss, configTs)
- last++
- //If we use lastConf again we need to enable this. It is
- //commented out because go vet complains about an ineffectual
- // assignment.
- //lastConf++
+ essInfos = append(essInfos, info)
+ essInfoToTs[info] = ts
+ allSnapInfos[info.SnapName()] = info
}
+ // now add/chain the tasksets in the right order based on essential
+ // snap types
+ chainSorted(essInfos, essInfoToTs)
// chain together configuring core, kernel, and gadget after
// installing them so that defaults are availabble from gadget
if len(configTss) > 0 {
- configTss[0].WaitAll(tsAll[last])
+ configTss[0].WaitAll(tsAll[len(tsAll)-1])
tsAll = append(tsAll, configTss...)
- last += len(configTss)
}
// ensure we install in the right order
- infoToTs := make(map[*snap.Info]*state.TaskSet, len(seed.Snaps))
- infos := make([]*snap.Info, 0, len(seed.Snaps))
-
- for _, sn := range seed.Snaps {
- if alreadySeeded[sn.Name] {
- continue
- }
+ infoToTs := make(map[*snap.Info]*state.TaskSet, len(seedSnaps))
+ infos := make([]*snap.Info, 0, len(seedSnaps))
+ for _, seedSnap := range seedSnaps {
var flags snapstate.Flags
- if required[sn.Name] {
- flags.Required = true
- }
-
- ts, info, err := installSeedSnap(st, sn, flags, tm)
+ ts, info, err := installSeedSnap(st, seedSnap, flags)
if err != nil {
return nil, err
}
infos = append(infos, info)
infoToTs[info] = ts
+ allSnapInfos[info.SnapName()] = info
+ }
+
+ // validate that all snaps have bases
+ errs := snap.ValidateBasesAndProviders(allSnapInfos)
+ if errs != nil {
+ // only report the first error encountered
+ return nil, errs[0]
}
// now add/chain the tasksets in the right order, note that we
// only have tasksets that we did not already seeded
- sort.Stable(snap.ByType(infos))
- for _, info := range infos {
- ts := infoToTs[info]
- ts.WaitAll(tsAll[last])
- tsAll = append(tsAll, ts)
- last++
- }
+ chainSorted(infos, infoToTs)
if len(tsAll) == 0 {
return nil, fmt.Errorf("cannot proceed, no snaps to seed")
@@ -271,26 +207,21 @@
return tsAll, nil
}
-func readAsserts(fn string, batch *assertstate.Batch) ([]*asserts.Ref, error) {
- f, err := os.Open(fn)
- if err != nil {
- return nil, err
- }
- defer f.Close()
- return batch.AddStream(f)
-}
-
-func importAssertionsFromSeed(st *state.State) (*asserts.Model, error) {
+func importAssertionsFromSeed(st *state.State, deviceSeed seed.Seed) (*asserts.Model, error) {
// TODO: use some kind of context fo Device/SetDevice?
device, err := internal.Device(st)
if err != nil {
return nil, err
}
+ // collect and
// set device,model from the model assertion
- assertSeedDir := filepath.Join(dirs.SnapSeedDir, "assertions")
- dc, err := ioutil.ReadDir(assertSeedDir)
- if release.OnClassic && os.IsNotExist(err) {
+ commitTo := func(batch *asserts.Batch) error {
+ return assertstate.AddBatch(st, batch, nil)
+ }
+
+ err = deviceSeed.LoadAssertions(assertstate.DB(st), commitTo)
+ if err == seed.ErrNoAssertions && release.OnClassic {
// on classic seeding is optional
// set the fallback model
err := setClassicFallbackModel(st, device)
@@ -300,41 +231,13 @@
return nil, errNothingToDo
}
if err != nil {
- return nil, fmt.Errorf("cannot read assert seed dir: %s", err)
- }
-
- // collect
- var modelRef *asserts.Ref
- batch := assertstate.NewBatch()
- for _, fi := range dc {
- fn := filepath.Join(assertSeedDir, fi.Name())
- refs, err := readAsserts(fn, batch)
- if err != nil {
- return nil, fmt.Errorf("cannot read assertions: %s", err)
- }
- for _, ref := range refs {
- if ref.Type == asserts.ModelType {
- if modelRef != nil && modelRef.Unique() != ref.Unique() {
- return nil, fmt.Errorf("cannot add more than one model assertion")
- }
- modelRef = ref
- }
- }
- }
- // verify we have one model assertion
- if modelRef == nil {
- return nil, fmt.Errorf("need a model assertion")
- }
-
- if err := batch.Commit(st); err != nil {
return nil, err
}
- a, err := modelRef.Resolve(assertstate.DB(st).Find)
+ modelAssertion, err := deviceSeed.Model()
if err != nil {
- return nil, fmt.Errorf("internal error: cannot find just added assertion %v: %v", modelRef, err)
+ return nil, err
}
- modelAssertion := a.(*asserts.Model)
classicModel := modelAssertion.Classic()
if release.OnClassic != classicModel {
diff -Nru snapd-2.40/overlord/devicestate/firstboot_test.go snapd-2.42.1/overlord/devicestate/firstboot_test.go
--- snapd-2.40/overlord/devicestate/firstboot_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/devicestate/firstboot_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -33,9 +33,8 @@
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/asserts/assertstest"
- "github.com/snapcore/snapd/asserts/sysdb"
- "github.com/snapcore/snapd/boot/boottest"
"github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/bootloader/bootloadertest"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord"
@@ -50,6 +49,8 @@
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
+ "github.com/snapcore/snapd/seed"
+ "github.com/snapcore/snapd/seed/seedtest"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/systemd"
@@ -58,27 +59,31 @@
)
type FirstBootTestSuite struct {
+ testutil.BaseTest
+
systemctl *testutil.MockCmd
- storeSigning *assertstest.StoreStack
- restore func()
+ // TestingSeed helps populating seeds (it provides
+ // MakeAssertedSnap, WriteAssertions etc.) for tests.
+ *seedtest.TestingSeed
- brands *assertstest.SigningAccounts
+ devAcct *asserts.Account
overlord *overlord.Overlord
perfTimings timings.Measurer
-
- restoreOnClassic func()
- restoreBackends func()
}
var _ = Suite(&FirstBootTestSuite{})
func (s *FirstBootTestSuite) SetUpTest(c *C) {
+ s.BaseTest.SetUpTest(c)
+
tempdir := c.MkDir()
dirs.SetRootDir(tempdir)
- s.restoreOnClassic = release.MockOnClassic(false)
+ s.AddCleanup(func() { dirs.SetRootDir("/") })
+
+ s.AddCleanup(release.MockOnClassic(false))
// mock the world!
err := os.MkdirAll(filepath.Join(dirs.SnapSeedDir, "snaps"), 0755)
@@ -89,25 +94,33 @@
err = os.MkdirAll(dirs.SnapServicesDir, 0755)
c.Assert(err, IsNil)
os.Setenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS", "1")
+ s.AddCleanup(func() { os.Unsetenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS") })
s.systemctl = testutil.MockCommand(c, "systemctl", "")
+ s.AddCleanup(s.systemctl.Restore)
err = ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), nil, 0644)
c.Assert(err, IsNil)
- s.storeSigning = assertstest.NewStoreStack("can0nical", nil)
- s.restore = sysdb.InjectTrusted(s.storeSigning.Trusted)
-
- s.brands = assertstest.NewSigningAccounts(s.storeSigning)
- s.brands.Register("my-brand", brandPrivKey, map[string]interface{}{
+ s.TestingSeed = &seedtest.TestingSeed{}
+ s.SetupAssertSigning("can0nical", s)
+ s.Brands.Register("my-brand", brandPrivKey, map[string]interface{}{
"verification": "verified",
})
- s.restoreBackends = ifacestate.MockSecurityBackends(nil)
+ s.SnapsDir = filepath.Join(dirs.SnapSeedDir, "snaps")
+ s.AssertsDir = filepath.Join(dirs.SnapSeedDir, "assertions")
+
+ s.devAcct = assertstest.NewAccount(s.StoreSigning, "developer", map[string]interface{}{
+ "account-id": "developerid",
+ }, "")
- ovld, err := overlord.New()
+ s.AddCleanup(ifacestate.MockSecurityBackends(nil))
+
+ ovld, err := overlord.New(nil)
c.Assert(err, IsNil)
ovld.InterfaceManager().DisableUDevMonitor()
s.overlord = ovld
+ c.Assert(ovld.StartUp(), IsNil)
// don't actually try to talk to the store on snapstate.Ensure
// needs doing after the call to devicestate.Manager (which happens in overlord.New)
@@ -116,16 +129,6 @@
s.perfTimings = timings.New(nil)
}
-func (s *FirstBootTestSuite) TearDownTest(c *C) {
- os.Unsetenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS")
- s.systemctl.Restore()
-
- s.restore()
- s.restoreOnClassic()
- s.restoreBackends()
- dirs.SetRootDir("/")
-}
-
func checkTrivialSeeding(c *C, tsAll []*state.TaskSet) {
// run internal core config and mark seeded
c.Check(tsAll, HasLen, 2)
@@ -142,6 +145,31 @@
c.Check(tasks[0].Kind(), Equals, "mark-seeded")
}
+func (s *FirstBootTestSuite) modelHeaders(modelStr string, reqSnaps ...string) map[string]interface{} {
+ headers := map[string]interface{}{
+ "architecture": "amd64",
+ "store": "canonical",
+ }
+ if strings.HasSuffix(modelStr, "-classic") {
+ headers["classic"] = "true"
+ } else {
+ headers["kernel"] = "pc-kernel"
+ headers["gadget"] = "pc"
+ }
+ if len(reqSnaps) != 0 {
+ reqs := make([]interface{}, len(reqSnaps))
+ for i, req := range reqSnaps {
+ reqs[i] = req
+ }
+ headers["required-snaps"] = reqs
+ }
+ return headers
+}
+
+func (s *FirstBootTestSuite) makeModelAssertionChain(c *C, modName string, extraHeaders map[string]interface{}, reqSnaps ...string) []asserts.Assertion {
+ return s.MakeModelAssertionChain("my-brand", modName, s.modelHeaders(modName, reqSnaps...), extraHeaders)
+}
+
func (s *FirstBootTestSuite) TestPopulateFromSeedOnClassicNoop(c *C) {
restore := release.MockOnClassic(true)
defer restore()
@@ -180,17 +208,13 @@
restore := release.MockOnClassic(true)
defer restore()
- ovld, err := overlord.New()
+ ovld, err := overlord.New(nil)
c.Assert(err, IsNil)
st := ovld.State()
- // add a bunch of assert files
+ // add the model assertion and its chain
assertsChain := s.makeModelAssertionChain(c, "my-model-classic", nil)
- 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)
- }
+ s.WriteAssertions("model.asserts", assertsChain...)
err = os.Remove(filepath.Join(dirs.SnapSeedDir, "seed.yaml"))
c.Assert(err, IsNil)
@@ -212,17 +236,13 @@
restore := release.MockOnClassic(true)
defer restore()
- ovld, err := overlord.New()
+ ovld, err := overlord.New(nil)
c.Assert(err, IsNil)
st := ovld.State()
- // add a bunch of assert files
+ // add the model assertion and its chain
assertsChain := s.makeModelAssertionChain(c, "my-model-classic", nil)
- 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)
- }
+ s.WriteAssertions("model.asserts", assertsChain...)
// create an empty seed.yaml
err = ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), nil, 0644)
@@ -241,13 +261,9 @@
st := s.overlord.State()
- // add a bunch of assert files
+ // add the model assertion and its chain
assertsChain := s.makeModelAssertionChain(c, "my-model-classic", nil)
- 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)
- }
+ s.WriteAssertions("model.asserts", assertsChain...)
err := os.Remove(filepath.Join(dirs.SnapSeedDir, "seed.yaml"))
c.Assert(err, IsNil)
@@ -323,67 +339,6 @@
c.Assert(err, ErrorMatches, "cannot populate state: already seeded")
}
-func (s *FirstBootTestSuite) makeAssertedSnap(c *C, snapYaml string, files [][]string, revision snap.Revision, developerID string) (snapFname string, snapDecl *asserts.SnapDeclaration, snapRev *asserts.SnapRevision) {
- info, err := snap.InfoFromSnapYaml([]byte(snapYaml))
- c.Assert(err, IsNil)
- snapName := info.InstanceName()
-
- mockSnapFile := snaptest.MakeTestSnapWithFiles(c, snapYaml, files)
- snapFname = filepath.Base(mockSnapFile)
-
- targetFile := filepath.Join(dirs.SnapSeedDir, "snaps", snapFname)
- err = os.Rename(mockSnapFile, targetFile)
- c.Assert(err, IsNil)
-
- snapID := (snapName + "-snap-" + strings.Repeat("id", 20))[:32]
- // FIXME: snapd is special in the interface policy code and it
- // identified by its snap-id. so we fake the real snap-id
- // here. Instead we should add a "type: snapd" for snaps.
- if snapName == "snapd" {
- snapID = "PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4"
- }
-
- declA, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
- "series": "16",
- "snap-id": snapID,
- "publisher-id": developerID,
- "snap-name": snapName,
- "timestamp": time.Now().UTC().Format(time.RFC3339),
- }, nil, "")
- c.Assert(err, IsNil)
-
- sha3_384, size, err := asserts.SnapFileSHA3_384(targetFile)
- c.Assert(err, IsNil)
-
- revA, err := s.storeSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
- "snap-sha3-384": sha3_384,
- "snap-size": fmt.Sprintf("%d", size),
- "snap-id": snapID,
- "developer-id": developerID,
- "snap-revision": revision.String(),
- "timestamp": time.Now().UTC().Format(time.RFC3339),
- }, nil, "")
- c.Assert(err, IsNil)
-
- return snapFname, declA.(*asserts.SnapDeclaration), revA.(*asserts.SnapRevision)
-}
-
-func checkSeedTasks(c *C, tsAll []*state.TaskSet) {
- // the tasks of the last taskset must be gadget-connect, mark-seeded
- lastTasks := tsAll[len(tsAll)-1].Tasks()
- c.Check(lastTasks, HasLen, 2)
- gadgetConnectTask := lastTasks[0]
- markSeededTask := lastTasks[1]
- c.Check(gadgetConnectTask.Kind(), Equals, "gadget-connect")
- c.Check(markSeededTask.Kind(), Equals, "mark-seeded")
- // and the gadget-connect must wait for the other tasks
- prevTasks := tsAll[len(tsAll)-2].Tasks()
- otherTask := prevTasks[len(prevTasks)-1]
- c.Check(gadgetConnectTask.WaitTasks(), testutil.Contains, otherTask)
- // add the mark-seeded waits for gadget-connects
- c.Check(markSeededTask.WaitTasks(), DeepEquals, []*state.Task{gadgetConnectTask})
-}
-
func (s *FirstBootTestSuite) makeCoreSnaps(c *C, extraGadgetYaml string) (coreFname, kernelFname, gadgetFname string) {
files := [][]string{}
if strings.Contains(extraGadgetYaml, "defaults:") {
@@ -394,17 +349,15 @@
snapYaml := `name: core
version: 1.0
type: os`
- coreFname, coreDecl, coreRev := s.makeAssertedSnap(c, snapYaml, files, snap.R(1), "canonical")
-
- writeAssertionsToFile("core.asserts", []asserts.Assertion{coreRev, coreDecl})
+ coreFname, coreDecl, coreRev := s.MakeAssertedSnap(c, snapYaml, files, snap.R(1), "canonical")
+ s.WriteAssertions("core.asserts", coreRev, coreDecl)
// put kernel snap into the SnapBlobDir
snapYaml = `name: pc-kernel
version: 1.0
type: kernel`
- kernelFname, kernelDecl, kernelRev := s.makeAssertedSnap(c, snapYaml, files, snap.R(1), "canonical")
-
- writeAssertionsToFile("kernel.asserts", []asserts.Assertion{kernelRev, kernelDecl})
+ kernelFname, kernelDecl, kernelRev := s.MakeAssertedSnap(c, snapYaml, files, snap.R(1), "canonical")
+ s.WriteAssertions("kernel.asserts", kernelRev, kernelDecl)
gadgetYaml := `
volumes:
@@ -419,50 +372,75 @@
snapYaml = `name: pc
version: 1.0
type: gadget`
- gadgetFname, gadgetDecl, gadgetRev := s.makeAssertedSnap(c, snapYaml, files, snap.R(1), "canonical")
-
- writeAssertionsToFile("gadget.asserts", []asserts.Assertion{gadgetRev, gadgetDecl})
+ gadgetFname, gadgetDecl, gadgetRev := s.MakeAssertedSnap(c, snapYaml, files, snap.R(1), "canonical")
+ s.WriteAssertions("gadget.asserts", gadgetRev, gadgetDecl)
return coreFname, kernelFname, gadgetFname
}
-func (s *FirstBootTestSuite) makeBecomeOperationalChange(c *C, st *state.State) *state.Change {
+func checkOrder(c *C, tsAll []*state.TaskSet, snaps ...string) {
+ matched := 0
+ var prevTask *state.Task
+ for i, ts := range tsAll {
+ task0 := ts.Tasks()[0]
+ waitTasks := task0.WaitTasks()
+ if i == 0 {
+ c.Check(waitTasks, HasLen, 0)
+ } else {
+ c.Check(waitTasks, testutil.Contains, prevTask)
+ }
+ prevTask = task0
+ if task0.Kind() != "prerequisites" {
+ continue
+ }
+ snapsup, err := snapstate.TaskSnapSetup(task0)
+ c.Assert(err, IsNil, Commentf("%#v", task0))
+ c.Check(snapsup.InstanceName(), Equals, snaps[matched])
+ matched++
+ }
+ c.Check(matched, Equals, len(snaps))
+}
+
+func checkSeedTasks(c *C, tsAll []*state.TaskSet) {
+ // the tasks of the last taskset must be gadget-connect, mark-seeded
+ lastTasks := tsAll[len(tsAll)-1].Tasks()
+ c.Check(lastTasks, HasLen, 2)
+ gadgetConnectTask := lastTasks[0]
+ markSeededTask := lastTasks[1]
+ c.Check(gadgetConnectTask.Kind(), Equals, "gadget-connect")
+ c.Check(markSeededTask.Kind(), Equals, "mark-seeded")
+ // and the gadget-connect must wait for the other tasks
+ prevTasks := tsAll[len(tsAll)-2].Tasks()
+ otherTask := prevTasks[len(prevTasks)-1]
+ c.Check(gadgetConnectTask.WaitTasks(), testutil.Contains, otherTask)
+ // add the mark-seeded waits for gadget-connects
+ c.Check(markSeededTask.WaitTasks(), DeepEquals, []*state.Task{gadgetConnectTask})
+}
+
+func (s *FirstBootTestSuite) makeSeedChange(c *C, st *state.State) *state.Change {
coreFname, kernelFname, gadgetFname := s.makeCoreSnaps(c, "")
- devAcct := assertstest.NewAccount(s.storeSigning, "developer", map[string]interface{}{
- "account-id": "developerid",
- }, "")
- devAcctFn := filepath.Join(dirs.SnapSeedDir, "assertions", "developer.account")
- err := ioutil.WriteFile(devAcctFn, asserts.Encode(devAcct), 0644)
- c.Assert(err, IsNil)
+ s.WriteAssertions("developer.account", s.devAcct)
// put a firstboot snap into the SnapBlobDir
snapYaml := `name: foo
version: 1.0`
- fooFname, fooDecl, fooRev := s.makeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid")
+ fooFname, fooDecl, fooRev := s.MakeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid")
+ s.WriteAssertions("foo.snap-declaration", fooDecl)
+ s.WriteAssertions("foo.snap-revision", fooRev)
// put a firstboot local snap into the SnapBlobDir
snapYaml = `name: local
version: 1.0`
mockSnapFile := snaptest.MakeTestSnapWithFiles(c, snapYaml, nil)
targetSnapFile2 := filepath.Join(dirs.SnapSeedDir, "snaps", filepath.Base(mockSnapFile))
- err = os.Rename(mockSnapFile, targetSnapFile2)
- c.Assert(err, IsNil)
-
- declFn := filepath.Join(dirs.SnapSeedDir, "assertions", "foo.snap-declaration")
- err = ioutil.WriteFile(declFn, asserts.Encode(fooDecl), 0644)
- c.Assert(err, IsNil)
-
- revFn := filepath.Join(dirs.SnapSeedDir, "assertions", "foo.snap-revision")
- err = ioutil.WriteFile(revFn, asserts.Encode(fooRev), 0644)
+ err := os.Rename(mockSnapFile, targetSnapFile2)
c.Assert(err, IsNil)
// add a model assertion and its chain
assertsChain := s.makeModelAssertionChain(c, "my-model", nil, "foo")
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)
+ s.WriteAssertions(strconv.Itoa(i), as)
}
// create a seed.yaml
@@ -491,19 +469,7 @@
tsAll, err := devicestate.PopulateStateFromSeedImpl(st, s.perfTimings)
c.Assert(err, IsNil)
- // the first taskset installs core and waits for noone
- i := 0
- tCore := tsAll[i].Tasks()[0]
- c.Check(tCore.WaitTasks(), HasLen, 0)
- // the next installs the kernel and that will wait for core
- i++
- tKernel := tsAll[i].Tasks()[0]
- c.Check(tKernel.WaitTasks(), testutil.Contains, tCore)
- // the next installs the gadget and will wait for the kernel
- i++
- tGadget := tsAll[i].Tasks()[0]
- c.Check(tGadget.WaitTasks(), testutil.Contains, tKernel)
-
+ checkOrder(c, tsAll, "core", "pc-kernel", "pc", "foo", "local")
checkSeedTasks(c, tsAll)
// now run the change and check the result
@@ -522,16 +488,14 @@
}
func (s *FirstBootTestSuite) TestPopulateFromSeedHappy(c *C) {
- loader := boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(loader)
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
defer bootloader.Force(nil)
- loader.SetBootVars(map[string]string{
- "snap_core": "core_1.snap",
- "snap_kernel": "pc-kernel_1.snap",
- })
+ bloader.SetBootKernel("pc-kernel_1.snap")
+ bloader.SetBootBase("core_1.snap")
st := s.overlord.State()
- chg := s.makeBecomeOperationalChange(c, st)
+ chg := s.makeSeedChange(c, st)
err := s.overlord.Settle(settleTimeout)
c.Assert(err, IsNil)
@@ -561,7 +525,7 @@
_, err = snapstate.CurrentInfo(state, "pc")
c.Assert(err, IsNil)
- // ensure requied flag is set on all essential snaps
+ // ensure required flag is set on all essential snaps
var snapst snapstate.SnapState
for _, reqName := range []string{"core", "pc-kernel", "pc"} {
err = snapstate.Get(state, reqName, &snapst)
@@ -572,7 +536,7 @@
// check foo
info, err := snapstate.CurrentInfo(state, "foo")
c.Assert(err, IsNil)
- c.Assert(info.SnapID, Equals, "foo-snap-idididididididididididi")
+ c.Assert(info.SnapID, Equals, "foodidididididididididididididid")
c.Assert(info.Revision, Equals, snap.R(128))
c.Assert(info.Contact, Equals, "mailto:some.guy@example.com")
pubAcct, err := assertstate.Publisher(st, info.SnapID)
@@ -625,13 +589,15 @@
ifacemgr, err := ifacestate.Manager(st, nil, o.TaskRunner(), nil, nil)
c.Assert(err, IsNil)
o.AddManager(ifacemgr)
+ c.Assert(o.StartUp(), IsNil)
+
st.Lock()
assertstate.ReplaceDB(st, db.(*asserts.Database))
st.Unlock()
o.AddManager(o.TaskRunner())
- chg := s.makeBecomeOperationalChange(c, st)
+ chg := s.makeSeedChange(c, st)
se := o.StateEngine()
// we cannot use Settle because the Change will not become Clean
@@ -646,54 +612,30 @@
c.Assert(chg.Err(), ErrorMatches, `(?s).* cannot determine bootloader.*`)
}
-func writeAssertionsToFile(fn string, assertions []asserts.Assertion) {
- multifn := filepath.Join(dirs.SnapSeedDir, "assertions", fn)
- f, err := os.Create(multifn)
- if err != nil {
- panic(err)
- }
- defer f.Close()
- enc := asserts.NewEncoder(f)
- for _, a := range assertions {
- err := enc.Encode(a)
- if err != nil {
- panic(err)
- }
- }
-}
-
func (s *FirstBootTestSuite) TestPopulateFromSeedHappyMultiAssertsFiles(c *C) {
- loader := boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(loader)
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
defer bootloader.Force(nil)
- loader.SetBootVars(map[string]string{
- "snap_core": "core_1.snap",
- "snap_kernel": "pc-kernel_1.snap",
- })
+ bloader.SetBootKernel("pc-kernel_1.snap")
+ bloader.SetBootBase("core_1.snap")
coreFname, kernelFname, gadgetFname := s.makeCoreSnaps(c, "")
- devAcct := assertstest.NewAccount(s.storeSigning, "developer", map[string]interface{}{
- "account-id": "developerid",
- }, "")
-
// put a firstboot snap into the SnapBlobDir
snapYaml := `name: foo
version: 1.0`
- fooFname, fooDecl, fooRev := s.makeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid")
-
- writeAssertionsToFile("foo.asserts", []asserts.Assertion{devAcct, fooRev, fooDecl})
+ fooFname, fooDecl, fooRev := s.MakeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid")
+ s.WriteAssertions("foo.asserts", s.devAcct, fooRev, fooDecl)
// put a 2nd firstboot snap into the SnapBlobDir
snapYaml = `name: bar
version: 1.0`
- barFname, barDecl, barRev := s.makeAssertedSnap(c, snapYaml, nil, snap.R(65), "developerid")
-
- writeAssertionsToFile("bar.asserts", []asserts.Assertion{devAcct, barDecl, barRev})
+ barFname, barDecl, barRev := s.MakeAssertedSnap(c, snapYaml, nil, snap.R(65), "developerid")
+ s.WriteAssertions("bar.asserts", s.devAcct, barDecl, barRev)
// add a model assertion and its chain
assertsChain := s.makeModelAssertionChain(c, "my-model", nil)
- writeAssertionsToFile("model.asserts", assertsChain)
+ s.WriteAssertions("model.asserts", assertsChain...)
// create a seed.yaml
content := []byte(fmt.Sprintf(`
@@ -753,7 +695,7 @@
// check foo
info, err := snapstate.CurrentInfo(state, "foo")
c.Assert(err, IsNil)
- c.Check(info.SnapID, Equals, "foo-snap-idididididididididididi")
+ c.Check(info.SnapID, Equals, "foodidididididididididididididid")
c.Check(info.Revision, Equals, snap.R(128))
pubAcct, err := assertstate.Publisher(st, info.SnapID)
c.Assert(err, IsNil)
@@ -762,99 +704,45 @@
// check bar
info, err = snapstate.CurrentInfo(state, "bar")
c.Assert(err, IsNil)
- c.Check(info.SnapID, Equals, "bar-snap-idididididididididididi")
+ c.Check(info.SnapID, Equals, "bardidididididididididididididid")
c.Check(info.Revision, Equals, snap.R(65))
pubAcct, err = assertstate.Publisher(st, info.SnapID)
c.Assert(err, IsNil)
c.Check(pubAcct.AccountID(), Equals, "developerid")
}
-func (s *FirstBootTestSuite) makeModelAssertion(c *C, modelStr string, extraHeaders map[string]interface{}, reqSnaps ...string) *asserts.Model {
- headers := map[string]interface{}{
- "architecture": "amd64",
- "store": "canonical",
- }
- if strings.HasSuffix(modelStr, "-classic") {
- headers["classic"] = "true"
- } else {
- headers["kernel"] = "pc-kernel"
- headers["gadget"] = "pc"
- }
- if len(reqSnaps) != 0 {
- reqs := make([]interface{}, len(reqSnaps))
- for i, req := range reqSnaps {
- reqs[i] = req
- }
- headers["required-snaps"] = reqs
- }
- return s.brands.Model("my-brand", modelStr, headers, extraHeaders)
-}
-
-func (s *FirstBootTestSuite) makeModelAssertionChain(c *C, modName string, extraHeaders map[string]interface{}, reqSnaps ...string) []asserts.Assertion {
- assertChain := []asserts.Assertion{}
-
- assertChain = append(assertChain, s.brands.Account("my-brand"))
- assertChain = append(assertChain, s.brands.AccountKey("my-brand"))
-
- model := s.makeModelAssertion(c, modName, extraHeaders, reqSnaps...)
- assertChain = append(assertChain, model)
-
- storeAccountKey := s.storeSigning.StoreAccountKey("")
- assertChain = append(assertChain, storeAccountKey)
- return assertChain
-}
-
func (s *FirstBootTestSuite) TestPopulateFromSeedConfigureHappy(c *C) {
- loader := boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(loader)
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
defer bootloader.Force(nil)
- loader.SetBootVars(map[string]string{
- "snap_core": "core_1.snap",
- "snap_kernel": "pc-kernel_1.snap",
- })
+ bloader.SetBootKernel("pc-kernel_1.snap")
+ bloader.SetBootBase("core_1.snap")
const defaultsYaml = `
defaults:
- foo-snap-idididididididididididi:
+ foodidididididididididididididid:
foo-cfg: foo.
- core-snap-ididididididididididid:
+ coreidididididididididididididid:
core-cfg: core_cfg_defl
- pc-kernel-snap-ididididididididi:
+ pckernelidididididididididididid:
pc-kernel-cfg: pc-kernel_cfg_defl
- pc-snap-idididididididididididid:
+ pcididididididididididididididid:
pc-cfg: pc_cfg_defl
`
coreFname, kernelFname, gadgetFname := s.makeCoreSnaps(c, defaultsYaml)
- devAcct := assertstest.NewAccount(s.storeSigning, "developer", map[string]interface{}{
- "account-id": "developerid",
- }, "")
-
- devAcctFn := filepath.Join(dirs.SnapSeedDir, "assertions", "developer.account")
- err := ioutil.WriteFile(devAcctFn, asserts.Encode(devAcct), 0644)
- c.Assert(err, IsNil)
+ s.WriteAssertions("developer.account", s.devAcct)
// put a firstboot snap into the SnapBlobDir
files := [][]string{{"meta/hooks/configure", ""}}
snapYaml := `name: foo
version: 1.0`
- fooFname, fooDecl, fooRev := s.makeAssertedSnap(c, snapYaml, files, snap.R(128), "developerid")
-
- declFn := filepath.Join(dirs.SnapSeedDir, "assertions", "foo.snap-declaration")
- err = ioutil.WriteFile(declFn, asserts.Encode(fooDecl), 0644)
- c.Assert(err, IsNil)
-
- revFn := filepath.Join(dirs.SnapSeedDir, "assertions", "foo.snap-revision")
- err = ioutil.WriteFile(revFn, asserts.Encode(fooRev), 0644)
- c.Assert(err, IsNil)
+ fooFname, fooDecl, fooRev := s.MakeAssertedSnap(c, snapYaml, files, snap.R(128), "developerid")
+ s.WriteAssertions("foo.asserts", fooDecl, fooRev)
// add a model assertion and its chain
assertsChain := s.makeModelAssertionChain(c, "my-model", nil, "foo")
- 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)
- }
+ s.WriteAssertions("model.asserts", assertsChain...)
// create a seed.yaml
content := []byte(fmt.Sprintf(`
@@ -868,7 +756,7 @@
- name: foo
file: %s
`, coreFname, kernelFname, gadgetFname, fooFname))
- err = ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
+ err := ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
c.Assert(err, IsNil)
// run the firstboot stuff
@@ -956,7 +844,7 @@
// check foo
info, err := snapstate.CurrentInfo(state, "foo")
c.Assert(err, IsNil)
- c.Assert(info.SnapID, Equals, "foo-snap-idididididididididididi")
+ c.Assert(info.SnapID, Equals, "foodidididididididididididididid")
c.Assert(info.Revision, Equals, snap.R(128))
pubAcct, err := assertstate.Publisher(st, info.SnapID)
c.Assert(err, IsNil)
@@ -977,50 +865,31 @@
}
func (s *FirstBootTestSuite) TestPopulateFromSeedGadgetConnectHappy(c *C) {
- loader := boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(loader)
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
defer bootloader.Force(nil)
- loader.SetBootVars(map[string]string{
- "snap_core": "core_1.snap",
- "snap_kernel": "pc-kernel_1.snap",
- })
+ bloader.SetBootKernel("pc-kernel_1.snap")
+ bloader.SetBootBase("core_1.snap")
const connectionsYaml = `
connections:
- - plug: foo-snap-idididididididididididi:network-control
+ - plug: foodidididididididididididididid:network-control
`
coreFname, kernelFname, gadgetFname := s.makeCoreSnaps(c, connectionsYaml)
- devAcct := assertstest.NewAccount(s.storeSigning, "developer", map[string]interface{}{
- "account-id": "developerid",
- }, "")
-
- devAcctFn := filepath.Join(dirs.SnapSeedDir, "assertions", "developer.account")
- err := ioutil.WriteFile(devAcctFn, asserts.Encode(devAcct), 0644)
- c.Assert(err, IsNil)
+ s.WriteAssertions("developer.account", s.devAcct)
snapYaml := `name: foo
version: 1.0
plugs:
network-control:
`
- fooFname, fooDecl, fooRev := s.makeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid")
-
- declFn := filepath.Join(dirs.SnapSeedDir, "assertions", "foo.snap-declaration")
- err = ioutil.WriteFile(declFn, asserts.Encode(fooDecl), 0644)
- c.Assert(err, IsNil)
-
- revFn := filepath.Join(dirs.SnapSeedDir, "assertions", "foo.snap-revision")
- err = ioutil.WriteFile(revFn, asserts.Encode(fooRev), 0644)
- c.Assert(err, IsNil)
+ fooFname, fooDecl, fooRev := s.MakeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid")
+ s.WriteAssertions("foo.asserts", fooDecl, fooRev)
// add a model assertion and its chain
assertsChain := s.makeModelAssertionChain(c, "my-model", nil, "foo")
- 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)
- }
+ s.WriteAssertions("model.asserts", assertsChain...)
// create a seed.yaml
content := []byte(fmt.Sprintf(`
@@ -1034,7 +903,7 @@
- name: foo
file: %s
`, coreFname, kernelFname, gadgetFname, fooFname))
- err = ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
+ err := ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
c.Assert(err, IsNil)
// run the firstboot stuff
@@ -1079,7 +948,7 @@
// check foo
info, err := snapstate.CurrentInfo(state, "foo")
c.Assert(err, IsNil)
- c.Assert(info.SnapID, Equals, "foo-snap-idididididididididididi")
+ c.Assert(info.SnapID, Equals, "foodidididididididididididididid")
c.Assert(info.Revision, Equals, snap.R(128))
pubAcct, err := assertstate.Publisher(st, info.SnapID)
c.Assert(err, IsNil)
@@ -1107,65 +976,68 @@
restore := release.MockOnClassic(true)
defer restore()
- ovld, err := overlord.New()
+ ovld, err := overlord.New(nil)
c.Assert(err, IsNil)
st := ovld.State()
- // add a bunch of assert files
+ // add the odel assertion and its chain
assertsChain := s.makeModelAssertionChain(c, "my-model", nil)
- 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)
- }
+ s.WriteAssertions("model.asserts", assertsChain...)
// import them
st.Lock()
defer st.Unlock()
- _, err = devicestate.ImportAssertionsFromSeed(st)
+ deviceSeed, err := seed.Open(dirs.SnapSeedDir)
+ c.Assert(err, IsNil)
+
+ _, err = devicestate.ImportAssertionsFromSeed(st, deviceSeed)
c.Assert(err, ErrorMatches, "cannot seed a classic system with an all-snaps model")
}
func (s *FirstBootTestSuite) TestImportAssertionsFromSeedAllSnapsModelMismatch(c *C) {
- ovld, err := overlord.New()
+ ovld, err := overlord.New(nil)
c.Assert(err, IsNil)
st := ovld.State()
- // add a bunch of assert files
+ // add the model assertion and its chain
assertsChain := s.makeModelAssertionChain(c, "my-model-classic", nil)
- 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)
- }
+ s.WriteAssertions("model.asserts", assertsChain...)
// import them
st.Lock()
defer st.Unlock()
- _, err = devicestate.ImportAssertionsFromSeed(st)
+ deviceSeed, err := seed.Open(dirs.SnapSeedDir)
+ c.Assert(err, IsNil)
+
+ _, err = devicestate.ImportAssertionsFromSeed(st, deviceSeed)
c.Assert(err, ErrorMatches, "cannot seed an all-snaps system with a classic model")
}
func (s *FirstBootTestSuite) TestImportAssertionsFromSeedHappy(c *C) {
- ovld, err := overlord.New()
+ ovld, err := overlord.New(nil)
c.Assert(err, IsNil)
st := ovld.State()
- // add a bunch of assert files
+ // add a bunch of assertions (model assertion and its chain)
assertsChain := s.makeModelAssertionChain(c, "my-model", nil)
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)
+ fname := strconv.Itoa(i)
+ if as.Type() == asserts.ModelType {
+ fname = "model"
+ }
+ s.WriteAssertions(fname, as)
}
// import them
st.Lock()
defer st.Unlock()
- model, err := devicestate.ImportAssertionsFromSeed(st)
+ deviceSeed, err := seed.Open(dirs.SnapSeedDir)
+ c.Assert(err, IsNil)
+
+ model, err := devicestate.ImportAssertionsFromSeed(st, deviceSeed)
c.Assert(err, IsNil)
c.Assert(model, NotNil)
@@ -1198,17 +1070,18 @@
assertsChain := s.makeModelAssertionChain(c, "my-model", nil)
for _, as := range assertsChain {
if as.Type() == asserts.ModelType {
- fn := filepath.Join(dirs.SnapSeedDir, "assertions", "model")
- err := ioutil.WriteFile(fn, asserts.Encode(as), 0644)
- c.Assert(err, IsNil)
+ s.WriteAssertions("model", as)
break
}
}
+ deviceSeed, err := seed.Open(dirs.SnapSeedDir)
+ c.Assert(err, IsNil)
+
// try import and verify that its rejects because other assertions are
// missing
- _, err := devicestate.ImportAssertionsFromSeed(st)
- c.Assert(err, ErrorMatches, "cannot find account-key .*")
+ _, err = devicestate.ImportAssertionsFromSeed(st, deviceSeed)
+ c.Assert(err, ErrorMatches, "cannot resolve prerequisite assertion: account-key .*")
}
func (s *FirstBootTestSuite) TestImportAssertionsFromSeedTwoModelAsserts(c *C) {
@@ -1217,20 +1090,19 @@
defer st.Unlock()
// write out two model assertions
- model := s.makeModelAssertion(c, "my-model", nil)
- fn := filepath.Join(dirs.SnapSeedDir, "assertions", "model")
- err := ioutil.WriteFile(fn, asserts.Encode(model), 0644)
- c.Assert(err, IsNil)
+ model := s.Brands.Model("my-brand", "my-model", s.modelHeaders("my-model"))
+ s.WriteAssertions("model", model)
+
+ model2 := s.Brands.Model("my-brand", "my-second-model", s.modelHeaders("my-second-model"))
+ s.WriteAssertions("model2", model2)
- model2 := s.makeModelAssertion(c, "my-second-model", nil)
- fn = filepath.Join(dirs.SnapSeedDir, "assertions", "model2")
- err = ioutil.WriteFile(fn, asserts.Encode(model2), 0644)
+ deviceSeed, err := seed.Open(dirs.SnapSeedDir)
c.Assert(err, IsNil)
// try import and verify that its rejects because other assertions are
// missing
- _, err = devicestate.ImportAssertionsFromSeed(st)
- c.Assert(err, ErrorMatches, "cannot add more than one model assertion")
+ _, err = devicestate.ImportAssertionsFromSeed(st, deviceSeed)
+ c.Assert(err, ErrorMatches, "cannot have multiple model assertions in seed")
}
func (s *FirstBootTestSuite) TestImportAssertionsFromSeedNoModelAsserts(c *C) {
@@ -1239,57 +1111,75 @@
defer st.Unlock()
assertsChain := s.makeModelAssertionChain(c, "my-model", nil)
- for _, as := range assertsChain {
+ for i, as := range assertsChain {
if as.Type() != asserts.ModelType {
- fn := filepath.Join(dirs.SnapSeedDir, "assertions", "model")
- err := ioutil.WriteFile(fn, asserts.Encode(as), 0644)
- c.Assert(err, IsNil)
- break
+ s.WriteAssertions(strconv.Itoa(i), as)
}
}
+ deviceSeed, err := seed.Open(dirs.SnapSeedDir)
+ c.Assert(err, IsNil)
+
// try import and verify that its rejects because other assertions are
// missing
- _, err := devicestate.ImportAssertionsFromSeed(st)
- c.Assert(err, ErrorMatches, "need a model assertion")
+ _, err = devicestate.ImportAssertionsFromSeed(st, deviceSeed)
+ c.Assert(err, ErrorMatches, "seed must have a model assertion")
+}
+
+type core18SnapsOpts struct {
+ classic bool
+ gadget bool
}
-func (s *FirstBootTestSuite) makeCore18Snaps(c *C) (core18Fn, snapdFn, kernelFn, gadgetFn string) {
+func (s *FirstBootTestSuite) makeCore18Snaps(c *C, opts *core18SnapsOpts) (core18Fn, snapdFn, kernelFn, gadgetFn string) {
+ if opts == nil {
+ opts = &core18SnapsOpts{}
+ }
+
files := [][]string{}
core18Yaml := `name: core18
version: 1.0
type: base`
- core18Fname, core18Decl, core18Rev := s.makeAssertedSnap(c, core18Yaml, files, snap.R(1), "canonical")
- writeAssertionsToFile("core18.asserts", []asserts.Assertion{core18Rev, core18Decl})
+ core18Fname, core18Decl, core18Rev := s.MakeAssertedSnap(c, core18Yaml, files, snap.R(1), "canonical")
+ s.WriteAssertions("core18.asserts", core18Rev, core18Decl)
snapdYaml := `name: snapd
version: 1.0
`
- snapdFname, snapdDecl, snapdRev := s.makeAssertedSnap(c, snapdYaml, nil, snap.R(2), "canonical")
- writeAssertionsToFile("snapd.asserts", []asserts.Assertion{snapdRev, snapdDecl})
+ snapdFname, snapdDecl, snapdRev := s.MakeAssertedSnap(c, snapdYaml, nil, snap.R(2), "canonical")
+ s.WriteAssertions("snapd.asserts", snapdRev, snapdDecl)
- kernelYaml := `name: pc-kernel
+ var kernelFname string
+ if !opts.classic {
+ kernelYaml := `name: pc-kernel
version: 1.0
type: kernel`
- kernelFname, kernelDecl, kernelRev := s.makeAssertedSnap(c, kernelYaml, files, snap.R(1), "canonical")
-
- writeAssertionsToFile("kernel.asserts", []asserts.Assertion{kernelRev, kernelDecl})
+ fname, kernelDecl, kernelRev := s.MakeAssertedSnap(c, kernelYaml, files, snap.R(1), "canonical")
+ s.WriteAssertions("kernel.asserts", kernelRev, kernelDecl)
+ kernelFname = fname
+ }
- gadgetYaml := `
+ if !opts.classic {
+ gadgetYaml := `
volumes:
volume-id:
bootloader: grub
`
- files = append(files, []string{"meta/gadget.yaml", gadgetYaml})
- gaYaml := `name: pc
+ files = append(files, []string{"meta/gadget.yaml", gadgetYaml})
+ }
+
+ var gadgetFname string
+ if !opts.classic || opts.gadget {
+ gaYaml := `name: pc
version: 1.0
type: gadget
base: core18
`
- gadgetFname, gadgetDecl, gadgetRev := s.makeAssertedSnap(c, gaYaml, files, snap.R(1), "canonical")
-
- writeAssertionsToFile("gadget.asserts", []asserts.Assertion{gadgetRev, gadgetDecl})
+ fname, gadgetDecl, gadgetRev := s.MakeAssertedSnap(c, gaYaml, files, snap.R(1), "canonical")
+ s.WriteAssertions("gadget.asserts", gadgetRev, gadgetDecl)
+ gadgetFname = fname
+ }
return core18Fname, snapdFname, kernelFname, gadgetFname
}
@@ -1302,31 +1192,19 @@
})
defer systemctlRestorer()
- loader := boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(loader)
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
defer bootloader.Force(nil)
- loader.SetBootVars(map[string]string{
- "snap_core": "core18_1.snap",
- "snap_kernel": "pc-kernel_1.snap",
- })
+ bloader.SetBootKernel("pc-kernel_1.snap")
+ bloader.SetBootBase("core18_1.snap")
- core18Fname, snapdFname, kernelFname, gadgetFname := s.makeCore18Snaps(c)
+ core18Fname, snapdFname, kernelFname, gadgetFname := s.makeCore18Snaps(c, nil)
- devAcct := assertstest.NewAccount(s.storeSigning, "developer", map[string]interface{}{
- "account-id": "developerid",
- }, "")
-
- devAcctFn := filepath.Join(dirs.SnapSeedDir, "assertions", "developer.account")
- err := ioutil.WriteFile(devAcctFn, asserts.Encode(devAcct), 0644)
- c.Assert(err, IsNil)
+ s.WriteAssertions("developer.account", s.devAcct)
// add a model assertion and its chain
assertsChain := s.makeModelAssertionChain(c, "my-model", map[string]interface{}{"base": "core18"})
- 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)
- }
+ s.WriteAssertions("model.asserts", assertsChain...)
// create a seed.yaml
content := []byte(fmt.Sprintf(`
@@ -1340,7 +1218,7 @@
- name: pc
file: %s
`, snapdFname, core18Fname, kernelFname, gadgetFname))
- err = ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
+ err := ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
c.Assert(err, IsNil)
// run the firstboot stuff
@@ -1350,22 +1228,7 @@
tsAll, err := devicestate.PopulateStateFromSeedImpl(st, s.perfTimings)
c.Assert(err, IsNil)
- // the first taskset installs snapd and waits for noone
- i := 0
- tSnapd := tsAll[i].Tasks()[0]
- c.Check(tSnapd.WaitTasks(), HasLen, 0)
- // the next installs the core18 and that will wait for snapd
- i++
- tCore18 := tsAll[i].Tasks()[0]
- c.Check(tCore18.WaitTasks(), testutil.Contains, tSnapd)
- // the next installs the kernel and will wait for the core18
- i++
- tKernel := tsAll[i].Tasks()[0]
- c.Check(tKernel.WaitTasks(), testutil.Contains, tCore18)
- // the next installs the gadget and will wait for the kernel
- i++
- tGadget := tsAll[i].Tasks()[0]
- c.Check(tGadget.WaitTasks(), testutil.Contains, tKernel)
+ checkOrder(c, tsAll, "snapd", "pc-kernel", "core18", "pc")
// now run the change and check the result
// use the expected kind otherwise settle with start another one
@@ -1415,7 +1278,7 @@
_, err = snapstate.CurrentInfo(state, "pc")
c.Check(err, IsNil)
- // ensure requied flag is set on all essential snaps
+ // ensure required flag is set on all essential snaps
var snapst snapstate.SnapState
for _, reqName := range []string{"snapd", "core18", "pc-kernel", "pc"} {
err = snapstate.Get(state, reqName, &snapst)
@@ -1440,36 +1303,26 @@
}
func (s *FirstBootTestSuite) TestPopulateFromSeedOrdering(c *C) {
- devAcct := assertstest.NewAccount(s.storeSigning, "developer", map[string]interface{}{
- "account-id": "developerid",
- }, "")
-
- devAcctFn := filepath.Join(dirs.SnapSeedDir, "assertions", "developer.account")
- err := ioutil.WriteFile(devAcctFn, asserts.Encode(devAcct), 0644)
- c.Assert(err, IsNil)
+ s.WriteAssertions("developer.account", s.devAcct)
// add a model assertion and its chain
assertsChain := s.makeModelAssertionChain(c, "my-model", map[string]interface{}{"base": "core18"})
- 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)
- }
+ s.WriteAssertions("model.asserts", assertsChain...)
- core18Fname, snapdFname, kernelFname, gadgetFname := s.makeCore18Snaps(c)
+ core18Fname, snapdFname, kernelFname, gadgetFname := s.makeCore18Snaps(c, nil)
snapYaml := `name: snap-req-other-base
version: 1.0
base: other-base
`
- snapFname, snapDecl, snapRev := s.makeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid")
- writeAssertionsToFile("snap-req-other-base.asserts", []asserts.Assertion{devAcct, snapRev, snapDecl})
+ snapFname, snapDecl, snapRev := s.MakeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid")
+ s.WriteAssertions("snap-req-other-base.asserts", s.devAcct, snapRev, snapDecl)
baseYaml := `name: other-base
version: 1.0
type: base
`
- baseFname, baseDecl, baseRev := s.makeAssertedSnap(c, baseYaml, nil, snap.R(127), "developerid")
- writeAssertionsToFile("other-base.asserts", []asserts.Assertion{devAcct, baseRev, baseDecl})
+ baseFname, baseDecl, baseRev := s.MakeAssertedSnap(c, baseYaml, nil, snap.R(127), "developerid")
+ s.WriteAssertions("other-base.asserts", s.devAcct, baseRev, baseDecl)
// create a seed.yaml
content := []byte(fmt.Sprintf(`
@@ -1487,7 +1340,7 @@
- name: other-base
file: %s
`, snapdFname, core18Fname, kernelFname, gadgetFname, snapFname, baseFname))
- err = ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
+ err := ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
c.Assert(err, IsNil)
// run the firstboot stuff
@@ -1497,50 +1350,17 @@
tsAll, err := devicestate.PopulateStateFromSeedImpl(st, s.perfTimings)
c.Assert(err, IsNil)
- // the first taskset installs snapd and waits for noone
- i := 0
- tSnapd := tsAll[i].Tasks()[0]
- c.Check(tSnapd.WaitTasks(), HasLen, 0)
- // the next installs the core18 and that will wait for snapd
- i++
- tCore18 := tsAll[i].Tasks()[0]
- c.Check(tCore18.WaitTasks(), testutil.Contains, tSnapd)
- // the next installs the kernel and will wait for the core18
- i++
- tKernel := tsAll[i].Tasks()[0]
- c.Check(tKernel.WaitTasks(), testutil.Contains, tCore18)
- // the next installs the gadget and will wait for the kernel
- i++
- tGadget := tsAll[i].Tasks()[0]
- c.Check(tGadget.WaitTasks(), testutil.Contains, tKernel)
- // the next installs the base and waits for the gadget
- i++
- tOtherBase := tsAll[i].Tasks()[0]
- c.Check(tOtherBase.WaitTasks(), testutil.Contains, tGadget)
- // and finally the app
- i++
- tSnap := tsAll[i].Tasks()[0]
- c.Check(tSnap.WaitTasks(), testutil.Contains, tOtherBase)
+ checkOrder(c, tsAll, "snapd", "pc-kernel", "core18", "pc", "other-base", "snap-req-other-base")
}
func (s *FirstBootTestSuite) TestFirstbootGadgetBaseModelBaseMismatch(c *C) {
- devAcct := assertstest.NewAccount(s.storeSigning, "developer", map[string]interface{}{
- "account-id": "developerid",
- }, "")
-
- devAcctFn := filepath.Join(dirs.SnapSeedDir, "assertions", "developer.account")
- err := ioutil.WriteFile(devAcctFn, asserts.Encode(devAcct), 0644)
- c.Assert(err, IsNil)
+ s.WriteAssertions("developer.account", s.devAcct)
// add a model assertion and its chain
assertsChain := s.makeModelAssertionChain(c, "my-model", map[string]interface{}{"base": "core18"})
- 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)
- }
+ s.WriteAssertions("model.asserts", assertsChain...)
- core18Fname, snapdFname, kernelFname, _ := s.makeCore18Snaps(c)
+ core18Fname, snapdFname, kernelFname, _ := s.makeCore18Snaps(c, nil)
// take the gadget without "base: core18"
_, _, gadgetFname := s.makeCoreSnaps(c, "")
@@ -1556,7 +1376,7 @@
- name: pc
file: %s
`, snapdFname, core18Fname, kernelFname, gadgetFname))
- err = ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
+ err := ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
c.Assert(err, IsNil)
// run the firstboot stuff
@@ -1565,24 +1385,18 @@
defer st.Unlock()
_, err = devicestate.PopulateStateFromSeedImpl(st, s.perfTimings)
- c.Assert(err, ErrorMatches, `cannot use gadget snap because its base "" is different from model base "core18"`)
+ c.Assert(err, ErrorMatches, `cannot use gadget snap because its base "core" is different from model base "core18"`)
}
func (s *FirstBootTestSuite) TestPopulateFromSeedWrongContentProviderOrder(c *C) {
- loader := boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(loader)
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
defer bootloader.Force(nil)
- loader.SetBootVars(map[string]string{
- "snap_core": "core_1.snap",
- "snap_kernel": "pc-kernel_1.snap",
- })
+ bloader.SetBootKernel("pc-kernel_1.snap")
+ bloader.SetBootBase("core_1.snap")
coreFname, kernelFname, gadgetFname := s.makeCoreSnaps(c, "")
- devAcct := assertstest.NewAccount(s.storeSigning, "developer", map[string]interface{}{
- "account-id": "developerid",
- }, "")
-
// a snap that uses content providers
snapYaml := `name: gnome-calculator
version: 1.0
@@ -1592,9 +1406,8 @@
default-provider: gtk-common-themes
target: $SNAP/data-dir/themes
`
- calcFname, calcDecl, calcRev := s.makeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid")
-
- writeAssertionsToFile("calc.asserts", []asserts.Assertion{devAcct, calcRev, calcDecl})
+ calcFname, calcDecl, calcRev := s.MakeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid")
+ s.WriteAssertions("calc.asserts", s.devAcct, calcRev, calcDecl)
// put a 2nd firstboot snap into the SnapBlobDir
snapYaml = `name: gtk-common-themes
@@ -1606,13 +1419,12 @@
read:
- $SNAP/share/themes/Adawaita
`
- themesFname, themesDecl, themesRev := s.makeAssertedSnap(c, snapYaml, nil, snap.R(65), "developerid")
-
- writeAssertionsToFile("themes.asserts", []asserts.Assertion{devAcct, themesDecl, themesRev})
+ themesFname, themesDecl, themesRev := s.MakeAssertedSnap(c, snapYaml, nil, snap.R(65), "developerid")
+ s.WriteAssertions("themes.asserts", s.devAcct, themesDecl, themesRev)
// add a model assertion and its chain
assertsChain := s.makeModelAssertionChain(c, "my-model", nil)
- writeAssertionsToFile("model.asserts", assertsChain)
+ s.WriteAssertions("model.asserts", assertsChain...)
// create a seed.yaml
content := []byte(fmt.Sprintf(`
@@ -1666,3 +1478,268 @@
c.Check(conn.(map[string]interface{})["auto"], Equals, true)
c.Check(conn.(map[string]interface{})["interface"], Equals, "content")
}
+
+func (s *FirstBootTestSuite) TestPopulateFromSeedMissingBase(c *C) {
+ s.WriteAssertions("developer.account", s.devAcct)
+
+ // add a model assertion and its chain
+ assertsChain := s.makeModelAssertionChain(c, "my-model", nil)
+ s.WriteAssertions("model.asserts", assertsChain...)
+
+ coreFname, kernelFname, gadgetFname := s.makeCoreSnaps(c, "")
+
+ // TODO: this test doesn't particularly need to use a local snap
+ // local snap with unknown base
+ snapYaml = `name: local
+base: foo
+version: 1.0`
+ mockSnapFile := snaptest.MakeTestSnapWithFiles(c, snapYaml, nil)
+ localFname := filepath.Base(mockSnapFile)
+ targetSnapFile2 := filepath.Join(dirs.SnapSeedDir, "snaps", localFname)
+ c.Assert(os.Rename(mockSnapFile, targetSnapFile2), IsNil)
+
+ // create a seed.yaml
+ content := []byte(fmt.Sprintf(`
+snaps:
+ - name: core
+ file: %s
+ - name: pc-kernel
+ file: %s
+ - name: pc
+ file: %s
+ - name: local
+ unasserted: true
+ file: %s
+`, coreFname, kernelFname, gadgetFname, localFname))
+
+ c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644), IsNil)
+
+ // run the firstboot stuff
+ st := s.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ _, err := devicestate.PopulateStateFromSeedImpl(st, s.perfTimings)
+ c.Assert(err, ErrorMatches, `cannot use snap "local": base "foo" is missing`)
+}
+
+func (s *FirstBootTestSuite) TestPopulateFromSeedOnClassicWithSnapdOnlyHappy(c *C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+
+ var sysdLog [][]string
+ systemctlRestorer := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) {
+ sysdLog = append(sysdLog, cmd)
+ return []byte("ActiveState=inactive\n"), nil
+ })
+ defer systemctlRestorer()
+
+ core18Fname, snapdFname, _, _ := s.makeCore18Snaps(c, &core18SnapsOpts{
+ classic: true,
+ })
+
+ // put a firstboot snap into the SnapBlobDir
+ snapYaml := `name: foo
+version: 1.0
+base: core18
+`
+ fooFname, fooDecl, fooRev := s.MakeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid")
+ s.WriteAssertions("foo.asserts", s.devAcct, fooRev, fooDecl)
+
+ // add a model assertion and its chain
+ assertsChain := s.makeModelAssertionChain(c, "my-model-classic", nil)
+ s.WriteAssertions("model.asserts", assertsChain...)
+
+ // create a seed.yaml
+ content := []byte(fmt.Sprintf(`
+snaps:
+ - name: snapd
+ file: %s
+ - name: foo
+ file: %s
+ - name: core18
+ file: %s
+`, snapdFname, fooFname, core18Fname))
+ err := ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
+ c.Assert(err, IsNil)
+
+ // run the firstboot stuff
+ st := s.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ tsAll, err := devicestate.PopulateStateFromSeedImpl(st, s.perfTimings)
+ c.Assert(err, IsNil)
+
+ checkOrder(c, tsAll, "snapd", "core18", "foo")
+
+ // now run the change and check the result
+ // 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)
+ }
+ c.Assert(st.Changes(), HasLen, 1)
+
+ c.Assert(chg.Err(), IsNil)
+
+ // avoid device reg
+ chg1 := st.NewChange("become-operational", "init device")
+ chg1.SetStatus(state.DoingStatus)
+
+ // run change until it wants to restart
+ st.Unlock()
+ err = s.overlord.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ // at this point the system is "restarting", pretend the restart has
+ // happened
+ c.Assert(chg.Status(), Equals, state.DoingStatus)
+ state.MockRestarting(st, state.RestartUnset)
+ st.Unlock()
+ err = s.overlord.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+ c.Assert(chg.Status(), Equals, state.DoneStatus)
+
+ // verify
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ state, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ state.Lock()
+ defer state.Unlock()
+ // check snapd, core18, kernel, gadget
+ _, err = snapstate.CurrentInfo(state, "snapd")
+ c.Check(err, IsNil)
+ _, err = snapstate.CurrentInfo(state, "core18")
+ c.Check(err, IsNil)
+ _, err = snapstate.CurrentInfo(state, "foo")
+ c.Check(err, IsNil)
+
+ // and ensure state is now considered seeded
+ var seeded bool
+ err = state.Get("seeded", &seeded)
+ c.Assert(err, IsNil)
+ c.Check(seeded, Equals, true)
+
+ // check we set seed-time
+ var seedTime time.Time
+ err = state.Get("seed-time", &seedTime)
+ c.Assert(err, IsNil)
+ c.Check(seedTime.IsZero(), Equals, false)
+}
+
+func (s *FirstBootTestSuite) TestPopulateFromSeedOnClassicWithSnapdOnlyAndGadgetHappy(c *C) {
+ restore := release.MockOnClassic(true)
+ defer restore()
+
+ var sysdLog [][]string
+ systemctlRestorer := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) {
+ sysdLog = append(sysdLog, cmd)
+ return []byte("ActiveState=inactive\n"), nil
+ })
+ defer systemctlRestorer()
+
+ core18Fname, snapdFname, _, gadgetFname := s.makeCore18Snaps(c, &core18SnapsOpts{
+ classic: true,
+ gadget: true,
+ })
+
+ // put a firstboot snap into the SnapBlobDir
+ snapYaml := `name: foo
+version: 1.0
+base: core18
+`
+ fooFname, fooDecl, fooRev := s.MakeAssertedSnap(c, snapYaml, nil, snap.R(128), "developerid")
+
+ s.WriteAssertions("foo.asserts", s.devAcct, fooRev, fooDecl)
+
+ // add a model assertion and its chain
+ assertsChain := s.makeModelAssertionChain(c, "my-model-classic", map[string]interface{}{"gadget": "pc"})
+ s.WriteAssertions("model.asserts", assertsChain...)
+
+ // create a seed.yaml
+ content := []byte(fmt.Sprintf(`
+snaps:
+ - name: snapd
+ file: %s
+ - name: foo
+ file: %s
+ - name: core18
+ file: %s
+ - name: pc
+ file: %s
+`, snapdFname, fooFname, core18Fname, gadgetFname))
+ err := ioutil.WriteFile(filepath.Join(dirs.SnapSeedDir, "seed.yaml"), content, 0644)
+ c.Assert(err, IsNil)
+
+ // run the firstboot stuff
+ st := s.overlord.State()
+ st.Lock()
+ defer st.Unlock()
+ tsAll, err := devicestate.PopulateStateFromSeedImpl(st, s.perfTimings)
+ c.Assert(err, IsNil)
+
+ checkOrder(c, tsAll, "snapd", "core18", "pc", "foo")
+
+ // now run the change and check the result
+ // 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)
+ }
+ c.Assert(st.Changes(), HasLen, 1)
+
+ c.Assert(chg.Err(), IsNil)
+
+ // avoid device reg
+ chg1 := st.NewChange("become-operational", "init device")
+ chg1.SetStatus(state.DoingStatus)
+
+ // run change until it wants to restart
+ st.Unlock()
+ err = s.overlord.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ // at this point the system is "restarting", pretend the restart has
+ // happened
+ c.Assert(chg.Status(), Equals, state.DoingStatus)
+ state.MockRestarting(st, state.RestartUnset)
+ st.Unlock()
+ err = s.overlord.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%s", chg.Err()))
+
+ // verify
+ r, err := os.Open(dirs.SnapStateFile)
+ c.Assert(err, IsNil)
+ state, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ state.Lock()
+ defer state.Unlock()
+ // check snapd, core18, kernel, gadget
+ _, err = snapstate.CurrentInfo(state, "snapd")
+ c.Check(err, IsNil)
+ _, err = snapstate.CurrentInfo(state, "core18")
+ c.Check(err, IsNil)
+ _, err = snapstate.CurrentInfo(state, "pc")
+ c.Check(err, IsNil)
+ _, err = snapstate.CurrentInfo(state, "foo")
+ c.Check(err, IsNil)
+
+ // and ensure state is now considered seeded
+ var seeded bool
+ err = state.Get("seeded", &seeded)
+ c.Assert(err, IsNil)
+ c.Check(seeded, Equals, true)
+
+ // check we set seed-time
+ var seedTime time.Time
+ err = state.Get("seed-time", &seedTime)
+ c.Assert(err, IsNil)
+ c.Check(seedTime.IsZero(), Equals, false)
+}
diff -Nru snapd-2.40/overlord/devicestate/handlers.go snapd-2.42.1/overlord/devicestate/handlers.go
--- snapd-2.40/overlord/devicestate/handlers.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/devicestate/handlers.go 2019-10-30 12:17:43.000000000 +0000
@@ -24,6 +24,7 @@
"encoding/json"
"errors"
"fmt"
+ "io"
"net"
"net/http"
"net/url"
@@ -49,6 +50,7 @@
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/naming"
"github.com/snapcore/snapd/timings"
)
@@ -92,23 +94,24 @@
// unmark no-longer required snaps
requiredSnaps := getAllRequiredSnapsForModel(new)
+ // TODO:XXX: have AllByRef
snapStates, err := snapstate.All(st)
if err != nil {
return err
}
for snapName, snapst := range snapStates {
// TODO: remove this type restriction once we remodel
- // kernels/gadgets and add tests that ensure
+ // gadgets and add tests that ensure
// that the required flag is properly set/unset
typ, err := snapst.Type()
if err != nil {
return err
}
- if typ != snap.TypeApp && typ != snap.TypeBase {
+ if typ != snap.TypeApp && typ != snap.TypeBase && typ != snap.TypeKernel {
continue
}
// clean required flag if no-longer needed
- if snapst.Flags.Required && !requiredSnaps[snapName] {
+ if snapst.Flags.Required && !requiredSnaps.Contains(naming.Snap(snapName)) {
snapst.Flags.Required = false
snapstate.Set(st, snapName, snapst)
}
@@ -120,6 +123,15 @@
return remodCtx.Finish()
}
+func (m *DeviceManager) cleanupRemodel(t *state.Task, _ *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+ // cleanup the cached remodel context
+ cleanupRemodelCtx(t.Change())
+ return nil
+}
+
func useStaging() bool {
return osutil.GetenvBool("SNAPPY_USE_STAGING_STORE")
}
@@ -149,6 +161,9 @@
reqIdRef = mustParse("request-id")
serialRef = mustParse("serial")
devicesRef = mustParse("devices")
+
+ // we accept a stream with the serial assertion as well
+ registrationCapabilities = []string{"serial-stream"}
)
func newEnoughProxy(st *state.State, proxyURL *url.URL, client *http.Client) bool {
@@ -313,6 +328,13 @@
// registrationCtx returns a registrationContext appropriate for the task and its change.
func (m *DeviceManager) registrationCtx(t *state.Task) (registrationContext, error) {
+ remodCtx, err := remodelCtxFromTask(t)
+ if err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+ if regCtx, ok := remodCtx.(registrationContext); ok {
+ return regCtx, nil
+ }
model, err := m.Model()
if err != nil {
return nil, err
@@ -456,64 +478,83 @@
var errPoll = errors.New("serial-request accepted, poll later")
-func submitSerialRequest(t *state.Task, serialRequest string, client *http.Client, cfg *serialRequestConfig) (*asserts.Serial, error) {
+func submitSerialRequest(t *state.Task, serialRequest string, client *http.Client, cfg *serialRequestConfig) (*asserts.Serial, *asserts.Batch, error) {
st := t.State()
st.Unlock()
defer st.Lock()
req, err := http.NewRequest("POST", cfg.serialRequestURL, bytes.NewBufferString(serialRequest))
if err != nil {
- return nil, fmt.Errorf("internal error: cannot create serial-request request %q", cfg.serialRequestURL)
+ return nil, nil, fmt.Errorf("internal error: cannot create serial-request request %q", cfg.serialRequestURL)
}
req.Header.Set("User-Agent", httputil.UserAgent())
+ req.Header.Set("Snap-Device-Capabilities", strings.Join(registrationCapabilities, " "))
cfg.applyHeaders(req)
req.Header.Set("Content-Type", asserts.MediaType)
resp, err := client.Do(req)
if err != nil {
- return nil, retryErr(t, 0, "cannot deliver device serial request: %v", err)
+ return nil, nil, retryErr(t, 0, "cannot deliver device serial request: %v", err)
}
defer resp.Body.Close()
switch resp.StatusCode {
case 200, 201:
case 202:
- return nil, errPoll
+ return nil, nil, errPoll
default:
- return nil, retryBadStatus(t, 0, "cannot deliver device serial request", resp)
+ return nil, nil, retryBadStatus(t, 0, "cannot deliver device serial request", resp)
}
- // TODO: support a stream of assertions instead of just the serial
-
- // decode body with serial assertion
+ var serial *asserts.Serial
+ var batch *asserts.Batch
+ // decode body with stream of assertions, of which one is the serial
dec := asserts.NewDecoder(resp.Body)
- got, err := dec.Decode()
- if err != nil { // assume broken i/o
- return nil, retryErr(t, 0, "cannot read response to request for a serial: %v", err)
+ for {
+ got, err := dec.Decode()
+ if err == io.EOF {
+ break
+ }
+ if err != nil { // assume broken i/o
+ return nil, nil, retryErr(t, 0, "cannot read response to request for a serial: %v", err)
+ }
+ if got.Type() == asserts.SerialType {
+ if serial != nil {
+ return nil, nil, fmt.Errorf("cannot accept more than a single device serial assertion from the device service")
+ }
+ serial = got.(*asserts.Serial)
+ } else {
+ if batch == nil {
+ batch = asserts.NewBatch(nil)
+ }
+ if err := batch.Add(got); err != nil {
+ return nil, nil, err
+ }
+ }
+ // TODO: consider a size limit?
}
- serial, ok := got.(*asserts.Serial)
- if !ok {
- return nil, fmt.Errorf("cannot use device serial assertion of type %q", got.Type().Name)
+ if serial == nil {
+ return nil, nil, fmt.Errorf("cannot proceed, received assertion stream from the device service missing device serial assertion")
}
- return serial, nil
+ return serial, batch, nil
}
-func getSerial(t *state.Task, regCtx registrationContext, privKey asserts.PrivateKey, device *auth.DeviceState, tm timings.Measurer) (*asserts.Serial, error) {
+func getSerial(t *state.Task, regCtx registrationContext, privKey asserts.PrivateKey, device *auth.DeviceState, tm timings.Measurer) (serial *asserts.Serial, ancillaryBatch *asserts.Batch, err error) {
var serialSup serialSetup
- err := t.Get("serial-setup", &serialSup)
+ err = t.Get("serial-setup", &serialSup)
if err != nil && err != state.ErrNoState {
- return nil, err
+ return nil, nil, err
}
if serialSup.Serial != "" {
// we got a serial, just haven't managed to save its info yet
a, err := asserts.Decode([]byte(serialSup.Serial))
if err != nil {
- return nil, fmt.Errorf("internal error: cannot decode previously saved serial: %v", err)
+ return nil, nil, fmt.Errorf("internal error: cannot decode previously saved serial: %v", err)
}
- return a.(*asserts.Serial), nil
+ return a.(*asserts.Serial), nil, nil
}
st := t.State()
@@ -526,7 +567,7 @@
cfg, err := getSerialRequestConfig(t, regCtx, client)
if err != nil {
- return nil, err
+ return nil, nil, err
}
// NB: until we get at least an Accepted (202) we need to
@@ -540,39 +581,40 @@
serialRequest, err = prepareSerialRequest(t, regCtx, privKey, device, client, cfg)
})
if err != nil { // errors & retries
- return nil, err
+ return nil, nil, err
}
serialSup.SerialRequest = serialRequest
}
- var serial *asserts.Serial
timings.Run(tm, "submit-serial-request", "submit device serial request", func(timings.Measurer) {
- serial, err = submitSerialRequest(t, serialSup.SerialRequest, client, cfg)
+ serial, ancillaryBatch, err = submitSerialRequest(t, serialSup.SerialRequest, client, cfg)
})
if err == errPoll {
// we can/should reuse the serial-request
t.Set("serial-setup", serialSup)
- return nil, errPoll
+ return nil, nil, errPoll
}
if err != nil { // errors & retries
- return nil, err
+ return nil, nil, err
}
keyID := privKey.PublicKey().ID()
if serial.BrandID() != device.Brand || serial.Model() != device.Model || serial.DeviceKey().ID() != keyID {
- return nil, fmt.Errorf("obtained serial assertion does not match provided device identity information (brand, model, key id): %s / %s / %s != %s / %s / %s", serial.BrandID(), serial.Model(), serial.DeviceKey().ID(), device.Brand, device.Model, keyID)
+ return nil, nil, fmt.Errorf("obtained serial assertion does not match provided device identity information (brand, model, key id): %s / %s / %s != %s / %s / %s", serial.BrandID(), serial.Model(), serial.DeviceKey().ID(), device.Brand, device.Model, keyID)
}
- serialSup.Serial = string(asserts.Encode(serial))
- t.Set("serial-setup", serialSup)
+ if ancillaryBatch == nil {
+ serialSup.Serial = string(asserts.Encode(serial))
+ t.Set("serial-setup", serialSup)
+ }
if repeatRequestSerial == "after-got-serial" {
// For testing purposes, ensure a crash in this state works.
- return nil, &state.Retry{}
+ return nil, nil, &state.Retry{}
}
- return serial, nil
+ return serial, ancillaryBatch, nil
}
type serialRequestConfig struct {
@@ -710,8 +752,9 @@
}
var serial *asserts.Serial
+ var ancillaryBatch *asserts.Batch
timings.Run(perfTimings, "get-serial", "get device serial", func(tm timings.Measurer) {
- serial, err = getSerial(t, regCtx, privKey, device, tm)
+ serial, ancillaryBatch, err = getSerial(t, regCtx, privKey, device, tm)
})
if err == errPoll {
t.Logf("Will poll for device serial assertion in 60 seconds")
@@ -722,6 +765,33 @@
}
+ if ancillaryBatch == nil {
+ // the device service returned only the serial
+ if err := acceptSerialOnly(t, serial, perfTimings); err != nil {
+ return err
+ }
+ } else {
+ // the device service returned a stream of assertions
+ timings.Run(perfTimings, "fetch-keys", "fetch signing key chain", func(timings.Measurer) {
+ err = acceptSerialPlusBatch(t, serial, ancillaryBatch)
+ })
+ if err != nil {
+ t.Errorf("cannot accept stream of assertions from device service: %v", err)
+ return err
+ }
+ }
+
+ if repeatRequestSerial == "after-add-serial" {
+ // For testing purposes, ensure a crash in this state works.
+ return &state.Retry{}
+ }
+
+ return finish(serial)
+}
+
+func acceptSerialOnly(t *state.Task, serial *asserts.Serial, perfTimings *timings.Timings) error {
+ st := t.State()
+ var err error
var errAcctKey error
// try to fetch the signing key chain of the serial
timings.Run(perfTimings, "fetch-keys", "fetch signing key chain", func(timings.Measurer) {
@@ -742,46 +812,103 @@
return err
}
- if repeatRequestSerial == "after-add-serial" {
- // For testing purposes, ensure a crash in this state works.
- return &state.Retry{}
- }
+ return nil
+}
- return finish(serial)
+func acceptSerialPlusBatch(t *state.Task, serial *asserts.Serial, batch *asserts.Batch) error {
+ st := t.State()
+ err := batch.Add(serial)
+ if err != nil {
+ return err
+ }
+ return assertstate.AddBatch(st, batch, &asserts.CommitOptions{Precheck: true})
}
var repeatRequestSerial string // for tests
func fetchKeys(st *state.State, keyID string) (errAcctKey error, err error) {
// TODO: right now any store should be good enough here but
- // that might change, also this is brittle, best would be to
- // receive a stream with any relevant assertions
+ // that might change. As an alternative we do support
+ // receiving a stream with any relevant assertions.
sto := snapstate.Store(st, nil)
db := assertstate.DB(st)
- for {
- _, err := db.FindPredefined(asserts.AccountKeyType, map[string]string{
- "public-key-sha3-384": keyID,
- })
- if err == nil {
- return nil, nil
- }
- if !asserts.IsNotFound(err) {
- return nil, err
- }
+
+ retrieveError := false
+ retrieve := func(ref *asserts.Ref) (asserts.Assertion, error) {
st.Unlock()
- a, errAcctKey := sto.Assertion(asserts.AccountKeyType, []string{keyID}, nil)
- st.Lock()
- if errAcctKey != nil {
- return errAcctKey, nil
- }
+ defer st.Lock()
+ a, err := sto.Assertion(ref.Type, ref.PrimaryKey, nil)
+ retrieveError = err != nil
+ return a, err
+ }
+
+ save := func(a asserts.Assertion) error {
err = assertstate.Add(st, a)
- if err != nil {
- if !asserts.IsUnaccceptedUpdate(err) {
- return nil, err
- }
+ if err != nil && !asserts.IsUnaccceptedUpdate(err) {
+ return err
+ }
+ return nil
+ }
+
+ f := asserts.NewFetcher(db, retrieve, save)
+
+ keyRef := &asserts.Ref{
+ Type: asserts.AccountKeyType,
+ PrimaryKey: []string{keyID},
+ }
+ if err := f.Fetch(keyRef); err != nil {
+ if retrieveError {
+ return err, nil
+ } else {
+ return nil, err
}
- keyID = a.SignKeyID()
}
+ return nil, nil
+}
+
+func (m *DeviceManager) doPrepareRemodeling(t *state.Task, tmb *tomb.Tomb) error {
+ st := t.State()
+ st.Lock()
+ defer st.Unlock()
+
+ remodCtx, err := remodelCtxFromTask(t)
+ if err != nil {
+ return err
+ }
+ current, err := findModel(st)
+ if err != nil {
+ return err
+ }
+
+ sto := remodCtx.Store()
+ if sto == nil {
+ return fmt.Errorf("internal error: re-registration remodeling should have built a store")
+ }
+ // ensure a new session accounting for the new brand/model
+ st.Unlock()
+ _, err = sto.EnsureDeviceSession()
+ st.Lock()
+ if err != nil {
+ return fmt.Errorf("cannot get a store session based on the new model assertion: %v", err)
+ }
+
+ chgID := t.Change().ID()
+
+ tss, err := remodelTasks(tmb.Context(nil), st, current, remodCtx.Model(), remodCtx, chgID)
+ if err != nil {
+ return err
+ }
+
+ allTs := state.NewTaskSet()
+ for _, ts := range tss {
+ allTs.AddAll(ts)
+ }
+ snapstate.InjectTasks(t, allTs)
+
+ st.EnsureBefore(0)
+ t.SetStatus(state.DoneStatus)
+
+ return nil
}
func snapState(st *state.State, name string) (*snapstate.SnapState, error) {
@@ -803,68 +930,63 @@
return rollbackDir, nil
}
-func currentGadgetInfo(snapst *snapstate.SnapState) (*gadget.Info, error) {
- var gi *gadget.Info
-
+func currentGadgetInfo(snapst *snapstate.SnapState) (*gadget.GadgetData, error) {
currentInfo, err := snapst.CurrentInfo()
if err != nil && err != snapstate.ErrNoCurrent {
return nil, err
}
- if currentInfo != nil {
- const onClassic = false
- gi, err = snap.ReadGadgetInfo(currentInfo, onClassic)
- if err != nil {
- return nil, err
- }
+ if currentInfo == nil {
+ // no current yet
+ return nil, nil
+ }
+ const onClassic = false
+ gi, err := gadget.ReadInfo(currentInfo.MountDir(), onClassic)
+ if err != nil {
+ return nil, err
}
- return gi, nil
+ return &gadget.GadgetData{Info: gi, RootDir: currentInfo.MountDir()}, nil
}
-func pendingGadgetInfo(snapsup *snapstate.SnapSetup) (*gadget.Info, error) {
+func pendingGadgetInfo(snapsup *snapstate.SnapSetup) (*gadget.GadgetData, error) {
info, err := snap.ReadInfo(snapsup.InstanceName(), snapsup.SideInfo)
if err != nil {
return nil, err
}
const onClassic = false
- update, err := snap.ReadGadgetInfo(info, onClassic)
+ update, err := gadget.ReadInfo(info.MountDir(), onClassic)
if err != nil {
return nil, err
}
- return update, nil
+ return &gadget.GadgetData{Info: update, RootDir: info.MountDir()}, nil
}
-func gadgetCurrentAndUpdate(st *state.State, snapsup *snapstate.SnapSetup) (current *gadget.Info, update *gadget.Info, err error) {
+func gadgetCurrentAndUpdate(st *state.State, snapsup *snapstate.SnapSetup) (current *gadget.GadgetData, update *gadget.GadgetData, err error) {
snapst, err := snapState(st, snapsup.InstanceName())
if err != nil {
return nil, nil, err
}
- currentInfo, err := currentGadgetInfo(snapst)
+ currentData, err := currentGadgetInfo(snapst)
if err != nil {
- return nil, nil, err
+ return nil, nil, fmt.Errorf("cannot read current gadget snap details: %v", err)
}
-
- if currentInfo == nil {
+ if currentData == nil {
// don't bother reading update if there is no current
return nil, nil, nil
}
- newInfo, err := pendingGadgetInfo(snapsup)
+ newData, err := pendingGadgetInfo(snapsup)
if err != nil {
- return nil, nil, err
+ return nil, nil, fmt.Errorf("cannot read candidate gadget snap details: %v", err)
}
- return currentInfo, newInfo, nil
+ return currentData, newData, nil
}
var (
- gadgetUpdate = nopGadgetOp
+ gadgetUpdate = gadget.Update
)
-func nopGadgetOp(current, update *gadget.Info, rollbackRootDir string) error {
- return nil
-}
-
func (m *DeviceManager) doUpdateGadgetAssets(t *state.Task, _ *tomb.Tomb) error {
if release.OnClassic {
return fmt.Errorf("cannot run update gadget assets task on a classic system")
@@ -879,11 +1001,11 @@
return err
}
- current, update, err := gadgetCurrentAndUpdate(t.State(), snapsup)
+ currentData, updateData, err := gadgetCurrentAndUpdate(t.State(), snapsup)
if err != nil {
return err
}
- if current == nil {
+ if currentData == nil {
// no updates during first boot & seeding
return nil
}
@@ -894,7 +1016,7 @@
}
st.Unlock()
- err = gadgetUpdate(current, update, snapRollbackDir)
+ err = gadgetUpdate(*currentData, *updateData, snapRollbackDir)
st.Lock()
if err != nil {
if err == gadget.ErrNoUpdate {
@@ -911,6 +1033,8 @@
logger.Noticef("failed to remove gadget update rollback directory %q: %v", snapRollbackDir, err)
}
+ // TODO: consider having the option to do this early via recovery in
+ // core20, have fallback code as well there
st.RequestRestart(state.RestartSystem)
return nil
diff -Nru snapd-2.40/overlord/devicestate/handlers_test.go snapd-2.42.1/overlord/devicestate/handlers_test.go
--- snapd-2.40/overlord/devicestate/handlers_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/devicestate/handlers_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,14 +20,20 @@
package devicestate_test
import (
+ "context"
+ "fmt"
+
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/overlord/assertstate"
"github.com/snapcore/snapd/overlord/auth"
+ "github.com/snapcore/snapd/overlord/devicestate"
"github.com/snapcore/snapd/overlord/devicestate/devicestatetest"
"github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/overlord/storecontext"
+ "github.com/snapcore/snapd/snap"
)
// TODO: should we move this into a new handlers suite?
@@ -38,18 +44,54 @@
Model: "pc-model",
})
s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{
- "architecture": "amd64",
- "kernel": "pc-kernel",
- "gadget": "pc",
- "revision": "1",
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "revision": "1",
+ "required-snaps": []interface{}{"foo", "bar"},
+ })
+ // foo and bar
+ fooSI := &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(1),
+ }
+ barSI := &snap.SideInfo{
+ RealName: "foo",
+ Revision: snap.R(1),
+ }
+ pcKernelSI := &snap.SideInfo{
+ RealName: "pc-kernel",
+ Revision: snap.R(1),
+ }
+ snapstate.Set(s.state, "foo", &snapstate.SnapState{
+ SnapType: "app",
+ Active: true,
+ Sequence: []*snap.SideInfo{fooSI},
+ Current: fooSI.Revision,
+ Flags: snapstate.Flags{Required: true},
+ })
+ snapstate.Set(s.state, "bar", &snapstate.SnapState{
+ SnapType: "app",
+ Active: true,
+ Sequence: []*snap.SideInfo{barSI},
+ Current: barSI.Revision,
+ Flags: snapstate.Flags{Required: true},
+ })
+ snapstate.Set(s.state, "pc-kernel", &snapstate.SnapState{
+ SnapType: "kernel",
+ Active: true,
+ Sequence: []*snap.SideInfo{pcKernelSI},
+ Current: pcKernelSI.Revision,
+ Flags: snapstate.Flags{Required: true},
})
s.state.Unlock()
newModel := s.brands.Model("canonical", "pc-model", map[string]interface{}{
- "architecture": "amd64",
- "kernel": "pc-kernel",
- "gadget": "pc",
- "revision": "2",
+ "architecture": "amd64",
+ "kernel": "other-kernel",
+ "gadget": "pc",
+ "revision": "2",
+ "required-snaps": []interface{}{"foo"},
})
s.state.Lock()
@@ -70,6 +112,21 @@
c.Assert(m, DeepEquals, newModel)
c.Assert(chg.Err(), IsNil)
+
+ // check required
+ var fooState snapstate.SnapState
+ var barState snapstate.SnapState
+ err = snapstate.Get(s.state, "foo", &fooState)
+ c.Assert(err, IsNil)
+ err = snapstate.Get(s.state, "bar", &barState)
+ c.Assert(err, IsNil)
+ c.Check(fooState.Flags.Required, Equals, true)
+ c.Check(barState.Flags.Required, Equals, false)
+ // the kernel is no longer required
+ var kernelState snapstate.SnapState
+ err = snapstate.Get(s.state, "pc-kernel", &kernelState)
+ c.Assert(err, IsNil)
+ c.Check(kernelState.Flags.Required, Equals, false)
}
func (s *deviceMgrSuite) TestSetModelHandlerSameRevisionNoError(c *C) {
@@ -166,4 +223,204 @@
Model: "pc-model",
SessionMacaroon: "switched-store-session",
})
+
+ // cleanup
+ _, ok := devicestate.CachedRemodelCtx(chg)
+ c.Check(ok, Equals, true)
+
+ s.state.Unlock()
+
+ s.se.Ensure()
+ s.se.Wait()
+
+ s.state.Lock()
+
+ _, ok = devicestate.CachedRemodelCtx(chg)
+ c.Check(ok, Equals, false)
+}
+
+func (s *deviceMgrSuite) TestSetModelHandlerRereg(c *C) {
+ s.state.Lock()
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc-model",
+ Serial: "orig-serial",
+ })
+ s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ })
+ s.makeSerialAssertionInState(c, "canonical", "pc-model", "orig-serial")
+ s.state.Unlock()
+
+ newModel := s.brands.Model("canonical", "rereg-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ })
+
+ s.newFakeStore = func(devBE storecontext.DeviceBackend) snapstate.StoreService {
+ mod, err := devBE.Model()
+ c.Check(err, IsNil)
+ if err == nil {
+ c.Check(mod, DeepEquals, newModel)
+ }
+ return &freshSessionStore{}
+ }
+
+ s.state.Lock()
+ t := s.state.NewTask("set-model", "set-model test")
+ chg := s.state.NewChange("dummy", "...")
+ chg.Set("new-model", string(asserts.Encode(newModel)))
+ chg.Set("device", auth.DeviceState{
+ Brand: "canonical",
+ Model: "rereg-model",
+ Serial: "orig-serial",
+ SessionMacaroon: "switched-store-session",
+ })
+ chg.AddTask(t)
+
+ s.state.Unlock()
+
+ s.se.Ensure()
+ s.se.Wait()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ c.Assert(chg.Err(), IsNil)
+
+ m, err := s.mgr.Model()
+ c.Assert(err, IsNil)
+ c.Assert(m, DeepEquals, newModel)
+
+ device, err := devicestatetest.Device(s.state)
+ c.Assert(err, IsNil)
+ c.Check(device, DeepEquals, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "rereg-model",
+ Serial: "orig-serial",
+ SessionMacaroon: "switched-store-session",
+ })
+}
+
+func (s *deviceMgrSuite) TestDoPrepareRemodeling(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ s.state.Set("seeded", true)
+ s.state.Set("refresh-privacy-key", "some-privacy-key")
+
+ var testStore snapstate.StoreService
+
+ restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) {
+ c.Check(flags.Required, Equals, true)
+ c.Check(deviceCtx, NotNil)
+ c.Check(deviceCtx.ForRemodeling(), Equals, true)
+
+ tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name))
+ tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name))
+ tValidate.WaitFor(tDownload)
+ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name))
+ tInstall.WaitFor(tValidate)
+ ts := state.NewTaskSet(tDownload, tValidate, tInstall)
+ ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge)
+ return ts, nil
+ })
+ defer restore()
+
+ // set a model assertion
+ s.makeModelAssertionInState(c, "canonical", "pc-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core18",
+ })
+ s.makeSerialAssertionInState(c, "canonical", "pc-model", "orig-serial")
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc-model",
+ Serial: "orig-serial",
+ SessionMacaroon: "old-session",
+ })
+
+ new := s.brands.Model("canonical", "rereg-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core18",
+ "required-snaps": []interface{}{"new-required-snap-1", "new-required-snap-2"},
+ })
+
+ freshStore := &freshSessionStore{}
+ testStore = freshStore
+
+ s.newFakeStore = func(devBE storecontext.DeviceBackend) snapstate.StoreService {
+ mod, err := devBE.Model()
+ c.Check(err, IsNil)
+ if err == nil {
+ c.Check(mod, DeepEquals, new)
+ }
+ return testStore
+ }
+
+ cur, err := s.mgr.Model()
+ c.Assert(err, IsNil)
+
+ remodCtx, err := devicestate.RemodelCtx(s.state, cur, new)
+ c.Assert(err, IsNil)
+
+ c.Check(remodCtx.Kind(), Equals, devicestate.ReregRemodel)
+
+ chg := s.state.NewChange("remodel", "...")
+ remodCtx.Init(chg)
+ t := s.state.NewTask("prepare-remodeling", "...")
+ chg.AddTask(t)
+
+ // set new serial
+ s.makeSerialAssertionInState(c, "canonical", "rereg-model", "orig-serial")
+ chg.Set("device", auth.DeviceState{
+ Brand: "canonical",
+ Model: "rereg-model",
+ Serial: "orig-serial",
+ SessionMacaroon: "switched-store-session",
+ })
+
+ s.state.Unlock()
+
+ s.se.Ensure()
+ s.se.Wait()
+
+ s.state.Lock()
+ c.Assert(chg.Err(), IsNil)
+
+ c.Check(freshStore.ensureDeviceSession, Equals, 1)
+
+ // check that the expected tasks were injected
+ tl := chg.Tasks()
+ // 1 prepare-remodeling
+ // 2 snaps * 3 tasks (from the mock install above) +
+ // 1 "set-model" task at the end
+ c.Assert(tl, HasLen, 1+2*3+1)
+
+ // sanity
+ c.Check(tl[1].Kind(), Equals, "fake-download")
+ c.Check(tl[1+2*3].Kind(), Equals, "set-model")
+
+ // cleanup
+ // fake completion
+ for _, t := range tl[1:] {
+ t.SetStatus(state.DoneStatus)
+ }
+ _, ok := devicestate.CachedRemodelCtx(chg)
+ c.Check(ok, Equals, true)
+
+ s.state.Unlock()
+
+ s.se.Ensure()
+ s.se.Wait()
+
+ s.state.Lock()
+
+ _, ok = devicestate.CachedRemodelCtx(chg)
+ c.Check(ok, Equals, false)
}
diff -Nru snapd-2.40/overlord/devicestate/remodel.go snapd-2.42.1/overlord/devicestate/remodel.go
--- snapd-2.40/overlord/devicestate/remodel.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/devicestate/remodel.go 2019-10-30 12:17:43.000000000 +0000
@@ -119,7 +119,7 @@
// initialDevice takes the current/initial device state
// when setting up the remodel context
- initialDevice(device *auth.DeviceState)
+ initialDevice(device *auth.DeviceState) error
// associate associates the remodel context with the change
// and caches it
associate(chg *state.Change)
@@ -137,14 +137,11 @@
// simple context for the simple case
remodCtx = &updateRemodelContext{baseRemodelContext{newModel}}
case StoreSwitchRemodel:
- storeSwitchCtx := &newStoreRemodelContext{
- baseRemodelContext: baseRemodelContext{newModel},
- st: st,
- deviceMgr: devMgr,
+ remodCtx = newNewStoreRemodelContext(st, devMgr, newModel)
+ case ReregRemodel:
+ remodCtx = &reregRemodelContext{
+ newStoreRemodelContext: newNewStoreRemodelContext(st, devMgr, newModel),
}
- storeSwitchCtx.store = devMgr.newStore(storeSwitchCtx.deviceBackend())
- remodCtx = storeSwitchCtx
- // TODO: support ReregRemodel
default:
return nil, fmt.Errorf("unsupported remodel: %s", kind)
}
@@ -153,7 +150,9 @@
if err != nil {
return nil, err
}
- remodCtx.initialDevice(device)
+ if err := remodCtx.initialDevice(device); err != nil {
+ return nil, err
+ }
return remodCtx, nil
}
@@ -214,8 +213,9 @@
return rc.newModel
}
-func (rc baseRemodelContext) initialDevice(*auth.DeviceState) {
+func (rc baseRemodelContext) initialDevice(*auth.DeviceState) error {
// do nothing
+ return nil
}
func (rc baseRemodelContext) cacheViaChange(chg *state.Change, remodCtx remodelContext) {
@@ -271,6 +271,15 @@
deviceMgr *DeviceManager
}
+func newNewStoreRemodelContext(st *state.State, devMgr *DeviceManager, newModel *asserts.Model) *newStoreRemodelContext {
+ rc := &newStoreRemodelContext{}
+ rc.baseRemodelContext = baseRemodelContext{newModel}
+ rc.st = st
+ rc.deviceMgr = devMgr
+ rc.store = devMgr.newStore(rc.deviceBackend())
+ return rc
+}
+
func (rc *newStoreRemodelContext) Kind() RemodelKind {
return StoreSwitchRemodel
}
@@ -280,17 +289,23 @@
rc.cacheViaChange(chg, rc)
}
-func (rc *newStoreRemodelContext) initialDevice(device *auth.DeviceState) {
+func (rc *newStoreRemodelContext) initialDevice(device *auth.DeviceState) error {
device1 := *device
// we will need a new one, it might embed the store as well
device1.SessionMacaroon = ""
rc.deviceState = &device1
+ return nil
}
-func (rc *newStoreRemodelContext) Init(chg *state.Change) {
- rc.init(chg)
+func (rc *newStoreRemodelContext) init(chg *state.Change) {
+ rc.baseRemodelContext.init(chg)
+
chg.Set("device", rc.deviceState)
rc.deviceState = nil
+}
+
+func (rc *newStoreRemodelContext) Init(chg *state.Change) {
+ rc.init(chg)
rc.associate(chg)
}
@@ -360,3 +375,80 @@
}
return findSerial(b.st, device)
}
+
+// reregRemodelContext: remodel for a change of brand/model
+type reregRemodelContext struct {
+ *newStoreRemodelContext
+
+ origModel *asserts.Model
+ origSerial *asserts.Serial
+}
+
+func (rc *reregRemodelContext) Kind() RemodelKind {
+ return ReregRemodel
+}
+
+func (rc *reregRemodelContext) associate(chg *state.Change) {
+ rc.remodelChange = chg
+ rc.cacheViaChange(chg, rc)
+}
+
+func (rc *reregRemodelContext) initialDevice(device *auth.DeviceState) error {
+ origModel, err := findModel(rc.st)
+ if err != nil {
+ return err
+ }
+ origSerial, err := findSerial(rc.st, nil)
+ if err != nil {
+ return fmt.Errorf("cannot find current serial before proceeding with re-registration: %v", err)
+ }
+ rc.origModel = origModel
+ rc.origSerial = origSerial
+
+ // starting almost from scratch with only device-key
+ rc.deviceState = &auth.DeviceState{
+ Brand: rc.newModel.BrandID(),
+ Model: rc.newModel.Model(),
+ KeyID: device.KeyID,
+ }
+ return nil
+}
+
+func (rc *reregRemodelContext) Init(chg *state.Change) {
+ rc.init(chg)
+
+ rc.associate(chg)
+}
+
+// reregRemodelContext impl of registrationContext
+
+func (rc *reregRemodelContext) Device() (*auth.DeviceState, error) {
+ return rc.device()
+}
+
+func (rc *reregRemodelContext) GadgetForSerialRequestConfig() string {
+ return rc.origModel.Gadget()
+}
+
+func (rc *reregRemodelContext) SerialRequestExtraHeaders() map[string]interface{} {
+ return map[string]interface{}{
+ "original-brand-id": rc.origSerial.BrandID(),
+ "original-model": rc.origSerial.Model(),
+ "original-serial": rc.origSerial.Serial(),
+ }
+}
+
+func (rc *reregRemodelContext) SerialRequestAncillaryAssertions() []asserts.Assertion {
+ return []asserts.Assertion{rc.newModel, rc.origSerial}
+}
+
+func (rc *reregRemodelContext) FinishRegistration(serial *asserts.Serial) error {
+ device, err := rc.device()
+ if err != nil {
+ return err
+ }
+
+ device.Serial = serial.Serial()
+ rc.setCtxDevice(device)
+ return nil
+}
diff -Nru snapd-2.40/overlord/devicestate/remodel_test.go snapd-2.42.1/overlord/devicestate/remodel_test.go
--- snapd-2.40/overlord/devicestate/remodel_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/devicestate/remodel_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,6 +20,8 @@
package devicestate_test
import (
+ "time"
+
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/asserts"
@@ -252,7 +254,8 @@
remodCtx, err := devicestate.RemodelCtx(s.state, oldModel, newModel)
c.Assert(err, IsNil)
- devBE := devicestate.RemodelDeviceBackend(remodCtx)
+ devBE := s.capturedDevBE
+ c.Check(devBE, NotNil)
device, err := devBE.Device()
c.Assert(err, IsNil)
@@ -312,12 +315,12 @@
remodCtx, err := devicestate.RemodelCtx(s.state, oldModel, newModel)
c.Assert(err, IsNil)
+ devBE := devicestate.RemodelDeviceBackend(remodCtx)
+
chg := s.state.NewChange("remodel", "...")
remodCtx.Init(chg)
- devBE := devicestate.RemodelDeviceBackend(remodCtx)
-
device, err := devBE.Device()
c.Assert(err, IsNil)
c.Check(device, DeepEquals, &auth.DeviceState{
@@ -562,27 +565,24 @@
c.Check(device1, DeepEquals, expectedGlobalDevice)
}
-func (s *remodelLogicSuite) TestRemodelDeviceBackendSerial(c *C) {
+func (s *remodelLogicSuite) TestRemodelDeviceBackendKeptSerial(c *C) {
oldModel := fakeRemodelingModel(nil)
newModel := fakeRemodelingModel(map[string]interface{}{
"store": "my-other-store",
"revision": "1",
})
- // the logic is shared and correct also for re-reg
- // XXX (this is really the re-reg case)
-
s.state.Lock()
defer s.state.Unlock()
// we have a device state and serial
devicestatetest.SetDevice(s.state, &auth.DeviceState{
- Brand: "canonical",
- Model: "base-model",
+ Brand: "my-brand",
+ Model: "my-model",
Serial: "serialserialserial1",
})
- makeSerialAssertionInState(c, s.brands, s.state, "canonical", "base-model", "serialserialserial1")
+ makeSerialAssertionInState(c, s.brands, s.state, "my-brand", "my-model", "serialserialserial1")
serial, err := s.mgr.Serial()
c.Assert(err, IsNil)
@@ -604,43 +604,6 @@
serial0, err = devBE.Serial()
c.Assert(err, IsNil)
c.Check(serial0.Serial(), Equals, "serialserialserial1")
-
- err = devBE.SetDevice(&auth.DeviceState{
- Brand: "canonical",
- Model: "other-model",
- })
- c.Assert(err, IsNil)
-
- // lookup the serial, nothing there
- _, err = devBE.Serial()
- c.Check(err, Equals, state.ErrNoState)
-
- makeSerialAssertionInState(c, s.brands, s.state, "canonical", "other-model", "serialserialserial2")
-
- // same
- _, err = devBE.Serial()
- c.Check(err, Equals, state.ErrNoState)
-
- err = devBE.SetDevice(&auth.DeviceState{
- Brand: "canonical",
- Model: "other-model",
- Serial: "serialserialserial2",
- })
- c.Assert(err, IsNil)
-
- serial, err = devBE.Serial()
- c.Check(err, IsNil)
- c.Check(serial.Model(), Equals, "other-model")
- c.Check(serial.Serial(), Equals, "serialserialserial2")
-
- // finish
- // XXX test separately
- err = remodCtx.Finish()
- c.Assert(err, IsNil)
-
- serial, err = s.mgr.Serial()
- c.Assert(err, IsNil)
- c.Check(serial.Model(), Equals, "other-model")
}
func (s *remodelLogicSuite) TestRemodelContextForTaskAndCaching(c *C) {
@@ -673,8 +636,8 @@
t := s.state.NewTask("remodel-task-1", "...")
chg.AddTask(t)
- // caching
- remodCtx1, err := devicestate.RemodelCtxFromTask(t)
+ // caching, internally this use remodelCtxFromTask
+ remodCtx1, err := devicestate.DeviceCtx(s.state, t, nil)
c.Assert(err, IsNil)
c.Check(remodCtx1, Equals, remodCtx)
@@ -682,7 +645,7 @@
// compute a new one
devicestate.CleanupRemodelCtx(chg)
- remodCtx2, err := devicestate.RemodelCtxFromTask(t)
+ remodCtx2, err := devicestate.DeviceCtx(s.state, t, nil)
c.Assert(err, IsNil)
c.Check(remodCtx2 != remodCtx, Equals, true)
c.Check(remodCtx2.Model(), DeepEquals, newModel)
@@ -692,19 +655,230 @@
s.state.Lock()
defer s.state.Unlock()
+ // internally these use remodelCtxFromTask
+
// task is nil
- remodCtx1, err := devicestate.RemodelCtxFromTask(nil)
+ remodCtx1, err := devicestate.DeviceCtx(s.state, nil, nil)
c.Check(err, Equals, state.ErrNoState)
c.Check(remodCtx1, IsNil)
// no change
t := s.state.NewTask("random-task", "...")
- _, err = devicestate.RemodelCtxFromTask(t)
+ _, err = devicestate.DeviceCtx(s.state, t, nil)
c.Check(err, Equals, state.ErrNoState)
// not a remodel change
chg := s.state.NewChange("not-remodel", "...")
chg.AddTask(t)
- _, err = devicestate.RemodelCtxFromTask(t)
+ _, err = devicestate.DeviceCtx(s.state, t, nil)
+ c.Check(err, Equals, state.ErrNoState)
+}
+
+func (s *remodelLogicSuite) setupForRereg(c *C) (oldModel, newModel *asserts.Model) {
+ oldModel = s.brands.Model("my-brand", "my-model", modelDefaults)
+ newModel = s.brands.Model("my-brand", "my-model", modelDefaults, map[string]interface{}{
+ "authority-id": "other-brand",
+ "brand-id": "other-brand",
+ "model": "other-model",
+ "store": "other-store",
+ })
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ encDevKey, err := asserts.EncodePublicKey(devKey.PublicKey())
+ c.Assert(err, IsNil)
+ serial, err := s.brands.Signing("my-brand").Sign(asserts.SerialType, map[string]interface{}{
+ "authority-id": "my-brand",
+ "brand-id": "my-brand",
+ "model": "my-model",
+ "serial": "orig-serial",
+ "device-key": string(encDevKey),
+ "device-key-sha3-384": devKey.PublicKey().ID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ assertstatetest.AddMany(s.state, oldModel, serial)
+
+ return oldModel, newModel
+}
+
+func (s *remodelLogicSuite) TestReregRemodelContextInit(c *C) {
+ oldModel, newModel := s.setupForRereg(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // we have a device state
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "my-brand",
+ Model: "my-model",
+ Serial: "orig-serial",
+ KeyID: "device-key-id",
+ SessionMacaroon: "prev-session",
+ })
+
+ remodCtx, err := devicestate.RemodelCtx(s.state, oldModel, newModel)
+ c.Assert(err, IsNil)
+
+ c.Check(remodCtx.ForRemodeling(), Equals, true)
+ c.Check(remodCtx.Kind(), Equals, devicestate.ReregRemodel)
+
+ chg := s.state.NewChange("remodel", "...")
+
+ remodCtx.Init(chg)
+
+ var encNewModel string
+ c.Assert(chg.Get("new-model", &encNewModel), IsNil)
+
+ c.Check(encNewModel, Equals, string(asserts.Encode(newModel)))
+
+ var device *auth.DeviceState
+ c.Assert(chg.Get("device", &device), IsNil)
+ // fresh device state before registration but with device-key
+ c.Check(device, DeepEquals, &auth.DeviceState{
+ Brand: "other-brand",
+ Model: "other-model",
+ KeyID: "device-key-id",
+ })
+
+ c.Check(remodCtx.Model(), DeepEquals, newModel)
+
+ // caching
+ t := s.state.NewTask("remodel-task-1", "...")
+ chg.AddTask(t)
+
+ remodCtx1, err := devicestate.DeviceCtx(s.state, t, nil)
+ c.Assert(err, IsNil)
+ c.Check(remodCtx1, Equals, remodCtx)
+}
+
+func (s *remodelLogicSuite) TestReregRemodelContextAsRegistrationContext(c *C) {
+ oldModel, newModel := s.setupForRereg(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // we have a device state
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "my-brand",
+ Model: "my-model",
+ Serial: "orig-serial",
+ KeyID: "device-key-id",
+ SessionMacaroon: "prev-session",
+ })
+
+ remodCtx, err := devicestate.RemodelCtx(s.state, oldModel, newModel)
+ c.Assert(err, IsNil)
+
+ c.Check(remodCtx.Kind(), Equals, devicestate.ReregRemodel)
+
+ chg := s.state.NewChange("remodel", "...")
+
+ remodCtx.Init(chg)
+
+ regCtx := remodCtx.(devicestate.RegistrationContext)
+
+ c.Check(regCtx.ForRemodeling(), Equals, true)
+ device1, err := regCtx.Device()
+ c.Assert(err, IsNil)
+ // fresh device state before registration but with device-key
+ c.Check(device1, DeepEquals, &auth.DeviceState{
+ Brand: "other-brand",
+ Model: "other-model",
+ KeyID: "device-key-id",
+ })
+ c.Check(regCtx.GadgetForSerialRequestConfig(), Equals, "my-brand-gadget")
+ c.Check(regCtx.SerialRequestExtraHeaders(), DeepEquals, map[string]interface{}{
+ "original-brand-id": "my-brand",
+ "original-model": "my-model",
+ "original-serial": "orig-serial",
+ })
+
+ serial, err := s.mgr.Serial()
+ c.Assert(err, IsNil)
+ c.Check(regCtx.SerialRequestAncillaryAssertions(), DeepEquals, []asserts.Assertion{newModel, serial})
+}
+
+func (s *remodelLogicSuite) TestReregRemodelContextNewSerial(c *C) {
+ // re-registration case
+ oldModel := s.brands.Model("my-brand", "my-model", modelDefaults)
+ newModel := fakeRemodelingModel(map[string]interface{}{
+ "model": "other-model",
+ })
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ assertstatetest.AddMany(s.state, oldModel)
+
+ // we have a device state and serial
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "my-brand",
+ Model: "my-model",
+ Serial: "serialserialserial1",
+ })
+
+ makeSerialAssertionInState(c, s.brands, s.state, "my-brand", "my-model", "serialserialserial1")
+
+ serial, err := s.mgr.Serial()
+ c.Assert(err, IsNil)
+ c.Check(serial.Serial(), Equals, "serialserialserial1")
+
+ remodCtx, err := devicestate.RemodelCtx(s.state, oldModel, newModel)
+ c.Assert(err, IsNil)
+
+ devBE := devicestate.RemodelDeviceBackend(remodCtx)
+
+ // no new serial yet
+ _, err = devBE.Serial()
+ c.Assert(err, Equals, state.ErrNoState)
+
+ chg := s.state.NewChange("remodel", "...")
+
+ remodCtx.Init(chg)
+
+ // sanity check
+ device1, err := devBE.Device()
+ c.Assert(err, IsNil)
+ c.Check(device1, DeepEquals, &auth.DeviceState{
+ Brand: "my-brand",
+ Model: "other-model",
+ })
+
+ // still no new serial
+ _, err = devBE.Serial()
+ c.Assert(err, Equals, state.ErrNoState)
+
+ newSerial := makeSerialAssertionInState(c, s.brands, s.state, "my-brand", "other-model", "serialserialserial2")
+
+ // same
+ _, err = devBE.Serial()
c.Check(err, Equals, state.ErrNoState)
+
+ // finish registration
+ regCtx := remodCtx.(devicestate.RegistrationContext)
+ err = regCtx.FinishRegistration(newSerial)
+ c.Assert(err, IsNil)
+
+ serial, err = devBE.Serial()
+ c.Check(err, IsNil)
+ c.Check(serial.Model(), Equals, "other-model")
+ c.Check(serial.Serial(), Equals, "serialserialserial2")
+
+ // not exposed yet
+ serial, err = s.mgr.Serial()
+ c.Assert(err, IsNil)
+ c.Check(serial.Model(), Equals, "my-model")
+ c.Check(serial.Serial(), Equals, "serialserialserial1")
+
+ // finish
+ err = remodCtx.Finish()
+ c.Assert(err, IsNil)
+
+ serial, err = s.mgr.Serial()
+ c.Assert(err, IsNil)
+ c.Check(serial.Model(), Equals, "other-model")
+ c.Check(serial.Serial(), Equals, "serialserialserial2")
}
diff -Nru snapd-2.40/overlord/export_test.go snapd-2.42.1/overlord/export_test.go
--- snapd-2.40/overlord/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -25,6 +25,7 @@
"github.com/snapcore/snapd/overlord/configstate"
"github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/overlord/storecontext"
"github.com/snapcore/snapd/store"
)
@@ -74,7 +75,7 @@
}
}
-func MockConfigstateInit(new func(hookmgr *hookstate.HookManager)) (restore func()) {
+func MockConfigstateInit(new func(*state.State, *hookstate.HookManager) error) (restore func()) {
configstateInit = new
return func() {
configstateInit = configstate.Init
diff -Nru snapd-2.40/overlord/healthstate/export_test.go snapd-2.42.1/overlord/healthstate/export_test.go
--- snapd-2.40/overlord/healthstate/export_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/overlord/healthstate/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,34 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package healthstate
+
+import (
+ "time"
+)
+
+func MockCheckTimeout(t time.Duration) (restore func()) {
+ old := checkTimeout
+ checkTimeout = t
+ return func() {
+ checkTimeout = old
+ }
+}
+
+var KnownStatuses = knownStatuses
diff -Nru snapd-2.40/overlord/healthstate/healthstate.go snapd-2.42.1/overlord/healthstate/healthstate.go
--- snapd-2.40/overlord/healthstate/healthstate.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/overlord/healthstate/healthstate.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,226 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package healthstate
+
+import (
+ "encoding/json"
+ "fmt"
+ "os"
+ "regexp"
+ "time"
+
+ "github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/strutil"
+)
+
+var checkTimeout = 30 * time.Second
+
+func init() {
+ if s, ok := os.LookupEnv("SNAPD_CHECK_HEALTH_HOOK_TIMEOUT"); ok {
+ if to, err := time.ParseDuration(s); err == nil {
+ checkTimeout = to
+ } else {
+ logger.Debugf("cannot override check-health timeout: %v", err)
+ }
+ }
+
+ snapstate.CheckHealthHook = Hook
+}
+
+func Hook(st *state.State, snapName string, snapRev snap.Revision) *state.Task {
+ summary := fmt.Sprintf("Run health check of %q snap", snapName)
+ hooksup := &hookstate.HookSetup{
+ Snap: snapName,
+ Revision: snapRev,
+ Hook: "check-health",
+ Optional: true,
+ Timeout: checkTimeout,
+ }
+
+ return hookstate.HookTask(st, summary, hooksup, nil)
+}
+
+type HealthStatus int
+
+const (
+ UnknownStatus = HealthStatus(iota)
+ OkayStatus
+ WaitingStatus
+ BlockedStatus
+ ErrorStatus
+)
+
+var knownStatuses = []string{"unknown", "okay", "waiting", "blocked", "error"}
+
+func StatusLookup(str string) (HealthStatus, error) {
+ for i, k := range knownStatuses {
+ if k == str {
+ return HealthStatus(i), nil
+ }
+ }
+ return -1, fmt.Errorf("invalid status %q, must be one of %s", str, strutil.Quoted(knownStatuses))
+}
+
+func (s HealthStatus) String() string {
+ if s < 0 || s >= HealthStatus(len(knownStatuses)) {
+ return fmt.Sprintf("invalid (%d)", s)
+ }
+ return knownStatuses[s]
+}
+
+type HealthState struct {
+ Revision snap.Revision `json:"revision"`
+ Timestamp time.Time `json:"timestamp"`
+ Status HealthStatus `json:"status"`
+ Message string `json:"message,omitempty"`
+ Code string `json:"code,omitempty"`
+}
+
+func Init(hookManager *hookstate.HookManager) {
+ hookManager.Register(regexp.MustCompile("^check-health$"), newHealthHandler)
+}
+
+func newHealthHandler(ctx *hookstate.Context) hookstate.Handler {
+ return &healthHandler{context: ctx}
+}
+
+type healthHandler struct {
+ context *hookstate.Context
+}
+
+// Before is called just before the hook runs -- nothing to do beyond setting a marker
+func (h *healthHandler) Before() error {
+ // we use the 'health' entry as a marker to not add OnDone to
+ // the snapctl set-health execution
+ h.context.Lock()
+ h.context.Set("health", struct{}{})
+ h.context.Unlock()
+ return nil
+}
+
+func (h *healthHandler) Done() error {
+ var health HealthState
+
+ h.context.Lock()
+ err := h.context.Get("health", &health)
+ h.context.Unlock()
+
+ if err != nil && err != state.ErrNoState {
+ // note it can't actually be state.ErrNoState because Before sets it
+ // (but if it were, health.Timestamp would still be zero)
+ return err
+ }
+ if health.Timestamp.IsZero() {
+ // health was actually the marker (or err == state.ErrNoState)
+ health = HealthState{
+ Revision: h.context.SnapRevision(),
+ Timestamp: time.Now(),
+ Status: UnknownStatus,
+ Code: "snapd-hook-no-health-set",
+ Message: "hook did not call set-health",
+ }
+ }
+
+ return h.appendHealth(&health)
+}
+
+func (h *healthHandler) Error(err error) error {
+ return h.appendHealth(&HealthState{
+ Revision: h.context.SnapRevision(),
+ Timestamp: time.Now(),
+ Status: UnknownStatus,
+ Code: "snapd-hook-failed",
+ Message: "hook failed",
+ })
+}
+
+func (h *healthHandler) appendHealth(health *HealthState) error {
+ st := h.context.State()
+ st.Lock()
+ defer st.Unlock()
+
+ return appendHealth(h.context, health)
+}
+
+func appendHealth(ctx *hookstate.Context, health *HealthState) error {
+ st := ctx.State()
+
+ var hs map[string]*HealthState
+ if err := st.Get("health", &hs); err != nil {
+ if err != state.ErrNoState {
+ return err
+ }
+ hs = map[string]*HealthState{}
+ }
+ hs[ctx.InstanceName()] = health
+ st.Set("health", hs)
+
+ return nil
+}
+
+// SetFromHookContext extracts the health of a snap from a hook
+// context, and saves it in snapd's state.
+// Must be called with the context lock held.
+func SetFromHookContext(ctx *hookstate.Context) error {
+ var health HealthState
+ err := ctx.Get("health", &health)
+
+ if err != nil {
+ if err == state.ErrNoState {
+ return nil
+ }
+ return err
+ }
+ return appendHealth(ctx, &health)
+}
+
+func All(st *state.State) (map[string]*HealthState, error) {
+ var hs map[string]*HealthState
+ if err := st.Get("health", &hs); err != nil && err != state.ErrNoState {
+ return nil, err
+ }
+ return hs, nil
+}
+
+func Get(st *state.State, snap string) (*HealthState, error) {
+ var hs map[string]json.RawMessage
+ if err := st.Get("health", &hs); err != nil {
+ if err != state.ErrNoState {
+ return nil, err
+ }
+ return nil, nil
+ }
+
+ buf := hs[snap]
+ if len(buf) == 0 {
+ return nil, nil
+ }
+
+ var health HealthState
+ if err := json.Unmarshal(buf, &health); err != nil {
+ return nil, err
+ }
+
+ return &health, nil
+}
diff -Nru snapd-2.40/overlord/healthstate/healthstate_test.go snapd-2.42.1/overlord/healthstate/healthstate_test.go
--- snapd-2.40/overlord/healthstate/healthstate_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/overlord/healthstate/healthstate_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,255 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package healthstate_test
+
+import (
+ "io/ioutil"
+ "os"
+ "path/filepath"
+ "testing"
+ "time"
+
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord"
+ "github.com/snapcore/snapd/overlord/healthstate"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/store/storetest"
+ "github.com/snapcore/snapd/testutil"
+)
+
+func TestHealthState(t *testing.T) { check.TestingT(t) }
+
+type healthSuite struct {
+ testutil.BaseTest
+ o *overlord.Overlord
+ se *overlord.StateEngine
+ state *state.State
+ hookMgr *hookstate.HookManager
+ info *snap.Info
+}
+
+var _ = check.Suite(&healthSuite{})
+
+func (s *healthSuite) SetUpTest(c *check.C) {
+ s.BaseTest.SetUpTest(c)
+ s.AddCleanup(healthstate.MockCheckTimeout(time.Second))
+ dirs.SetRootDir(c.MkDir())
+
+ s.o = overlord.Mock()
+ s.state = s.o.State()
+
+ var err error
+ s.hookMgr, err = hookstate.Manager(s.state, s.o.TaskRunner())
+ c.Assert(err, check.IsNil)
+ s.se = s.o.StateEngine()
+ s.o.AddManager(s.hookMgr)
+ s.o.AddManager(s.o.TaskRunner())
+
+ healthstate.Init(s.hookMgr)
+
+ c.Assert(s.o.StartUp(), check.IsNil)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.ReplaceStore(s.state, storetest.Store{})
+ sideInfo := &snap.SideInfo{RealName: "test-snap", Revision: snap.R(42)}
+ snapstate.Set(s.state, "test-snap", &snapstate.SnapState{
+ Sequence: []*snap.SideInfo{sideInfo},
+ Current: snap.R(42),
+ Active: true,
+ SnapType: "app",
+ })
+ s.info = snaptest.MockSnapCurrent(c, "{name: test-snap, version: v1}", sideInfo)
+}
+
+func (s *healthSuite) TearDownTest(c *check.C) {
+ s.hookMgr.StopHooks()
+ s.se.Stop()
+ s.BaseTest.TearDownTest(c)
+}
+
+type healthHookTestCondition int
+
+const (
+ noHook = iota
+ badHook
+ goodHook
+ captainHook
+)
+
+func (s *healthSuite) TestHealthNoHook(c *check.C) {
+ s.testHealth(c, noHook)
+}
+
+func (s *healthSuite) TestHealthFailingHook(c *check.C) {
+ s.testHealth(c, badHook)
+}
+
+func (s *healthSuite) TestHealth(c *check.C) {
+ s.testHealth(c, goodHook)
+}
+
+func (s *healthSuite) testHealth(c *check.C, cond healthHookTestCondition) {
+ var cmd *testutil.MockCmd
+ switch cond {
+ case badHook:
+ cmd = testutil.MockCommand(c, "snap", "exit 1")
+ default:
+ cmd = testutil.MockCommand(c, "snap", "exit 0")
+ }
+
+ if cond != noHook {
+ hookFn := filepath.Join(s.info.MountDir(), "meta", "hooks", "check-health")
+ c.Assert(os.MkdirAll(filepath.Dir(hookFn), 0755), check.IsNil)
+ // the hook won't actually be called, but needs to exist
+ c.Assert(ioutil.WriteFile(hookFn, nil, 0755), check.IsNil)
+ }
+
+ s.state.Lock()
+ task := healthstate.Hook(s.state, "test-snap", snap.R(42))
+ change := s.state.NewChange("kind", "summary")
+ change.AddTask(task)
+ s.state.Unlock()
+
+ c.Assert(task.Kind(), check.Equals, "run-hook")
+ var hooksup hookstate.HookSetup
+
+ s.state.Lock()
+ err := task.Get("hook-setup", &hooksup)
+ s.state.Unlock()
+ c.Check(err, check.IsNil)
+
+ c.Check(hooksup, check.DeepEquals, hookstate.HookSetup{
+ Snap: "test-snap",
+ Hook: "check-health",
+ Revision: snap.R(42),
+ Optional: true,
+ Timeout: time.Second,
+ IgnoreError: false,
+ TrackError: false,
+ })
+
+ t0 := time.Now()
+ s.se.Ensure()
+ s.se.Wait()
+ tf := time.Now()
+ var healths map[string]*healthstate.HealthState
+ var health *healthstate.HealthState
+ var err2 error
+ s.state.Lock()
+ status := change.Status()
+ err = s.state.Get("health", &healths)
+ health, err2 = healthstate.Get(s.state, "test-snap")
+ s.state.Unlock()
+ c.Assert(err2, check.IsNil)
+
+ switch cond {
+ case badHook:
+ c.Assert(status, check.Equals, state.ErrorStatus)
+ default:
+ c.Assert(status, check.Equals, state.DoneStatus)
+ }
+ if cond != noHook {
+ c.Assert(err, check.IsNil)
+ c.Assert(healths, check.HasLen, 1)
+ c.Assert(healths["test-snap"], check.NotNil)
+ c.Check(health, check.DeepEquals, healths["test-snap"])
+ c.Check(health.Revision, check.Equals, snap.R(42))
+ c.Check(health.Status, check.Equals, healthstate.UnknownStatus)
+ if cond == badHook {
+ c.Check(health.Message, check.Equals, "hook failed")
+ c.Check(health.Code, check.Equals, "snapd-hook-failed")
+ } else {
+ c.Check(health.Message, check.Equals, "hook did not call set-health")
+ c.Check(health.Code, check.Equals, "snapd-hook-no-health-set")
+ }
+ com := check.Commentf("%s ⩼ %s ⩼ %s", t0.Format(time.StampNano), health.Timestamp.Format(time.StampNano), tf.Format(time.StampNano))
+ c.Check(health.Timestamp.After(t0) && health.Timestamp.Before(tf), check.Equals, true, com)
+ c.Check(cmd.Calls(), check.DeepEquals, [][]string{{"snap", "run", "--hook", "check-health", "-r", "42", "test-snap"}})
+ } else {
+ // no script -> no health
+ c.Assert(err, check.Equals, state.ErrNoState)
+ c.Check(healths, check.IsNil)
+ c.Check(health, check.IsNil)
+ c.Check(cmd.Calls(), check.HasLen, 0)
+ }
+}
+
+func (*healthSuite) TestStatusHappy(c *check.C) {
+ for i, str := range healthstate.KnownStatuses {
+ status, err := healthstate.StatusLookup(str)
+ c.Check(err, check.IsNil, check.Commentf("%v", str))
+ c.Check(status, check.Equals, healthstate.HealthStatus(i), check.Commentf("%v", str))
+ c.Check(healthstate.HealthStatus(i).String(), check.Equals, str, check.Commentf("%v", str))
+ }
+}
+
+func (*healthSuite) TestStatusUnhappy(c *check.C) {
+ status, err := healthstate.StatusLookup("rabbits")
+ c.Check(status, check.Equals, healthstate.HealthStatus(-1))
+ c.Check(err, check.ErrorMatches, `invalid status "rabbits".*`)
+ c.Check(status.String(), check.Equals, "invalid (-1)")
+}
+
+func (s *healthSuite) TestSetFromHookContext(c *check.C) {
+ ctx, err := hookstate.NewContext(nil, s.state, &hookstate.HookSetup{Snap: "foo"}, nil, "")
+ c.Assert(err, check.IsNil)
+
+ ctx.Lock()
+ defer ctx.Unlock()
+
+ var hs map[string]*healthstate.HealthState
+ c.Check(s.state.Get("health", &hs), check.Equals, state.ErrNoState)
+
+ ctx.Set("health", &healthstate.HealthState{Status: 42})
+
+ err = healthstate.SetFromHookContext(ctx)
+ c.Assert(err, check.IsNil)
+
+ hs, err = healthstate.All(s.state)
+ c.Check(err, check.IsNil)
+ c.Check(hs, check.DeepEquals, map[string]*healthstate.HealthState{
+ "foo": {Status: 42},
+ })
+}
+
+func (s *healthSuite) TestSetFromHookContextEmpty(c *check.C) {
+ ctx, err := hookstate.NewContext(nil, s.state, &hookstate.HookSetup{Snap: "foo"}, nil, "")
+ c.Assert(err, check.IsNil)
+
+ ctx.Lock()
+ defer ctx.Unlock()
+
+ var hs map[string]healthstate.HealthState
+ c.Check(s.state.Get("health", &hs), check.Equals, state.ErrNoState)
+
+ err = healthstate.SetFromHookContext(ctx)
+ c.Assert(err, check.IsNil)
+
+ // no health in the context -> no health in state
+ c.Check(s.state.Get("health", &hs), check.Equals, state.ErrNoState)
+}
diff -Nru snapd-2.40/overlord/hookstate/ctlcmd/ctlcmd.go snapd-2.42.1/overlord/hookstate/ctlcmd/ctlcmd.go
--- snapd-2.40/overlord/hookstate/ctlcmd/ctlcmd.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/hookstate/ctlcmd/ctlcmd.go 2019-10-30 12:17:43.000000000 +0000
@@ -79,16 +79,19 @@
shortHelp string
longHelp string
generator func() command
+ hidden bool
}
var commands = make(map[string]*commandInfo)
-func addCommand(name, shortHelp, longHelp string, generator func() command) {
- commands[name] = &commandInfo{
+func addCommand(name, shortHelp, longHelp string, generator func() command) *commandInfo {
+ cmd := &commandInfo{
shortHelp: shortHelp,
longHelp: longHelp,
generator: generator,
}
+ commands[name] = cmd
+ return cmd
}
// ForbiddenCommandError conveys that a command cannot be invoked in some context
@@ -121,7 +124,7 @@
var data interface{}
// commands listed here will be allowed for regular users
// note: commands still need valid context and snaps can only access own config.
- if uid == 0 || name == "get" || name == "services" {
+ if uid == 0 || name == "get" || name == "services" || name == "set-health" {
cmd := cmdInfo.generator()
cmd.setStdout(&stdoutBuffer)
cmd.setStderr(&stderrBuffer)
@@ -130,7 +133,8 @@
} else {
data = &ForbiddenCommand{Uid: uid, Name: name}
}
- _, err = parser.AddCommand(name, cmdInfo.shortHelp, cmdInfo.longHelp, data)
+ theCmd, err := parser.AddCommand(name, cmdInfo.shortHelp, cmdInfo.longHelp, data)
+ theCmd.Hidden = cmdInfo.hidden
if err != nil {
logger.Panicf("cannot add command %q: %s", name, err)
}
diff -Nru snapd-2.40/overlord/hookstate/ctlcmd/ctlcmd_test.go snapd-2.42.1/overlord/hookstate/ctlcmd/ctlcmd_test.go
--- snapd-2.40/overlord/hookstate/ctlcmd/ctlcmd_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/hookstate/ctlcmd/ctlcmd_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,13 +20,16 @@
package ctlcmd_test
import (
+ "fmt"
"testing"
+ "github.com/jessevdk/go-flags"
"github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
"github.com/snapcore/snapd/overlord/hookstate/hooktest"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
. "gopkg.in/check.v1"
)
@@ -74,3 +77,36 @@
c.Check(string(stderr), Equals, "test stderr")
c.Check(mockCommand.Args, DeepEquals, []string{"foo"})
}
+
+func taskKinds(tasks []*state.Task) []string {
+ kinds := make([]string, len(tasks))
+ for i, task := range tasks {
+ k := task.Kind()
+ if k == "run-hook" {
+ var hooksup hookstate.HookSetup
+ if err := task.Get("hook-setup", &hooksup); err != nil {
+ panic(err)
+ }
+ k = fmt.Sprintf("%s[%s]", k, hooksup.Hook)
+ }
+ kinds[i] = k
+ }
+ return kinds
+}
+
+func (s *ctlcmdSuite) TestHiddenCommand(c *C) {
+ ctlcmd.AddHiddenMockCommand("mock-hidden")
+ ctlcmd.AddMockCommand("mock-shown")
+ defer ctlcmd.RemoveCommand("mock-hidden")
+ defer ctlcmd.RemoveCommand("mock-shown")
+
+ _, _, err := ctlcmd.Run(s.mockContext, []string{"--help"}, 0)
+ // help message output is returned as *flags.Error with
+ // Type as flags.ErrHelp
+ c.Assert(err, FitsTypeOf, &flags.Error{})
+ c.Check(err.(*flags.Error).Type, Equals, flags.ErrHelp)
+ // mock-shown is in the help message
+ c.Check(err.Error(), testutil.Contains, " mock-shown\n")
+ // mock-hidden is not in the help message
+ c.Check(err.Error(), Not(testutil.Contains), " mock-hidden\n")
+}
diff -Nru snapd-2.40/overlord/hookstate/ctlcmd/export_test.go snapd-2.42.1/overlord/hookstate/ctlcmd/export_test.go
--- snapd-2.40/overlord/hookstate/ctlcmd/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/hookstate/ctlcmd/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -21,6 +21,7 @@
import (
"fmt"
+
"github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/servicestate"
"github.com/snapcore/snapd/overlord/state"
@@ -36,8 +37,17 @@
}
func AddMockCommand(name string) *MockCommand {
+ return addMockCmd(name, false)
+}
+
+func AddHiddenMockCommand(name string) *MockCommand {
+ return addMockCmd(name, true)
+}
+
+func addMockCmd(name string, hidden bool) *MockCommand {
mockCommand := NewMockCommand()
- addCommand(name, "", "", func() command { return mockCommand })
+ cmd := addCommand(name, "", "", func() command { return mockCommand })
+ cmd.hidden = hidden
return mockCommand
}
diff -Nru snapd-2.40/overlord/hookstate/ctlcmd/get_test.go snapd-2.42.1/overlord/hookstate/ctlcmd/get_test.go
--- snapd-2.40/overlord/hookstate/ctlcmd/get_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/hookstate/ctlcmd/get_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -265,8 +265,7 @@
// Verify config value
var value interface{}
tr := config.NewTransaction(s.mockContext.State())
- c.Assert(tr.Get("test-snap", "foo", &value), IsNil)
- c.Assert(value, IsNil)
+ c.Assert(config.IsNoOption(tr.Get("test-snap", "foo", &value)), Equals, true)
c.Assert(tr.Get("test-snap", "bar", &value), IsNil)
c.Assert(value, DeepEquals, []interface{}{nil})
}
diff -Nru snapd-2.40/overlord/hookstate/ctlcmd/health.go snapd-2.42.1/overlord/hookstate/ctlcmd/health.go
--- snapd-2.40/overlord/hookstate/ctlcmd/health.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/overlord/hookstate/ctlcmd/health.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,148 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package ctlcmd
+
+import (
+ "fmt"
+ "regexp"
+ "time"
+
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/overlord/healthstate"
+ "github.com/snapcore/snapd/overlord/state"
+)
+
+var (
+ shortHealthHelp = i18n.G("Report the health status of a snap")
+ longHealthHelp = i18n.G(`
+The set-health command is called from within a snap to inform the system of the
+snap's overall health.
+
+It can be called from any hook, and even from the apps themselves. A snap can
+optionally provide a 'check-health' hook to better manage these calls, which is
+then called periodically and with increased frequency while the snap is
+"unhealthy". Any health regression will issue a warning to the user.
+
+Note: the health is of the snap only, not of the apps it contains; it’s up to
+ the snap developer to determine how the health of the individual apps is
+ reflected in the overall health of the snap.
+
+status can be one of:
+
+- okay: the snap is healthy. This status takes no message and no code.
+
+- waiting: a resource needed by the snap (e.g. a device, network, or service) is
+ not ready and the user will need to wait. The message must explain what
+ resource is being waited for.
+
+- blocked: something needs doing to unblock the snap (e.g. a service needs to be
+ configured); the message must be sufficient to point the user in the right
+ direction.
+
+- error: something is broken; the message must explain what.
+`)
+)
+
+func init() {
+ addCommand("set-health", shortHealthHelp, longHealthHelp, func() command { return &healthCommand{} })
+}
+
+type healthPositional struct {
+ Status string `positional-arg-name:"" required:"yes" description:"a valid health status; required."`
+ Message string `positional-arg-name:"" description:"a short human-readable explanation of the status (when not okay). Must be longer than 7 characters, and will be truncated if over 70. Message cannot be provided if status is okay, but is required otherwise."`
+}
+
+type healthCommand struct {
+ baseCommand
+ healthPositional `positional-args:"yes"`
+ Code string `long:"code" value-name:"" description:"optional tool-friendly value representing the problem that makes the snap unhealthy. Not a number, but a word with 3-30 characters matching [a-z](-?[a-z0-9])+"`
+}
+
+var (
+ validCode = regexp.MustCompile(`^[a-z](?:-?[a-z0-9])+$`).MatchString
+)
+
+func (c *healthCommand) Execute([]string) error {
+ if c.Status == "okay" && (len(c.Message) > 0 || len(c.Code) > 0) {
+ return fmt.Errorf(`when status is "okay", message and code must be empty`)
+ }
+
+ status, err := healthstate.StatusLookup(c.Status)
+ if err != nil {
+ return err
+ }
+ if status == healthstate.UnknownStatus {
+ return fmt.Errorf(`status cannot be manually set to "unknown"`)
+ }
+
+ if len(c.Code) > 0 {
+ if len(c.Code) < 3 || len(c.Code) > 30 {
+ return fmt.Errorf("code must have between 3 and 30 characters, got %d", len(c.Code))
+ }
+ if !validCode(c.Code) {
+ return fmt.Errorf("invalid code %q (code must start with lowercase ASCII letters, and contain only ASCII letters and numbers, optionally separated by single dashes)", c.Code) // technically not dashes but hyphen-minuses
+ }
+ }
+
+ if status != healthstate.OkayStatus {
+ if len(c.Message) == 0 {
+ return fmt.Errorf(`when status is not "okay", message is required`)
+ }
+
+ rmsg := []rune(c.Message)
+ if len(rmsg) < 7 {
+ return fmt.Errorf(`message must be at least 7 characters long (got %d)`, len(rmsg))
+ }
+ if len(rmsg) > 70 {
+ c.Message = string(rmsg[:69]) + "…"
+ }
+ }
+
+ ctx := c.context()
+ if ctx == nil {
+ // reuses the i18n'ed error message from service ctl
+ return fmt.Errorf(i18n.G("cannot %s without a context"), "set-health")
+ }
+ ctx.Lock()
+ defer ctx.Unlock()
+
+ var v struct{}
+
+ // if 'health' is there we've either already added an OnDone (and the
+ // following Set("health"), or we're in the set-health hook itself
+ // (which sets it to a dummy entry for this purpose).
+ if err := ctx.Get("health", &v); err == state.ErrNoState {
+ ctx.OnDone(func() error {
+ return healthstate.SetFromHookContext(ctx)
+ })
+ }
+
+ health := &healthstate.HealthState{
+ Revision: ctx.SnapRevision(), // will be "unset" for unasserted installs, and trys
+ Timestamp: time.Now(),
+ Status: status,
+ Message: c.Message,
+ Code: c.Code,
+ }
+
+ ctx.Set("health", health)
+
+ return nil
+}
diff -Nru snapd-2.40/overlord/hookstate/ctlcmd/health_test.go snapd-2.42.1/overlord/hookstate/ctlcmd/health_test.go
--- snapd-2.40/overlord/hookstate/ctlcmd/health_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/overlord/hookstate/ctlcmd/health_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,144 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package ctlcmd_test
+
+import (
+ "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/healthstate"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
+ "github.com/snapcore/snapd/overlord/hookstate/hooktest"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type healthSuite struct {
+ testutil.BaseTest
+ state *state.State
+ mockContext *hookstate.Context
+ mockHandler *hooktest.MockHandler
+}
+
+var _ = check.Suite(&healthSuite{})
+
+func (s *healthSuite) SetUpTest(c *check.C) {
+ s.BaseTest.SetUpTest(c)
+ dirs.SetRootDir(c.MkDir())
+
+ testutil.MockCommand(c, "systemctl", "")
+ s.mockHandler = hooktest.NewMockHandler()
+
+ s.state = state.New(nil)
+ s.state.Lock()
+ defer s.state.Unlock()
+ task := s.state.NewTask("test-task", "my test task")
+ setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(42), Hook: "check-health"}
+
+ ctx, err := hookstate.NewContext(task, s.state, setup, s.mockHandler, "")
+ c.Assert(err, check.IsNil)
+ s.mockContext = ctx
+}
+
+func (s *healthSuite) TestBadArgs(c *check.C) {
+ type tableT struct {
+ args []string
+ err string
+ }
+ table := []tableT{
+ {
+ []string{"set-health"},
+ "the required argument `` was not provided",
+ }, {
+ []string{"set-health", "bananas", "message"},
+ `invalid status "bananas".*`,
+ }, {
+ []string{"set-health", "unknown", "message"},
+ `status cannot be manually set to "unknown"`,
+ }, {
+ []string{"set-health", "okay", "message"},
+ `when status is "okay", message and code must be empty`,
+ }, {
+ []string{"set-health", "okay", "--code=what"},
+ `when status is "okay", message and code must be empty`,
+ }, {
+ []string{"set-health", "blocked"},
+ `when status is not "okay", message is required`,
+ }, {
+ []string{"set-health", "blocked", "message", "--code=xx"},
+ `code must have between 3 and 30 characters, got 2`,
+ }, {
+ []string{"set-health", "blocked", "message", "--code=abcdefghijklmnopqrstuvwxyz12345"},
+ `code must have between 3 and 30 characters, got 31`,
+ }, {
+ []string{"set-health", "blocked", "message", "--code=☠☢☣💣💢🐍✴👿‼"},
+ `code must have between 3 and 30 characters, got 31`,
+ }, {
+ []string{"set-health", "blocked", "message", "--code=123"},
+ `invalid code "123".*`,
+ }, {
+ []string{"set-health", "blocked", "what"},
+ `message must be at least 7 characters long \(got 4\)`,
+ }, {
+ []string{"set-health", "blocked", "áéíóú"},
+ `message must be at least 7 characters long \(got 5\)`,
+ }, {
+ []string{"set-health", "blocked", "message"},
+ `cannot set-health without a context`,
+ },
+ }
+
+ for i, t := range table {
+ _, _, err := ctlcmd.Run(nil, t.args, 0)
+ c.Check(err, check.ErrorMatches, t.err, check.Commentf("%d", i))
+ }
+}
+
+func (s *healthSuite) TestRegularRun(c *check.C) {
+ _, _, err := ctlcmd.Run(s.mockContext, []string{"set-health", "blocked", "message", "--code=some-code"}, 0)
+ c.Assert(err, check.IsNil)
+
+ s.mockContext.Lock()
+ defer s.mockContext.Unlock()
+
+ var health healthstate.HealthState
+ c.Assert(s.mockContext.Get("health", &health), check.IsNil)
+ c.Check(health.Revision, check.Equals, snap.R(42))
+ c.Check(health.Status, check.Equals, healthstate.BlockedStatus)
+ c.Check(health.Message, check.Equals, "message")
+ c.Check(health.Code, check.Equals, "some-code")
+}
+
+func (s *healthSuite) TestMessageTruncation(c *check.C) {
+ _, _, err := ctlcmd.Run(s.mockContext, []string{"set-health", "waiting", "Sometimes messages will get a little bit too verbose and this can lead to some rather nasty UX (as well as potential memory problems in extreme cases) so we kinda have to deal with that", "--code=some-code"}, 0)
+ c.Assert(err, check.IsNil)
+
+ s.mockContext.Lock()
+ defer s.mockContext.Unlock()
+
+ var health healthstate.HealthState
+ c.Assert(s.mockContext.Get("health", &health), check.IsNil)
+ c.Check(health.Revision, check.Equals, snap.R(42))
+ c.Check(health.Status, check.Equals, healthstate.WaitingStatus)
+ c.Check(health.Message, check.Equals, "Sometimes messages will get a little bit too verbose and this can lea…")
+ c.Check(health.Code, check.Equals, "some-code")
+}
diff -Nru snapd-2.40/overlord/hookstate/ctlcmd/helpers.go snapd-2.42.1/overlord/hookstate/ctlcmd/helpers.go
--- snapd-2.40/overlord/hookstate/ctlcmd/helpers.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/hookstate/ctlcmd/helpers.go 2019-10-30 12:17:43.000000000 +0000
@@ -36,6 +36,15 @@
"github.com/snapcore/snapd/snap"
)
+var finalTasks map[string]bool
+
+func init() {
+ finalTasks = make(map[string]bool, len(snapstate.FinalTasks))
+ for _, kind := range snapstate.FinalTasks {
+ finalTasks[kind] = true
+ }
+}
+
func getServiceInfos(st *state.State, snapName string, serviceNames []string) ([]*snap.AppInfo, error) {
st.Lock()
defer st.Unlock()
@@ -103,7 +112,14 @@
}
for _, ts := range tts {
- ts.WaitAll(state.NewTaskSet(tasks...))
+ for _, t := range tasks {
+ // queue service command after all tasks, except for final tasks which must come after service commands
+ if finalTasks[t.Kind()] {
+ t.WaitAll(ts)
+ } else {
+ ts.WaitFor(t)
+ }
+ }
change.AddAll(ts)
}
// As this can be run from what was originally the last task of a change,
@@ -115,6 +131,7 @@
func runServiceCommand(context *hookstate.Context, inst *servicestate.Instruction, serviceNames []string) error {
if context == nil {
+ // this message is reused in health.go
return fmt.Errorf(i18n.G("cannot %s without a context"), inst.Action)
}
diff -Nru snapd-2.40/overlord/hookstate/ctlcmd/services_test.go snapd-2.42.1/overlord/hookstate/ctlcmd/services_test.go
--- snapd-2.40/overlord/hookstate/ctlcmd/services_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/hookstate/ctlcmd/services_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -301,6 +301,47 @@
c.Check(err, ErrorMatches, `snap "test-snap" has "conflicting change" change in progress`)
}
+var (
+ installTaskKinds = []string{
+ "prerequisites",
+ "download-snap",
+ "validate-snap",
+ "mount-snap",
+ "copy-snap-data",
+ "setup-profiles",
+ "link-snap",
+ "auto-connect",
+ "set-auto-aliases",
+ "setup-aliases",
+ "run-hook[install]",
+ "start-snap-services",
+ "run-hook[configure]",
+ "run-hook[check-health]",
+ }
+
+ refreshTaskKinds = []string{
+ "prerequisites",
+ "download-snap",
+ "validate-snap",
+ "mount-snap",
+ "run-hook[pre-refresh]",
+ "stop-snap-services",
+ "remove-aliases",
+ "unlink-current-snap",
+ "copy-snap-data",
+ "setup-profiles",
+ "link-snap",
+ "auto-connect",
+ "set-auto-aliases",
+ "setup-aliases",
+ "run-hook[post-refresh]",
+ "start-snap-services",
+ "cleanup",
+ "run-hook[configure]",
+ "run-hook[check-health]",
+ }
+)
+
func (s *servicectlSuite) TestQueuedCommands(c *C) {
s.st.Lock()
@@ -309,8 +350,8 @@
c.Assert(err, IsNil)
c.Check(installed, DeepEquals, []string{"one", "two"})
c.Assert(tts, HasLen, 2)
- c.Assert(tts[0].Tasks(), HasLen, 13)
- c.Assert(tts[1].Tasks(), HasLen, 13)
+ c.Assert(taskKinds(tts[0].Tasks()), DeepEquals, installTaskKinds)
+ c.Assert(taskKinds(tts[1].Tasks()), DeepEquals, installTaskKinds)
chg.AddAll(tts[0])
chg.AddAll(tts[1])
@@ -336,14 +377,73 @@
s.st.Lock()
defer s.st.Unlock()
+ expectedTaskKinds := append(installTaskKinds, "exec-command", "exec-command", "exec-command")
for i := 1; i <= 2; i++ {
laneTasks := chg.LaneTasks(i)
- c.Assert(laneTasks, HasLen, 16)
+ c.Assert(taskKinds(laneTasks), DeepEquals, expectedTaskKinds)
c.Check(laneTasks[12].Summary(), Matches, `Run configure hook of .* snap if present`)
- c.Check(laneTasks[13].Summary(), Equals, "stop of [test-snap.test-service]")
- c.Check(laneTasks[14].Summary(), Equals, "start of [test-snap.test-service]")
- c.Check(laneTasks[15].Summary(), Equals, "restart of [test-snap.test-service]")
+ c.Check(laneTasks[14].Summary(), Equals, "stop of [test-snap.test-service]")
+ c.Check(laneTasks[15].Summary(), Equals, "start of [test-snap.test-service]")
+ c.Check(laneTasks[16].Summary(), Equals, "restart of [test-snap.test-service]")
+ }
+}
+
+func (s *servicectlSuite) testQueueCommandsOrdering(c *C, finalTaskKind string) {
+ s.st.Lock()
+
+ chg := s.st.NewChange("seeding change", "seeding change")
+ finalTask := s.st.NewTask(finalTaskKind, "")
+ chg.AddTask(finalTask)
+ configure := s.st.NewTask("run-hook", "")
+ chg.AddTask(configure)
+
+ s.st.Unlock()
+
+ setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "configure"}
+ context, err := hookstate.NewContext(configure, configure.State(), setup, s.mockHandler, "")
+ c.Assert(err, IsNil)
+
+ _, _, err = ctlcmd.Run(context, []string{"stop", "test-snap.test-service"}, 0)
+ c.Check(err, IsNil)
+ _, _, err = ctlcmd.Run(context, []string{"start", "test-snap.test-service"}, 0)
+ c.Check(err, IsNil)
+
+ s.st.Lock()
+ defer s.st.Unlock()
+
+ finalTaskWt := finalTask.WaitTasks()
+ c.Assert(finalTaskWt, HasLen, 2)
+
+ for _, t := range finalTaskWt {
+ // mark-seeded tasks should wait for both exec-command tasks
+ c.Check(t.Kind(), Equals, "exec-command")
+ var argv []string
+ c.Assert(t.Get("argv", &argv), IsNil)
+ c.Check(argv, HasLen, 3)
+
+ commandWt := make(map[string]bool)
+ for _, wt := range t.WaitTasks() {
+ commandWt[wt.Kind()] = true
+ }
+ // exec-command for "stop" should wait for configure hook task, "start" should wait for "stop" and "configure" hook task.
+ switch argv[1] {
+ case "stop":
+ c.Check(commandWt, DeepEquals, map[string]bool{"run-hook": true})
+ case "start":
+ c.Check(commandWt, DeepEquals, map[string]bool{"run-hook": true, "exec-command": true})
+ default:
+ c.Fatalf("unexpected command: %q", argv[1])
+ }
}
+ c.Check(finalTask.HaltTasks(), HasLen, 0)
+}
+
+func (s *servicectlSuite) TestQueuedCommandsRunBeforeMarkSeeded(c *C) {
+ s.testQueueCommandsOrdering(c, "mark-seeded")
+}
+
+func (s *servicectlSuite) TestQueuedCommandsRunBeforeSetModel(c *C) {
+ s.testQueueCommandsOrdering(c, "set-model")
}
func (s *servicectlSuite) TestQueuedCommandsUpdateMany(c *C) {
@@ -356,14 +456,14 @@
s.st.Lock()
chg := s.st.NewChange("update many change", "update change")
- installed, tts, err := snapstate.UpdateMany(context.TODO(), s.st, []string{"test-snap", "other-snap"}, 0, nil)
+ installed, tts, err := snapstate.UpdateMany(context.Background(), s.st, []string{"test-snap", "other-snap"}, 0, nil)
c.Assert(err, IsNil)
sort.Strings(installed)
c.Check(installed, DeepEquals, []string{"other-snap", "test-snap"})
c.Assert(tts, HasLen, 3)
- c.Assert(tts[0].Tasks(), HasLen, 18)
- c.Assert(tts[1].Tasks(), HasLen, 18)
- c.Assert(tts[2].Tasks(), HasLen, 1)
+ c.Assert(taskKinds(tts[0].Tasks()), DeepEquals, refreshTaskKinds)
+ c.Assert(taskKinds(tts[1].Tasks()), DeepEquals, refreshTaskKinds)
+ c.Assert(taskKinds(tts[2].Tasks()), DeepEquals, []string{"check-rerefresh"})
c.Assert(tts[2].Tasks()[0].Kind(), Equals, "check-rerefresh")
chg.AddAll(tts[0])
chg.AddAll(tts[1])
@@ -390,13 +490,14 @@
s.st.Lock()
defer s.st.Unlock()
+ expectedTaskKinds := append(refreshTaskKinds, "exec-command", "exec-command", "exec-command")
for i := 1; i <= 2; i++ {
laneTasks := chg.LaneTasks(i)
- c.Assert(laneTasks, HasLen, 21)
+ c.Assert(taskKinds(laneTasks), DeepEquals, expectedTaskKinds)
c.Check(laneTasks[17].Summary(), Matches, `Run configure hook of .* snap if present`)
- c.Check(laneTasks[18].Summary(), Equals, "stop of [test-snap.test-service]")
- c.Check(laneTasks[19].Summary(), Equals, "start of [test-snap.test-service]")
- c.Check(laneTasks[20].Summary(), Equals, "restart of [test-snap.test-service]")
+ c.Check(laneTasks[19].Summary(), Equals, "stop of [test-snap.test-service]")
+ c.Check(laneTasks[20].Summary(), Equals, "start of [test-snap.test-service]")
+ c.Check(laneTasks[21].Summary(), Equals, "restart of [test-snap.test-service]")
}
}
@@ -404,9 +505,9 @@
s.st.Lock()
chg := s.st.NewChange("install change", "install change")
- ts, err := snapstate.Install(s.st, "one", &snapstate.RevisionOptions{Revision: snap.R(1)}, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.st, "one", &snapstate.RevisionOptions{Revision: snap.R(1)}, 0, snapstate.Flags{})
c.Assert(err, IsNil)
- c.Assert(ts.Tasks(), HasLen, 13)
+ c.Assert(taskKinds(ts.Tasks()), DeepEquals, installTaskKinds)
chg.AddAll(ts)
s.st.Unlock()
@@ -430,11 +531,11 @@
defer s.st.Unlock()
laneTasks := chg.LaneTasks(0)
- c.Assert(laneTasks, HasLen, 16)
+ c.Assert(taskKinds(laneTasks), DeepEquals, append(installTaskKinds, "exec-command", "exec-command", "exec-command"))
c.Check(laneTasks[12].Summary(), Matches, `Run configure hook of .* snap if present`)
- c.Check(laneTasks[13].Summary(), Equals, "stop of [test-snap.test-service]")
- c.Check(laneTasks[14].Summary(), Equals, "start of [test-snap.test-service]")
- c.Check(laneTasks[15].Summary(), Equals, "restart of [test-snap.test-service]")
+ c.Check(laneTasks[14].Summary(), Equals, "stop of [test-snap.test-service]")
+ c.Check(laneTasks[15].Summary(), Equals, "start of [test-snap.test-service]")
+ c.Check(laneTasks[16].Summary(), Equals, "restart of [test-snap.test-service]")
}
func (s *servicectlSuite) TestTwoServices(c *C) {
diff -Nru snapd-2.40/overlord/hookstate/ctlcmd/set.go snapd-2.42.1/overlord/hookstate/ctlcmd/set.go
--- snapd-2.40/overlord/hookstate/ctlcmd/set.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/hookstate/ctlcmd/set.go 2019-10-30 12:17:43.000000000 +0000
@@ -53,6 +53,9 @@
$ snapctl set author.name=frank
+Configuration option may be unset with exclamation mark:
+ $ snapctl set author!
+
Plug and slot attributes may be set in the respective prepare and connect hooks by
naming the respective plug or slot:
@@ -99,6 +102,11 @@
for _, patchValue := range s.Positional.ConfValues {
parts := strings.SplitN(patchValue, "=", 2)
+ if len(parts) == 1 && strings.HasSuffix(patchValue, "!") {
+ key := strings.TrimSuffix(patchValue, "!")
+ tr.Set(s.context().InstanceName(), key, nil)
+ continue
+ }
if len(parts) != 2 {
return fmt.Errorf(i18n.G("invalid parameter: %q (want key=value)"), patchValue)
}
diff -Nru snapd-2.40/overlord/hookstate/ctlcmd/set_test.go snapd-2.42.1/overlord/hookstate/ctlcmd/set_test.go
--- snapd-2.40/overlord/hookstate/ctlcmd/set_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/hookstate/ctlcmd/set_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -99,38 +99,15 @@
c.Check(value, Equals, "qux")
}
-func (s *getSuite) TestSetRegularUserForbidden(c *C) {
- state := state.New(nil)
- state.Lock()
-
- task := state.NewTask("test-task", "my test task")
- setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "test-hook"}
-
- state.Unlock()
-
- mockHandler := hooktest.NewMockHandler()
- mockContext, err := hookstate.NewContext(task, task.State(), setup, mockHandler, "")
- c.Assert(err, IsNil)
- _, _, err = ctlcmd.Run(mockContext, []string{"set", "test-key1"}, 1000)
- c.Assert(err, NotNil)
+func (s *setSuite) TestSetRegularUserForbidden(c *C) {
+ _, _, err := ctlcmd.Run(s.mockContext, []string{"set", "test-key1"}, 1000)
c.Assert(err, ErrorMatches, `cannot use "set" with uid 1000, try with sudo`)
forbidden, _ := err.(*ctlcmd.ForbiddenCommandError)
c.Assert(forbidden, NotNil)
}
-func (s *getSuite) TestSetHelpRegularUserAllowed(c *C) {
- state := state.New(nil)
- state.Lock()
-
- task := state.NewTask("test-task", "my test task")
- setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "test-hook"}
-
- state.Unlock()
-
- mockHandler := hooktest.NewMockHandler()
- mockContext, err := hookstate.NewContext(task, task.State(), setup, mockHandler, "")
- c.Assert(err, IsNil)
- _, _, err = ctlcmd.Run(mockContext, []string{"set", "-h"}, 1000)
+func (s *setSuite) TestSetHelpRegularUserAllowed(c *C) {
+ _, _, err := ctlcmd.Run(s.mockContext, []string{"set", "-h"}, 1000)
c.Assert(err, NotNil)
c.Assert(strings.HasPrefix(err.Error(), "Usage:"), Equals, true)
}
@@ -153,6 +130,58 @@
c.Check(value, Equals, "192.168.0.1:5555")
}
+func (s *setSuite) TestUnsetConfigOptionWithInitialConfiguration(c *C) {
+ // Setup an initial configuration
+ s.mockContext.State().Lock()
+ tr := config.NewTransaction(s.mockContext.State())
+ tr.Set("test-snap", "test-key1", "test-value1")
+ tr.Set("test-snap", "test-key2", "test-value2")
+ tr.Set("test-snap", "test-key3.foo", "foo-value")
+ tr.Set("test-snap", "test-key3.bar", "bar-value")
+ tr.Commit()
+ s.mockContext.State().Unlock()
+
+ stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "test-key1!", "test-key3.foo!"}, 0)
+ c.Check(err, IsNil)
+ c.Check(string(stdout), Equals, "")
+ c.Check(string(stderr), Equals, "")
+
+ // Notify the context that we're done. This should save the config.
+ s.mockContext.Lock()
+ defer s.mockContext.Unlock()
+ c.Check(s.mockContext.Done(), IsNil)
+
+ // Verify that the global config has been updated.
+ var value string
+ tr = config.NewTransaction(s.mockContext.State())
+ c.Check(tr.Get("test-snap", "test-key2", &value), IsNil)
+ c.Check(value, Equals, "test-value2")
+ c.Check(tr.Get("test-snap", "test-key1", &value), ErrorMatches, `snap "test-snap" has no "test-key1" configuration option`)
+ var value2 interface{}
+ c.Check(tr.Get("test-snap", "test-key3", &value2), IsNil)
+ c.Check(value2, DeepEquals, map[string]interface{}{"bar": "bar-value"})
+}
+
+func (s *setSuite) TestUnsetConfigOptionWithNoInitialConfiguration(c *C) {
+ stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "test-key.key1=value1", "test-key.key2=value2", "test-key.key1!"}, 0)
+ c.Check(err, IsNil)
+ c.Check(string(stdout), Equals, "")
+ c.Check(string(stderr), Equals, "")
+
+ // Notify the context that we're done. This should save the config.
+ s.mockContext.Lock()
+ defer s.mockContext.Unlock()
+ c.Check(s.mockContext.Done(), IsNil)
+
+ // Verify that the global config has been updated.
+ var value interface{}
+ tr := config.NewTransaction(s.mockContext.State())
+ c.Check(tr.Get("test-snap", "test-key.key2", &value), IsNil)
+ c.Check(value, DeepEquals, "value2")
+ c.Check(tr.Get("test-snap", "test-key.key1", &value), ErrorMatches, `snap "test-snap" has no "test-key.key1" configuration option`)
+ c.Check(value, DeepEquals, "value2")
+}
+
func (s *setSuite) TestSetNumbers(c *C) {
stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"set", "foo=1234567890", "bar=123456.7890"}, 0)
c.Check(err, IsNil)
@@ -300,7 +329,7 @@
func (s *setAttrSuite) TestPlugOrSlotEmpty(c *C) {
stdout, stderr, err := ctlcmd.Run(s.mockPlugHookContext, []string{"set", ":", "foo=bar"}, 0)
- c.Check(err.Error(), Equals, "plug or slot name not provided")
+ c.Check(err, ErrorMatches, "plug or slot name not provided")
c.Check(string(stdout), Equals, "")
c.Check(string(stderr), Equals, "")
}
@@ -319,8 +348,7 @@
c.Assert(err, IsNil)
stdout, stderr, err := ctlcmd.Run(mockContext, []string{"set", ":aplug", "foo=bar"}, 0)
- c.Check(err, NotNil)
- c.Check(err.Error(), Equals, `interface attributes can only be set during the execution of prepare hooks`)
+ c.Check(err, ErrorMatches, `interface attributes can only be set during the execution of prepare hooks`)
c.Check(string(stdout), Equals, "")
c.Check(string(stderr), Equals, "")
}
diff -Nru snapd-2.40/overlord/hookstate/ctlcmd/unset.go snapd-2.42.1/overlord/hookstate/ctlcmd/unset.go
--- snapd-2.40/overlord/hookstate/ctlcmd/unset.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/overlord/hookstate/ctlcmd/unset.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,74 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package ctlcmd
+
+import (
+ "fmt"
+
+ "github.com/snapcore/snapd/i18n"
+ "github.com/snapcore/snapd/overlord/configstate"
+)
+
+type unsetCommand struct {
+ baseCommand
+
+ Positional struct {
+ ConfKeys []string
+ } `positional-args:"yes"`
+}
+
+var shortUnsetHelp = i18n.G("Remove configuration options")
+var longUnsetHelp = i18n.G(`
+The unset command removes the provided configuration options as requested.
+
+$ snapctl unset name address
+
+All configuration changes are persisted at once, and only after the
+snap's configuration hook returns successfully.
+
+Nested values may be removed via a dotted path:
+
+$ snapctl unset user.name
+`)
+
+func init() {
+ addCommand("unset", shortUnsetHelp, longUnsetHelp, func() command { return &unsetCommand{} })
+}
+
+func (s *unsetCommand) Execute(args []string) error {
+ if len(s.Positional.ConfKeys) == 0 {
+ return fmt.Errorf(i18n.G("unset which option?"))
+ }
+
+ context := s.context()
+ if context == nil {
+ return fmt.Errorf("cannot unset without a context")
+ }
+
+ context.Lock()
+ tr := configstate.ContextTransaction(context)
+ context.Unlock()
+
+ for _, confKey := range s.Positional.ConfKeys {
+ tr.Set(context.InstanceName(), confKey, nil)
+ }
+
+ return nil
+}
diff -Nru snapd-2.40/overlord/hookstate/ctlcmd/unset_test.go snapd-2.42.1/overlord/hookstate/ctlcmd/unset_test.go
--- snapd-2.40/overlord/hookstate/ctlcmd/unset_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/overlord/hookstate/ctlcmd/unset_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,137 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package ctlcmd_test
+
+import (
+ "strings"
+
+ "github.com/snapcore/snapd/overlord/configstate/config"
+ "github.com/snapcore/snapd/overlord/hookstate"
+ "github.com/snapcore/snapd/overlord/hookstate/ctlcmd"
+ "github.com/snapcore/snapd/overlord/hookstate/hooktest"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+
+ . "gopkg.in/check.v1"
+)
+
+type unsetSuite struct {
+ mockContext *hookstate.Context
+ mockHandler *hooktest.MockHandler
+}
+
+var _ = Suite(&unsetSuite{})
+
+func (s *unsetSuite) SetUpTest(c *C) {
+ s.mockHandler = hooktest.NewMockHandler()
+
+ state := state.New(nil)
+ state.Lock()
+ defer state.Unlock()
+
+ task := state.NewTask("test-task", "my test task")
+ setup := &hookstate.HookSetup{Snap: "test-snap", Revision: snap.R(1), Hook: "hook"}
+
+ var err error
+ s.mockContext, err = hookstate.NewContext(task, task.State(), setup, s.mockHandler, "")
+ c.Assert(err, IsNil)
+}
+
+func (s *unsetSuite) TestInvalidArguments(c *C) {
+ _, _, err := ctlcmd.Run(s.mockContext, []string{"unset"}, 0)
+ c.Check(err, ErrorMatches, "unset which option.*")
+}
+
+func (s *unsetSuite) TestUnsetOne(c *C) {
+ // Setup an initial configuration
+ s.mockContext.State().Lock()
+ tr := config.NewTransaction(s.mockContext.State())
+ tr.Set("test-snap", "foo", "a")
+ tr.Commit()
+ s.mockContext.State().Unlock()
+
+ // Sanity check
+ var value interface{}
+ s.mockContext.State().Lock()
+ tr = config.NewTransaction(s.mockContext.State())
+ c.Check(tr.Get("test-snap", "foo", &value), IsNil)
+ s.mockContext.State().Unlock()
+ c.Check(value, Equals, "a")
+
+ stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"unset", "foo"}, 0)
+ c.Check(err, IsNil)
+ c.Check(string(stdout), Equals, "")
+ c.Check(string(stderr), Equals, "")
+
+ // Notify the context that we're done. This should save the config.
+ s.mockContext.Lock()
+ defer s.mockContext.Unlock()
+ c.Check(s.mockContext.Done(), IsNil)
+
+ // Verify that the global config has been updated.
+ tr = config.NewTransaction(s.mockContext.State())
+ c.Check(tr.Get("test-snap", "foo", &value), ErrorMatches, `snap "test-snap" has no "foo" configuration option`)
+}
+
+func (s *unsetSuite) TestUnsetMany(c *C) {
+ // Setup an initial configuration
+ s.mockContext.State().Lock()
+ tr := config.NewTransaction(s.mockContext.State())
+ tr.Set("test-snap", "foo", "a")
+ tr.Set("test-snap", "bar", "b")
+ tr.Set("test-snap", "baz", "c")
+ tr.Commit()
+ s.mockContext.State().Unlock()
+
+ stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"unset", "foo", "bar"}, 0)
+ c.Check(err, IsNil)
+ c.Check(string(stdout), Equals, "")
+ c.Check(string(stderr), Equals, "")
+
+ // Notify the context that we're done. This should save the config.
+ s.mockContext.Lock()
+ defer s.mockContext.Unlock()
+ c.Check(s.mockContext.Done(), IsNil)
+
+ // Verify that the global config has been updated.
+ var value interface{}
+ tr = config.NewTransaction(s.mockContext.State())
+ c.Check(tr.Get("test-snap", "foo", &value), ErrorMatches, `snap "test-snap" has no "foo" configuration option`)
+ c.Check(tr.Get("test-snap", "bar", &value), ErrorMatches, `snap "test-snap" has no "bar" configuration option`)
+ c.Check(tr.Get("test-snap", "baz", &value), IsNil)
+ c.Check(value, Equals, "c")
+}
+
+func (s *unsetSuite) TestUnsetRegularUserForbidden(c *C) {
+ _, _, err := ctlcmd.Run(s.mockContext, []string{"unset", "key"}, 1000)
+ c.Assert(err, ErrorMatches, `cannot use "unset" with uid 1000, try with sudo`)
+ forbidden, _ := err.(*ctlcmd.ForbiddenCommandError)
+ c.Assert(forbidden, NotNil)
+}
+
+func (s *unsetSuite) TestUnsetHelpRegularUserAllowed(c *C) {
+ _, _, err := ctlcmd.Run(s.mockContext, []string{"unset", "-h"}, 1000)
+ c.Assert(strings.HasPrefix(err.Error(), "Usage:"), Equals, true)
+}
+
+func (s *unsetSuite) TestCommandWithoutContext(c *C) {
+ _, _, err := ctlcmd.Run(nil, []string{"unset", "foo"}, 0)
+ c.Check(err, ErrorMatches, ".*cannot unset without a context.*")
+}
diff -Nru snapd-2.40/overlord/hookstate/hookstate_test.go snapd-2.42.1/overlord/hookstate/hookstate_test.go
--- snapd-2.40/overlord/hookstate/hookstate_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/hookstate/hookstate_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -75,6 +75,7 @@
s.se = s.o.StateEngine()
s.o.AddManager(s.manager)
s.o.AddManager(s.o.TaskRunner())
+ c.Assert(s.o.StartUp(), IsNil)
s.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
diff -Nru snapd-2.40/overlord/ifacestate/export_test.go snapd-2.42.1/overlord/ifacestate/export_test.go
--- snapd-2.40/overlord/ifacestate/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/ifacestate/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -55,6 +55,8 @@
AllocHotplugSeq = allocHotplugSeq
AddHotplugSeqWaitTask = addHotplugSeqWaitTask
AddHotplugSlot = addHotplugSlot
+
+ BatchConnectTasks = batchConnectTasks
)
func NewConnectOptsWithAutoSet() connectOpts {
@@ -65,6 +67,10 @@
return disconnectOpts{ByHotplug: true}
}
+func NewConnectOptsWithDelayProfilesSet() connectOpts {
+ return connectOpts{AutoConnect: true, ByGadget: false, DelayedSetupProfiles: true}
+}
+
func MockRemoveStaleConnections(f func(st *state.State) error) (restore func()) {
old := removeStaleConnections
removeStaleConnections = f
diff -Nru snapd-2.40/overlord/ifacestate/handlers.go snapd-2.42.1/overlord/ifacestate/handlers.go
--- snapd-2.40/overlord/ifacestate/handlers.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/ifacestate/handlers.go 2019-10-30 12:17:43.000000000 +0000
@@ -28,6 +28,7 @@
"gopkg.in/tomb.v2"
+ "github.com/snapcore/snapd/i18n"
"github.com/snapcore/snapd/interfaces"
"github.com/snapcore/snapd/interfaces/hotplug"
"github.com/snapcore/snapd/logger"
@@ -401,6 +402,10 @@
if err := task.Get("by-gadget", &byGadget); err != nil && err != state.ErrNoState {
return err
}
+ var delayedSetupProfiles bool
+ if err := task.Get("delayed-setup-profiles", &delayedSetupProfiles); err != nil && err != state.ErrNoState {
+ return err
+ }
deviceCtx, err := snapstate.DeviceCtx(st, task, nil)
if err != nil {
@@ -475,14 +480,18 @@
return err
}
- slotOpts := confinementOptions(slotSnapst.Flags)
- if err := m.setupSnapSecurity(task, slot.Snap, slotOpts, perfTimings); err != nil {
- return err
- }
+ if !delayedSetupProfiles {
+ slotOpts := confinementOptions(slotSnapst.Flags)
+ if err := m.setupSnapSecurity(task, slot.Snap, slotOpts, perfTimings); err != nil {
+ return err
+ }
- plugOpts := confinementOptions(plugSnapst.Flags)
- if err := m.setupSnapSecurity(task, plug.Snap, plugOpts, perfTimings); err != nil {
- return err
+ plugOpts := confinementOptions(plugSnapst.Flags)
+ if err := m.setupSnapSecurity(task, plug.Snap, plugOpts, perfTimings); err != nil {
+ return err
+ }
+ } else {
+ logger.Debugf("Connect handler: skipping setupSnapSecurity for snaps %q and %q", plug.Snap.InstanceName(), slot.Snap.InstanceName())
}
conns[connRef.ID()] = &connState{
@@ -895,6 +904,47 @@
return false
}
+// batchConnectTasks creates connect tasks and interface hooks for
+// conns and sets their wait chain with regard to the setupProfiles
+// task.
+//
+// The tasks are chained so that: - prepare-plug-, prepare-slot- and
+// connect tasks are all executed before setup-profiles -
+// connect-plug-, connect-slot- are all executed after setup-profiles.
+// The "delayed-setup-profiles" flag is set on the connect tasks to
+// indicate that doConnect handler should not set security backends up
+// because this will be done later by the setup-profiles task.
+func batchConnectTasks(st *state.State, snapsup *snapstate.SnapSetup, conns map[string]*interfaces.ConnRef, autoconnect bool) (*state.TaskSet, error) {
+ setupProfiles := st.NewTask("setup-profiles", fmt.Sprintf(i18n.G("Setup snap %q (%s) security profiles for auto-connections"), snapsup.InstanceName(), snapsup.Revision()))
+ setupProfiles.Set("snap-setup", snapsup)
+
+ ts := state.NewTaskSet()
+ for _, conn := range conns {
+ connectTs, err := connect(st, conn.PlugRef.Snap, conn.PlugRef.Name, conn.SlotRef.Snap, conn.SlotRef.Name, connectOpts{AutoConnect: autoconnect, DelayedSetupProfiles: true})
+ if err != nil {
+ return nil, fmt.Errorf("internal error: auto-connect of %q failed: %s", conn, err)
+ }
+
+ // setup-profiles needs to wait for the main "connect" task
+ connectTask, _ := connectTs.Edge(ConnectTaskEdge)
+ if connectTask == nil {
+ return nil, fmt.Errorf("internal error: no 'connect' task found for %q", conn)
+ }
+ setupProfiles.WaitFor(connectTask)
+
+ // setup-profiles must be run before the task that marks the end of connect-plug- and connect-slot- hooks
+ afterConnectTask, _ := connectTs.Edge(AfterConnectHooksEdge)
+ if afterConnectTask != nil {
+ afterConnectTask.WaitFor(setupProfiles)
+ }
+ ts.AddAll(connectTs)
+ }
+ if len(ts.Tasks()) > 0 {
+ ts.AddTask(setupProfiles)
+ }
+ return ts, nil
+}
+
// doAutoConnect creates task(s) to connect the given snap to viable candidates.
func (m *InterfaceManager) doAutoConnect(task *state.Task, _ *tomb.Tomb) error {
st := task.State()
@@ -926,7 +976,6 @@
snapName := snapsup.InstanceName()
- autots := state.NewTaskSet()
autochecker, err := newAutoConnectChecker(st, deviceCtx)
if err != nil {
return err
@@ -1068,13 +1117,10 @@
}
}
- // Create connect tasks and interface hooks
- for _, conn := range newconns {
- ts, err := connect(st, conn.PlugRef.Snap, conn.PlugRef.Name, conn.SlotRef.Snap, conn.SlotRef.Name, connectOpts{AutoConnect: true})
- if err != nil {
- return fmt.Errorf("internal error: auto-connect of %q failed: %s", conn, err)
- }
- autots.AddAll(ts)
+ autoconnect := true
+ autots, err := batchConnectTasks(st, snapsup, newconns, autoconnect)
+ if err != nil {
+ return err
}
if len(autots.Tasks()) > 0 {
@@ -1226,12 +1272,17 @@
st.Lock()
defer st.Unlock()
+ deviceCtx, err := snapstate.DeviceCtx(st, task, nil)
+ if err != nil {
+ return err
+ }
+
conns, err := getConns(st)
if err != nil {
return err
}
- gconns, err := snapstate.GadgetConnections(st)
+ gconns, err := snapstate.GadgetConnections(st, deviceCtx)
if err != nil {
return err
}
diff -Nru snapd-2.40/overlord/ifacestate/helpers.go snapd-2.42.1/overlord/ifacestate/helpers.go
--- snapd-2.40/overlord/ifacestate/helpers.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/ifacestate/helpers.go 2019-10-30 12:17:43.000000000 +0000
@@ -43,50 +43,9 @@
"github.com/snapcore/snapd/timings"
)
-func (m *InterfaceManager) initialize(extraInterfaces []interfaces.Interface, extraBackends []interfaces.SecurityBackend, tm timings.Measurer) error {
- m.state.Lock()
- defer m.state.Unlock()
-
- snaps, err := snapsWithSecurityProfiles(m.state)
- if err != nil {
- return err
- }
- // Before deciding about adding implicit slots to any snap we need to scan
- // the set of snaps we know about. If any of those is "snapd" then for the
- // duration of this process always add implicit slots to snapd and not to
- // any other type: os snap and use a mapper to use names core-snapd-system
- // on state, in memory and in API responses, respectively.
- m.selectInterfaceMapper(snaps)
-
- if err := m.addInterfaces(extraInterfaces); err != nil {
- return err
- }
- if err := m.addBackends(extraBackends); err != nil {
- return err
- }
- if err := m.addSnaps(snaps); err != nil {
- return err
- }
- if err := m.renameCorePlugConnection(); err != nil {
- return err
- }
- if err := removeStaleConnections(m.state); err != nil {
- return err
- }
- if _, err := m.reloadConnections(""); err != nil {
- return err
- }
- if profilesNeedRegeneration() {
- if err := m.regenerateAllSecurityProfiles(tm); err != nil {
- return err
- }
- }
- return nil
-}
-
func (m *InterfaceManager) selectInterfaceMapper(snaps []*snap.Info) {
for _, snapInfo := range snaps {
- if snapInfo.SnapName() == "snapd" {
+ if snapInfo.GetType() == snap.TypeSnapd {
mapper = &CoreSnapdSystemMapper{}
break
}
@@ -428,22 +387,7 @@
}
func (m *InterfaceManager) setupSnapSecurity(task *state.Task, snapInfo *snap.Info, opts interfaces.ConfinementOptions, tm timings.Measurer) error {
- st := task.State()
- instanceName := snapInfo.InstanceName()
-
- for _, backend := range m.repo.Backends() {
- st.Unlock()
- var err error
- timings.Run(tm, "setup-security-backend", fmt.Sprintf("setup security backend %q for snap %q", backend.Name(), snapInfo.InstanceName()), func(nesttm timings.Measurer) {
- err = backend.Setup(snapInfo, opts, m.repo, nesttm)
- })
- st.Lock()
- if err != nil {
- task.Errorf("cannot setup %s for snap %q: %s", backend.Name(), instanceName, err)
- return err
- }
- }
- return nil
+ return m.setupSecurityByBackend(task, []*snap.Info{snapInfo}, []interfaces.ConfinementOptions{opts}, tm)
}
func (m *InterfaceManager) removeSnapSecurity(task *state.Task, instanceName string) error {
diff -Nru snapd-2.40/overlord/ifacestate/helpers_test.go snapd-2.42.1/overlord/ifacestate/helpers_test.go
--- snapd-2.40/overlord/ifacestate/helpers_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/ifacestate/helpers_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -406,8 +406,10 @@
log, restore := logger.MockLogger()
defer restore()
- // Construct the interface manager.
- _, err = ifacestate.Manager(st, nil, ovld.TaskRunner(), nil, nil)
+ // Construct and start up the interface manager.
+ mgr, err := ifacestate.Manager(st, nil, ovld.TaskRunner(), nil, nil)
+ c.Assert(err, IsNil)
+ err = mgr.StartUp()
c.Assert(err, IsNil)
// Check that system key is not on disk.
diff -Nru snapd-2.40/overlord/ifacestate/hotplug.go snapd-2.42.1/overlord/ifacestate/hotplug.go
--- snapd-2.40/overlord/ifacestate/hotplug.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/ifacestate/hotplug.go 2019-10-30 12:17:43.000000000 +0000
@@ -126,7 +126,13 @@
return
}
- gadget, err := snapstate.GadgetInfo(st)
+ deviceCtx, err := snapstate.DeviceCtxFromState(st, nil)
+ if err != nil {
+ logger.Noticef("internal error: cannot get global device context: %v", err)
+ return
+ }
+
+ gadget, err := snapstate.GadgetInfo(st, deviceCtx)
if err != nil && err != state.ErrNoState {
logger.Noticef("internal error: cannot get gadget information: %v", err)
}
diff -Nru snapd-2.40/overlord/ifacestate/hotplug_test.go snapd-2.42.1/overlord/ifacestate/hotplug_test.go
--- snapd-2.40/overlord/ifacestate/hotplug_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/ifacestate/hotplug_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -181,6 +181,13 @@
s.mgr, err = ifacestate.Manager(s.state, hookMgr, s.o.TaskRunner(), nil, nil)
c.Assert(err, IsNil)
+ s.o.AddManager(s.mgr)
+ s.o.AddManager(s.o.TaskRunner())
+
+ // startup
+ err = s.o.StartUp()
+ c.Assert(err, IsNil)
+
autoConnectNo := func(*snap.PlugInfo, *snap.SlotInfo) bool {
return false
}
@@ -253,9 +260,6 @@
s.AddCleanup(builtin.MockInterface(iface))
}
- s.o.AddManager(s.mgr)
- s.o.AddManager(s.o.TaskRunner())
-
// single Ensure to have udev monitor created and wired up by interface manager
c.Assert(s.mgr.Ensure(), IsNil)
}
@@ -334,7 +338,9 @@
}
func (s *hotplugSuite) TestHotplugConnectWithGadgetSlot(c *C) {
- s.MockModel(c, nil)
+ s.MockModel(c, map[string]interface{}{
+ "gadget": "the-gadget",
+ })
st := s.state
st.Lock()
@@ -522,7 +528,7 @@
SnapType: "app",
})
- core, err := snapstate.CoreInfo(s.state)
+ core, err := snapstate.CurrentInfo(s.state, "core")
c.Assert(err, IsNil)
c.Assert(repo.AddSlot(&snap.SlotInfo{
Interface: "test-a",
@@ -606,7 +612,7 @@
Current: snap.R(1),
SnapType: "app"})
- core, err := snapstate.CoreInfo(s.state)
+ core, err := snapstate.CurrentInfo(s.state, "core")
c.Assert(err, IsNil)
c.Assert(repo.AddSlot(&snap.SlotInfo{
Interface: "test-a",
@@ -708,7 +714,7 @@
Current: snap.R(1),
SnapType: "app"})
- core, err := snapstate.CoreInfo(s.state)
+ core, err := snapstate.CurrentInfo(s.state, "core")
c.Assert(err, IsNil)
c.Assert(repo.AddSlot(&snap.SlotInfo{
Interface: "test-a",
diff -Nru snapd-2.40/overlord/ifacestate/ifacemgr.go snapd-2.42.1/overlord/ifacestate/ifacemgr.go
--- snapd-2.40/overlord/ifacestate/ifacemgr.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/ifacestate/ifacemgr.go 2019-10-30 12:17:43.000000000 +0000
@@ -55,6 +55,10 @@
enumerationDone bool
// maps sysfs path -> [(interface name, device key)...]
hotplugDevicePaths map[string][]deviceData
+
+ // extras
+ extraInterfaces []interfaces.Interface
+ extraBackends []interfaces.SecurityBackend
}
// Manager returns a new InterfaceManager.
@@ -62,8 +66,6 @@
func Manager(s *state.State, hookManager *hookstate.HookManager, runner *state.TaskRunner, extraInterfaces []interfaces.Interface, extraBackends []interfaces.SecurityBackend) (*InterfaceManager, error) {
delayedCrossMgrInit()
- perfTimings := timings.New(map[string]string{"startup": "ifacemgr"})
-
// NOTE: hookManager is nil only when testing.
if hookManager != nil {
setupHooks(hookManager)
@@ -76,16 +78,11 @@
// note: enumeratedDeviceKeys is reset to nil when enumeration is done
enumeratedDeviceKeys: make(map[string]map[snap.HotplugKey]bool),
hotplugDevicePaths: make(map[string][]deviceData),
+ // extras
+ extraInterfaces: extraInterfaces,
+ extraBackends: extraBackends,
}
- if err := m.initialize(extraInterfaces, extraBackends, perfTimings); err != nil {
- return nil, err
- }
-
- s.Lock()
- ifacerepo.Replace(s, m.repo)
- s.Unlock()
-
taskKinds := map[string]bool{}
addHandler := func(kind string, do, undo state.HandlerFunc) {
taskKinds[kind] = true
@@ -127,11 +124,57 @@
return false
})
+ return m, nil
+}
+
+// StartUp implements StateStarterUp.Startup.
+func (m *InterfaceManager) StartUp() error {
+ s := m.state
+ perfTimings := timings.New(map[string]string{"startup": "ifacemgr"})
+
s.Lock()
+ defer s.Unlock()
+
+ snaps, err := snapsWithSecurityProfiles(m.state)
+ if err != nil {
+ return err
+ }
+ // Before deciding about adding implicit slots to any snap we need to scan
+ // the set of snaps we know about. If any of those is "snapd" then for the
+ // duration of this process always add implicit slots to snapd and not to
+ // any other type: os snap and use a mapper to use names core-snapd-system
+ // on state, in memory and in API responses, respectively.
+ m.selectInterfaceMapper(snaps)
+
+ if err := m.addInterfaces(m.extraInterfaces); err != nil {
+ return err
+ }
+ if err := m.addBackends(m.extraBackends); err != nil {
+ return err
+ }
+ if err := m.addSnaps(snaps); err != nil {
+ return err
+ }
+ if err := m.renameCorePlugConnection(); err != nil {
+ return err
+ }
+ if err := removeStaleConnections(m.state); err != nil {
+ return err
+ }
+ if _, err := m.reloadConnections(""); err != nil {
+ return err
+ }
+ if profilesNeedRegeneration() {
+ if err := m.regenerateAllSecurityProfiles(perfTimings); err != nil {
+ return err
+ }
+ }
+
+ ifacerepo.Replace(s, m.repo)
+
perfTimings.Save(s)
- s.Unlock()
- return m, nil
+ return nil
}
// Ensure implements StateManager.Ensure.
diff -Nru snapd-2.40/overlord/ifacestate/ifacestate.go snapd-2.42.1/overlord/ifacestate/ifacestate.go
--- snapd-2.40/overlord/ifacestate/ifacestate.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/ifacestate/ifacestate.go 2019-10-30 12:17:43.000000000 +0000
@@ -44,6 +44,11 @@
Connection interfaces.ConnRef
}
+const (
+ ConnectTaskEdge = state.TaskSetEdge("connect-task")
+ AfterConnectHooksEdge = state.TaskSetEdge("after-connect-hooks")
+)
+
func (e ErrAlreadyConnected) Error() string {
return fmt.Sprintf("already connected: %q", e.Connection.ID())
}
@@ -77,6 +82,8 @@
type connectOpts struct {
ByGadget bool
AutoConnect bool
+
+ DelayedSetupProfiles bool
}
// Connect returns a set of tasks for connecting an interface.
@@ -198,6 +205,9 @@
if flags.ByGadget {
connectInterface.Set("by-gadget", true)
}
+ if flags.DelayedSetupProfiles {
+ connectInterface.Set("delayed-setup-profiles", true)
+ }
// Expose a copy of all plug and slot attributes coming from yaml to interface hooks. The hooks will be able
// to modify them but all attributes will be checked against assertions after the hooks are run.
@@ -213,6 +223,11 @@
addTask(connectInterface)
prev = connectInterface
+ if flags.DelayedSetupProfiles {
+ // mark as the last task in connect prepare
+ tasks.MarkEdge(connectInterface, ConnectTaskEdge)
+ }
+
connectSlotHookName := fmt.Sprintf("connect-slot-%s", slotName)
if slotSnapInfo.Hooks[connectSlotHookName] != nil {
connectSlotHookSetup := &hookstate.HookSetup{
@@ -231,6 +246,9 @@
connectSlotConnection := hookstate.HookTaskWithUndo(st, summary, connectSlotHookSetup, undoConnectSlotHookSetup, initialContext)
addTask(connectSlotConnection)
prev = connectSlotConnection
+ if flags.DelayedSetupProfiles {
+ tasks.MarkEdge(connectSlotConnection, AfterConnectHooksEdge)
+ }
}
connectPlugHookName := fmt.Sprintf("connect-plug-%s", plugName)
@@ -250,6 +268,13 @@
summary := fmt.Sprintf(i18n.G("Run hook %s of snap %q"), connectPlugHookSetup.Hook, connectPlugHookSetup.Snap)
connectPlugConnection := hookstate.HookTaskWithUndo(st, summary, connectPlugHookSetup, undoConnectPlugHookSetup, initialContext)
addTask(connectPlugConnection)
+
+ if flags.DelayedSetupProfiles {
+ // only mark AfterConnectHooksEdge if not already set on connect-slot- hook task
+ if edge, _ := tasks.Edge(AfterConnectHooksEdge); edge == nil {
+ tasks.MarkEdge(connectPlugConnection, AfterConnectHooksEdge)
+ }
+ }
prev = connectPlugConnection
}
return tasks, nil
@@ -411,11 +436,6 @@
return err
}
- if snapInfo.SnapID == "" {
- // no SnapID means --dangerous was given, so skip interface checks
- return nil
- }
-
modelAs := deviceCtx.Model()
var storeAs *asserts.Store
@@ -432,6 +452,17 @@
return fmt.Errorf("internal error: cannot find base declaration: %v", err)
}
+ if snapInfo.SnapID == "" {
+ // no SnapID means --dangerous was given, perform a minimal check about the compatibility of the snap type and the interface
+ ic := policy.InstallCandidateMinimalCheck{
+ Snap: snapInfo,
+ BaseDeclaration: baseDecl,
+ Model: modelAs,
+ Store: storeAs,
+ }
+ return ic.Check()
+ }
+
snapDecl, err := assertstate.SnapDeclaration(st, snapInfo.SnapID)
if err != nil {
return fmt.Errorf("cannot find snap declaration for %q: %v", snapInfo.InstanceName(), err)
diff -Nru snapd-2.40/overlord/ifacestate/ifacestate_test.go snapd-2.42.1/overlord/ifacestate/ifacestate_test.go
--- snapd-2.40/overlord/ifacestate/ifacestate_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/ifacestate/ifacestate_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -51,6 +51,7 @@
"github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
+ seccomp_compiler "github.com/snapcore/snapd/sandbox/seccomp"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/testutil"
@@ -212,9 +213,7 @@
s.log = buf
s.BaseTest.AddCleanup(ifacestate.MockConnectRetryTimeout(0))
- restore = interfaces.MockSeccompCompilerVersionInfo(func(_ string) (string, error) {
- return "abcdef 1.2.3 1234abcd -", nil
- })
+ restore = seccomp_compiler.MockCompilerVersionInfo("abcdef 1.2.3 1234abcd -")
s.BaseTest.AddCleanup(restore)
}
@@ -248,6 +247,8 @@
s.o.AddManager(s.o.TaskRunner())
+ c.Assert(s.o.StartUp(), IsNil)
+
// ensure the re-generation of security profiles did not
// confuse the tests
s.secBackend.SetupCalls = nil
@@ -315,6 +316,10 @@
i++
task = ts.Tasks()[i]
c.Assert(task.Kind(), Equals, "connect")
+ var flag bool
+ c.Assert(task.Get("auto", &flag), Equals, state.ErrNoState)
+ c.Assert(task.Get("delayed-setup-profiles", &flag), Equals, state.ErrNoState)
+ c.Assert(task.Get("by-gadget", &flag), Equals, state.ErrNoState)
var plug interfaces.PlugRef
c.Assert(task.Get("plug", &plug), IsNil)
c.Assert(plug.Snap, Equals, "consumer")
@@ -324,6 +329,10 @@
c.Assert(slot.Snap, Equals, "producer")
c.Assert(slot.Name, Equals, "slot")
+ // "connect" task edge is not present
+ _, err = ts.Edge(ifacestate.ConnectTaskEdge)
+ c.Assert(err, ErrorMatches, `internal error: missing .* edge in task set`)
+
var autoconnect bool
err = task.Get("auto", &autoconnect)
c.Assert(err, Equals, state.ErrNoState)
@@ -358,6 +367,82 @@
c.Assert(hs, Equals, hookstate.HookSetup{Snap: "consumer", Hook: "connect-plug-plug", Optional: true})
c.Assert(task.Get("undo-hook-setup", &undoHookSetup), IsNil)
c.Assert(undoHookSetup, Equals, hookstate.HookSetup{Snap: "consumer", Hook: "disconnect-plug-plug", Optional: true, IgnoreError: true})
+
+ // after-connect-hooks task edge is not present
+ _, err = ts.Edge(ifacestate.AfterConnectHooksEdge)
+ c.Assert(err, ErrorMatches, `internal error: missing .* edge in task set`)
+}
+
+func (s *interfaceManagerSuite) TestConnectTasksDelayProfilesFlag(c *C) {
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, producerYaml)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ ts, err := ifacestate.ConnectPriv(s.state, "consumer", "plug", "producer", "slot", ifacestate.NewConnectOptsWithDelayProfilesSet())
+ c.Assert(err, IsNil)
+ c.Assert(ts.Tasks(), HasLen, 5)
+ connectTask := ts.Tasks()[2]
+ c.Assert(connectTask.Kind(), Equals, "connect")
+ var delayedSetupProfiles bool
+ connectTask.Get("delayed-setup-profiles", &delayedSetupProfiles)
+ c.Assert(delayedSetupProfiles, Equals, true)
+}
+
+func (s *interfaceManagerSuite) TestBatchConnectTasks(c *C) {
+ s.mockIfaces(c, &ifacetest.TestInterface{InterfaceName: "test"}, &ifacetest.TestInterface{InterfaceName: "test2"})
+ s.mockSnap(c, consumerYaml)
+ s.mockSnap(c, consumer2Yaml)
+ s.mockSnap(c, producerYaml)
+ _ = s.manager(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapsup := &snapstate.SnapSetup{SideInfo: &snap.SideInfo{RealName: "snap"}}
+ conns := make(map[string]*interfaces.ConnRef)
+
+ // no connections
+ autoconnect := true
+ ts, err := ifacestate.BatchConnectTasks(s.state, snapsup, conns, autoconnect)
+ c.Assert(err, IsNil)
+ c.Check(ts.Tasks(), HasLen, 0)
+
+ // two connections
+ cref1 := interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}}
+ cref2 := interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "consumer2", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}}
+ conns[cref1.ID()] = &cref1
+ conns[cref2.ID()] = &cref2
+
+ ts, err = ifacestate.BatchConnectTasks(s.state, snapsup, conns, autoconnect)
+ c.Assert(err, IsNil)
+ c.Check(ts.Tasks(), HasLen, 9)
+
+ // "setup-profiles" task waits for "connect" tasks of both connections
+ setupProfiles := ts.Tasks()[len(ts.Tasks())-1]
+ c.Assert(setupProfiles.Kind(), Equals, "setup-profiles")
+
+ wt := setupProfiles.WaitTasks()
+ c.Assert(wt, HasLen, 2)
+ for i := 0; i < 2; i++ {
+ c.Check(wt[i].Kind(), Equals, "connect")
+
+ // sanity, check flags on "connect" tasks
+ var flag bool
+ c.Assert(wt[i].Get("delayed-setup-profiles", &flag), IsNil)
+ c.Check(flag, Equals, true)
+ c.Assert(wt[i].Get("auto", &flag), IsNil)
+ c.Check(flag, Equals, true)
+ }
+
+ // connect-slot-slot hooks wait for "setup-profiles"
+ ht := setupProfiles.HaltTasks()
+ c.Assert(ht, HasLen, 2)
+ for i := 0; i < 2; i++ {
+ c.Check(ht[i].Kind(), Equals, "run-hook")
+ c.Check(ht[i].Summary(), Matches, "Run hook connect-slot-slot .*")
+ }
}
type interfaceHooksTestData struct {
@@ -400,41 +485,98 @@
}
-func (s *interfaceManagerSuite) TestConnectTaskHooksConditionals(c *C) {
+var connectHooksTests = []interfaceHooksTestData{{
+ consumer: []string{"prepare-plug-plug"},
+ producer: []string{"prepare-slot-slot"},
+ waitChain: []string{"hook:prepare-plug-plug", "hook:prepare-slot-slot", "task:connect"},
+}, {
+ consumer: []string{"prepare-plug-plug"},
+ producer: []string{"prepare-slot-slot", "connect-slot-slot"},
+ waitChain: []string{"hook:prepare-plug-plug", "hook:prepare-slot-slot", "task:connect", "hook:connect-slot-slot"},
+}, {
+ consumer: []string{"prepare-plug-plug"},
+ producer: []string{"connect-slot-slot"},
+ waitChain: []string{"hook:prepare-plug-plug", "task:connect", "hook:connect-slot-slot"},
+}, {
+ consumer: []string{"connect-plug-plug"},
+ producer: []string{"prepare-slot-slot", "connect-slot-slot"},
+ waitChain: []string{"hook:prepare-slot-slot", "task:connect", "hook:connect-slot-slot", "hook:connect-plug-plug"},
+}, {
+ consumer: []string{"connect-plug-plug"},
+ producer: []string{"connect-slot-slot"},
+ waitChain: []string{"task:connect", "hook:connect-slot-slot", "hook:connect-plug-plug"},
+}, {
+ consumer: []string{"prepare-plug-plug", "connect-plug-plug"},
+ producer: []string{"prepare-slot-slot"},
+ waitChain: []string{"hook:prepare-plug-plug", "hook:prepare-slot-slot", "task:connect", "hook:connect-plug-plug"},
+}, {
+ consumer: []string{"prepare-plug-plug", "connect-plug-plug"},
+ producer: []string{"prepare-slot-slot", "connect-slot-slot"},
+ waitChain: []string{"hook:prepare-plug-plug", "hook:prepare-slot-slot", "task:connect", "hook:connect-slot-slot", "hook:connect-plug-plug"},
+}}
+
+func (s *interfaceManagerSuite) TestConnectTaskHookdEdges(c *C) {
s.mockIfaces(c, &ifacetest.TestInterface{InterfaceName: "test"})
- hooksTests := []interfaceHooksTestData{{
- consumer: []string{"prepare-plug-plug"},
- producer: []string{"prepare-slot-slot"},
- waitChain: []string{"hook:prepare-plug-plug", "hook:prepare-slot-slot", "task:connect"},
- }, {
- consumer: []string{"prepare-plug-plug"},
- producer: []string{"prepare-slot-slot", "connect-slot-slot"},
- waitChain: []string{"hook:prepare-plug-plug", "hook:prepare-slot-slot", "task:connect", "hook:connect-slot-slot"},
- }, {
- consumer: []string{"prepare-plug-plug"},
- producer: []string{"connect-slot-slot"},
- waitChain: []string{"hook:prepare-plug-plug", "task:connect", "hook:connect-slot-slot"},
- }, {
- consumer: []string{"connect-plug-plug"},
- producer: []string{"prepare-slot-slot", "connect-slot-slot"},
- waitChain: []string{"hook:prepare-slot-slot", "task:connect", "hook:connect-slot-slot", "hook:connect-plug-plug"},
- }, {
- consumer: []string{"connect-plug-plug"},
- producer: []string{"connect-slot-slot"},
- waitChain: []string{"task:connect", "hook:connect-slot-slot", "hook:connect-plug-plug"},
- }, {
- consumer: []string{"prepare-plug-plug", "connect-plug-plug"},
- producer: []string{"prepare-slot-slot"},
- waitChain: []string{"hook:prepare-plug-plug", "hook:prepare-slot-slot", "task:connect", "hook:connect-plug-plug"},
- }, {
- consumer: []string{"prepare-plug-plug", "connect-plug-plug"},
- producer: []string{"prepare-slot-slot", "connect-slot-slot"},
- waitChain: []string{"hook:prepare-plug-plug", "hook:prepare-slot-slot", "task:connect", "hook:connect-slot-slot", "hook:connect-plug-plug"},
- }}
+ _ = s.manager(c)
+ for _, hooks := range connectHooksTests {
+ var hooksYaml string
+ for _, name := range hooks.consumer {
+ hooksYaml = fmt.Sprintf("%s %s:\n", hooksYaml, name)
+ }
+ consumer := fmt.Sprintf(consumerYaml3, hooksYaml)
+
+ hooksYaml = ""
+ for _, name := range hooks.producer {
+ hooksYaml = fmt.Sprintf("%s %s:\n", hooksYaml, name)
+ }
+ producer := fmt.Sprintf(producerYaml3, hooksYaml)
+
+ s.mockSnap(c, consumer)
+ s.mockSnap(c, producer)
+
+ s.state.Lock()
+
+ ts, err := ifacestate.ConnectPriv(s.state, "consumer", "plug", "producer", "slot", ifacestate.NewConnectOptsWithDelayProfilesSet())
+ c.Assert(err, IsNil)
+
+ // check task edges
+ edge, err := ts.Edge(ifacestate.ConnectTaskEdge)
+ c.Assert(err, IsNil)
+ c.Check(edge.Kind(), Equals, "connect")
+
+ // AfterConnectHooks task edge is set on "connect-slot-" or "connect-plug-" hook task (whichever comes first after "connect")
+ // and is not present if neither of them exists.
+ var expectedAfterConnectEdge string
+ for _, hookName := range hooks.producer {
+ if strings.HasPrefix(hookName, "connect-") {
+ expectedAfterConnectEdge = "hook:" + hookName
+ }
+ }
+ if expectedAfterConnectEdge == "" {
+ for _, hookName := range hooks.consumer {
+ if strings.HasPrefix(hookName, "connect-") {
+ expectedAfterConnectEdge = "hook:" + hookName
+ }
+ }
+ }
+ edge, err = ts.Edge(ifacestate.AfterConnectHooksEdge)
+ if expectedAfterConnectEdge != "" {
+ c.Assert(err, IsNil)
+ c.Check(hookNameOrTaskKind(c, edge), Equals, expectedAfterConnectEdge)
+ } else {
+ c.Assert(err, ErrorMatches, `internal error: missing .* edge in task set`)
+ }
+
+ s.state.Unlock()
+ }
+}
+
+func (s *interfaceManagerSuite) TestConnectTaskHooksConditionals(c *C) {
+ s.mockIfaces(c, &ifacetest.TestInterface{InterfaceName: "test"})
_ = s.manager(c)
- for _, hooks := range hooksTests {
+ for _, hooks := range connectHooksTests {
var hooksYaml string
for _, name := range hooks.consumer {
hooksYaml = fmt.Sprintf("%s %s:\n", hooksYaml, name)
@@ -1730,6 +1872,23 @@
interface: network
`
+var sampleSnapYamlManyPlugs = `
+name: snap
+version: 1
+apps:
+ app:
+ command: foo
+plugs:
+ network:
+ interface: network
+ home:
+ interface: home
+ x11:
+ interface: x11
+ wayland:
+ interface: wayland
+`
+
var consumerYaml = `
name: consumer
version: 1
@@ -2011,6 +2170,7 @@
Revision: snapInfo.Revision,
},
})
+
s.settle(c)
s.state.Lock()
@@ -3007,12 +3167,65 @@
// Ensure that both snaps were setup correctly.
c.Assert(s.secBackend.SetupCalls, HasLen, 3)
c.Assert(s.secBackend.RemoveCalls, HasLen, 0)
- // The sample snap was setup, with the correct new revision.
+
+ // The sample snap was setup, with the correct new revision:
+ // 1st call is for initial setup-profiles, 2nd call is for setup-profiles after connect task.
c.Check(s.secBackend.SetupCalls[0].SnapInfo.InstanceName(), Equals, snapInfo.InstanceName())
c.Check(s.secBackend.SetupCalls[0].SnapInfo.Revision, Equals, snapInfo.Revision)
+ c.Check(s.secBackend.SetupCalls[1].SnapInfo.InstanceName(), Equals, snapInfo.InstanceName())
+ c.Check(s.secBackend.SetupCalls[1].SnapInfo.Revision, Equals, snapInfo.Revision)
+
// The OS snap was setup (because its connected to sample snap).
- c.Check(s.secBackend.SetupCalls[1].SnapInfo.InstanceName(), Equals, coreSnapInfo.InstanceName())
- c.Check(s.secBackend.SetupCalls[1].SnapInfo.Revision, Equals, coreSnapInfo.Revision)
+ c.Check(s.secBackend.SetupCalls[2].SnapInfo.InstanceName(), Equals, coreSnapInfo.InstanceName())
+ c.Check(s.secBackend.SetupCalls[2].SnapInfo.Revision, Equals, coreSnapInfo.Revision)
+}
+
+// auto-connect needs to setup security for connected slots after autoconnection
+func (s *interfaceManagerSuite) TestAutoConnectSetupSecurityOnceWithMultiplePlugs(c *C) {
+ s.MockModel(c, nil)
+
+ // Add an OS snap.
+ _ = s.mockSnap(c, ubuntuCoreSnapYaml)
+
+ // Initialize the manager. This registers the OS snap.
+ mgr := s.manager(c)
+
+ // Add a sample snap with a multiple plugs which should be auto-connected.
+ snapInfo := s.mockSnap(c, sampleSnapYamlManyPlugs)
+
+ // Run the setup-snap-security task and let it finish.
+ change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: snapInfo.SnapName(),
+ Revision: snapInfo.Revision,
+ },
+ })
+ s.settle(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Ensure that the task succeeded.
+ c.Assert(change.Err(), IsNil)
+ c.Assert(change.Status(), Equals, state.DoneStatus)
+
+ repo := mgr.Repository()
+
+ for _, ifaceName := range []string{"network", "home", "x11", "wayland"} {
+ cref := &interfaces.ConnRef{PlugRef: interfaces.PlugRef{Snap: "snap", Name: ifaceName}, SlotRef: interfaces.SlotRef{Snap: "ubuntu-core", Name: ifaceName}}
+ conn, _ := repo.Connection(cref)
+ c.Check(conn, NotNil, Commentf("missing connection for %s interface", ifaceName))
+ }
+
+ // Three backend calls: initial setup profiles, 2 setup calls for both core and snap.
+ c.Assert(s.secBackend.SetupCalls, HasLen, 3)
+ c.Assert(s.secBackend.RemoveCalls, HasLen, 0)
+ setupCalls := make(map[string]int)
+ for _, sc := range s.secBackend.SetupCalls {
+ setupCalls[sc.SnapInfo.InstanceName()]++
+ }
+ c.Check(setupCalls["snap"], Equals, 2)
+ c.Check(setupCalls["ubuntu-core"], Equals, 1)
}
func (s *interfaceManagerSuite) TestDoDiscardConnsPlug(c *C) {
@@ -3718,7 +3931,8 @@
c.Check(ifacestate.CheckInterfaces(s.state, snapInfo, deviceCtx), ErrorMatches, "installation denied.*")
}
-func (s *interfaceManagerSuite) TestCheckInterfacesDenySkippedIfNoDecl(c *C) {
+func (s *interfaceManagerSuite) TestCheckInterfacesNoDenyIfNoDecl(c *C) {
+ deviceCtx := s.TrivialDeviceContext(c, nil)
restore := assertstest.MockBuiltinBaseDeclaration([]byte(`
type: base-declaration
authority-id: canonical
@@ -3735,7 +3949,55 @@
s.state.Lock()
defer s.state.Unlock()
- c.Check(ifacestate.CheckInterfaces(s.state, snapInfo, nil), IsNil)
+ c.Check(ifacestate.CheckInterfaces(s.state, snapInfo, deviceCtx), IsNil)
+}
+
+func (s *interfaceManagerSuite) TestCheckInterfacesDisallowBasedOnSnapTypeNoSnapDecl(c *C) {
+ deviceCtx := s.TrivialDeviceContext(c, nil)
+
+ restore := assertstest.MockBuiltinBaseDeclaration([]byte(`
+type: base-declaration
+authority-id: canonical
+series: 16
+slots:
+ test:
+ allow-installation:
+ slot-snap-type:
+ - core
+`))
+ defer restore()
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+
+ // no snap decl
+ snapInfo := s.mockSnap(c, producerYaml)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ c.Check(ifacestate.CheckInterfaces(s.state, snapInfo, deviceCtx), ErrorMatches, `installation not allowed by "slot" slot rule of interface "test"`)
+}
+
+func (s *interfaceManagerSuite) TestCheckInterfacesAllowBasedOnSnapTypeNoSnapDecl(c *C) {
+ deviceCtx := s.TrivialDeviceContext(c, nil)
+
+ restore := assertstest.MockBuiltinBaseDeclaration([]byte(`
+type: base-declaration
+authority-id: canonical
+series: 16
+slots:
+ test:
+ allow-installation:
+ slot-snap-type:
+ - app
+`))
+ defer restore()
+ s.mockIface(c, &ifacetest.TestInterface{InterfaceName: "test"})
+
+ // no snap decl
+ snapInfo := s.mockSnap(c, producerYaml)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+ c.Check(ifacestate.CheckInterfaces(s.state, snapInfo, deviceCtx), IsNil)
}
func (s *interfaceManagerSuite) TestCheckInterfacesAllow(c *C) {
@@ -3937,11 +4199,12 @@
}
func (s *interfaceManagerSuite) TestCheckInterfacesConsidersImplicitSlots(c *C) {
+ deviceCtx := s.TrivialDeviceContext(c, nil)
snapInfo := s.mockSnap(c, ubuntuCoreSnapYaml)
s.state.Lock()
defer s.state.Unlock()
- c.Check(ifacestate.CheckInterfaces(s.state, snapInfo, nil), IsNil)
+ c.Check(ifacestate.CheckInterfaces(s.state, snapInfo, deviceCtx), IsNil)
c.Check(snapInfo.Slots["home"], NotNil)
}
@@ -5003,6 +5266,7 @@
err := ioutil.WriteFile(filepath.Join(gadgetInfo.MountDir(), "meta", "gadget.yaml"), gadgetYaml, 0644)
c.Assert(err, IsNil)
+ s.MockModel(c, nil)
}
func (s *interfaceManagerSuite) TestGadgetConnect(c *C) {
@@ -5138,6 +5402,8 @@
s.MockSnapDecl(c, "producer", "publisher2", nil)
s.mockSnap(c, producerYaml)
+ s.MockModel(c, nil)
+
s.manager(c)
gadgetInfo := s.mockSnap(c, `name: gadget
@@ -5345,6 +5611,7 @@
mgr, err := ifacestate.Manager(s.state, nil, s.o.TaskRunner(), nil, nil)
c.Assert(err, IsNil)
s.o.AddManager(mgr)
+ c.Assert(s.o.StartUp(), IsNil)
// succesfull initialization should result in exactly 1 connect and run call
for i := 0; i < 5; i++ {
@@ -5384,6 +5651,7 @@
mgr, err := ifacestate.Manager(s.state, nil, s.o.TaskRunner(), nil, nil)
c.Assert(err, IsNil)
s.o.AddManager(mgr)
+ c.Assert(s.o.StartUp(), IsNil)
c.Assert(s.se.Ensure(), ErrorMatches, ".*Connect failed.*")
c.Assert(u.ConnectCalls, Equals, 1)
@@ -5420,6 +5688,7 @@
mgr, err := ifacestate.Manager(s.state, nil, s.o.TaskRunner(), nil, nil)
c.Assert(err, IsNil)
s.o.AddManager(mgr)
+ c.Assert(s.o.StartUp(), IsNil)
for i := 0; i < 5; i++ {
c.Assert(s.se.Ensure(), IsNil)
diff -Nru snapd-2.40/overlord/ifacestate/implicit.go snapd-2.42.1/overlord/ifacestate/implicit.go
--- snapd-2.40/overlord/ifacestate/implicit.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/ifacestate/implicit.go 2019-10-30 12:17:43.000000000 +0000
@@ -44,13 +44,13 @@
// Implicit slots can be added to the special "snapd" snap or to snaps with
// type "os". Currently there are no other snaps that gain implicit
// interfaces.
- if snapInfo.GetType() != snap.TypeOS && snapInfo.InstanceName() != "snapd" {
+ if snapInfo.GetType() != snap.TypeOS && snapInfo.GetType() != snap.TypeSnapd {
return nil
}
// If the manager has chosen to put implicit slots on the "snapd" snap
// then stop adding them to any other core snaps.
- if shouldSnapdHostImplicitSlots(mapper) && snapInfo.InstanceName() != "snapd" {
+ if shouldSnapdHostImplicitSlots(mapper) && snapInfo.GetType() != snap.TypeSnapd {
return nil
}
diff -Nru snapd-2.40/overlord/managers_test.go snapd-2.42.1/overlord/managers_test.go
--- snapd-2.40/overlord/managers_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/managers_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -43,8 +43,8 @@
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/asserts/assertstest"
"github.com/snapcore/snapd/asserts/sysdb"
- "github.com/snapcore/snapd/boot/boottest"
"github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/bootloader/bootloadertest"
"github.com/snapcore/snapd/client"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/interfaces"
@@ -200,7 +200,9 @@
s.AddCleanup(ifacestate.MockSecurityBackends(nil))
- o, err := overlord.New()
+ o, err := overlord.New(nil)
+ c.Assert(err, IsNil)
+ err = o.StartUp()
c.Assert(err, IsNil)
o.InterfaceManager().DisableUDevMonitor()
s.o = o
@@ -631,6 +633,10 @@
w.Write(asserts.Encode(a))
return
case "download":
+ if s.sessionMacaroon != "" {
+ // FIXME: download is still using the old headers!
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, fmt.Sprintf(`Macaroon root="%s"`, s.sessionMacaroon))
+ }
if s.failNextDownload == comps[1] {
s.failNextDownload = ""
w.WriteHeader(418)
@@ -648,7 +654,6 @@
case "v2:refresh":
if s.sessionMacaroon != "" {
c.Check(r.Header.Get("Snap-Device-Authorization"), Equals, fmt.Sprintf(`Macaroon root="%s"`, s.sessionMacaroon))
-
}
dec := json.NewDecoder(r.Body)
var input struct {
@@ -795,7 +800,7 @@
st.Lock()
defer st.Unlock()
- ts, err := snapstate.Install(st, "foo", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.TODO(), st, "foo", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
chg := st.NewChange("install-snap", "...")
chg.AddAll(ts)
@@ -896,7 +901,7 @@
st.Lock()
defer st.Unlock()
- ts, err := snapstate.Install(st, "foo", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.TODO(), st, "foo", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
chg := st.NewChange("install-snap", "...")
chg.AddAll(ts)
@@ -977,7 +982,7 @@
st.Lock()
defer st.Unlock()
- ts, err := snapstate.Install(st, "foo", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.TODO(), st, "foo", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
chg := st.NewChange("install-snap", "...")
chg.AddAll(ts)
@@ -1407,7 +1412,7 @@
st.Lock()
defer st.Unlock()
- ts, err := snapstate.Install(st, "foo", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.TODO(), st, "foo", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
chg := st.NewChange("install-snap", "...")
chg.AddAll(ts)
@@ -1524,8 +1529,8 @@
}
func (s *mgrsSuite) TestInstallCoreSnapUpdatesBootloaderAndSplitsAcrossRestart(c *C) {
- loader := boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(loader)
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
defer bootloader.Force(nil)
restore := release.MockOnClassic(false)
@@ -1574,15 +1579,15 @@
c.Assert(t.Status(), Equals, state.DoingStatus, Commentf("install-snap change failed with: %v", chg.Err()))
// this is already set
- c.Assert(loader.BootVars, DeepEquals, map[string]string{
+ c.Assert(bloader.BootVars, DeepEquals, map[string]string{
"snap_try_core": "core_x1.snap",
"snap_mode": "try",
})
// simulate successful restart happened
state.MockRestarting(st, state.RestartUnset)
- loader.BootVars["snap_mode"] = ""
- loader.BootVars["snap_core"] = "core_x1.snap"
+ bloader.BootVars["snap_mode"] = ""
+ bloader.SetBootBase("core_x1.snap")
st.Unlock()
err = s.o.Settle(settleTimeout)
@@ -1594,8 +1599,8 @@
}
func (s *mgrsSuite) TestInstallKernelSnapUpdatesBootloader(c *C) {
- loader := boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(loader)
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
defer bootloader.Force(nil)
restore := release.MockOnClassic(false)
@@ -1654,7 +1659,7 @@
c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
- c.Assert(loader.BootVars, DeepEquals, map[string]string{
+ c.Assert(bloader.BootVars, DeepEquals, map[string]string{
"snap_try_kernel": "pc-kernel_x1.snap",
"snap_mode": "try",
})
@@ -1896,7 +1901,7 @@
st.Lock()
defer st.Unlock()
- ts, err := snapstate.Install(st, "foo", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.TODO(), st, "foo", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
chg := st.NewChange("install-snap", "...")
chg.AddAll(ts)
@@ -1956,7 +1961,7 @@
st.Lock()
defer st.Unlock()
- ts, err := snapstate.Install(st, "foo", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.TODO(), st, "foo", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
chg := st.NewChange("install-snap", "...")
chg.AddAll(ts)
@@ -2062,7 +2067,7 @@
st.Lock()
defer st.Unlock()
- ts, err := snapstate.Install(st, "foo", nil, 0, snapstate.Flags{Unaliased: true})
+ ts, err := snapstate.Install(context.TODO(), st, "foo", nil, 0, snapstate.Flags{Unaliased: true})
c.Assert(err, IsNil)
chg := st.NewChange("install-snap", "...")
chg.AddAll(ts)
@@ -2175,7 +2180,7 @@
st.Lock()
defer st.Unlock()
- ts, err := snapstate.Install(st, "foo", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.TODO(), st, "foo", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
chg := st.NewChange("install-snap", "...")
chg.AddAll(ts)
@@ -2189,7 +2194,7 @@
c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
- ts, err = snapstate.Install(st, "bar", nil, 0, snapstate.Flags{})
+ ts, err = snapstate.Install(context.TODO(), st, "bar", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
chg = st.NewChange("install-snap", "...")
chg.AddAll(ts)
@@ -2322,7 +2327,7 @@
st.Lock()
defer st.Unlock()
- ts, err := snapstate.Install(st, "foo", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.TODO(), st, "foo", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
chg := st.NewChange("install-snap", "...")
chg.AddAll(ts)
@@ -2363,7 +2368,7 @@
st.Lock()
defer st.Unlock()
- ts, err := snapstate.Install(st, "foo", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.TODO(), st, "foo", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
chg := st.NewChange("install-snap", "...")
chg.AddAll(ts)
@@ -2434,7 +2439,7 @@
s.restoreBackends = ifacestate.MockSecurityBackends(nil)
- o, err := overlord.New()
+ o, err := overlord.New(nil)
c.Assert(err, IsNil)
o.InterfaceManager().DisableUDevMonitor()
s.o = o
@@ -2609,9 +2614,12 @@
c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err()))
tasks := chg.Tasks()
- connectTask := tasks[len(tasks)-1]
+ connectTask := tasks[len(tasks)-2]
c.Assert(connectTask.Kind(), Equals, "connect")
+ setupProfilesTask := tasks[len(tasks)-1]
+ c.Assert(setupProfilesTask.Kind(), Equals, "setup-profiles")
+
// verify connect task data
var plugRef interfaces.PlugRef
var slotRef interfaces.SlotRef
@@ -3204,6 +3212,8 @@
i++
c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Run configure hook of "%s" snap if present`, name))
i++
+ c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Run health check of "%s" snap`, name))
+ i++
return i
}
@@ -3239,6 +3249,8 @@
i++
c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Run configure hook of "%s" snap if present`, name))
i++
+ c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Run health check of "%s" snap`, name))
+ i++
return i
}
@@ -3390,8 +3402,10 @@
}
func (s *mgrsSuite) TestRemodelSwitchKernelTrack(c *C) {
- loader := boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(loader)
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bloader.SetBootKernel("pc-kernel_1.snap")
+ bloader.SetBootBase("core_1.snap")
+ bootloader.Force(bloader)
defer bootloader.Force(nil)
restore := release.MockOnClassic(false)
@@ -3484,6 +3498,139 @@
c.Assert(tasks, HasLen, i+1)
}
+func (ms *mgrsSuite) TestRemodelSwitchToDifferentKernel(c *C) {
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
+ defer bootloader.Force(nil)
+
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ mockServer := ms.mockStore(c)
+ defer mockServer.Close()
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ si := &snap.SideInfo{RealName: "pc-kernel", SnapID: fakeSnapID("pc-kernel"), Revision: snap.R(1)}
+ snapstate.Set(st, "pc-kernel", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{si},
+ Current: snap.R(1),
+ SnapType: "kernel",
+ })
+ bloader.SetBootVars(map[string]string{
+ "snap_mode": "",
+ "snap_core": "core_1.snap",
+ "snap_kernel": "pc-kernel_1.snap",
+ })
+ si2 := &snap.SideInfo{RealName: "pc", SnapID: fakeSnapID("pc"), Revision: snap.R(1)}
+ gadgetSnapYaml := "name: pc\nversion: 1.0\ntype: gadget"
+ snapstate.Set(st, "pc", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{si2},
+ Current: snap.R(1),
+ SnapType: "gadget",
+ })
+ gadgetYaml := `
+volumes:
+ volume-id:
+ bootloader: grub
+`
+ snaptest.MockSnapWithFiles(c, gadgetSnapYaml, si2, [][]string{
+ {"meta/gadget.yaml", gadgetYaml},
+ })
+
+ // add "brand-kernel" snap to fake store
+ const brandKernelYaml = `name: brand-kernel
+type: kernel
+version: 1.0`
+ ms.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "brand-kernel",
+ "publisher-id": "can0nical",
+ })
+ snapPath, _ := ms.makeStoreTestSnap(c, brandKernelYaml, "2")
+ ms.serveSnap(snapPath, "2")
+
+ // add "foo" snap to fake store
+ ms.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "foo",
+ })
+ snapPath, _ = ms.makeStoreTestSnap(c, `{name: "foo", version: 1.0}`, "1")
+ ms.serveSnap(snapPath, "1")
+
+ // create/set custom model assertion
+ model := ms.brands.Model("can0nical", "my-model", modelDefaults)
+
+ // setup model assertion
+ devicestatetest.SetDevice(st, &auth.DeviceState{
+ Brand: "can0nical",
+ Model: "my-model",
+ Serial: "serialserialserial",
+ })
+ err := assertstate.Add(st, model)
+ c.Assert(err, IsNil)
+
+ // create a new model
+ newModel := ms.brands.Model("can0nical", "my-model", modelDefaults, map[string]interface{}{
+ "kernel": "brand-kernel",
+ "revision": "1",
+ "required-snaps": []interface{}{"foo"},
+ })
+
+ chg, err := devicestate.Remodel(st, newModel)
+ c.Assert(err, IsNil)
+
+ st.Unlock()
+ // regular settleTimeout is not enough on arm buildds :/
+ err = ms.o.Settle(4 * settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+ c.Assert(chg.Err(), IsNil)
+
+ // system waits for a restart because of the new kernel
+ t := findKind(chg, "auto-connect")
+ c.Assert(t, NotNil)
+ c.Assert(t.Status(), Equals, state.DoingStatus)
+
+ // simulate successful restart happened
+ state.MockRestarting(st, state.RestartUnset)
+
+ // continue
+ st.Unlock()
+ // regular settleTimeout is not enough on arm buildds :/
+ err = ms.o.Settle(4 * settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("upgrade-snap change failed with: %v", chg.Err()))
+
+ // ensure tasks were run in the right order
+ tasks := chg.Tasks()
+ sort.Sort(byReadyTime(tasks))
+
+ // first all downloads/checks in sequential order
+ var i int
+ i += validateDownloadCheckTasks(c, tasks[i:], "brand-kernel", "2", "stable")
+ i += validateDownloadCheckTasks(c, tasks[i:], "foo", "1", "stable")
+
+ // then all installs in sequential order
+ i += validateInstallTasks(c, tasks[i:], "brand-kernel", "2")
+ i += validateInstallTasks(c, tasks[i:], "foo", "1")
+
+ // ensure that we only have the tasks we checked (plus the one
+ // extra "set-model" task)
+ c.Assert(tasks, HasLen, i+1)
+
+ // ensure we did not try device registration
+ for _, t := range st.Tasks() {
+ if t.Kind() == "request-serial" {
+ c.Fatalf("test should not create a request-serial task but did")
+ }
+ }
+}
+
func (s *mgrsSuite) TestRemodelStoreSwitch(c *C) {
s.prereqSnapAssertions(c, map[string]interface{}{
"snap-name": "foo",
@@ -3491,6 +3638,7 @@
snapPath, _ := s.makeStoreTestSnap(c, fmt.Sprintf("{name: %s, version: 1.0}", "foo"), "1")
s.serveSnap(snapPath, "1")
+ // track the creation of new DeviceAndAutContext (for new Store)
newDAC := false
mockServer := s.mockStore(c)
@@ -3616,13 +3764,14 @@
err = assertstate.Add(st, model)
c.Assert(err, IsNil)
- signSerial := func(c *C, bhv *devicestatetest.DeviceServiceBehavior, headers map[string]interface{}, body []byte) (asserts.Assertion, error) {
+ signSerial := func(c *C, bhv *devicestatetest.DeviceServiceBehavior, headers map[string]interface{}, body []byte) (serial asserts.Assertion, ancillary []asserts.Assertion, err error) {
brandID := headers["brand-id"].(string)
model := headers["model"].(string)
c.Check(brandID, Equals, "my-brand")
c.Check(model, Equals, "my-model")
headers["authority-id"] = brandID
- return s.brands.Signing("my-brand").Sign(asserts.SerialType, headers, body, "")
+ a, err := s.brands.Signing("my-brand").Sign(asserts.SerialType, headers, body, "")
+ return a, nil, err
}
bhv := &devicestatetest.DeviceServiceBehavior{
@@ -3691,3 +3840,243 @@
c.Check(serial.DeviceKey().ID(), Equals, device.KeyID)
}
+
+func (s *mgrsSuite) TestRemodelReregistration(c *C) {
+ s.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "foo",
+ })
+ snapPath, _ := s.makeStoreTestSnap(c, fmt.Sprintf("{name: %s, version: 1.0}", "foo"), "1")
+ s.serveSnap(snapPath, "1")
+
+ // track the creation of new DeviceAndAutContext (for new Store)
+ newDAC := false
+
+ mockServer := s.mockStore(c)
+ defer mockServer.Close()
+
+ st := s.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ s.checkDeviceAndAuthContext = func(dac store.DeviceAndAuthContext) {
+ // the DeviceAndAuthContext assumes state is unlocked
+ st.Unlock()
+ defer st.Lock()
+ c.Check(dac, NotNil)
+ stoID, err := dac.StoreID("")
+ c.Assert(err, IsNil)
+ c.Check(stoID, Equals, "my-brand-substore")
+ newDAC = true
+ }
+
+ model := s.brands.Model("my-brand", "my-model", modelDefaults, map[string]interface{}{
+ "gadget": "gadget",
+ })
+
+ // setup initial device identity
+ kpMgr, err := asserts.OpenFSKeypairManager(dirs.SnapDeviceDir)
+ c.Assert(err, IsNil)
+ err = kpMgr.Put(deviceKey)
+ c.Assert(err, IsNil)
+
+ assertstatetest.AddMany(st, s.brands.AccountsAndKeys("my-brand")...)
+ devicestatetest.SetDevice(st, &auth.DeviceState{
+ Brand: "my-brand",
+ Model: "my-model",
+ KeyID: deviceKey.PublicKey().ID(),
+ Serial: "orig-serial",
+ })
+ err = assertstate.Add(st, model)
+ c.Assert(err, IsNil)
+
+ encDevKey, err := asserts.EncodePublicKey(deviceKey.PublicKey())
+ c.Assert(err, IsNil)
+ serialHeaders := map[string]interface{}{
+ "brand-id": "my-brand",
+ "model": "my-model",
+ "serial": "orig-serial",
+ "device-key": string(encDevKey),
+ "device-key-sha3-384": deviceKey.PublicKey().ID(),
+ "timestamp": time.Now().Format(time.RFC3339),
+ }
+ serialA, err := s.brands.Signing("my-brand").Sign(asserts.SerialType, serialHeaders, nil, "")
+ c.Assert(err, IsNil)
+ serial := serialA.(*asserts.Serial)
+ err = assertstate.Add(st, serial)
+ c.Assert(err, IsNil)
+
+ signSerial := func(c *C, bhv *devicestatetest.DeviceServiceBehavior, headers map[string]interface{}, body []byte) (serial asserts.Assertion, ancillary []asserts.Assertion, err error) {
+ brandID := headers["brand-id"].(string)
+ model := headers["model"].(string)
+ c.Check(brandID, Equals, "my-brand")
+ c.Check(model, Equals, "other-model")
+ headers["authority-id"] = brandID
+ a, err := s.brands.Signing("my-brand").Sign(asserts.SerialType, headers, body, "")
+ return a, nil, err
+ }
+
+ bhv := &devicestatetest.DeviceServiceBehavior{
+ ReqID: "REQID-1",
+ RequestIDURLPath: "/svc/request-id",
+ SerialURLPath: "/svc/serial",
+ SignSerial: signSerial,
+ }
+
+ mockDeviceService := devicestatetest.MockDeviceService(c, bhv)
+ defer mockDeviceService.Close()
+
+ r := devicestatetest.MockGadget(c, st, "gadget", snap.R(2), nil)
+ defer r()
+
+ // set registration config on gadget
+ tr := config.NewTransaction(st)
+ c.Assert(tr.Set("gadget", "device-service.url", mockDeviceService.URL+"/svc/"), IsNil)
+ c.Assert(tr.Set("gadget", "registration.proposed-serial", "orig-serial"), IsNil)
+ tr.Commit()
+
+ // run the remodel
+ // create a new model
+ newModel := s.brands.Model("my-brand", "other-model", modelDefaults, map[string]interface{}{
+ "store": "my-brand-substore",
+ "gadget": "gadget",
+ "required-snaps": []interface{}{"foo"},
+ })
+
+ s.expectedSerial = "orig-serial"
+ s.expectedStore = "my-brand-substore"
+ s.sessionMacaroon = "other-store-session"
+
+ chg, err := devicestate.Remodel(st, newModel)
+ c.Assert(err, IsNil)
+
+ st.Unlock()
+ err = s.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("upgrade-snap change failed with: %v", chg.Err()))
+
+ device, err := devicestatetest.Device(st)
+ c.Assert(err, IsNil)
+ c.Check(device.Brand, Equals, "my-brand")
+ c.Check(device.Model, Equals, "other-model")
+ c.Check(device.Serial, Equals, "orig-serial")
+
+ a, err := assertstate.DB(st).Find(asserts.SerialType, map[string]string{
+ "brand-id": "my-brand",
+ "model": "other-model",
+ "serial": "orig-serial",
+ })
+ c.Assert(err, IsNil)
+ serial = a.(*asserts.Serial)
+
+ c.Check(serial.Body(), HasLen, 0)
+ c.Check(serial.DeviceKey().ID(), Equals, device.KeyID)
+
+ // the new required-snap "foo" is installed
+ var snapst snapstate.SnapState
+ err = snapstate.Get(st, "foo", &snapst)
+ c.Assert(err, IsNil)
+
+ // and marked required
+ c.Check(snapst.Required, Equals, true)
+
+ // a new store was made
+ c.Check(newDAC, Equals, true)
+
+ // we have a session with the new store
+ c.Check(device.SessionMacaroon, Equals, "other-store-session")
+}
+
+func (s *mgrsSuite) TestCheckRefreshFailureWithConcurrentRemoveOfConnectedSnap(c *C) {
+ hookMgr := s.o.HookManager()
+ c.Assert(hookMgr, NotNil)
+
+ // force configure hook failure for some-snap.
+ hookMgr.RegisterHijack("configure", "some-snap", func(ctx *hookstate.Context) error {
+ return fmt.Errorf("failing configure hook")
+ })
+
+ snapPath, _ := s.makeStoreTestSnap(c, someSnapYaml, "40")
+ s.serveSnap(snapPath, "40")
+ snapPath, _ = s.makeStoreTestSnap(c, otherSnapYaml, "50")
+ s.serveSnap(snapPath, "50")
+
+ mockServer := s.mockStore(c)
+ defer mockServer.Close()
+
+ st := s.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ st.Set("conns", map[string]interface{}{
+ "other-snap:media-hub some-snap:media-hub": map[string]interface{}{"interface": "media-hub", "auto": false},
+ })
+
+ si := &snap.SideInfo{RealName: "some-snap", SnapID: fakeSnapID("some-snap"), Revision: snap.R(1)}
+ snapInfo := snaptest.MockSnap(c, someSnapYaml, si)
+
+ oi := &snap.SideInfo{RealName: "other-snap", SnapID: fakeSnapID("other-snap"), Revision: snap.R(1)}
+ otherInfo := snaptest.MockSnap(c, otherSnapYaml, oi)
+
+ snapstate.Set(st, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{si},
+ Current: snap.R(1),
+ SnapType: "app",
+ })
+ snapstate.Set(st, "other-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{oi},
+ Current: snap.R(1),
+ SnapType: "app",
+ })
+
+ // add snaps to the repo and connect them
+ repo := s.o.InterfaceManager().Repository()
+ c.Assert(repo.AddSnap(snapInfo), IsNil)
+ c.Assert(repo.AddSnap(otherInfo), IsNil)
+ _, err := repo.Connect(&interfaces.ConnRef{
+ PlugRef: interfaces.PlugRef{Snap: "other-snap", Name: "media-hub"},
+ SlotRef: interfaces.SlotRef{Snap: "some-snap", Name: "media-hub"},
+ }, nil, nil, nil, nil, nil)
+ c.Assert(err, IsNil)
+
+ // refresh all
+ c.Assert(assertstate.RefreshSnapDeclarations(st, 0), IsNil)
+
+ ts, err := snapstate.Update(st, "some-snap", nil, 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg := st.NewChange("refresh", "...")
+ chg.AddAll(ts)
+
+ // remove other-snap
+ ts2, err := snapstate.Remove(st, "other-snap", snap.R(0), nil)
+ c.Assert(err, IsNil)
+ chg2 := st.NewChange("remove-snap", "...")
+ chg2.AddAll(ts2)
+
+ st.Unlock()
+ err = s.o.Settle(settleTimeout)
+ st.Lock()
+
+ c.Check(err, IsNil)
+
+ // the refresh change has failed due to configure hook error
+ c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks:\n.*failing configure hook.*`)
+ c.Check(chg.Status(), Equals, state.ErrorStatus)
+
+ // download-snap is one of the first tasks in the refresh change, check that it was undone
+ var downloadSnapStatus state.Status
+ for _, t := range chg.Tasks() {
+ if t.Kind() == "download-snap" {
+ downloadSnapStatus = t.Status()
+ break
+ }
+ }
+ c.Check(downloadSnapStatus, Equals, state.UndoneStatus)
+
+ // the remove change succeeded
+ c.Check(chg2.Err(), IsNil)
+ c.Check(chg2.Status(), Equals, state.DoneStatus)
+}
diff -Nru snapd-2.40/overlord/overlord.go snapd-2.42.1/overlord/overlord.go
--- snapd-2.40/overlord/overlord.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/overlord.go 2019-10-30 12:17:43.000000000 +0000
@@ -41,6 +41,7 @@
"github.com/snapcore/snapd/overlord/configstate"
"github.com/snapcore/snapd/overlord/configstate/proxyconf"
"github.com/snapcore/snapd/overlord/devicestate"
+ "github.com/snapcore/snapd/overlord/healthstate"
"github.com/snapcore/snapd/overlord/hookstate"
"github.com/snapcore/snapd/overlord/ifacestate"
"github.com/snapcore/snapd/overlord/patch"
@@ -77,9 +78,10 @@
ensureRun int32
pruneTicker *time.Ticker
// restarts
- restartHandler func(t state.RestartType)
+ restartBehavior RestartBehavior
// managers
inited bool
+ startedUp bool
runner *state.TaskRunner
snapMgr *snapstate.SnapManager
assertMgr *assertstate.AssertManager
@@ -92,13 +94,27 @@
proxyConf func(req *http.Request) (*url.URL, error)
}
+// RestartBehavior controls how to hanndle and carry forward restart requests
+// via the state.
+type RestartBehavior interface {
+ HandleRestart(t state.RestartType)
+ // RebootAsExpected is called early when either a reboot was
+ // requested by snapd and happened or no reboot was expected at all.
+ RebootAsExpected(st *state.State) error
+ // RebootDidNotHappen is called early instead when a reboot was
+ // requested by snad but did not happen.
+ RebootDidNotHappen(st *state.State) error
+}
+
var storeNew = store.New
// New creates a new Overlord with all its state managers.
-func New() (*Overlord, error) {
+// It can be provided with an optional RestartBehavior.
+func New(restartBehavior RestartBehavior) (*Overlord, error) {
o := &Overlord{
- loopTomb: new(tomb.Tomb),
- inited: true,
+ loopTomb: new(tomb.Tomb),
+ inited: true,
+ restartBehavior: restartBehavior,
}
backend := &overlordStateBackend{
@@ -106,7 +122,7 @@
ensureBefore: o.ensureBefore,
requestRestart: o.requestRestart,
}
- s, err := loadState(backend)
+ s, err := loadState(backend, restartBehavior)
if err != nil {
return nil, err
}
@@ -153,7 +169,10 @@
o.addManager(cmdstate.Manager(s, o.runner))
o.addManager(snapshotstate.Manager(s, o.runner))
- configstateInit(hookMgr)
+ if err := configstateInit(s, hookMgr); err != nil {
+ return nil, err
+ }
+ healthstate.Init(hookMgr)
// the shared task runner should be added last!
o.stateEng.AddManager(o.runner)
@@ -167,10 +186,6 @@
snapstate.ReplaceStore(s, sto)
- if err := o.snapMgr.SyncCookies(s); err != nil {
- return nil, fmt.Errorf("failed to generate cookies: %q", err)
- }
-
return o, nil
}
@@ -194,7 +209,12 @@
o.stateEng.AddManager(mgr)
}
-func loadState(backend state.Backend) (*state.State, error) {
+func loadState(backend state.Backend, restartBehavior RestartBehavior) (*state.State, error) {
+ curBootID, err := osutil.BootID()
+ if err != nil {
+ return nil, fmt.Errorf("fatal: cannot find current boot id: %v", err)
+ }
+
perfTimings := timings.New(map[string]string{"startup": "load-state"})
if !osutil.FileExists(dirs.SnapStateFile) {
@@ -205,6 +225,9 @@
return nil, fmt.Errorf("fatal: directory %q must be present", stateDir)
}
s := state.New(backend)
+ s.Lock()
+ s.VerifyReboot(curBootID)
+ s.Unlock()
patch.Init(s)
return s, nil
}
@@ -226,6 +249,11 @@
perfTimings.Save(s)
s.Unlock()
+ err = verifyReboot(s, curBootID, restartBehavior)
+ if err != nil {
+ return nil, err
+ }
+
// one-shot migrations
err = patch.Apply(s)
if err != nil {
@@ -234,6 +262,26 @@
return s, nil
}
+func verifyReboot(s *state.State, curBootID string, restartBehavior RestartBehavior) error {
+ s.Lock()
+ defer s.Unlock()
+ err := s.VerifyReboot(curBootID)
+ if err != nil && err != state.ErrExpectedReboot {
+ return err
+ }
+ expectedRebootDidNotHappen := err == state.ErrExpectedReboot
+ if restartBehavior != nil {
+ if expectedRebootDidNotHappen {
+ return restartBehavior.RebootDidNotHappen(s)
+ }
+ return restartBehavior.RebootAsExpected(s)
+ }
+ if expectedRebootDidNotHappen {
+ logger.Noticef("expected system restart but it did not happen")
+ }
+ return nil
+}
+
func (o *Overlord) newStoreWithContext(storeCtx store.DeviceAndAuthContext) snapstate.StoreService {
cfg := store.DefaultConfig()
cfg.Proxy = o.proxyConf
@@ -250,6 +298,42 @@
return o.newStoreWithContext(stoCtx)
}
+// StartUp proceeds to run any expensive Overlord or managers initialization. After this is done once it is a noop.
+func (o *Overlord) StartUp() error {
+ if o.startedUp {
+ return nil
+ }
+ o.startedUp = true
+
+ // slow down for tests
+ if s := os.Getenv("SNAPD_SLOW_STARTUP"); s != "" {
+ if d, err := time.ParseDuration(s); err == nil {
+ logger.Noticef("slowing down startup by %v as requested", d)
+
+ time.Sleep(d)
+ }
+ }
+
+ return o.stateEng.StartUp()
+}
+
+// StartupTimeout computes a usable timeout for the startup
+// initializations by using a pessimistic estimate.
+func (o *Overlord) StartupTimeout() (timeout time.Duration, reasoning string, err error) {
+ // TODO: adjust based on real hardware measurements
+ st := o.State()
+ st.Lock()
+ defer st.Unlock()
+ n, err := snapstate.NumSnaps(st)
+ if err != nil {
+ return 0, "", err
+ }
+ // number of snaps (and connections) play a role
+ reasoning = "pessimistic estimate of 30s plus 5s per snap"
+ to := (30 * time.Second) + time.Duration(n)*(5*time.Second)
+ return to, reasoning, nil
+}
+
func (o *Overlord) ensureTimerSetup() {
o.ensureLock.Lock()
defer o.ensureLock.Unlock()
@@ -293,18 +377,13 @@
}
func (o *Overlord) requestRestart(t state.RestartType) {
- if o.restartHandler == nil {
- logger.Noticef("restart requested but no handler set")
+ if o.restartBehavior == nil {
+ logger.Noticef("restart requested but no behavior set")
} else {
- o.restartHandler(t)
+ o.restartBehavior.HandleRestart(t)
}
}
-// SetRestartHandler sets a handler to fulfill restart requests asynchronously.
-func (o *Overlord) SetRestartHandler(handleRestart func(t state.RestartType)) {
- o.restartHandler = handleRestart
-}
-
// Loop runs a loop in a goroutine to ensure the current state regularly through StateEngine Ensure.
func (o *Overlord) Loop() {
o.ensureTimerSetup()
@@ -348,6 +427,10 @@
}
func (o *Overlord) settle(timeout time.Duration, beforeCleanups func()) error {
+ if err := o.StartUp(); err != nil {
+ return err
+ }
+
func() {
o.ensureLock.Lock()
defer o.ensureLock.Unlock()
@@ -417,7 +500,7 @@
// is scheduled. It then waits similarly for all ready changes to
// reach the clean state. Chiefly for tests. Cannot be used in
// conjunction with Loop. If timeout is non-zero and settling takes
-// longer than timeout, returns an error.
+// longer than timeout, returns an error. Calls StartUp as well.
func (o *Overlord) Settle(timeout time.Duration) error {
return o.settle(timeout, nil)
}
@@ -429,7 +512,7 @@
// changes to reach the clean state, but calls once the provided
// callback before doing that. Chiefly for tests. Cannot be used in
// conjunction with Loop. If timeout is non-zero and settling takes
-// longer than timeout, returns an error.
+// longer than timeout, returns an error. Calls StartUp as well.
func (o *Overlord) SettleObserveBeforeCleanups(timeout time.Duration, beforeCleanups func()) error {
return o.settle(timeout, beforeCleanups)
}
@@ -494,9 +577,18 @@
// Mock creates an Overlord without any managers and with a backend
// not using disk. Managers can be added with AddManager. For testing.
func Mock() *Overlord {
+ return MockWithRestartHandler(nil)
+}
+
+// MockWithRestartHandler creates an Overlord without any managers and
+// with a backend not using disk. It will use the given handler on
+// restart requests. Managers can be added with AddManager. For
+// testing.
+func MockWithRestartHandler(handleRestart func(state.RestartType)) *Overlord {
o := &Overlord{
- loopTomb: new(tomb.Tomb),
- inited: false,
+ loopTomb: new(tomb.Tomb),
+ inited: false,
+ restartBehavior: mockRestartBehavior(handleRestart),
}
s := state.New(mockBackend{o: o})
o.stateEng = NewStateEngine(s)
@@ -514,6 +606,23 @@
o.addManager(mgr)
}
+type mockRestartBehavior func(state.RestartType)
+
+func (rb mockRestartBehavior) HandleRestart(t state.RestartType) {
+ if rb == nil {
+ return
+ }
+ rb(t)
+}
+
+func (rb mockRestartBehavior) RebootAsExpected(*state.State) error {
+ panic("internal error: overlord.Mock should not invoke RebootAsExpected")
+}
+
+func (rb mockRestartBehavior) RebootDidNotHappen(*state.State) error {
+ panic("internal error: overlord.Mock should not invoke RebootDidNotHappen")
+}
+
type mockBackend struct {
o *Overlord
}
diff -Nru snapd-2.40/overlord/overlord_test.go snapd-2.42.1/overlord/overlord_test.go
--- snapd-2.40/overlord/overlord_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/overlord_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -21,6 +21,7 @@
import (
"encoding/json"
+ "errors"
"fmt"
"io/ioutil"
"os"
@@ -34,6 +35,7 @@
"github.com/snapcore/snapd/cmd"
"github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/devicestate/devicestatetest"
@@ -42,6 +44,7 @@
"github.com/snapcore/snapd/overlord/patch"
"github.com/snapcore/snapd/overlord/snapstate"
"github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/store"
"github.com/snapcore/snapd/testutil"
)
@@ -72,11 +75,12 @@
defer restore()
var configstateInitCalled bool
- overlord.MockConfigstateInit(func(*hookstate.HookManager) {
+ overlord.MockConfigstateInit(func(*state.State, *hookstate.HookManager) error {
configstateInitCalled = true
+ return nil
})
- o, err := overlord.New()
+ o, err := overlord.New(nil)
c.Assert(err, IsNil)
c.Check(o, NotNil)
@@ -117,7 +121,7 @@
func (ovs *overlordSuite) TestNewStore(c *C) {
// this is a shallow test, the deep testing happens in the
// remodeling tests in managers_test.go
- o, err := overlord.New()
+ o, err := overlord.New(nil)
c.Assert(err, IsNil)
devBE := o.DeviceManager().StoreContextBackend()
@@ -132,7 +136,7 @@
err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
c.Assert(err, IsNil)
- o, err := overlord.New()
+ o, err := overlord.New(nil)
c.Assert(err, IsNil)
state := o.State()
@@ -160,7 +164,7 @@
err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
c.Assert(err, IsNil)
- o, err := overlord.New()
+ o, err := overlord.New(nil)
c.Assert(err, IsNil)
state := o.State()
@@ -178,7 +182,7 @@
err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
c.Assert(err, IsNil)
- _, err = overlord.New()
+ _, err = overlord.New(nil)
c.Assert(err, ErrorMatches, "cannot read state: EOF")
}
@@ -197,7 +201,7 @@
err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
c.Assert(err, IsNil)
- o, err := overlord.New()
+ o, err := overlord.New(nil)
c.Assert(err, IsNil)
state := o.State()
@@ -223,11 +227,34 @@
c.Check(b, Equals, true)
}
+func (ovs *overlordSuite) TestNewFailedConfigstate(c *C) {
+ restore := patch.Mock(42, 2, nil)
+ defer restore()
+
+ var configstateInitCalled bool
+ restore = overlord.MockConfigstateInit(func(*state.State, *hookstate.HookManager) error {
+ configstateInitCalled = true
+ return fmt.Errorf("bad bad")
+ })
+ defer restore()
+
+ o, err := overlord.New(nil)
+ c.Assert(err, ErrorMatches, "bad bad")
+ c.Check(o, IsNil)
+ c.Check(configstateInitCalled, Equals, true)
+}
+
type witnessManager struct {
state *state.State
expectedEnsure int
ensureCalled chan struct{}
ensureCallback func(s *state.State) error
+ startedUp int
+}
+
+func (wm *witnessManager) StartUp() error {
+ wm.startedUp++
+ return nil
}
func (wm *witnessManager) Ensure() error {
@@ -255,13 +282,16 @@
}
func (ovs *overlordSuite) TestTrivialRunAndStop(c *C) {
- o, err := overlord.New()
+ o, err := overlord.New(nil)
c.Assert(err, IsNil)
markSeeded(o)
// make sure we don't try to talk to the store
snapstate.CanAutoRefresh = nil
+ err = o.StartUp()
+ c.Assert(err, IsNil)
+
o.Loop()
err = o.Stop()
@@ -269,7 +299,7 @@
}
func (ovs *overlordSuite) TestUnknownTasks(c *C) {
- o, err := overlord.New()
+ o, err := overlord.New(nil)
c.Assert(err, IsNil)
o.InterfaceManager().DisableUDevMonitor()
@@ -305,6 +335,9 @@
}
o.AddManager(witness)
+ err := o.StartUp()
+ c.Assert(err, IsNil)
+
o.Loop()
defer o.Stop()
@@ -316,8 +349,10 @@
}
c.Check(time.Since(t0) >= 10*time.Millisecond, Equals, true)
- err := o.Stop()
+ err = o.Stop()
c.Assert(err, IsNil)
+
+ c.Check(witness.startedUp, Equals, 1)
}
func (ovs *overlordSuite) TestEnsureLoopMediatedEnsureBeforeImmediate(c *C) {
@@ -338,6 +373,8 @@
}
o.AddManager(witness)
+ c.Assert(o.StartUp(), IsNil)
+
o.Loop()
defer o.Stop()
@@ -366,6 +403,8 @@
}
o.AddManager(witness)
+ c.Assert(o.StartUp(), IsNil)
+
o.Loop()
defer o.Stop()
@@ -395,6 +434,8 @@
}
o.AddManager(witness)
+ c.Assert(o.StartUp(), IsNil)
+
o.Loop()
defer o.Stop()
@@ -424,6 +465,8 @@
}
o.AddManager(witness)
+ c.Assert(o.StartUp(), IsNil)
+
o.Loop()
defer o.Stop()
@@ -453,6 +496,8 @@
}
o.AddManager(witness)
+ c.Assert(o.StartUp(), IsNil)
+
o.Loop()
defer o.Stop()
@@ -509,6 +554,8 @@
}
o.AddManager(witness)
+ c.Assert(o.StartUp(), IsNil)
+
o.Loop()
select {
@@ -575,7 +622,7 @@
oldUmask := syscall.Umask(0)
defer syscall.Umask(oldUmask)
- o, err := overlord.New()
+ o, err := overlord.New(nil)
c.Assert(err, IsNil)
s := o.State()
@@ -816,25 +863,114 @@
}
func (ovs *overlordSuite) TestRequestRestartNoHandler(c *C) {
- o, err := overlord.New()
+ o, err := overlord.New(nil)
c.Assert(err, IsNil)
o.State().RequestRestart(state.RestartDaemon)
}
+type testRestartBehavior struct {
+ restartRequested state.RestartType
+ rebootState string
+ rebootVerifiedErr error
+}
+
+func (rb *testRestartBehavior) HandleRestart(t state.RestartType) {
+ rb.restartRequested = t
+}
+
+func (rb *testRestartBehavior) RebootAsExpected(_ *state.State) error {
+ rb.rebootState = "as-expected"
+ return rb.rebootVerifiedErr
+}
+
+func (rb *testRestartBehavior) RebootDidNotHappen(_ *state.State) error {
+ rb.rebootState = "did-not-happen"
+ return rb.rebootVerifiedErr
+}
+
func (ovs *overlordSuite) TestRequestRestartHandler(c *C) {
- o, err := overlord.New()
+ rb := &testRestartBehavior{}
+
+ o, err := overlord.New(rb)
c.Assert(err, IsNil)
- restartRequested := false
+ o.State().RequestRestart(state.RestartDaemon)
- o.SetRestartHandler(func(t state.RestartType) {
- restartRequested = true
- })
+ c.Check(rb.restartRequested, Equals, state.RestartDaemon)
+}
- o.State().RequestRestart(state.RestartDaemon)
+func (ovs *overlordSuite) TestVerifyRebootNoPendingReboot(c *C) {
+ fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"patch-sublevel":%d,"some":"data","refresh-privacy-key":"0123456789ABCDEF"},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0,"last-lane-id":0}`, patch.Level, patch.Sublevel))
+ err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
+ c.Assert(err, IsNil)
+
+ rb := &testRestartBehavior{}
+
+ _, err = overlord.New(rb)
+ c.Assert(err, IsNil)
+
+ c.Check(rb.rebootState, Equals, "as-expected")
+}
- c.Check(restartRequested, Equals, true)
+func (ovs *overlordSuite) TestVerifyRebootOK(c *C) {
+ fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"patch-sublevel":%d,"some":"data","refresh-privacy-key":"0123456789ABCDEF","system-restart-from-boot-id":%q},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0,"last-lane-id":0}`, patch.Level, patch.Sublevel, "boot-id-prev"))
+ err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
+ c.Assert(err, IsNil)
+
+ rb := &testRestartBehavior{}
+
+ _, err = overlord.New(rb)
+ c.Assert(err, IsNil)
+
+ c.Check(rb.rebootState, Equals, "as-expected")
+}
+
+func (ovs *overlordSuite) TestVerifyRebootOKButError(c *C) {
+ fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"patch-sublevel":%d,"some":"data","refresh-privacy-key":"0123456789ABCDEF","system-restart-from-boot-id":%q},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0,"last-lane-id":0}`, patch.Level, patch.Sublevel, "boot-id-prev"))
+ err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
+ c.Assert(err, IsNil)
+
+ e := errors.New("boom")
+ rb := &testRestartBehavior{rebootVerifiedErr: e}
+
+ _, err = overlord.New(rb)
+ c.Assert(err, Equals, e)
+
+ c.Check(rb.rebootState, Equals, "as-expected")
+}
+
+func (ovs *overlordSuite) TestVerifyRebootDidNotHappen(c *C) {
+ curBootID, err := osutil.BootID()
+ c.Assert(err, IsNil)
+
+ fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"patch-sublevel":%d,"some":"data","refresh-privacy-key":"0123456789ABCDEF","system-restart-from-boot-id":%q},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0,"last-lane-id":0}`, patch.Level, patch.Sublevel, curBootID))
+ err = ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
+ c.Assert(err, IsNil)
+
+ rb := &testRestartBehavior{}
+
+ _, err = overlord.New(rb)
+ c.Assert(err, IsNil)
+
+ c.Check(rb.rebootState, Equals, "did-not-happen")
+}
+
+func (ovs *overlordSuite) TestVerifyRebootDidNotHappenError(c *C) {
+ curBootID, err := osutil.BootID()
+ c.Assert(err, IsNil)
+
+ fakeState := []byte(fmt.Sprintf(`{"data":{"patch-level":%d,"patch-sublevel":%d,"some":"data","refresh-privacy-key":"0123456789ABCDEF","system-restart-from-boot-id":%q},"changes":null,"tasks":null,"last-change-id":0,"last-task-id":0,"last-lane-id":0}`, patch.Level, patch.Sublevel, curBootID))
+ err = ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600)
+ c.Assert(err, IsNil)
+
+ e := errors.New("boom")
+ rb := &testRestartBehavior{rebootVerifiedErr: e}
+
+ _, err = overlord.New(rb)
+ c.Assert(err, Equals, e)
+
+ c.Check(rb.rebootState, Equals, "did-not-happen")
}
func (ovs *overlordSuite) TestOverlordCanStandby(c *C) {
@@ -848,6 +984,8 @@
}
o.AddManager(witness)
+ c.Assert(o.StartUp(), IsNil)
+
// can only standby after loop ran once
c.Assert(o.CanStandby(), Equals, false)
@@ -862,3 +1000,42 @@
c.Assert(o.CanStandby(), Equals, true)
}
+
+func (ovs *overlordSuite) TestStartupTimeout(c *C) {
+ o, err := overlord.New(nil)
+ c.Assert(err, IsNil)
+
+ to, _, err := o.StartupTimeout()
+ c.Assert(err, IsNil)
+ c.Check(to, Equals, 30*time.Second)
+
+ st := o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ // have two snaps
+ snapstate.Set(st, "core18", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "core18", Revision: snap.R(1)},
+ },
+ Current: snap.R(1),
+ SnapType: "base",
+ })
+ snapstate.Set(st, "foo", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "foo", Revision: snap.R(1)},
+ },
+ Current: snap.R(1),
+ SnapType: "app",
+ })
+
+ st.Unlock()
+ to, reasoning, err := o.StartupTimeout()
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Check(to, Equals, (30+5+5)*time.Second)
+ c.Check(reasoning, Equals, "pessimistic estimate of 30s plus 5s per snap")
+}
diff -Nru snapd-2.40/overlord/patch/export_test.go snapd-2.42.1/overlord/patch/export_test.go
--- snapd-2.40/overlord/patch/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/patch/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -42,7 +42,24 @@
Level = lv
oldSublvl := Sublevel
Sublevel = sublvl
+ oldPatches := make(map[int][]PatchFunc)
+ for k, v := range patches {
+ oldPatches[k] = v
+ }
+
+ for level, sublevels := range patches {
+ if level > lv {
+ delete(patches, level)
+ continue
+ }
+ if level == lv && len(sublevels)-1 > sublvl {
+ sublevels = sublevels[:sublvl+1]
+ patches[level] = sublevels
+ }
+ }
+
return func() {
+ patches = oldPatches
Level = old
Sublevel = oldSublvl
}
diff -Nru snapd-2.40/overlord/patch/patch6_2.go snapd-2.42.1/overlord/patch/patch6_2.go
--- snapd-2.40/overlord/patch/patch6_2.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/overlord/patch/patch6_2.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,156 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package patch
+
+import (
+ "encoding/json"
+ "fmt"
+ "time"
+
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+type patch62SideInfo struct {
+ RealName string `yaml:"name,omitempty" json:"name,omitempty"`
+ SnapID string `yaml:"snap-id" json:"snap-id"`
+ Revision snap.Revision `yaml:"revision" json:"revision"`
+ Channel string `yaml:"channel,omitempty" json:"channel,omitempty"`
+ Contact string `yaml:"contact,omitempty" json:"contact,omitempty"`
+ EditedTitle string `yaml:"title,omitempty" json:"title,omitempty"`
+ EditedSummary string `yaml:"summary,omitempty" json:"summary,omitempty"`
+ EditedDescription string `yaml:"description,omitempty" json:"description,omitempty"`
+ Private bool `yaml:"private,omitempty" json:"private,omitempty"`
+ Paid bool `yaml:"paid,omitempty" json:"paid,omitempty"`
+}
+
+type patch62Flags struct {
+ DevMode bool `json:"devmode,omitempty"`
+ JailMode bool `json:"jailmode,omitempty"`
+ Classic bool `json:"classic,omitempty"`
+ TryMode bool `json:"trymode,omitempty"`
+ Revert bool `json:"revert,omitempty"`
+ RemoveSnapPath bool `json:"remove-snap-path,omitempty"`
+ IgnoreValidation bool `json:"ignore-validation,omitempty"`
+ Required bool `json:"required,omitempty"`
+ SkipConfigure bool `json:"skip-configure,omitempty"`
+ Unaliased bool `json:"unaliased,omitempty"`
+ Amend bool `json:"amend,omitempty"`
+ IsAutoRefresh bool `json:"is-auto-refresh,omitempty"`
+ NoReRefresh bool `json:"no-rerefresh,omitempty"`
+ RequireTypeBase bool `json:"require-base-type,omitempty"`
+}
+
+type patch62SnapState struct {
+ SnapType string `json:"type"`
+ Sequence []*patch62SideInfo `json:"sequence"`
+ Active bool `json:"active,omitempty"`
+ Current snap.Revision `json:"current"`
+ Channel string `json:"channel,omitempty"`
+ patch62Flags
+ Aliases interface{} `json:"aliases,omitempty"`
+ AutoAliasesDisabled bool `json:"auto-aliases-disabled,omitempty"`
+ AliasesPending bool `json:"aliases-pending,omitempty"`
+ UserID int `json:"user-id,omitempty"`
+ InstanceKey string `json:"instance-key,omitempty"`
+ CohortKey string `json:"cohort-key,omitempty"`
+ RefreshInhibitedTime *time.Time `json:"refresh-inhibited-time,omitempty"`
+}
+
+type patch62SnapSetup struct {
+ Channel string `json:"channel,omitempty"`
+ UserID int `json:"user-id,omitempty"`
+ Base string `json:"base,omitempty"`
+ Type snap.Type `json:"type,omitempty"`
+ PlugsOnly bool `json:"plugs-only,omitempty"`
+ CohortKey string `json:"cohort-key,omitempty"`
+ Prereq []string `json:"prereq,omitempty"`
+ patch62Flags
+ SnapPath string `json:"snap-path,omitempty"`
+ DownloadInfo interface{} `json:"download-info,omitempty"`
+ SideInfo *patch62SideInfo `json:"side-info,omitempty"`
+ patch62auxStoreInfo
+ InstanceKey string `json:"instance-key,omitempty"`
+}
+
+type patch62auxStoreInfo struct {
+ Media interface{} `json:"media,omitempty"`
+}
+
+func hasSnapdSnapID(snapst patch62SnapState) bool {
+ for _, seq := range snapst.Sequence {
+ if snap.IsSnapd(seq.SnapID) {
+ return true
+ }
+ }
+ return false
+}
+
+// patch6_2:
+// - ensure snapd snaps in the snapstate have TypeSnapd for backward compatibility with old snapd snap releases.
+// - ensure snapd snaps have TypeSnapd in pending install tasks.
+func patch6_2(st *state.State) error {
+ var snaps map[string]*json.RawMessage
+ if err := st.Get("snaps", &snaps); err != nil && err != state.ErrNoState {
+ return fmt.Errorf("internal error: cannot get snaps: %s", err)
+ }
+
+ // Migrate snapstate
+ for name, raw := range snaps {
+ var snapst patch62SnapState
+ if err := json.Unmarshal([]byte(*raw), &snapst); err != nil {
+ return err
+ }
+ if hasSnapdSnapID(snapst) && snapst.SnapType != string(snap.TypeSnapd) {
+ snapst.SnapType = string(snap.TypeSnapd)
+ data, err := json.Marshal(snapst)
+ if err != nil {
+ return err
+ }
+ newRaw := json.RawMessage(data)
+ snaps[name] = &newRaw
+ st.Set("snaps", snaps)
+ // We can have at most one snapd snap
+ break
+ }
+ }
+
+ // migrate tasks' snap setup
+ for _, task := range st.Tasks() {
+ chg := task.Change()
+ if chg != nil && chg.Status().Ready() {
+ continue
+ }
+
+ var snapsup patch62SnapSetup
+ err := task.Get("snap-setup", &snapsup)
+ if err != nil && err != state.ErrNoState {
+ return fmt.Errorf("internal error: cannot get snap-setup of task %s: %s", task.ID(), err)
+ }
+
+ if err == nil && snapsup.SideInfo != nil {
+ if snapsup.Type != snap.TypeSnapd && snap.IsSnapd(snapsup.SideInfo.SnapID) {
+ snapsup.Type = snap.TypeSnapd
+ task.Set("snap-setup", snapsup)
+ }
+ }
+ }
+ return nil
+}
diff -Nru snapd-2.40/overlord/patch/patch6_2_test.go snapd-2.42.1/overlord/patch/patch6_2_test.go
--- snapd-2.40/overlord/patch/patch6_2_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/overlord/patch/patch6_2_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,377 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package patch_test
+
+import (
+ "bytes"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/overlord/patch"
+ "github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/snap"
+)
+
+type patch62Suite struct{}
+
+var _ = Suite(&patch62Suite{})
+
+// State with snapd snap marked as 'app' (to be converted to 'snapd' type) and a regular 'other' snap,
+// plus three tasks - two of them need to have their SnapSetup migrated to 'snapd' type.
+var statePatch6_2JSON = []byte(`
+{
+ "data": {
+ "patch-level": 6,
+ "snaps": {
+ "snapd": {
+ "type": "app",
+ "sequence": [
+ {
+ "name": "snapd",
+ "snap-id": "snapd-snap-id",
+ "revision": "2"
+ }
+ ],
+ "active": true,
+ "current": "2",
+ "channel": "stable"
+ },
+ "other": {
+ "type": "app",
+ "sequence": [
+ {
+ "name": "snapd",
+ "snap-id": "foo",
+ "revision": "2"
+ }
+ ],
+ "active": true,
+ "current": "2",
+ "channel": "stable"
+ }
+ }
+ },
+ "changes": {
+ "6": {
+ "id": "6",
+ "kind": "auto-refresh",
+ "summary": "...",
+ "status": 0,
+ "clean": true,
+ "data": {
+ "api-data": {
+ "snap-names": ["snapd"]
+ },
+ "snap-names": ["snapd"]
+ },
+ "task-ids": ["1", "8"]
+ },
+ "9": {
+ "id": "9",
+ "kind": "auto-refresh",
+ "summary": "...",
+ "status": 4,
+ "clean": true,
+ "data": {
+ "api-data": {
+ "snap-names": ["snapd"]
+ },
+ "snap-names": ["snapd"]
+ },
+ "task-ids": ["10"]
+ }
+ },
+ "tasks": {
+ "1": {
+ "id": "1",
+ "kind": "download-snap",
+ "summary": "...",
+ "status": 2,
+ "clean": true,
+ "data": {
+ "snap-setup": {
+ "channel": "stable",
+ "type": "app",
+ "is-auto-refresh": true,
+ "snap-path": "/path",
+ "download-info": {
+ "download-url": "foo",
+ "size": 1234,
+ "sha3-384": "123456",
+ "deltas": [
+ {
+ "from-revision": 10934,
+ "to-revision": 10972,
+ "format": "xdelta3",
+ "download-url": "foo",
+ "size": 16431136,
+ "sha3-384": "1"
+ }
+ ]
+ },
+ "side-info": {
+ "name": "snapd",
+ "snap-id": "snapd-snap-id",
+ "revision": "1",
+ "channel": "stable",
+ "title": "snapd"
+ },
+ "media": [
+ {
+ "type": "icon",
+ "url": "a"
+ },
+ {
+ "type": "screenshot",
+ "url": "2"
+ },
+ {
+ "type": "screenshot",
+ "url": "3"
+ },
+ {
+ "type": "screenshot",
+ "url": "4"
+ },
+ {
+ "type": "video",
+ "url": "5"
+ }
+ ]
+ }
+ },
+ "change": "6"
+ },
+ "8": {
+ "id": "8",
+ "kind": "other",
+ "summary": "",
+ "status": 4,
+ "data": {
+ "snap-setup": {
+ "channel": "stable",
+ "type": "app",
+ "snap-path": "/path",
+ "side-info": {
+ "name": "snapd",
+ "snap-id": "snapd-snap-id",
+ "revision": "1",
+ "channel": "stable",
+ "title": "snapd"
+ }
+ }
+ },
+ "change": "6"
+ },
+ "10": {
+ "id": "10",
+ "kind": "other",
+ "summary": "",
+ "status": 4,
+ "data": {
+ "snap-setup": {
+ "channel": "stable",
+ "type": "app",
+ "snap-path": "/path",
+ "side-info": {
+ "name": "snapd",
+ "snap-id": "snapd-snap-id",
+ "revision": "1",
+ "channel": "stable",
+ "title": "snapd"
+ }
+ }
+ },
+ "change": "9"
+ }
+ }
+ }
+ }
+}`)
+
+// State with 'snapd' snap with proper snap type, and an extra 'other' snap
+// with snapd-snap-id but improper 'app' type.
+var statePatch6_2JSONWithSnapd = []byte(`
+{
+ "data": {
+ "patch-level": 6,
+ "snaps": {
+ "snapd": {
+ "type": "snapd",
+ "sequence": [
+ {
+ "name": "snapd",
+ "snap-id": "snapd-snap-id",
+ "revision": "2"
+ }
+ ],
+ "active": true,
+ "current": "2",
+ "channel": "stable"
+ },
+ "other": {
+ "type": "app",
+ "sequence": [
+ {
+ "name": "other",
+ "snap-id": "snapd-snap-id",
+ "revision": "1"
+ }
+ ],
+ "active": true,
+ "current": "1",
+ "channel": "stable"
+ }
+ }
+ },
+ "changes": {}
+ },
+ "tasks": {}
+ }
+}`)
+
+// State with two snaps with snapd-snap-id and improper snap types
+var statePatch6_2JSONWithSnapd2 = []byte(`
+{
+ "data": {
+ "patch-level": 6,
+ "snaps": {
+ "snapd": {
+ "type": "app",
+ "sequence": [
+ {
+ "name": "snapd",
+ "snap-id": "snapd-snap-id",
+ "revision": "2"
+ }
+ ],
+ "active": true,
+ "current": "2",
+ "channel": "stable"
+ },
+ "other": {
+ "type": "app",
+ "sequence": [
+ {
+ "name": "other",
+ "snap-id": "snapd-snap-id",
+ "revision": "1"
+ }
+ ],
+ "active": true,
+ "current": "1",
+ "channel": "stable"
+ }
+ }
+ },
+ "changes": {}
+ },
+ "tasks": {}
+ }
+}`)
+
+func (s *patch62Suite) SetUpTest(c *C) {
+ dirs.SetRootDir(c.MkDir())
+ snap.MockSanitizePlugsSlots(func(*snap.Info) {})
+}
+
+func (s *patch62Suite) TestPatch62(c *C) {
+ restore1 := patch.MockLevel(6, 2)
+ defer restore1()
+
+ restore2 := snap.MockSnapdSnapID("snapd-snap-id")
+ defer restore2()
+
+ r := bytes.NewReader(statePatch6_2JSON)
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ c.Assert(patch.Apply(st), IsNil)
+ st.Lock()
+ defer st.Unlock()
+
+ // our mocks are correct
+ c.Assert(st.Changes(), HasLen, 2)
+ c.Assert(st.Tasks(), HasLen, 3)
+
+ var snapst snapstate.SnapState
+ c.Assert(snapstate.Get(st, "snapd", &snapst), IsNil)
+ c.Check(snapst.SnapType, Equals, "snapd")
+
+ // sanity check - "other" is untouched
+ c.Assert(snapstate.Get(st, "other", &snapst), IsNil)
+ c.Check(snapst.SnapType, Equals, "app")
+
+ // check tasks
+ task := st.Task("1")
+ c.Assert(task, NotNil)
+
+ var snapsup snapstate.SnapSetup
+ err = task.Get("snap-setup", &snapsup)
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Type, Equals, snap.TypeSnapd)
+
+ // sanity check, structures not defined explicitly via patch62* are preserved
+ c.Check(snapsup.Flags.IsAutoRefresh, Equals, true)
+ c.Assert(snapsup.Media, HasLen, 5)
+ c.Check(snapsup.Media[0].URL, Equals, "a")
+ c.Assert(snapsup.DownloadInfo, NotNil)
+ c.Check(snapsup.DownloadInfo.DownloadURL, Equals, "foo")
+ c.Check(snapsup.DownloadInfo.Deltas, HasLen, 1)
+
+ task = st.Task("8")
+ c.Assert(task, NotNil)
+ c.Assert(task.Get("snap-setup", &snapsup), IsNil)
+ c.Check(snapsup.Type, Equals, snap.TypeSnapd)
+
+ // task 10 not updated because the change is ready
+ task = st.Task("10")
+ c.Assert(task, NotNil)
+ c.Assert(task.Get("snap-setup", &snapsup), IsNil)
+ c.Check(snapsup.Type, Equals, snap.TypeApp)
+}
+
+func (s *patch62Suite) TestPatch62StopsAfterFirstSnapd(c *C) {
+ restore1 := patch.MockLevel(6, 2)
+ defer restore1()
+
+ restore2 := snap.MockSnapdSnapID("snapd-snap-id")
+ defer restore2()
+
+ r := bytes.NewReader(statePatch6_2JSONWithSnapd2)
+ st, err := state.ReadState(nil, r)
+ c.Assert(err, IsNil)
+
+ c.Assert(patch.Apply(st), IsNil)
+ st.Lock()
+ defer st.Unlock()
+
+ var snapdCount int
+ for _, name := range []string{"snapd", "other"} {
+ var snapst snapstate.SnapState
+ c.Assert(snapstate.Get(st, name, &snapst), IsNil)
+ if snapst.SnapType == "snapd" {
+ snapdCount++
+ }
+ }
+ c.Check(snapdCount, Equals, 1)
+}
diff -Nru snapd-2.40/overlord/patch/patch6.go snapd-2.42.1/overlord/patch/patch6.go
--- snapd-2.40/overlord/patch/patch6.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/patch/patch6.go 2019-10-30 12:17:43.000000000 +0000
@@ -25,7 +25,7 @@
)
func init() {
- patches[6] = []PatchFunc{patch6, patch6_1}
+ patches[6] = []PatchFunc{patch6, patch6_1, patch6_2}
}
type patch6Flags struct {
diff -Nru snapd-2.40/overlord/patch/patch.go snapd-2.42.1/overlord/patch/patch.go
--- snapd-2.40/overlord/patch/patch.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/patch/patch.go 2019-10-30 12:17:43.000000000 +0000
@@ -34,7 +34,7 @@
// Sublevel is the current implemented sublevel for the Level.
// Sublevel 0 is the first patch for the new Level, rollback below x.0 is not possible.
// Sublevel patches > 0 do not prevent rollbacks.
-var Sublevel = 1
+var Sublevel = 2
type PatchFunc func(s *state.State) error
diff -Nru snapd-2.40/overlord/snapstate/autorefresh_test.go snapd-2.42.1/overlord/snapstate/autorefresh_test.go
--- snapd-2.40/overlord/snapstate/autorefresh_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/autorefresh_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -376,8 +376,7 @@
tr = config.NewTransaction(s.state)
var t1 time.Time
err = tr.Get("core", "refresh.hold", &t1)
- c.Assert(err, IsNil)
- c.Check(t1.IsZero(), Equals, true)
+ c.Assert(config.IsNoOption(err), Equals, true)
}
func (s *autoRefreshTestSuite) TestLastRefreshRefreshHoldExpiredReschedule(c *C) {
@@ -412,8 +411,7 @@
tr = config.NewTransaction(s.state)
var t1 time.Time
err = tr.Get("core", "refresh.hold", &t1)
- c.Assert(err, IsNil)
- c.Check(t1.IsZero(), Equals, true)
+ c.Assert(config.IsNoOption(err), Equals, true)
// check next refresh
nextRefresh1 := af.NextRefresh()
diff -Nru snapd-2.40/overlord/snapstate/backend/link.go snapd-2.42.1/overlord/snapstate/backend/link.go
--- snapd-2.40/overlord/snapstate/backend/link.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/backend/link.go 2019-10-30 12:17:43.000000000 +0000
@@ -72,7 +72,7 @@
}
func hasFontConfigCache(info *snap.Info) bool {
- if info.InstanceName() == "core" || info.InstanceName() == "snapd" {
+ if info.GetType() == snap.TypeOS || info.GetType() == snap.TypeSnapd {
return true
}
return false
@@ -113,19 +113,8 @@
})
}
- // XXX/TODO: this needs to be a task with proper undo and tests!
- if model != nil && !release.OnClassic {
- bootBase := "core"
- if model.Base() != "" {
- bootBase = model.Base()
- }
- switch info.InstanceName() {
- case model.Kernel(), bootBase:
- // XXX: This *needs* to clean up if updateCurrentSymlinks fails
- if err := boot.SetNextBoot(info); err != nil {
- return err
- }
- }
+ if err := boot.Participant(info, info.GetType(), model, release.OnClassic).SetNextBoot(); err != nil {
+ return err
}
if err := updateCurrentSymlinks(info); err != nil {
@@ -154,22 +143,41 @@
return wrappers.StopServices(apps, reason, meter, tm)
}
-func generateWrappers(s *snap.Info) error {
+func generateWrappers(s *snap.Info) (err error) {
+ var cleanupFuncs []func(*snap.Info) error
+ defer func() {
+ if err != nil {
+ for _, cleanup := range cleanupFuncs {
+ cleanup(s)
+ }
+ }
+ }()
+
// add the CLI apps from the snap.yaml
- if err := wrappers.AddSnapBinaries(s); err != nil {
+ if err = wrappers.AddSnapBinaries(s); err != nil {
return err
}
+ cleanupFuncs = append(cleanupFuncs, wrappers.RemoveSnapBinaries)
+
// add the daemons from the snap.yaml
- if err := wrappers.AddSnapServices(s, progress.Null); err != nil {
- wrappers.RemoveSnapBinaries(s)
+ if err = wrappers.AddSnapServices(s, progress.Null); err != nil {
return err
}
+ cleanupFuncs = append(cleanupFuncs, func(s *snap.Info) error {
+ return wrappers.RemoveSnapServices(s, progress.Null)
+ })
+
// add the desktop files
- if err := wrappers.AddSnapDesktopFiles(s); err != nil {
- wrappers.RemoveSnapServices(s, progress.Null)
- wrappers.RemoveSnapBinaries(s)
+ if err = wrappers.AddSnapDesktopFiles(s); err != nil {
+ return err
+ }
+ cleanupFuncs = append(cleanupFuncs, wrappers.RemoveSnapDesktopFiles)
+
+ // add the desktop icons
+ if err = wrappers.AddSnapIcons(s); err != nil {
return err
}
+ cleanupFuncs = append(cleanupFuncs, wrappers.RemoveSnapIcons)
return nil
}
@@ -190,7 +198,12 @@
logger.Noticef("Cannot remove desktop files for %q: %v", s.InstanceName(), err3)
}
- return firstErr(err1, err2, err3)
+ err4 := wrappers.RemoveSnapIcons(s)
+ if err4 != nil {
+ logger.Noticef("Cannot remove desktop icons for %q: %v", s.InstanceName(), err4)
+ }
+
+ return firstErr(err1, err2, err3, err4)
}
// UnlinkSnap makes the snap unavailable to the system removing wrappers and symlinks.
diff -Nru snapd-2.40/overlord/snapstate/backend/mountunit_test.go snapd-2.42.1/overlord/snapstate/backend/mountunit_test.go
--- snapd-2.40/overlord/snapstate/backend/mountunit_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/backend/mountunit_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -63,7 +63,7 @@
}
func (s *mountunitSuite) TestAddMountUnit(c *C) {
- restore := squashfs.MockUseFuse(false)
+ restore := squashfs.MockNeedsFuse(false)
defer restore()
info := &snap.Info{
@@ -89,6 +89,7 @@
Where=%s/foo/13
Type=squashfs
Options=nodev,ro,x-gdu.hide
+LazyUnmount=yes
[Install]
WantedBy=multi-user.target
diff -Nru snapd-2.40/overlord/snapstate/backend/setup.go snapd-2.42.1/overlord/snapstate/backend/setup.go
--- snapd-2.40/overlord/snapstate/backend/setup.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/backend/setup.go 2019-10-30 12:17:43.000000000 +0000
@@ -26,6 +26,7 @@
"github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
)
@@ -73,13 +74,13 @@
return snapType, err
}
- if s.GetType() == snap.TypeKernel {
- if err := boot.ExtractKernelAssets(s, snapf); err != nil {
- return snapType, fmt.Errorf("cannot install kernel: %s", err)
- }
+ t := s.GetType()
+ // TODO: maybe look into passing the model
+ if err := boot.Kernel(s, t, nil, release.OnClassic).ExtractKernelAssets(snapf); err != nil {
+ return snapType, fmt.Errorf("cannot install kernel: %s", err)
}
- return s.GetType(), err
+ return t, nil
}
// RemoveSnapFiles removes the snap files from the disk after unmounting the snap.
@@ -99,10 +100,9 @@
snapPath := s.MountFile()
if _, err := os.Lstat(snapPath); err == nil {
// remove the kernel assets (if any)
- if typ == snap.TypeKernel {
- if err := boot.RemoveKernelAssets(s); err != nil {
- return err
- }
+ // TODO: maybe look into passing the model
+ if err := boot.Kernel(s, typ, nil, release.OnClassic).RemoveKernelAssets(); err != nil {
+ return err
}
// remove the snap
diff -Nru snapd-2.40/overlord/snapstate/backend/setup_test.go snapd-2.42.1/overlord/snapstate/backend/setup_test.go
--- snapd-2.40/overlord/snapstate/backend/setup_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/backend/setup_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -27,12 +27,13 @@
. "gopkg.in/check.v1"
- "github.com/snapcore/snapd/boot/boottest"
"github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/bootloader/bootloadertest"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/snapstate/backend"
"github.com/snapcore/snapd/progress"
+ "github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/systemd"
@@ -144,8 +145,10 @@
}
func (s *setupSuite) TestSetupDoUndoKernel(c *C) {
- loader := boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(loader)
+ // kernel snaps only happen on non-classic
+ defer release.MockOnClassic(false)()
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
// we don't get real mounting
os.Setenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS", "1")
@@ -171,15 +174,15 @@
snapType, err := s.be.SetupSnap(snapPath, "kernel", &si, progress.Null)
c.Assert(err, IsNil)
c.Check(snapType, Equals, snap.TypeKernel)
- c.Assert(loader.ExtractKernelAssetsCalls, HasLen, 1)
- c.Assert(loader.ExtractKernelAssetsCalls[0].InstanceName(), Equals, "kernel")
+ c.Assert(bloader.ExtractKernelAssetsCalls, HasLen, 1)
+ c.Assert(bloader.ExtractKernelAssetsCalls[0].InstanceName(), Equals, "kernel")
minInfo := snap.MinimalPlaceInfo("kernel", snap.R(140))
// undo deletes the kernel assets again
err = s.be.UndoSetupSnap(minInfo, "kernel", progress.Null)
c.Assert(err, IsNil)
- c.Assert(loader.RemoveKernelAssetsCalls, HasLen, 1)
- c.Assert(loader.RemoveKernelAssetsCalls[0].InstanceName(), Equals, "kernel")
+ c.Assert(bloader.RemoveKernelAssetsCalls, HasLen, 1)
+ c.Assert(bloader.RemoveKernelAssetsCalls[0].InstanceName(), Equals, "kernel")
}
func (s *setupSuite) TestSetupDoIdempotent(c *C) {
@@ -188,8 +191,10 @@
// this cannot check systemd own behavior though around mounts!
- loader := boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(loader)
+ // kernel snaps only happen on non-classic
+ defer release.MockOnClassic(false)()
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
// we don't get real mounting
os.Setenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS", "1")
defer os.Unsetenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS")
@@ -213,14 +218,14 @@
_, err := s.be.SetupSnap(snapPath, "kernel", &si, progress.Null)
c.Assert(err, IsNil)
- c.Assert(loader.ExtractKernelAssetsCalls, HasLen, 1)
- c.Assert(loader.ExtractKernelAssetsCalls[0].InstanceName(), Equals, "kernel")
+ c.Assert(bloader.ExtractKernelAssetsCalls, HasLen, 1)
+ c.Assert(bloader.ExtractKernelAssetsCalls[0].InstanceName(), Equals, "kernel")
// retry run
_, err = s.be.SetupSnap(snapPath, "kernel", &si, progress.Null)
c.Assert(err, IsNil)
- c.Assert(loader.ExtractKernelAssetsCalls, HasLen, 2)
- c.Assert(loader.ExtractKernelAssetsCalls[1].InstanceName(), Equals, "kernel")
+ c.Assert(bloader.ExtractKernelAssetsCalls, HasLen, 2)
+ c.Assert(bloader.ExtractKernelAssetsCalls[1].InstanceName(), Equals, "kernel")
minInfo := snap.MinimalPlaceInfo("kernel", snap.R(140))
// sanity checks
@@ -237,8 +242,10 @@
// this cannot check systemd own behavior though around mounts!
- loader := boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(loader)
+ // kernel snaps only happen on non-classic
+ defer release.MockOnClassic(false)()
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
// we don't get real mounting
os.Setenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS", "1")
defer os.Unsetenv("SNAPPY_SQUASHFS_UNPACK_FOR_TESTS")
@@ -279,8 +286,9 @@
c.Assert(osutil.FileExists(minInfo.MountFile()), Equals, false)
- l, _ = filepath.Glob(filepath.Join(loader.Dir(), "*"))
- c.Assert(l, HasLen, 0)
+ // assets got extracted and then removed again
+ c.Assert(bloader.ExtractKernelAssetsCalls, HasLen, 1)
+ c.Assert(bloader.RemoveKernelAssetsCalls, HasLen, 1)
}
func (s *setupSuite) TestSetupCleanupAfterFail(c *C) {
diff -Nru snapd-2.40/overlord/snapstate/backend_test.go snapd-2.42.1/overlord/snapstate/backend_test.go
--- snapd-2.40/overlord/snapstate/backend_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/backend_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -202,6 +202,8 @@
typ = snap.TypeGadget
case "some-snapd":
typ = snap.TypeSnapd
+ case "snapd":
+ typ = snap.TypeSnapd
case "some-snap-now-classic":
confinement = "classic"
case "some-epoch-snap":
diff -Nru snapd-2.40/overlord/snapstate/booted.go snapd-2.42.1/overlord/snapstate/booted.go
--- snapd-2.40/overlord/snapstate/booted.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/booted.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,31 +20,15 @@
package snapstate
import (
- "errors"
"fmt"
- "strings"
- "github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/logger"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
)
-func nameAndRevnoFromSnap(sn string) (string, snap.Revision, error) {
- l := strings.Split(sn, "_")
- if len(l) < 2 {
- return "", snap.Revision{}, fmt.Errorf("input %q has invalid format (not enough '_')", sn)
- }
- name := l[0]
- revnoNSuffix := l[1]
- rev, err := snap.ParseRevision(strings.Split(revnoNSuffix, ".snap")[0])
- if err != nil {
- return "", snap.Revision{}, err
- }
- return name, rev, nil
-}
-
// UpdateBootRevisions synchronizes the active kernel and OS snap versions
// with the versions that actually booted. This is needed because a
// system may install "os=v2" but that fails to boot. The bootloader
@@ -61,40 +45,34 @@
}
// nothing to check if there's no kernel
- _, err := KernelInfo(st)
- if err == state.ErrNoState {
- return nil
- }
+ ok, err := HasSnapOfType(st, snap.TypeKernel)
if err != nil {
return fmt.Errorf(errorPrefix+"%s", err)
}
+ if !ok {
+ return nil
+ }
- loader, err := bootloader.Find()
+ kernel, err := boot.GetCurrentBoot(snap.TypeKernel)
if err != nil {
return fmt.Errorf(errorPrefix+"%s", err)
}
-
- m, err := loader.GetBootVars("snap_kernel", "snap_core")
+ base, err := boot.GetCurrentBoot(snap.TypeBase)
if err != nil {
return fmt.Errorf(errorPrefix+"%s", err)
}
var tsAll []*state.TaskSet
- for _, snapNameAndRevno := range []string{m["snap_kernel"], m["snap_core"]} {
- name, rev, err := nameAndRevnoFromSnap(snapNameAndRevno)
- if err != nil {
- logger.Noticef("cannot parse %q: %s", snapNameAndRevno, err)
- continue
- }
- info, err := CurrentInfo(st, name)
+ for _, actual := range []*boot.NameAndRevision{kernel, base} {
+ info, err := CurrentInfo(st, actual.Name)
if err != nil {
- logger.Noticef("cannot get info for %q: %s", name, err)
+ logger.Noticef("cannot get info for %q: %s", actual.Name, err)
continue
}
- if rev != info.SideInfo.Revision {
+ if actual.Revision != info.SideInfo.Revision {
// FIXME: check that there is no task
// for this already in progress
- ts, err := RevertToRevision(st, name, rev, Flags{})
+ ts, err := RevertToRevision(st, actual.Name, actual.Revision, Flags{})
if err != nil {
return err
}
@@ -115,58 +93,3 @@
return nil
}
-
-var ErrBootNameAndRevisionAgain = errors.New("boot revision not yet established")
-
-// CurrentBootNameAndRevision returns the currently set name and
-// revision for boot for the given type of snap, which can be core or
-// kernel. Returns ErrBootNameAndRevisionAgain if the values are
-// temporarily not established.
-func CurrentBootNameAndRevision(typ snap.Type) (name string, revision snap.Revision, err error) {
- var kind string
- var bootVar string
-
- switch typ {
- case snap.TypeKernel:
- kind = "kernel"
- bootVar = "snap_kernel"
- case snap.TypeOS:
- kind = "core"
- bootVar = "snap_core"
- case snap.TypeBase:
- kind = "base"
- bootVar = "snap_core"
- default:
- return "", snap.Revision{}, fmt.Errorf("cannot find boot revision for anything but core and kernel")
- }
-
- errorPrefix := fmt.Sprintf("cannot retrieve boot revision for %s: ", kind)
- if release.OnClassic {
- return "", snap.Revision{}, fmt.Errorf(errorPrefix + "classic system")
- }
-
- loader, err := bootloader.Find()
- if err != nil {
- return "", snap.Revision{}, fmt.Errorf(errorPrefix+"%s", err)
- }
-
- m, err := loader.GetBootVars(bootVar, "snap_mode")
- if err != nil {
- return "", snap.Revision{}, fmt.Errorf(errorPrefix+"%s", err)
- }
-
- if m["snap_mode"] == "trying" {
- return "", snap.Revision{}, ErrBootNameAndRevisionAgain
- }
-
- snapNameAndRevno := m[bootVar]
- if snapNameAndRevno == "" {
- return "", snap.Revision{}, fmt.Errorf(errorPrefix + "unset")
- }
- name, rev, err := nameAndRevnoFromSnap(snapNameAndRevno)
- if err != nil {
- return "", snap.Revision{}, fmt.Errorf(errorPrefix+"%s", err)
- }
-
- return name, rev, nil
-}
diff -Nru snapd-2.40/overlord/snapstate/booted_test.go snapd-2.42.1/overlord/snapstate/booted_test.go
--- snapd-2.40/overlord/snapstate/booted_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/booted_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -28,8 +28,8 @@
. "gopkg.in/check.v1"
- "github.com/snapcore/snapd/boot/boottest"
"github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/bootloader/bootloadertest"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/overlord"
"github.com/snapcore/snapd/overlord/snapstate"
@@ -43,7 +43,7 @@
type bootedSuite struct {
testutil.BaseTest
- bootloader *boottest.MockBootloader
+ bootloader *bootloadertest.MockBootloader
o *overlord.Overlord
state *state.State
@@ -66,9 +66,9 @@
// booted is not running on classic
release.MockOnClassic(false)
- bs.bootloader = boottest.NewMockBootloader("mock", c.MkDir())
- bs.bootloader.BootVars["snap_core"] = "core_2.snap"
- bs.bootloader.BootVars["snap_kernel"] = "canonical-pc-linux_2.snap"
+ bs.bootloader = bootloadertest.Mock("mock", c.MkDir())
+ bs.bootloader.SetBootKernel("canonical-pc-linux_2.snap")
+ bs.bootloader.SetBootBase("core_2.snap")
bootloader.Force(bs.bootloader)
bs.fakeBackend = &fakeSnappyBackend{}
@@ -82,6 +82,8 @@
bs.o.AddManager(bs.snapmgr)
bs.o.AddManager(bs.o.TaskRunner())
+ c.Assert(bs.o.StartUp(), IsNil)
+
snapstate.SetSnapManagerBackend(bs.snapmgr, bs.fakeBackend)
snapstate.AutoAliases = func(*state.State, *snap.Info) (map[string]string, error) {
return nil, nil
@@ -135,7 +137,7 @@
bs.makeInstalledKernelOS(c, st)
- bs.bootloader.BootVars["snap_core"] = "core_1.snap"
+ bs.bootloader.SetBootBase("core_1.snap")
err := snapstate.UpdateBootRevisions(st)
c.Assert(err, IsNil)
@@ -169,7 +171,7 @@
bs.makeInstalledKernelOS(c, st)
- bs.bootloader.BootVars["snap_kernel"] = "canonical-pc-linux_1.snap"
+ bs.bootloader.SetBootKernel("canonical-pc-linux_1.snap")
err := snapstate.UpdateBootRevisions(st)
c.Assert(err, IsNil)
@@ -203,7 +205,7 @@
bs.makeInstalledKernelOS(c, st)
- bs.bootloader.BootVars["snap_kernel"] = "canonical-pc-linux_99.snap"
+ bs.bootloader.SetBootKernel("canonical-pc-linux_99.snap")
err := snapstate.UpdateBootRevisions(st)
c.Assert(err, ErrorMatches, `cannot find revision 99 for snap "canonical-pc-linux"`)
}
@@ -215,7 +217,7 @@
bs.makeInstalledKernelOS(c, st)
- bs.bootloader.BootVars["snap_core"] = "core_99.snap"
+ bs.bootloader.SetBootBase("core_99.snap")
err := snapstate.UpdateBootRevisions(st)
c.Assert(err, ErrorMatches, `cannot find revision 99 for snap "core"`)
}
@@ -244,7 +246,7 @@
})
bs.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "/core/1")
- bs.bootloader.BootVars["snap_core"] = "core_1.snap"
+ bs.bootloader.SetBootBase("core_1.snap")
err := snapstate.UpdateBootRevisions(st)
c.Assert(err, IsNil)
@@ -259,49 +261,6 @@
c.Assert(chg.Err(), ErrorMatches, `(?ms).*Make snap "core" \(1\) available to the system \(fail\).*`)
}
-func (bs *bootedSuite) TestNameAndRevnoFromSnapValid(c *C) {
- name, revno, err := snapstate.NameAndRevnoFromSnap("foo_2.snap")
- c.Assert(err, IsNil)
- c.Assert(name, Equals, "foo")
- c.Assert(revno, Equals, snap.R(2))
-}
-
-func (bs *bootedSuite) TestNameAndRevnoFromSnapInvalidFormat(c *C) {
- _, _, err := snapstate.NameAndRevnoFromSnap("invalid")
- c.Assert(err, ErrorMatches, `input "invalid" has invalid format \(not enough '_'\)`)
-}
-
-func (bs *bootedSuite) TestCurrentBootNameAndRevision(c *C) {
- name, revision, err := snapstate.CurrentBootNameAndRevision(snap.TypeOS)
- c.Check(err, IsNil)
- c.Check(name, Equals, "core")
- c.Check(revision, Equals, snap.R(2))
-
- name, revision, err = snapstate.CurrentBootNameAndRevision(snap.TypeKernel)
- c.Check(err, IsNil)
- c.Check(name, Equals, "canonical-pc-linux")
- c.Check(revision, Equals, snap.R(2))
-
- bs.bootloader.BootVars["snap_mode"] = "trying"
- _, _, err = snapstate.CurrentBootNameAndRevision(snap.TypeKernel)
- c.Check(err, Equals, snapstate.ErrBootNameAndRevisionAgain)
-}
-
-func (bs *bootedSuite) TestCurrentBootNameAndRevisionUnhappy(c *C) {
- delete(bs.bootloader.BootVars, "snap_kernel")
- _, _, err := snapstate.CurrentBootNameAndRevision(snap.TypeKernel)
- c.Check(err, ErrorMatches, "cannot retrieve boot revision for kernel: unset")
-
- delete(bs.bootloader.BootVars, "snap_core")
- _, _, err = snapstate.CurrentBootNameAndRevision(snap.TypeOS)
- c.Check(err, ErrorMatches, "cannot retrieve boot revision for core: unset")
-
- delete(bs.bootloader.BootVars, "snap_core")
- _, _, err = snapstate.CurrentBootNameAndRevision(snap.TypeBase)
- c.Check(err, ErrorMatches, "cannot retrieve boot revision for base: unset")
-
-}
-
func (bs *bootedSuite) TestWaitRestartCore(c *C) {
st := bs.state
st.Lock()
@@ -340,7 +299,7 @@
c.Check(err, IsNil)
// core snap, restarted, wrong core revision, rollback!
- bs.bootloader.BootVars["snap_core"] = "core_1.snap"
+ bs.bootloader.SetBootBase("core_1.snap")
err = snapstate.WaitRestart(task, snapsup)
c.Check(err, ErrorMatches, `cannot finish core installation, there was a rollback across reboot`)
}
@@ -387,12 +346,12 @@
// core snap, restarted, right core revision, no rollback
bs.bootloader.BootVars["snap_mode"] = ""
- bs.bootloader.BootVars["snap_core"] = "core18_2.snap"
+ bs.bootloader.SetBootBase("core18_2.snap")
err = snapstate.WaitRestart(task, snapsup)
c.Check(err, IsNil)
// core snap, restarted, wrong core revision, rollback!
- bs.bootloader.BootVars["snap_core"] = "core18_1.snap"
+ bs.bootloader.SetBootBase("core18_1.snap")
err = snapstate.WaitRestart(task, snapsup)
c.Check(err, ErrorMatches, `cannot finish core18 installation, there was a rollback across reboot`)
}
diff -Nru snapd-2.40/overlord/snapstate/catalogrefresh.go snapd-2.42.1/overlord/snapstate/catalogrefresh.go
--- snapd-2.40/overlord/snapstate/catalogrefresh.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/catalogrefresh.go 2019-10-30 12:17:43.000000000 +0000
@@ -33,6 +33,7 @@
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/auth"
"github.com/snapcore/snapd/overlord/state"
+ "github.com/snapcore/snapd/store"
"github.com/snapcore/snapd/timings"
)
@@ -88,9 +89,13 @@
logger.Debugf("Catalog refresh starting now; next scheduled for %s.", next)
err := refreshCatalogs(r.state, theStore)
- if err == nil {
+ switch err {
+ case nil:
logger.Debugf("Catalog refresh succeeded.")
- } else {
+ case store.ErrTooManyRequests:
+ logger.Debugf("Catalog refresh postponed.")
+ err = nil
+ default:
logger.Debugf("Catalog refresh failed: %v.", err)
}
return err
diff -Nru snapd-2.40/overlord/snapstate/catalogrefresh_test.go snapd-2.42.1/overlord/snapstate/catalogrefresh_test.go
--- snapd-2.40/overlord/snapstate/catalogrefresh_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/catalogrefresh_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -43,7 +43,8 @@
type catalogStore struct {
storetest.Store
- ops []string
+ ops []string
+ tooMany bool
}
func (r *catalogStore) WriteCatalogs(ctx context.Context, w io.Writer, a store.SnapAdder) error {
@@ -51,6 +52,9 @@
panic("Ensure marked context required")
}
r.ops = append(r.ops, "write-catalog")
+ if r.tooMany {
+ return store.ErrTooManyRequests
+ }
w.Write([]byte("pkg1\npkg2"))
a.AddSnap("foo", "1.0", "foo summary", []string{"foo", "meh"})
a.AddSnap("bar", "2.0", "bar summray", []string{"bar", "meh"})
@@ -62,6 +66,9 @@
panic("Ensure marked context required")
}
r.ops = append(r.ops, "sections")
+ if r.tooMany {
+ return nil, store.ErrTooManyRequests
+ }
return []string{"section1", "section2"}, nil
}
@@ -126,6 +133,29 @@
})
}
+func (s *catalogRefreshTestSuite) TestCatalogRefreshTooMany(c *C) {
+ s.store.tooMany = true
+
+ cr7 := snapstate.NewCatalogRefresh(s.state)
+ // next is initially zero
+ c.Check(snapstate.NextCatalogRefresh(cr7).IsZero(), Equals, true)
+ t0 := time.Now()
+
+ err := cr7.Ensure()
+ c.Check(err, IsNil) // !!
+
+ // next now has a delta (next refresh is not before t0 + delta)
+ c.Check(snapstate.NextCatalogRefresh(cr7).Before(t0.Add(snapstate.CatalogRefreshDelayWithDelta)), Equals, false)
+
+ // it tried one endpoint and bailed at the first 429
+ c.Check(s.store.ops, HasLen, 1)
+
+ // nothing got created
+ c.Check(osutil.FileExists(dirs.SnapSectionsFile), Equals, false)
+ c.Check(osutil.FileExists(dirs.SnapNamesFile), Equals, false)
+ c.Check(osutil.FileExists(dirs.SnapCommandsDB), Equals, false)
+}
+
func (s *catalogRefreshTestSuite) TestCatalogRefreshNotNeeded(c *C) {
cr7 := snapstate.NewCatalogRefresh(s.state)
snapstate.MockCatalogRefreshNextRefresh(cr7, time.Now().Add(1*time.Hour))
diff -Nru snapd-2.40/overlord/snapstate/check_snap.go snapd-2.42.1/overlord/snapstate/check_snap.go
--- snapd-2.40/overlord/snapstate/check_snap.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/check_snap.go 2019-10-30 12:17:43.000000000 +0000
@@ -26,11 +26,14 @@
"strings"
"github.com/snapcore/snapd/arch"
+ "github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/cmd"
"github.com/snapcore/snapd/logger"
+ "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/snapstate/backend"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
+ seccomp_compiler "github.com/snapcore/snapd/sandbox/seccomp"
"github.com/snapcore/snapd/snap"
)
@@ -45,6 +48,134 @@
"command-chain": true,
}
+// supportedSystemUsernames for now contains the hardcoded list of system
+// users (and implied system group of same name) that snaps may specify. This
+// will eventually be moved out of here into the store.
+//
+// Since the snap is mounted read-only and to avoid problems associated with
+// different systems using different uids and gids for the same user name and
+// group name, snapd will create system-usernames where 'scope' is not
+// 'external' (currently snapd only supports 'scope: shared') with the
+// following characteristics:
+//
+// - uid and gid shall match for the specified system-username
+// - a snapd-allocated [ug]id for a user/group name shall never change
+// - snapd should avoid [ug]ids that are known to overlap with uid ranges of
+// common use cases and user namespace container managers so that DAC and
+// AppArmor owner match work as intended.
+// - [ug]id shall be < 2^31 to avoid (at least) broken devpts kernel code
+// - [ug]id shall be >= 524288 (0x00080000) to give plenty of room for large
+// sites, default uid/gid ranges for docker (231072-296608), LXD installs
+// that setup a default /etc/sub{uid,gid} (100000-165536) and podman whose
+// tutorials reference setting up a specific default user and range
+// (100000-165536)
+// - [ug]id shall be < 1,000,000 and > 1,001,000,000 (ie, 1,000,000 subordinate
+// uid with 1,000,000,000 range) to avoid overlapping with LXD's minimum and
+// maximum id ranges. LXD allows for any id range >= 65536 and doesn't
+// perform any [ug]id overlap detection with current users
+// - [ug]ids assigned by snapd initially will fall within a 65536 (2^16) range
+// (see below) where the first [ug]id in the range has the 16 lower bits all
+// set to zero. This allows snapd to conveniently be bitwise aligned, follows
+// sensible conventions (see https://systemd.io/UIDS-GIDS.html) but also
+// potentially discoverable by systemd-nspawn (it assigns a different 65536
+// range to each container. Its allocation algorithm is not sequential and
+// may choose anything within its range that isn't already allocated. It's
+// detection algorithm includes (effectively) performing a getpwent()
+// operation on CANDIDATE_UID & 0XFFFF0000 and selecting another range if it
+// is assigned).
+//
+// What [ug]id range(s) should snapd use?
+//
+// While snapd does not employ user namespaces, it will operate on systems with
+// container managers that do and will assign from a range of [ug]ids. It is
+// desirable that snapd assigns [ug]ids that minimally conflict with the system
+// and other software (potential conflicts with admin-assigned ranges in
+// /etc/subuid and /etc/subgid cannot be avoided, but can be documented as well
+// as detected/logged). Overlapping with container managers is non-fatal for
+// snapd and the container, but introduces the possibility that a uid in the
+// container matches a uid a snap is using, which is undesirable in terms of
+// security (eg, DoS via ulimit, same ownership of files between container and
+// snap (even if the other's files are otherwise inaccessible), etc).
+//
+// snapd shall assign [ug]ids from range(s) of 65536 where the lowest value in
+// the range has the 16 lower bits all set to zero (initially just one range,
+// but snapd can add more as needed).
+//
+// To avoid [ug]id overlaps, snapd shall only assign [ug]ids >= 524288
+// (0x00080000) and <= 983040 (0x000F0000, ie the first 65536 range under LXD's
+// minimum where the lower 16 bits are all zeroes). While [ug]ids >= 1001062400
+// (0x3BAB0000, the first 65536 range above LXD's maximum where the lower 16
+// bits are all zeroes) would also avoid overlap, considering nested containers
+// (eg, LXD snap runs a container that runs a container that runs snapd),
+// choosing >= 1001062400 would mean that the admin would need to increase the
+// LXD id range for these containers for snapd to be allowed to create its
+// [ug]ids in the deeply nested containers. The requirements would both be an
+// administrative burden and artificially limit the number of deeply nested
+// containers the host could have.
+//
+// Looking at the LSB and distribution defaults for login.defs, we can observe
+// uids and gids in the system's initial 65536 range (ie, 0-65536):
+//
+// - 0-99 LSB-suggested statically assigned range (eg, root, daemon,
+// etc)
+// - 0 mandatory 'root' user
+// - 100-499 LSB-suggested dynamically assigned range for system users
+// (distributions often prefer a higher range, see below)
+// - 500-999 typical distribution default for dynamically assigned range
+// for system users (some distributions use a smaller
+// SYS_[GU]ID_MIN)
+// - 1000-60000 typical distribution default for dynamically assigned range
+// for regular users
+// - 65535 (-1) should not be assigned since '-1' might be evaluated as this
+// with set[ug]id* and chown families of functions
+// - 65534 (-2) nobody/nogroup user for NFS/etc [ug]id anonymous squashing
+// - 65519-65533 systemd recommended reserved range for site-local anonymous
+// additions, etc
+//
+// To facilitate potential future use cases within the 65536 range snapd will
+// assign from, snapd will only assign from the following subset of ranges
+// relative to the range minimum (ie, its 'base' which has the lower 16 bits
+// all set to zero):
+//
+// - 60500-60999 'scope: shared' system-usernames
+// - 61000-65519 'scope: private' system-usernames
+//
+// Since the first [ug]id range must be >= 524288 and <= 983040 (see above) and
+// following the above guide for system-usernames [ug]ids within this 65536
+// range, the lowest 'scope: shared' user in this range is 584788 (0x0008EC54).
+//
+// Since this number is within systemd-nspawn's range of 524288-1879048191
+// (0x00080000-0x6FFFFFFF), the number's lower 16 bits are not all zeroes so
+// systemd-nspawn won't detect this allocation and could potentially assign the
+// 65536 range starting at 0x00080000 to a container. snapd will therefore also
+// create the 'snapd-range-524288-root' user and group with [ug]id 524288 to
+// work within systemd-nspawn's collision detection. This user/group will not
+// be assigned to snaps at this time.
+//
+// In short (phew!), use the following:
+//
+// $ snappy-debug.id-range 524288 # 0x00080000
+// Host range: 524288-589823 (00080000-0008ffff; 0-65535)
+// LSB static range: 524288-524387 (00080000-00080063; 0-99)
+// Useradd system range: 524788-525287 (000801f4-000803e7; 500-999)
+// Useradd regular range: 525288-584288 (000803e8-0008ea60; 1000-60000)
+// Snapd system range: 584788-585287 (0008ec54-0008ee47; 60500-60999)
+// Snapd private range: 585288-589807 (0008ee48-0008ffef; 61000-65519)
+//
+// Snapd is of course free to add more ranges (eg, 589824 (0x00090000)) with
+// new snapd-range--root users, or to allocate differently within its
+// 65536 range in the future (sequentially assigned [ug]ids are not required),
+// but for now start very regimented to avoid as many problems as possible.
+//
+// References:
+// https://forum.snapcraft.io/t/multiple-users-and-groups-in-snaps/
+// https://systemd.io/UIDS-GIDS.html
+// https://docs.docker.com/engine/security/userns-remap/
+// https://github.com/lxc/lxd/blob/master/doc/userns-idmap.md
+var supportedSystemUsernames = map[string]uint32{
+ "snap_daemon": 584788,
+}
+
func checkAssumes(si *snap.Info) error {
missing := ([]string)(nil)
for _, flag := range si.Assumes {
@@ -68,6 +199,7 @@
var versionExp = regexp.MustCompile(`^([1-9][0-9]*)(?:\.([0-9]+)(?:\.([0-9]+))?)?`)
func checkVersion(version string) bool {
+ // double check that the input looks like a snapd version
req := versionExp.FindStringSubmatch(version)
if req == nil || req[0] != version {
return false
@@ -77,6 +209,10 @@
return true // Development tree.
}
+ // We could (should?) use strutil.VersionCompare here and simplify
+ // this code (see PR#7344). However this would change current
+ // behavior, i.e. "2.41~pre1" would *not* match [snapd2.41] anymore
+ // (which the code below does).
cur := versionExp.FindStringSubmatch(cmd.Version)
if cur == nil {
return false
@@ -184,7 +320,7 @@
// verify we have a valid architecture
if !arch.IsSupportedArchitecture(info.Architectures) {
- return fmt.Errorf("snap %q supported architectures (%s) are incompatible with this system (%s)", info.InstanceName(), strings.Join(info.Architectures, ", "), arch.UbuntuArchitecture())
+ return fmt.Errorf("snap %q supported architectures (%s) are incompatible with this system (%s)", info.InstanceName(), strings.Join(info.Architectures, ", "), arch.DpkgArchitecture())
}
// check assumes
@@ -192,6 +328,11 @@
return err
}
+ // check and create system-usernames
+ if err := checkAndCreateSystemUsernames(info); err != nil {
+ return err
+ }
+
return nil
}
@@ -263,6 +404,18 @@
}
}
+func checkSnapdName(st *state.State, snapInfo, curInfo *snap.Info, flags Flags, deviceCtx DeviceContext) error {
+ if snapInfo.GetType() != snap.TypeSnapd {
+ // not a relevant check
+ return nil
+ }
+ if snapInfo.InstanceName() != "snapd" {
+ return fmt.Errorf(`cannot install snap %q of type "snapd" with a name other than "snapd"`, snapInfo.InstanceName())
+ }
+
+ return nil
+}
+
func checkCoreName(st *state.State, snapInfo, curInfo *snap.Info, flags Flags, deviceCtx DeviceContext) error {
if snapInfo.GetType() != snap.TypeOS {
// not a relevant check
@@ -272,7 +425,7 @@
// already one of these installed
return nil
}
- core, err := CoreInfo(st)
+ core, err := coreInfo(st)
if err == state.ErrNoState {
return nil
}
@@ -300,30 +453,51 @@
}
func checkGadgetOrKernel(st *state.State, snapInfo, curInfo *snap.Info, flags Flags, deviceCtx DeviceContext) error {
+ typ := snapInfo.GetType()
kind := ""
- var currentInfo func(*state.State) (*snap.Info, error)
- switch snapInfo.GetType() {
+ var whichName func(*asserts.Model) string
+ switch typ {
case snap.TypeGadget:
kind = "gadget"
- currentInfo = GadgetInfo
+ whichName = (*asserts.Model).Gadget
case snap.TypeKernel:
kind = "kernel"
- currentInfo = KernelInfo
+ whichName = (*asserts.Model).Kernel
default:
// not a relevant check
return nil
}
- currentSnap, err := currentInfo(st)
+ ok, err := HasSnapOfType(st, typ)
+ if err != nil {
+ return fmt.Errorf("cannot detect original %s snap: %v", kind, err)
+ }
// in firstboot we have no gadget/kernel yet - that is ok
// first install rules are in devicestate!
- if err == state.ErrNoState {
+ if !ok {
return nil
}
+
+ currentSnap, err := infoForDeviceSnap(st, deviceCtx, kind, whichName)
+ if err == state.ErrNoState {
+ // check if we are in the remodel case
+ if deviceCtx != nil && deviceCtx.ForRemodeling() {
+ model := deviceCtx.Model()
+ if model.Kernel() == snapInfo.InstanceName() {
+ return nil
+ }
+ }
+
+ return fmt.Errorf("internal error: cannot remodel gadget yet")
+ }
if err != nil {
return fmt.Errorf("cannot find original %s snap: %v", kind, err)
}
+ if currentSnap.SnapID != "" && snapInfo.SnapID == "" {
+ return fmt.Errorf("cannot replace signed %s snap with an unasserted one", kind)
+ }
+
if currentSnap.SnapID != "" && snapInfo.SnapID != "" {
if currentSnap.SnapID == snapInfo.SnapID {
// same snap
@@ -332,10 +506,6 @@
return fmt.Errorf("cannot replace %s snap with a different one", kind)
}
- if currentSnap.SnapID != "" && snapInfo.SnapID == "" {
- return fmt.Errorf("cannot replace signed %s snap with an unasserted one", kind)
- }
-
if currentSnap.InstanceName() != snapInfo.InstanceName() {
return fmt.Errorf("cannot replace %s snap with a different one", kind)
}
@@ -410,8 +580,84 @@
return checkEpochs(nil, info, cur, Flags{}, nil)
}
+// check that the listed system users are valid
+var osutilEnsureUserGroup = osutil.EnsureUserGroup
+
+func validateSystemUsernames(si *snap.Info) error {
+ for _, user := range si.SystemUsernames {
+ if _, ok := supportedSystemUsernames[user.Name]; !ok {
+ return fmt.Errorf(`snap %q requires unsupported system username "%s"`, si.InstanceName(), user.Name)
+ }
+
+ switch user.Scope {
+ case "shared":
+ // this is supported
+ continue
+ case "private", "external":
+ // not supported yet
+ return fmt.Errorf(`snap %q requires unsupported user scope "%s" for this version of snapd`, si.InstanceName(), user.Scope)
+ default:
+ return fmt.Errorf(`snap %q requires unsupported user scope "%s"`, si.InstanceName(), user.Scope)
+ }
+ }
+ return nil
+}
+
+func checkAndCreateSystemUsernames(si *snap.Info) error {
+ // No need to check support if no system-usernames
+ if len(si.SystemUsernames) == 0 {
+ return nil
+ }
+
+ // Run /.../snap-seccomp version-info
+ vi, err := seccomp_compiler.CompilerVersionInfo(cmd.InternalToolPath)
+ if err != nil {
+ return fmt.Errorf("cannot obtain seccomp compiler information: %v", err)
+ }
+
+ // If the system doesn't support robust argument filtering then we
+ // can't support system-usernames
+ if err := vi.SupportsRobustArgumentFiltering(); err != nil {
+ if re, ok := err.(*seccomp_compiler.BuildTimeRequirementError); ok {
+ return fmt.Errorf("snap %q system usernames require a snapd built against %s", si.InstanceName(), re.RequirementsString())
+ }
+ return err
+ }
+
+ // first validate
+ if err := validateSystemUsernames(si); err != nil {
+ return err
+ }
+
+ // then create
+ // TODO: move user creation to a more appropriate place like "link-snap"
+ extrausers := !release.OnClassic
+ for _, user := range si.SystemUsernames {
+ id := supportedSystemUsernames[user.Name]
+ switch user.Scope {
+ case "shared":
+ // Create the snapd-range--root user and group so
+ // systemd-nspawn can avoid our range. Our ranges will always
+ // be in 65536 chunks, so mask off the lower bits to obtain our
+ // base (see above)
+ rangeStart := id & 0xFFFF0000
+ rangeName := fmt.Sprintf("snapd-range-%d-root", rangeStart)
+ if err := osutilEnsureUserGroup(rangeName, rangeStart, extrausers); err != nil {
+ return fmt.Errorf(`cannot ensure users for snap %q required system username "%s": %v`, si.InstanceName(), user.Name, err)
+ }
+
+ // Create the requested user and group
+ if err := osutilEnsureUserGroup(user.Name, id, extrausers); err != nil {
+ return fmt.Errorf(`cannot ensure users for snap %q required system username "%s": %v`, si.InstanceName(), user.Name, err)
+ }
+ }
+ }
+ return nil
+}
+
func init() {
AddCheckSnapCallback(checkCoreName)
+ AddCheckSnapCallback(checkSnapdName)
AddCheckSnapCallback(checkGadgetOrKernel)
AddCheckSnapCallback(checkBases)
AddCheckSnapCallback(checkEpochs)
diff -Nru snapd-2.40/overlord/snapstate/check_snap_test.go snapd-2.42.1/overlord/snapstate/check_snap_test.go
--- snapd-2.40/overlord/snapstate/check_snap_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/check_snap_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -31,19 +31,23 @@
"github.com/snapcore/snapd/arch"
"github.com/snapcore/snapd/cmd"
"github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
+ seccomp_compiler "github.com/snapcore/snapd/sandbox/seccomp"
"github.com/snapcore/snapd/snap"
"github.com/snapcore/snapd/snap/snapdir"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/testutil"
"github.com/snapcore/snapd/overlord/snapstate"
+ "github.com/snapcore/snapd/overlord/snapstate/snapstatetest"
)
type checkSnapSuite struct {
testutil.BaseTest
- st *state.State
+ st *state.State
+ deviceCtx snapstate.DeviceContext
}
var _ = Suite(&checkSnapSuite{})
@@ -53,6 +57,10 @@
dirs.SetRootDir(c.MkDir())
s.st = state.New(nil)
s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
+ s.deviceCtx = &snapstatetest.TrivialDeviceContext{DeviceModel: MakeModel(map[string]interface{}{
+ "kernel": "kernel",
+ "gadget": "gadget",
+ })}
}
func (s *checkSnapSuite) TearDownTest(c *C) {
@@ -80,7 +88,7 @@
err = snapstate.CheckSnap(s.st, "snap-path", "hello", nil, nil, snapstate.Flags{}, nil)
- errorMsg := fmt.Sprintf(`snap "hello" supported architectures (yadayada, blahblah) are incompatible with this system (%s)`, arch.UbuntuArchitecture())
+ errorMsg := fmt.Sprintf(`snap "hello" supported architectures (yadayada, blahblah) are incompatible with this system (%s)`, arch.DpkgArchitecture())
c.Assert(err.Error(), Equals, errorMsg)
}
@@ -143,6 +151,11 @@
version: "2.15.0",
error: `.* unsupported features: snapd2\.15\.1 .*`,
}, {
+ // Note that this is different from how strconv.VersionCompare
+ // (dpkg version numbering) would behave - it would error here
+ assumes: "[snapd2.15]",
+ version: "2.15~pre1",
+}, {
assumes: "[command-chain]",
}}
@@ -272,7 +285,7 @@
defer restore()
st.Unlock()
- err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}, nil)
+ err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}, s.deviceCtx)
st.Lock()
c.Check(err, IsNil)
}
@@ -314,7 +327,7 @@
defer restore()
st.Unlock()
- err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}, nil)
+ err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}, s.deviceCtx)
st.Lock()
c.Check(err, IsNil)
}
@@ -355,7 +368,7 @@
defer restore()
st.Unlock()
- err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}, nil)
+ err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}, s.deviceCtx)
st.Lock()
c.Check(err, ErrorMatches, `cannot replace signed gadget snap with an unasserted one`)
}
@@ -396,7 +409,7 @@
defer restore()
st.Unlock()
- err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}, nil)
+ err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}, s.deviceCtx)
st.Lock()
c.Check(err, ErrorMatches, "cannot replace gadget snap with a different one")
}
@@ -438,7 +451,7 @@
defer restore()
st.Unlock()
- err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}, nil)
+ err = snapstate.CheckSnap(st, "snap-path", "gadget", nil, nil, snapstate.Flags{}, s.deviceCtx)
st.Lock()
c.Check(err, ErrorMatches, "cannot replace gadget snap with a different one")
}
@@ -598,7 +611,7 @@
defer restore()
st.Unlock()
- err = snapstate.CheckSnap(st, "snap-path", "kernel", nil, nil, snapstate.Flags{}, nil)
+ err = snapstate.CheckSnap(st, "snap-path", "kernel", nil, nil, snapstate.Flags{}, s.deviceCtx)
st.Lock()
c.Check(err, IsNil)
}
@@ -640,7 +653,7 @@
defer restore()
st.Unlock()
- err = snapstate.CheckSnap(st, "snap-path", "kernel", nil, nil, snapstate.Flags{}, nil)
+ err = snapstate.CheckSnap(st, "snap-path", "kernel", nil, nil, snapstate.Flags{}, s.deviceCtx)
st.Lock()
c.Check(err, ErrorMatches, "cannot replace kernel snap with a different one")
}
@@ -883,3 +896,253 @@
st.Lock()
c.Check(err, IsNil)
}
+
+func (s *checkSnapSuite) TestCheckSnapdHappy(c *C) {
+ st := state.New(nil)
+ st.Lock()
+ defer st.Unlock()
+
+ for _, t := range []struct {
+ yaml string
+ errStr string
+ }{
+ {"name: snapd\nversion: 1\ntype: snapd", ""},
+ {"name: some-snap\nversion: 1\ntype: snapd", `cannot install snap "some-snap" of type "snapd" with a name other than "snapd"`},
+ {"name: snapd_instance\nversion: 1\ntype: snapd", `cannot install snap "snapd_instance" of type "snapd" with a name other than "snapd"`},
+ } {
+ info, err := snap.InfoFromSnapYaml([]byte(t.yaml))
+ c.Assert(err, IsNil)
+
+ var openSnapFile = func(path string, si *snap.SideInfo) (*snap.Info, snap.Container, error) {
+ return info, emptyContainer(c), nil
+ }
+ restore := snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ st.Unlock()
+ err = snapstate.CheckSnap(st, "snap-path", "snapd", nil, nil, snapstate.Flags{}, nil)
+ st.Lock()
+ if t.errStr == "" {
+ c.Check(err, IsNil)
+ } else {
+ c.Check(err, ErrorMatches, t.errStr)
+ }
+ }
+}
+
+// Note, invalid usernames checked in snap/info_snap_yaml.go
+var systemUsernamesTests = []struct {
+ sysIDs string
+ classic bool
+ noRangeUser bool
+ noUser bool
+ scVer string
+ error string
+}{{
+ sysIDs: "snap_daemon: shared",
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+}, {
+ sysIDs: "snap_daemon:\n scope: shared",
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+}, {
+ sysIDs: "snap_daemon:\n scope: private",
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ error: `snap "foo" requires unsupported user scope "private" for this version of snapd`,
+}, {
+ sysIDs: "snap_daemon:\n scope: external",
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ error: `snap "foo" requires unsupported user scope "external" for this version of snapd`,
+}, {
+ sysIDs: "snap_daemon:\n scope: other",
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ error: `snap "foo" requires unsupported user scope "other"`,
+}, {
+ sysIDs: "snap_daemon: shared",
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ classic: true,
+}, {
+ sysIDs: "snap_daemon:\n scope: shared",
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ classic: true,
+}, {
+ sysIDs: "snap_daemon:\n scope: private",
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ classic: true,
+ error: `snap "foo" requires unsupported user scope "private" for this version of snapd`,
+}, {
+ sysIDs: "snap_daemon:\n scope: external",
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ classic: true,
+ error: `snap "foo" requires unsupported user scope "external" for this version of snapd`,
+}, {
+ sysIDs: "snap_daemon:\n scope: other",
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ classic: true,
+ error: `snap "foo" requires unsupported user scope "other"`,
+}, {
+ sysIDs: "snap_daemon: shared\n allowed-not: shared",
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ error: `snap "foo" requires unsupported system username "allowed-not"`,
+}, {
+ sysIDs: "allowed-not: shared\n snap_daemon: shared",
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ classic: true,
+ error: `snap "foo" requires unsupported system username "allowed-not"`,
+}, {
+ sysIDs: "snap_daemon: shared",
+ noUser: true,
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ error: `cannot ensure users for snap "foo" required system username "snap_daemon": cannot add user/group "snap_daemon", group exists and user does not`,
+}, {
+ sysIDs: "snap_daemon: shared",
+ classic: true,
+ noUser: true,
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ error: `cannot ensure users for snap "foo" required system username "snap_daemon": cannot add user/group "snap_daemon", group exists and user does not`,
+}, {
+ sysIDs: "snap_daemon: shared",
+ scVer: "dead 2.3.3 deadbeef bpf-actlog",
+ error: `snap "foo" system usernames require a snapd built against libseccomp >= 2.4`,
+}, {
+ sysIDs: "snap_daemon: shared",
+ classic: true,
+ scVer: "dead 2.3.3 deadbeef bpf-actlog",
+ error: `snap "foo" system usernames require a snapd built against libseccomp >= 2.4`,
+}, {
+ sysIDs: "snap_daemon: shared",
+ scVer: "dead 3.0.0 deadbeef bpf-actlog",
+}, {
+ sysIDs: "snap_daemon: shared",
+ classic: true,
+ scVer: "dead 3.0.0 deadbeef bpf-actlog",
+}, {
+ sysIDs: "snap_daemon: shared",
+ scVer: "dead 2.4.1 deadbeef -",
+ error: `snap "foo" system usernames require a snapd built against golang-seccomp >= 0.9.1`,
+}, {
+ sysIDs: "snap_daemon: shared",
+ classic: true,
+ scVer: "dead 2.4.1 deadbeef -",
+ error: `snap "foo" system usernames require a snapd built against golang-seccomp >= 0.9.1`,
+}, {
+ sysIDs: "snap_daemon: shared",
+ noRangeUser: true,
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ error: `cannot ensure users for snap "foo" required system username "snap_daemon": cannot add user/group "snapd-range-524288-root", group exists and user does not`,
+}, {
+ sysIDs: "snap_daemon: shared",
+ classic: true,
+ noRangeUser: true,
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ error: `cannot ensure users for snap "foo" required system username "snap_daemon": cannot add user/group "snapd-range-524288-root", group exists and user does not`,
+}, {
+ sysIDs: "snap_daemon: shared\n daemon: shared",
+ classic: true,
+ scVer: "dead 2.4.1 deadbeef bpf-actlog",
+ error: `snap "foo" requires unsupported system username "daemon"`,
+},
+}
+
+func (s *checkSnapSuite) TestCheckSnapSystemUsernames(c *C) {
+ for _, test := range systemUsernamesTests {
+ restore := seccomp_compiler.MockCompilerVersionInfo(test.scVer)
+ defer restore()
+
+ restore = release.MockOnClassic(test.classic)
+ defer restore()
+
+ var osutilEnsureUserGroupCalls int
+ if test.noRangeUser {
+ restore = snapstate.MockOsutilEnsureUserGroup(func(name string, id uint32, extraUsers bool) error {
+ return fmt.Errorf(`cannot add user/group "%s", group exists and user does not`, name)
+ })
+ } else if test.noUser {
+ restore = snapstate.MockOsutilEnsureUserGroup(func(name string, id uint32, extraUsers bool) error {
+ if name == "snapd-range-524288-root" {
+ return nil
+ }
+ return fmt.Errorf(`cannot add user/group "%s", group exists and user does not`, name)
+ })
+ } else {
+ restore = snapstate.MockOsutilEnsureUserGroup(func(name string, id uint32, extraUsers bool) error {
+ osutilEnsureUserGroupCalls++
+ return nil
+ })
+ }
+ defer restore()
+
+ yaml := fmt.Sprintf("name: foo\nsystem-usernames:\n %s\n", test.sysIDs)
+
+ 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, emptyContainer(c), nil
+ }
+ restore = snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+ err = snapstate.CheckSnap(s.st, "snap-path", "foo", nil, nil, snapstate.Flags{}, nil)
+ if test.error != "" {
+ c.Check(err, ErrorMatches, test.error)
+ c.Check(osutilEnsureUserGroupCalls, Equals, 0)
+ } else {
+ c.Assert(err, IsNil)
+ // one call for the range user, one for the system user
+ c.Check(osutilEnsureUserGroupCalls, Equals, 2)
+ }
+ }
+}
+
+func (s *checkSnapSuite) TestCheckSnapSystemUsernamesCalls(c *C) {
+ falsePath := osutil.LookPathDefault("false", "/bin/false")
+ for _, classic := range []bool{false, true} {
+ restore := release.MockOnClassic(classic)
+ defer restore()
+
+ restore = seccomp_compiler.MockCompilerVersionInfo("dead 2.4.1 deadbeef bpf-actlog")
+ defer restore()
+
+ const yaml = `name: foo
+version: 1.0
+system-usernames:
+ snap_daemon: shared`
+
+ 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, emptyContainer(c), nil
+ }
+ restore = snapstate.MockOpenSnapFile(openSnapFile)
+ defer restore()
+
+ mockGroupAdd := testutil.MockCommand(c, "groupadd", "")
+ defer mockGroupAdd.Restore()
+
+ mockUserAdd := testutil.MockCommand(c, "useradd", "")
+ defer mockUserAdd.Restore()
+
+ err = snapstate.CheckSnap(s.st, "snap-path", "foo", nil, nil, snapstate.Flags{}, nil)
+ c.Assert(err, IsNil)
+ if classic {
+ c.Check(mockGroupAdd.Calls(), DeepEquals, [][]string{
+ {"groupadd", "--system", "--gid", "524288", "snapd-range-524288-root"},
+ {"groupadd", "--system", "--gid", "584788", "snap_daemon"},
+ })
+ c.Check(mockUserAdd.Calls(), DeepEquals, [][]string{
+ {"useradd", "--system", "--home-dir", "/nonexistent", "--no-create-home", "--shell", falsePath, "--gid", "524288", "--no-user-group", "--uid", "524288", "snapd-range-524288-root"},
+ {"useradd", "--system", "--home-dir", "/nonexistent", "--no-create-home", "--shell", falsePath, "--gid", "584788", "--no-user-group", "--uid", "584788", "snap_daemon"},
+ })
+ } else {
+ c.Check(mockGroupAdd.Calls(), DeepEquals, [][]string{
+ {"groupadd", "--system", "--gid", "524288", "--extrausers", "snapd-range-524288-root"},
+ {"groupadd", "--system", "--gid", "584788", "--extrausers", "snap_daemon"},
+ })
+ c.Check(mockUserAdd.Calls(), DeepEquals, [][]string{
+ {"useradd", "--system", "--home-dir", "/nonexistent", "--no-create-home", "--shell", falsePath, "--gid", "524288", "--no-user-group", "--uid", "524288", "--extrausers", "snapd-range-524288-root"},
+ {"useradd", "--system", "--home-dir", "/nonexistent", "--no-create-home", "--shell", falsePath, "--gid", "584788", "--no-user-group", "--uid", "584788", "--extrausers", "snap_daemon"},
+ })
+
+ }
+ }
+}
diff -Nru snapd-2.40/overlord/snapstate/conflict.go snapd-2.42.1/overlord/snapstate/conflict.go
--- snapd-2.40/overlord/snapstate/conflict.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/conflict.go 2019-10-30 12:17:43.000000000 +0000
@@ -26,6 +26,11 @@
"github.com/snapcore/snapd/overlord/state"
)
+// FinalTasks are task kinds for final tasks in a change which means no further
+// change work should be performed afterward, usually these are tasks that
+// commit a full system transition.
+var FinalTasks = []string{"mark-seeded", "set-model"}
+
// ChangeConflictError represents an error because of snap conflicts between changes.
type ChangeConflictError struct {
Snap string
@@ -104,6 +109,9 @@
if chg.Kind() == "transition-ubuntu-core" {
return &ChangeConflictError{Message: "ubuntu-core to core transition in progress, no other changes allowed until this is done", ChangeKind: "transition-ubuntu-core"}
}
+ if chg.Kind() == "transition-to-snapd-snap" {
+ return &ChangeConflictError{Message: "transition to snapd snap in progress, no other changes allowed until this is done", ChangeKind: "transition-to-snapd-snap"}
+ }
if chg.Kind() == "remodel" {
if ignoreChangeID != "" && chg.ID() == ignoreChangeID {
continue
diff -Nru snapd-2.40/overlord/snapstate/export_test.go snapd-2.42.1/overlord/snapstate/export_test.go
--- snapd-2.40/overlord/snapstate/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -72,13 +72,19 @@
return func() { prerequisitesRetryTimeout = old }
}
+func MockOsutilEnsureUserGroup(mock func(name string, id uint32, extraUsers bool) error) (restore func()) {
+ old := osutilEnsureUserGroup
+ osutilEnsureUserGroup = mock
+ return func() { osutilEnsureUserGroup = old }
+}
+
var (
+ CoreInfoInternal = coreInfo
CheckSnap = checkSnap
CanRemove = canRemove
CanDisable = canDisable
CachedStore = cachedStore
DefaultRefreshSchedule = defaultRefreshSchedule
- NameAndRevnoFromSnap = nameAndRevnoFromSnap
DoInstall = doInstall
UserFromUserID = userFromUserID
ValidateFeatureFlags = validateFeatureFlags
diff -Nru snapd-2.40/overlord/snapstate/handlers.go snapd-2.42.1/overlord/snapstate/handlers.go
--- snapd-2.40/overlord/snapstate/handlers.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/handlers.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,6 +20,7 @@
package snapstate
import (
+ "context"
"encoding/json"
"fmt"
"os"
@@ -207,7 +208,7 @@
return nil
}
// snapd is special and has no prereqs
- if snapsup.InstanceName() == "snapd" {
+ if snapsup.Type == snap.TypeSnapd {
return nil
}
@@ -253,7 +254,7 @@
}
// not installed, nor queued for install -> install it
- ts, err := Install(st, snapName, &RevisionOptions{Channel: channel}, userID, Flags{RequireTypeBase: requireTypeBase})
+ ts, err := Install(context.TODO(), st, snapName, &RevisionOptions{Channel: channel}, userID, Flags{RequireTypeBase: requireTypeBase})
// something might have triggered an explicit install while
// the state was unlocked -> deal with that here by simply
@@ -469,7 +470,7 @@
st.Lock()
defer st.Unlock()
opts := &RevisionOptions{Channel: snapsup.Channel, CohortKey: snapsup.CohortKey, Revision: snapsup.Revision()}
- return installInfo(st, snapsup.InstanceName(), opts, snapsup.UserID, deviceCtx)
+ return installInfo(context.TODO(), st, snapsup.InstanceName(), opts, snapsup.UserID, deviceCtx)
}
// autoRefreshRateLimited returns the rate limit of auto-refreshes or 0 if
@@ -770,11 +771,6 @@
}
}
- // Make a copy of configuration of given snap revision
- if err = config.SaveRevisionConfig(st, snapsup.InstanceName(), snapst.Current); err != nil {
- return err
- }
-
snapst.Active = false
pb := NewTaskProgressAdapterLocked(t)
@@ -977,6 +973,9 @@
return err
}
+ // find if the snap is already installed before we modify snapst below
+ isInstalled := snapst.IsInstalled()
+
cand := snapsup.SideInfo
m.backend.Candidate(cand)
@@ -1055,7 +1054,15 @@
return err
}
- // Restore configuration of the target revision (if available) on revert
+ if isInstalled {
+ // Make a copy of configuration of current snap revision
+ if err = config.SaveRevisionConfig(st, snapsup.InstanceName(), oldCurrent); err != nil {
+ return err
+ }
+ }
+
+ // Restore configuration of the target revision (if available; nothing happens if it's not).
+ // We only do this on reverts (and not on refreshes).
if snapsup.Revert {
if err = config.RestoreRevisionConfig(st, snapsup.InstanceName(), snapsup.Revision()); err != nil {
return err
@@ -1147,36 +1154,53 @@
func maybeRestart(t *state.Task, info *snap.Info) {
st := t.State()
- if release.OnClassic {
- // ignore error here as we have no way to return to caller
- snapdSnapInstalled, _ := isInstalled(st, "snapd")
- if (info.GetType() == snap.TypeOS && !snapdSnapInstalled) ||
- info.InstanceName() == "snapd" {
- t.Logf("Requested daemon restart.")
- st.RequestRestart(state.RestartDaemon)
- }
+ model, err := ModelFromTask(t)
+ if err != nil {
+ logger.Noticef("cannot get model assertion: %v", model)
return
}
- // On a core system we may need a full reboot if
- // core/base or the kernel changes.
- if boot.ChangeRequiresReboot(info) {
+ typ := info.GetType()
+ bp := boot.Participant(info, typ, model, release.OnClassic)
+ if bp.ChangeRequiresReboot() {
t.Logf("Requested system restart.")
st.RequestRestart(state.RestartSystem)
return
}
- // On core systems that use a base snap we need to restart
- // snapd when the snapd snap changes.
- model, err := ModelFromTask(t)
- if err != nil {
- logger.Noticef("cannot get model assertion: %v", model)
+ // if bp is non-trivial then either we're not on classic, or the snap is
+ // snapd. So daemonRestartReason will always return "" which is what we
+ // want. If that combination stops being true and there's a situation
+ // where a non-trivial bp could return a non-empty reason, use IsTrivial
+ // to check and bail before reaching this far.
+
+ restartReason := daemonRestartReason(st, typ)
+ if restartReason == "" {
+ // no message -> no restart
return
}
- if model.Base() != "" && info.InstanceName() == "snapd" {
- t.Logf("Requested daemon restart (snapd snap).")
- st.RequestRestart(state.RestartDaemon)
+
+ t.Logf(restartReason)
+ st.RequestRestart(state.RestartDaemon)
+}
+
+func daemonRestartReason(st *state.State, typ snap.Type) string {
+ if !((release.OnClassic && typ == snap.TypeOS) || typ == snap.TypeSnapd) {
+ // not interesting
+ return ""
}
+
+ if typ == snap.TypeOS {
+ // ignore error here as we have no way to return to caller
+ snapdSnapInstalled, _ := isInstalled(st, "snapd")
+ if snapdSnapInstalled {
+ // this snap is the base, but snapd is running from the snapd snap
+ return ""
+ }
+ return "Requested daemon restart."
+ }
+
+ return "Requested daemon restart (snapd snap)."
}
func (m *SnapManager) undoLinkSnap(t *state.Task, _ *tomb.Tomb) error {
@@ -1289,6 +1313,8 @@
return err
}
+ // we need to undo potential changes to current snap configuration (e.g. if modified by post-refresh/install/configure hooks
+ // as part of failed refresh/install) by restoring the configuration of "old current".
if len(snapst.Sequence) > 0 {
if err = config.RestoreRevisionConfig(st, snapsup.InstanceName(), oldCurrent); err != nil {
return err
diff -Nru snapd-2.40/overlord/snapstate/handlers_link_test.go snapd-2.42.1/overlord/snapstate/handlers_link_test.go
--- snapd-2.40/overlord/snapstate/handlers_link_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/handlers_link_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -67,6 +67,7 @@
s.setup(c, s.stateBackend)
s.AddCleanup(snapstatetest.MockDeviceModel(DefaultModel()))
+ s.AddCleanup(snap.MockSnapdSnapID("snapd-snap-id"))
}
func checkHasCookieForSnap(c *C, st *state.State, instanceName string) {
@@ -386,7 +387,7 @@
c.Assert(err, Equals, state.ErrNoState)
// tried to cleanup
- c.Check(s.fakeBackend.ops, DeepEquals, fakeOps{
+ expected := fakeOps{
{
op: "candidate",
sinfo: *si,
@@ -399,7 +400,11 @@
op: "unlink-snap",
path: filepath.Join(dirs.SnapMountDir, "foo/35"),
},
- })
+ }
+
+ // start with an easier-to-read error if this fails:
+ c.Check(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Check(s.fakeBackend.ops, DeepEquals, expected)
}
func (s *linkSnapSuite) TestDoLinkSnapSuccessCoreRestarts(c *C) {
@@ -449,6 +454,7 @@
s.state.Lock()
si := &snap.SideInfo{
RealName: "snapd",
+ SnapID: "snapd-snap-id",
Revision: snap.R(22),
}
t := s.state.NewTask("link-snap", "test")
@@ -471,7 +477,7 @@
typ, err := snapst.Type()
c.Check(err, IsNil)
- c.Check(typ, Equals, snap.TypeApp)
+ c.Check(typ, Equals, snap.TypeSnapd)
c.Check(t.Status(), Equals, state.DoneStatus)
c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartDaemon})
@@ -486,6 +492,7 @@
s.state.Lock()
si := &snap.SideInfo{
RealName: "snapd",
+ SnapID: "snapd-snap-id",
Revision: snap.R(22),
}
t := s.state.NewTask("link-snap", "test")
@@ -508,7 +515,7 @@
typ, err := snapst.Type()
c.Check(err, IsNil)
- c.Check(typ, Equals, snap.TypeApp)
+ c.Check(typ, Equals, snap.TypeSnapd)
c.Check(t.Status(), Equals, state.DoneStatus)
c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartDaemon})
diff -Nru snapd-2.40/overlord/snapstate/handlers_prepare_test.go snapd-2.42.1/overlord/snapstate/handlers_prepare_test.go
--- snapd-2.40/overlord/snapstate/handlers_prepare_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/handlers_prepare_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -60,6 +60,7 @@
s.se = overlord.NewStateEngine(s.state)
s.se.AddManager(s.snapmgr)
s.se.AddManager(s.runner)
+ c.Assert(s.se.StartUp(), IsNil)
AddForeignTaskHandlers(s.runner, s.fakeBackend)
diff -Nru snapd-2.40/overlord/snapstate/handlers_prereq_test.go snapd-2.42.1/overlord/snapstate/handlers_prereq_test.go
--- snapd-2.40/overlord/snapstate/handlers_prereq_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/handlers_prereq_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -406,6 +406,8 @@
s.state.Lock()
t := s.state.NewTask("prerequisites", "test")
t.Set("snap-setup", &snapstate.SnapSetup{
+ // type is normally set from snap info at install time
+ Type: snap.TypeSnapd,
SideInfo: &snap.SideInfo{
RealName: "snapd",
Revision: snap.R(1),
diff -Nru snapd-2.40/overlord/snapstate/models_test.go snapd-2.42.1/overlord/snapstate/models_test.go
--- snapd-2.40/overlord/snapstate/models_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/models_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -54,3 +54,16 @@
}
return assertstest.FakeAssertion(model, override).(*asserts.Model)
}
+
+func ClassicModel() *asserts.Model {
+ headers := map[string]interface{}{
+ "type": "model",
+ "authority-id": "brand",
+ "series": "16",
+ "brand-id": "brand",
+ "model": "classicbaz-3000",
+ "classic": "true",
+ "timestamp": "2018-01-01T08:00:00+00:00",
+ }
+ return assertstest.FakeAssertion(headers).(*asserts.Model)
+}
diff -Nru snapd-2.40/overlord/snapstate/progress.go snapd-2.42.1/overlord/snapstate/progress.go
--- snapd-2.40/overlord/snapstate/progress.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/progress.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,6 +20,8 @@
package snapstate
import (
+ "math"
+
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/progress"
)
@@ -32,6 +34,8 @@
label string
total float64
current float64
+
+ lastReported float64
}
// NewTaskProgressAdapterUnlocked creates an adapter of the task into a progress.Meter to use while the state is unlocked
@@ -48,10 +52,23 @@
func (t *taskProgressAdapter) Start(label string, total float64) {
t.label = label
t.total = total
+ t.Set(0.0)
}
// Set sets the current progress
func (t *taskProgressAdapter) Set(current float64) {
+ t.current = current
+
+ // check if we made at least "minProgress" before we lock the state
+ // (using Abs to ensure that even if lastReported is smaller than
+ // current we still report progress)
+ const minProgress = 0.2 / 100.0
+ if current != 0.0 && math.Abs(t.current-t.lastReported)/t.total < minProgress {
+ return
+ }
+
+ t.lastReported = t.current
+ // set progress in task
if t.unlocked {
t.task.State().Lock()
defer t.task.State().Unlock()
@@ -75,13 +92,7 @@
// Write sets the current write progress
func (t *taskProgressAdapter) Write(p []byte) (n int, err error) {
- if t.unlocked {
- t.task.State().Lock()
- defer t.task.State().Unlock()
- }
-
- t.current += float64(len(p))
- t.task.SetProgress(t.label, int(t.current), int(t.total))
+ t.Set(t.current + float64(len(p)))
return len(p), nil
}
diff -Nru snapd-2.40/overlord/snapstate/progress_test.go snapd-2.42.1/overlord/snapstate/progress_test.go
--- snapd-2.40/overlord/snapstate/progress_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/progress_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -56,3 +56,35 @@
m.Write([]byte("some-bytes"))
c.Check(p.current, Equals, float64(len("some-bytes")))
}
+
+func (s *progressAdapterTestSuite) TestProgressAdapterSetTaskProgress(c *C) {
+ st := state.New(nil)
+
+ st.Lock()
+ t := st.NewTask("op", "msg")
+ m := NewTaskProgressAdapterUnlocked(t)
+ st.Unlock()
+
+ // we expect 1000 bytes
+ m.Start("msg", 1000)
+
+ // write a single byte (0.1% of the toal)
+ m.Write([]byte("1"))
+
+ // check that the progress is not updated yet
+ st.Lock()
+ msg, done, total := t.Progress()
+ st.Unlock()
+ c.Check(msg, Equals, "msg")
+ c.Check(done, Equals, 0)
+ c.Check(total, Equals, 1000)
+
+ // write another byte (0.2% now)
+ m.Write([]byte("2"))
+ // now the progress in the task gets updated (we update every 0.2%)
+ st.Lock()
+ _, done, total = t.Progress()
+ st.Unlock()
+ c.Check(done, Equals, 2)
+ c.Check(total, Equals, 1000)
+}
diff -Nru snapd-2.40/overlord/snapstate/snapmgr.go snapd-2.42.1/overlord/snapstate/snapmgr.go
--- snapd-2.40/overlord/snapstate/snapmgr.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/snapmgr.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,9 +20,11 @@
package snapstate
import (
+ "context"
"errors"
"fmt"
"io"
+ "math/rand"
"os"
"strings"
"time"
@@ -42,6 +44,10 @@
"github.com/snapcore/snapd/strutil"
)
+var (
+ snapdTransitionDelayWithRandomess = 3*time.Hour + time.Duration(rand.Int63n(int64(4*time.Hour)))
+)
+
// overridden in the tests
var errtrackerReport = errtracker.Report
@@ -415,9 +421,19 @@
// control serialisation
runner.AddBlocked(m.blockedTask)
+ return m, nil
+}
+
+// StartUp implements StateStarterUp.Startup.
+func (m *SnapManager) StartUp() error {
writeSnapReadme()
- return m, nil
+ m.state.Lock()
+ defer m.state.Unlock()
+ if err := m.SyncCookies(m.state); err != nil {
+ return fmt.Errorf("failed to generate cookies: %q", err)
+ }
+ return nil
}
func (m *SnapManager) CanStandby() bool {
@@ -530,6 +546,110 @@
return nil
}
+// changeInFlight returns true if there is any change in the state
+// in non-ready state.
+func changeInFlight(st *state.State) bool {
+ for _, chg := range st.Changes() {
+ if !chg.IsReady() {
+ // another change already in motion
+ return true
+ }
+ }
+ return false
+}
+
+// ensureSnapdSnapTransition will migrate systems to use the "snapd" snap
+func (m *SnapManager) ensureSnapdSnapTransition() error {
+ m.state.Lock()
+ defer m.state.Unlock()
+
+ // we only auto-transition people on classic systems, for core we
+ // will need to do a proper re-model
+ if !release.OnClassic {
+ return nil
+ }
+
+ // check if snapd snap is installed
+ var snapst SnapState
+ err := Get(m.state, "snapd", &snapst)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ // nothing to do
+ if snapst.IsInstalled() {
+ return nil
+ }
+
+ // check if the user opts into the snapd snap
+ optedIntoSnapdTransition, err := optedIntoSnapdSnap(m.state)
+ if err != nil {
+ return err
+ }
+ // nothing to do: the user does not want the snapd snap yet
+ if !optedIntoSnapdTransition {
+ return nil
+ }
+
+ // ensure we only transition systems that have snaps already
+ installedSnaps, err := NumSnaps(m.state)
+ if err != nil {
+ return err
+ }
+ // no installed snaps (yet): do nothing (fresh classic install)
+ if installedSnaps == 0 {
+ return nil
+ }
+
+ // get current core snap and use same channel/user for the snapd snap
+ err = Get(m.state, "core", &snapst)
+ // Note that state.ErrNoState should never happen in practise. However
+ // if it *does* happen we still want to fix those systems by installing
+ // the snapd snap.
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ coreChannel := snapst.Channel
+ // snapd/core are never blocked on auth so we don't need to copy
+ // the userID from the snapst here
+ userID := 0
+
+ if changeInFlight(m.state) {
+ // check that there is no change in flight already, this is a
+ // precaution to ensure the snapd transition is safe
+ return nil
+ }
+
+ // ensure we limit the retries in case something goes wrong
+ var lastSnapdTransitionAttempt time.Time
+ err = m.state.Get("snapd-transition-last-retry-time", &lastSnapdTransitionAttempt)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ now := time.Now()
+ if !lastSnapdTransitionAttempt.IsZero() && lastSnapdTransitionAttempt.Add(snapdTransitionDelayWithRandomess).After(now) {
+ return nil
+ }
+ m.state.Set("snapd-transition-last-retry-time", now)
+
+ var retryCount int
+ err = m.state.Get("snapd-transition-retry", &retryCount)
+ if err != nil && err != state.ErrNoState {
+ return err
+ }
+ m.state.Set("snapd-transition-retry", retryCount+1)
+
+ ts, err := Install(context.Background(), m.state, "snapd", &RevisionOptions{Channel: coreChannel}, userID, Flags{})
+ if err != nil {
+ return err
+ }
+
+ msg := i18n.G("Transition to the snapd snap")
+ chg := m.state.NewChange("transition-to-snapd-snap", msg)
+ chg.AddAll(ts)
+
+ return nil
+}
+
// ensureUbuntuCoreTransition will migrate systems that use "ubuntu-core"
// to the new "core" snap
func (m *SnapManager) ensureUbuntuCoreTransition() error {
@@ -547,11 +667,9 @@
// check that there is no change in flight already, this is a
// precaution to ensure the core transition is safe
- for _, chg := range m.state.Changes() {
- if !chg.Status().Ready() {
- // another change already in motion
- return nil
- }
+ if changeInFlight(m.state) {
+ // another change already in motion
+ return nil
}
// ensure we limit the retries in case something goes wrong
@@ -676,6 +794,7 @@
m.ensureAliasesV2(),
m.ensureForceDevmodeDropsDevmodeFromState(),
m.ensureUbuntuCoreTransition(),
+ m.ensureSnapdSnapTransition(),
// we should check for full regular refreshes before
// considering issuing a hint only refresh request
m.autoRefresh.Ensure(),
diff -Nru snapd-2.40/overlord/snapstate/snapstate.go snapd-2.42.1/overlord/snapstate/snapstate.go
--- snapd-2.40/overlord/snapstate/snapstate.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/snapstate.go 2019-10-30 12:17:43.000000000 +0000
@@ -30,6 +30,7 @@
"strings"
"time"
+ "github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/boot"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/features"
@@ -44,6 +45,7 @@
"github.com/snapcore/snapd/overlord/state"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/channel"
"github.com/snapcore/snapd/store"
"github.com/snapcore/snapd/strutil"
)
@@ -76,6 +78,15 @@
return fmt.Errorf("cannot install snap of type %v as %q", snapsup.Type, snapsup.InstanceName())
}
+func optedIntoSnapdSnap(st *state.State) (bool, error) {
+ tr := config.NewTransaction(st)
+ experimentalAllowSnapd, err := config.GetFeatureFlag(tr, features.SnapdSnap)
+ if err != nil && !config.IsNoOption(err) {
+ return false, err
+ }
+ return experimentalAllowSnapd, nil
+}
+
func doInstall(st *state.State, snapst *SnapState, snapsup *SnapSetup, flags int, fromChange string) (*state.TaskSet, error) {
// NB: we should strive not to need or propagate deviceCtx
// here, the resulting effects/changes were not pleasant at
@@ -89,7 +100,6 @@
if snapsup.InstanceName() == "system" {
return nil, fmt.Errorf("cannot install reserved snap name 'system'")
}
-
if snapst.IsInstalled() && !snapst.Active {
return nil, fmt.Errorf("cannot update disabled snap %q", snapsup.InstanceName())
}
@@ -339,12 +349,16 @@
}
// we do not support configuration for bases or the "snapd" snap yet
- if snapsup.Type != snap.TypeBase && snapsup.InstanceName() != "snapd" {
+ if snapsup.Type != snap.TypeBase && snapsup.Type != snap.TypeSnapd {
configSet := ConfigureSnap(st, snapsup.InstanceName(), confFlags)
configSet.WaitAll(ts)
ts.AddAll(configSet)
}
+ healthCheck := CheckHealthHook(st, snapsup.InstanceName(), snapsup.Revision())
+ healthCheck.WaitAll(ts)
+ ts.AddTask(healthCheck)
+
return ts, nil
}
@@ -380,6 +394,10 @@
panic("internal error: snapstate.SetupRemoveHook is unset")
}
+var CheckHealthHook = func(st *state.State, snapName string, rev snap.Revision) *state.Task {
+ panic("internal error: snapstate.CheckHealthHook is unset")
+}
+
// WaitRestart will return a Retry error if there is a pending restart
// and a real error if anything went wrong (like a rollback across
// restarts)
@@ -395,7 +413,7 @@
return err
}
- if snapsup.InstanceName() == "snapd" && os.Getenv("SNAPD_REVERT_TO_REV") != "" {
+ if snapsup.Type == snap.TypeSnapd && os.Getenv("SNAPD_REVERT_TO_REV") != "" {
return fmt.Errorf("there was a snapd rollback across the restart")
}
@@ -427,15 +445,15 @@
return nil
}
- name, rev, err := CurrentBootNameAndRevision(typ)
- if err == ErrBootNameAndRevisionAgain {
+ current, err := boot.GetCurrentBoot(typ)
+ if err == boot.ErrBootNameAndRevisionNotReady {
return &state.Retry{After: 5 * time.Second}
}
if err != nil {
return err
}
- if snapsup.InstanceName() != name || snapInfo.Revision != rev {
+ if snapsup.InstanceName() != current.Name || snapInfo.Revision != current.Revision {
// TODO: make sure this revision gets ignored for
// automatic refreshes
return fmt.Errorf("cannot finish %s installation, there was a rollback across reboot", snapsup.InstanceName())
@@ -528,17 +546,9 @@
func checkInstallPreconditions(st *state.State, info *snap.Info, flags Flags, snapst *SnapState, deviceCtx DeviceContext) error {
// Check if the snapd can be installed on Ubuntu Core systems, it is
// always ok to install on classic
- if info.InstanceName() == "snapd" && !release.OnClassic {
- tr := config.NewTransaction(st)
- experimentalAllowSnapd, err := config.GetFeatureFlag(tr, features.SnapdSnap)
- if err != nil && !config.IsNoOption(err) {
- return err
- }
-
+ if info.GetType() == snap.TypeSnapd && !release.OnClassic {
if deviceCtx.Model().Base() == "" {
- if !experimentalAllowSnapd {
- return fmt.Errorf("cannot install snapd snap on a model without a base snap yet")
- }
+ return fmt.Errorf("cannot install snapd snap on a model without a base snap yet")
}
}
@@ -658,8 +668,8 @@
// Note that the state must be locked by the caller.
//
// The returned TaskSet will contain a DownloadAndChecksDoneEdge.
-func Install(st *state.State, name string, opts *RevisionOptions, userID int, flags Flags) (*state.TaskSet, error) {
- return InstallWithDeviceContext(st, name, opts, userID, flags, nil)
+func Install(ctx context.Context, st *state.State, name string, opts *RevisionOptions, userID int, flags Flags) (*state.TaskSet, error) {
+ return InstallWithDeviceContext(ctx, st, name, opts, userID, flags, nil, "")
}
// InstallWithDeviceContext returns a set of tasks for installing a snap.
@@ -667,7 +677,7 @@
// Note that the state must be locked by the caller.
//
// The returned TaskSet will contain a DownloadAndChecksDoneEdge.
-func InstallWithDeviceContext(st *state.State, name string, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext) (*state.TaskSet, error) {
+func InstallWithDeviceContext(ctx context.Context, st *state.State, name string, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext, fromChange string) (*state.TaskSet, error) {
if opts == nil {
opts = &RevisionOptions{}
}
@@ -687,7 +697,6 @@
if snapst.IsInstalled() {
return nil, &snap.AlreadyInstalledError{Snap: name}
}
-
// need to have a model set before trying to talk the store
deviceCtx, err = DevicePastSeeding(st, deviceCtx)
if err != nil {
@@ -698,7 +707,7 @@
return nil, fmt.Errorf("invalid instance name: %v", err)
}
- info, err := installInfo(st, name, opts, userID, deviceCtx)
+ info, err := installInfo(ctx, st, name, opts, userID, deviceCtx)
if err != nil {
return nil, err
}
@@ -733,7 +742,7 @@
CohortKey: opts.CohortKey,
}
- return doInstall(st, &snapst, snapsup, 0, "")
+ return doInstall(st, &snapst, snapsup, 0, fromChange)
}
// InstallMany installs everything from the given list of names.
@@ -919,7 +928,7 @@
if len(mustPruneAutoAliases) != 0 {
var err error
- pruningAutoAliasesTs, err = applyAutoAliasesDelta(st, mustPruneAutoAliases, "prune", refreshAll, func(snapName string, _ *state.TaskSet) {
+ pruningAutoAliasesTs, err = applyAutoAliasesDelta(st, mustPruneAutoAliases, "prune", refreshAll, fromChange, func(snapName string, _ *state.TaskSet) {
if nameSet[snapName] {
reportUpdated[snapName] = true
}
@@ -1006,7 +1015,7 @@
// because of the sorting of updates we fill prereqs
// first (if branch) and only then use it to setup
// waits (else branch)
- if update.GetType() == snap.TypeOS || update.GetType() == snap.TypeBase || update.InstanceName() == "snapd" {
+ if t := update.GetType(); t == snap.TypeOS || t == snap.TypeBase || t == snap.TypeSnapd {
// prereq types come first in updates, we
// also assume bases don't have hooks, otherwise
// they would need to wait on core or snapd
@@ -1027,7 +1036,7 @@
}
if len(newAutoAliases) != 0 {
- addAutoAliasesTs, err := applyAutoAliasesDelta(st, newAutoAliases, "refresh", refreshAll, scheduleUpdate)
+ addAutoAliasesTs, err := applyAutoAliasesDelta(st, newAutoAliases, "refresh", refreshAll, fromChange, scheduleUpdate)
if err != nil {
return nil, nil, err
}
@@ -1054,7 +1063,7 @@
return updated, tasksets, nil
}
-func applyAutoAliasesDelta(st *state.State, delta map[string][]string, op string, refreshAll bool, linkTs func(instanceName string, ts *state.TaskSet)) (*state.TaskSet, error) {
+func applyAutoAliasesDelta(st *state.State, delta map[string][]string, op string, refreshAll bool, fromChange string, linkTs func(instanceName string, ts *state.TaskSet)) (*state.TaskSet, error) {
applyTs := state.NewTaskSet()
kind := "refresh-aliases"
msg := i18n.G("Refresh aliases for snap %q")
@@ -1063,7 +1072,7 @@
msg = i18n.G("Prune automatic aliases for snap %q")
}
for instanceName, aliases := range delta {
- if err := CheckChangeConflict(st, instanceName, nil); err != nil {
+ if err := checkChangeConflictIgnoringOneChange(st, instanceName, nil, fromChange); err != nil {
if refreshAll {
// doing "refresh all", just skip this snap
logger.Noticef("cannot %s automatic aliases for snap %q: %v", op, instanceName, err)
@@ -1197,24 +1206,19 @@
return newChannel, nil
}
- nch, err := snap.ParseChannelVerbatim(newChannel, "")
- if err != nil {
- return "", err
- }
-
- if nch.Track == "" {
- // channel name is valid and consist of risk level or
- // risk/branch only, do the right thing and default to risk (or
- // risk/branch) within the pinned track
- return pinnedTrack + "/" + newChannel, nil
- }
- if nch.Track != "" && nch.Track != pinnedTrack {
+ // channel name is valid and consist of risk level or
+ // risk/branch only, do the right thing and default to risk (or
+ // risk/branch) within the pinned track
+ resChannel, err := channel.ResolveLocked(pinnedTrack, newChannel)
+ if err == channel.ErrLockedTrackSwitch {
// switching to a different track is not allowed
- return "", fmt.Errorf("cannot switch from %s track %q as specified for the (device) model to %q", which, pinnedTrack, nch.Clean().String())
+ return "", fmt.Errorf("cannot switch from %s track %q as specified for the (device) model to %q", which, pinnedTrack, newChannel)
}
-
- return newChannel, nil
+ if err != nil {
+ return "", err
+ }
+ return resChannel, nil
}
var errRevisionSwitch = errors.New("cannot switch revision")
@@ -1348,7 +1352,7 @@
//
// The returned TaskSet will contain a DownloadAndChecksDoneEdge.
func Update(st *state.State, name string, opts *RevisionOptions, userID int, flags Flags) (*state.TaskSet, error) {
- return UpdateWithDeviceContext(st, name, opts, userID, flags, nil)
+ return UpdateWithDeviceContext(st, name, opts, userID, flags, nil, "")
}
// UpdateWithDeviceContext initiates a change updating a snap.
@@ -1356,7 +1360,7 @@
// Note that the state must be locked by the caller.
//
// The returned TaskSet will contain a DownloadAndChecksDoneEdge.
-func UpdateWithDeviceContext(st *state.State, name string, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext) (*state.TaskSet, error) {
+func UpdateWithDeviceContext(st *state.State, name string, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext, fromChange string) (*state.TaskSet, error) {
if opts == nil {
opts = &RevisionOptions{}
}
@@ -1424,7 +1428,7 @@
return opts, updateFlags, &snapst
}
- _, tts, err := doUpdate(context.TODO(), st, []string{name}, updates, params, userID, &flags, deviceCtx, "")
+ _, tts, err := doUpdate(context.TODO(), st, []string{name}, updates, params, userID, &flags, deviceCtx, fromChange)
if err != nil {
return nil, err
}
@@ -1437,7 +1441,7 @@
// NOTE: if we are in here, len(updates) == 0
// (so we're free to add tasks because there's no rerefresh)
- if err := CheckChangeConflict(st, name, nil); err != nil {
+ if err := checkChangeConflictIgnoringOneChange(st, name, nil, fromChange); err != nil {
return nil, err
}
@@ -1698,7 +1702,7 @@
for name, snapst := range snapStates {
for _, si := range snapst.Sequence {
if snapInfo, err := snap.ReadInfo(name, si); err == nil {
- if snapInfo.GetType() != snap.TypeApp || snapInfo.SnapName() == "snapd" {
+ if snapInfo.GetType() != snap.TypeApp || snapInfo.GetType() == snap.TypeSnapd {
continue
}
if snapInfo.Base == "" {
@@ -2081,7 +2085,7 @@
}
if !newSnapst.IsInstalled() {
var userID int
- newInfo, err := installInfo(st, newName, &RevisionOptions{Channel: oldSnapst.Channel}, userID, nil)
+ newInfo, err := installInfo(context.TODO(), st, newName, &RevisionOptions{Channel: oldSnapst.Channel}, userID, nil)
if err != nil {
return nil, err
}
@@ -2292,7 +2296,7 @@
return false, nil
}
-func infosForTypes(st *state.State, snapType snap.Type) ([]*snap.Info, error) {
+func infosForType(st *state.State, snapType snap.Type) ([]*snap.Info, error) {
var stateMap map[string]*SnapState
if err := st.Get("snaps", &stateMap); err != nil && err != state.ErrNoState {
return nil, err
@@ -2324,36 +2328,37 @@
return res, nil
}
-func infoForType(st *state.State, snapType snap.Type) (*snap.Info, error) {
- res, err := infosForTypes(st, snapType)
+func infoForDeviceSnap(st *state.State, deviceCtx DeviceContext, which string, whichName func(*asserts.Model) string) (*snap.Info, error) {
+ if deviceCtx == nil {
+ return nil, fmt.Errorf("internal error: unset deviceCtx")
+ }
+ model := deviceCtx.Model()
+ snapName := whichName(model)
+ if snapName == "" {
+ return nil, state.ErrNoState
+ }
+ var snapst SnapState
+ err := Get(st, snapName, &snapst)
if err != nil {
return nil, err
}
- return res[0], nil
+ return snapst.CurrentInfo()
}
-// XXX: remodeling: decide what to do with Gadget/KernelInfo and their derived functions
-
-// GadgetInfo finds the current gadget snap's info.
-func GadgetInfo(st *state.State) (*snap.Info, error) {
- return infoForType(st, snap.TypeGadget)
+// GadgetInfo finds the gadget snap's info for the given device context.
+func GadgetInfo(st *state.State, deviceCtx DeviceContext) (*snap.Info, error) {
+ return infoForDeviceSnap(st, deviceCtx, "gadget", (*asserts.Model).Gadget)
}
+// TODO: reintroduce a KernelInfo(state.State, DeviceContext) if needed
// KernelInfo finds the current kernel snap's info.
-func KernelInfo(st *state.State) (*snap.Info, error) {
- return infoForType(st, snap.TypeKernel)
-}
-// CoreInfo finds the current OS snap's info. If both
+// coreInfo finds the current OS snap's info. If both
// "core" and "ubuntu-core" is installed then "core"
// is preferred. Different core names are not supported
// currently and will result in an error.
-//
-// Once enough time has passed and everyone transitioned
-// from ubuntu-core to core we can simplify this again
-// and make it the same as the above "KernelInfo".
-func CoreInfo(st *state.State) (*snap.Info, error) {
- res, err := infosForTypes(st, snap.TypeOS)
+func coreInfo(st *state.State) (*snap.Info, error) {
+ res, err := infosForType(st, snap.TypeOS)
if err != nil {
return nil, err
}
@@ -2378,11 +2383,12 @@
return nil, fmt.Errorf("unexpected number of cores, got %d", len(res))
}
-// ConfigDefaults returns the configuration defaults for the snap specified in
-// the gadget. If gadget is absent or the snap has no snap-id it returns
+// ConfigDefaults returns the configuration defaults for the snap as
+// specified in the gadget for the given device context.
+// If gadget is absent or the snap has no snap-id it returns
// ErrNoState.
-func ConfigDefaults(st *state.State, snapName string) (map[string]interface{}, error) {
- gadget, err := GadgetInfo(st)
+func ConfigDefaults(st *state.State, deviceCtx DeviceContext, snapName string) (map[string]interface{}, error) {
+ info, err := GadgetInfo(st, deviceCtx)
if err != nil {
return nil, err
}
@@ -2392,11 +2398,7 @@
return nil, err
}
- core, err := CoreInfo(st)
- if err != nil {
- return nil, err
- }
- isCoreDefaults := core.InstanceName() == snapName
+ isCoreDefaults := snapName == defaultCoreSnapName
si := snapst.CurrentSideInfo()
// core snaps can be addressed even without a snap-id via the special
@@ -2406,7 +2408,7 @@
return nil, state.ErrNoState
}
- gadgetInfo, err := snap.ReadGadgetInfo(gadget, release.OnClassic)
+ gadgetInfo, err := gadget.ReadInfo(info.MountDir(), release.OnClassic)
if err != nil {
return nil, err
}
@@ -2431,14 +2433,15 @@
}
// GadgetConnections returns the interface connection instructions
-// specified in the gadget. If gadget is absent it returns ErrNoState.
-func GadgetConnections(st *state.State) ([]gadget.Connection, error) {
- gadget, err := GadgetInfo(st)
+// specified in the gadget for the given device context.
+// If gadget is absent it returns ErrNoState.
+func GadgetConnections(st *state.State, deviceCtx DeviceContext) ([]gadget.Connection, error) {
+ info, err := GadgetInfo(st, deviceCtx)
if err != nil {
return nil, err
}
- gadgetInfo, err := snap.ReadGadgetInfo(gadget, release.OnClassic)
+ gadgetInfo, err := gadget.ReadInfo(info.MountDir(), release.OnClassic)
if err != nil {
return nil, err
}
diff -Nru snapd-2.40/overlord/snapstate/snapstate_test.go snapd-2.42.1/overlord/snapstate/snapstate_test.go
--- snapd-2.40/overlord/snapstate/snapstate_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/snapstate_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -37,8 +37,8 @@
"gopkg.in/tomb.v2"
"github.com/snapcore/snapd/asserts"
- "github.com/snapcore/snapd/boot/boottest"
"github.com/snapcore/snapd/bootloader"
+ "github.com/snapcore/snapd/bootloader/bootloadertest"
"github.com/snapcore/snapd/dirs"
"github.com/snapcore/snapd/gadget"
"github.com/snapcore/snapd/interfaces"
@@ -127,6 +127,7 @@
s.o.AddManager(s.snapmgr)
s.o.AddManager(s.o.TaskRunner())
s.se = s.o.StateEngine()
+ c.Assert(s.o.StartUp(), IsNil)
s.BaseTest.AddCleanup(snapstate.MockSnapReadInfo(s.fakeBackend.ReadInfo))
s.BaseTest.AddCleanup(snapstate.MockOpenSnapFile(s.fakeBackend.OpenSnapFile))
@@ -233,6 +234,7 @@
runner.AddHandler("discard-conns", fakeHandler, fakeHandler)
runner.AddHandler("validate-snap", fakeHandler, nil)
runner.AddHandler("transition-ubuntu-core", fakeHandler, nil)
+ runner.AddHandler("transition-to-snapd-snap", fakeHandler, nil)
// Add handler to test full aborting of changes
erroringHandler := func(task *state.Task, _ *tomb.Tomb) error {
@@ -358,6 +360,7 @@
runCoreConfigure
doesReRefresh
updatesGadget
+ noConfigure
)
func taskKinds(tasks []*state.Task) []string {
@@ -417,8 +420,13 @@
"cleanup",
)
}
+ if opts&noConfigure == 0 {
+ expected = append(expected,
+ "run-hook[configure]",
+ )
+ }
expected = append(expected,
- "run-hook[configure]",
+ "run-hook[check-health]",
)
c.Assert(kinds, DeepEquals, expected)
@@ -477,6 +485,7 @@
}
expected = append(expected,
"run-hook[configure]",
+ "run-hook[check-health]",
)
if opts&doesReRefresh != 0 {
expected = append(expected, "check-rerefresh")
@@ -548,16 +557,16 @@
// if a snap is devmode, you can't install it without --devmode
opts := &snapstate.RevisionOptions{Channel: "channel-for-devmode"}
- _, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ _, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, ErrorMatches, `.* requires devmode or confinement override`)
// if a snap is devmode, you *can* install it with --devmode
- _, err = snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{DevMode: true})
+ _, err = snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{DevMode: true})
c.Assert(err, IsNil)
// if a snap is *not* devmode, you can still install it with --devmode
opts.Channel = "channel-for-strict"
- _, err = snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{DevMode: true})
+ _, err = snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{DevMode: true})
c.Assert(err, IsNil)
}
@@ -585,16 +594,16 @@
// if a snap is classic, you can't install it without --classic
opts := &snapstate.RevisionOptions{Channel: "channel-for-classic"}
- _, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ _, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, ErrorMatches, `.* requires classic confinement`)
// if a snap is classic, you *can* install it with --classic
- _, err = snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{Classic: true})
+ _, err = snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{Classic: true})
c.Assert(err, IsNil)
// if a snap is *not* classic, but can install it with --classic which gets ignored
opts.Channel = "channel-for-strict"
- _, err = snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{Classic: true})
+ _, err = snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{Classic: true})
c.Assert(err, IsNil)
}
@@ -611,7 +620,7 @@
dirs.SetRootDir(dirs.GlobalRootDir)
opts := &snapstate.RevisionOptions{Channel: "channel-for-classic"}
- _, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{Classic: true})
+ _, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{Classic: true})
c.Assert(err, ErrorMatches, "classic confinement requires snaps under /snap or symlink from /snap to "+dirs.SnapMountDir)
}
@@ -620,19 +629,37 @@
defer s.state.Unlock()
opts := &snapstate.RevisionOptions{Channel: "some-channel"}
- ts, err := snapstate.Install(s.state, "some-snap", opts, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, 0, snapstate.Flags{})
c.Assert(err, IsNil)
verifyInstallTasks(c, 0, 0, ts, s.state)
c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
}
+func (s *snapmgrTestSuite) TestInstallSnapdSnapType(c *C) {
+ restore := snap.MockSnapdSnapID("snapd-id") // id provided by fakeStore
+ defer restore()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ opts := &snapstate.RevisionOptions{Channel: "some-channel"}
+ ts, err := snapstate.Install(context.Background(), s.state, "snapd", opts, 0, snapstate.Flags{})
+ c.Assert(err, IsNil)
+
+ verifyInstallTasks(c, noConfigure, 0, ts, s.state)
+
+ snapsup, err := snapstate.TaskSnapSetup(ts.Tasks()[0])
+ c.Assert(err, IsNil)
+ c.Check(snapsup.Type, Equals, snap.TypeSnapd)
+}
+
func (s *snapmgrTestSuite) TestInstallCohortTasks(c *C) {
s.state.Lock()
defer s.state.Unlock()
opts := &snapstate.RevisionOptions{Channel: "some-channel", CohortKey: "what"}
- ts, err := snapstate.Install(s.state, "some-snap", opts, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, 0, snapstate.Flags{})
c.Assert(err, IsNil)
snapsup, err := snapstate.TaskSnapSetup(ts.Tasks()[0])
c.Assert(err, IsNil)
@@ -640,7 +667,6 @@
verifyInstallTasks(c, 0, 0, ts, s.state)
c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
-
}
func (s *snapmgrTestSuite) TestInstallWithDeviceContext(c *C) {
@@ -653,7 +679,7 @@
deviceCtx := &snapstatetest.TrivialDeviceContext{CtxStore: s.fakeStore}
opts := &snapstate.RevisionOptions{Channel: "some-channel"}
- ts, err := snapstate.InstallWithDeviceContext(s.state, "some-snap", opts, 0, snapstate.Flags{}, deviceCtx)
+ ts, err := snapstate.InstallWithDeviceContext(context.Background(), s.state, "some-snap", opts, 0, snapstate.Flags{}, deviceCtx, "")
c.Assert(err, IsNil)
verifyInstallTasks(c, 0, 0, ts, s.state)
@@ -681,11 +707,13 @@
c.Assert(err, IsNil)
runHooks := tasksWithKind(ts, "run-hook")
- // hook tasks for refresh and for configure hook only; no install hook
- c.Assert(runHooks, HasLen, 3)
- c.Assert(runHooks[0].Summary(), Equals, `Run pre-refresh hook of "some-snap" snap if present`)
- c.Assert(runHooks[1].Summary(), Equals, `Run post-refresh hook of "some-snap" snap if present`)
- c.Assert(runHooks[2].Summary(), Equals, `Run configure hook of "some-snap" snap if present`)
+ // no install hook task
+ c.Assert(taskKinds(runHooks), DeepEquals, []string{
+ "run-hook[pre-refresh]",
+ "run-hook[post-refresh]",
+ "run-hook[configure]",
+ "run-hook[check-health]",
+ })
}
type fullFlags struct{ before, change, after, setup snapstate.Flags }
@@ -723,6 +751,7 @@
"setup-aliases",
"start-snap-services",
"run-hook[configure]",
+ "run-hook[check-health]",
})
// a revert is a special refresh
verifyStopReason(c, ts, "refresh")
@@ -1006,7 +1035,7 @@
SnapType: "app",
})
- _, _, err := snapstate.UpdateMany(context.TODO(), s.state, nil, 0, nil)
+ _, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, 0, nil)
c.Check(err, FitsTypeOf, &snapstate.ChangeConflictError{})
c.Assert(err, ErrorMatches, `too early for operation, device not yet seeded or device model not acknowledged`)
}
@@ -1027,7 +1056,7 @@
SnapType: "app",
})
- updates, tts, err := snapstate.UpdateMany(context.TODO(), s.state, nil, 0, nil)
+ updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, 0, nil)
c.Assert(err, IsNil)
c.Assert(tts, HasLen, 2)
verifyLastTasksetIsReRefresh(c, tts)
@@ -1084,7 +1113,7 @@
InstanceKey: "instance",
})
- updates, tts, err := snapstate.UpdateMany(context.TODO(), s.state, nil, 0, nil)
+ updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, 0, nil)
c.Assert(err, IsNil)
c.Assert(tts, HasLen, 3)
verifyLastTasksetIsReRefresh(c, tts)
@@ -1128,7 +1157,7 @@
})
// updated snap is devmode, updatemany doesn't update it
- _, tts, _ := snapstate.UpdateMany(context.TODO(), s.state, []string{"some-snap"}, s.user.ID, nil)
+ _, tts, _ := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, s.user.ID, nil)
// FIXME: UpdateMany will not error out in this case (daemon catches this case, with a weird error)
c.Assert(tts, HasLen, 0)
}
@@ -1149,7 +1178,7 @@
})
// if a snap installed without --classic gets a classic update it isn't installed
- _, tts, _ := snapstate.UpdateMany(context.TODO(), s.state, []string{"some-snap"}, s.user.ID, nil)
+ _, tts, _ := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, s.user.ID, nil)
// FIXME: UpdateMany will not error out in this case (daemon catches this case, with a weird error)
c.Assert(tts, HasLen, 0)
}
@@ -1171,7 +1200,7 @@
})
// snap installed with classic: refresh gets classic
- _, tts, err := snapstate.UpdateMany(context.TODO(), s.state, []string{"some-snap"}, s.user.ID, nil)
+ _, tts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, s.user.ID, nil)
c.Assert(err, IsNil)
c.Assert(tts, HasLen, 2)
verifyLastTasksetIsReRefresh(c, tts)
@@ -1191,7 +1220,7 @@
SnapType: "app",
})
- updates, _, err := snapstate.UpdateMany(context.TODO(), s.state, []string{"some-snap"}, 0, nil)
+ updates, _, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, 0, nil)
c.Assert(err, IsNil)
c.Check(updates, HasLen, 1)
}
@@ -1210,7 +1239,7 @@
SnapType: "app",
})
- updates, _, err := snapstate.UpdateMany(context.TODO(), s.state, nil, 0, nil)
+ updates, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, 0, nil)
c.Assert(err, IsNil)
c.Check(updates, HasLen, 0)
}
@@ -1247,7 +1276,7 @@
Channel: "channel-for-base",
})
- updates, tts, err := snapstate.UpdateMany(context.TODO(), s.state, []string{"some-snap", "core", "some-base"}, 0, nil)
+ updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "core", "some-base"}, 0, nil)
c.Assert(err, IsNil)
c.Assert(tts, HasLen, 4)
verifyLastTasksetIsReRefresh(c, tts)
@@ -1287,6 +1316,9 @@
r := snapstatetest.MockDeviceModel(ModelWithBase("core18"))
defer r()
+ restore := snap.MockSnapdSnapID("snapd-id")
+ defer restore()
+
s.state.Lock()
defer s.state.Unlock()
@@ -1327,7 +1359,7 @@
Channel: "channel-for-base",
})
- updates, tts, err := snapstate.UpdateMany(context.TODO(), s.state, []string{"some-snap", "core18", "some-base", "snapd"}, 0, nil)
+ updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "core18", "some-base", "snapd"}, 0, nil)
c.Assert(err, IsNil)
c.Assert(tts, HasLen, 5)
verifyLastTasksetIsReRefresh(c, tts)
@@ -1393,7 +1425,7 @@
// hook it up
snapstate.ValidateRefreshes = validateRefreshes
- updates, tts, err := snapstate.UpdateMany(context.TODO(), s.state, nil, 0, nil)
+ updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, 0, nil)
c.Assert(err, IsNil)
c.Assert(tts, HasLen, 2)
verifyLastTasksetIsReRefresh(c, tts)
@@ -1451,7 +1483,7 @@
// hook it up
snapstate.ValidateRefreshes = validateRefreshes
- updates, tts, err := snapstate.UpdateMany(context.TODO(), s.state, nil, 0, nil)
+ updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, 0, nil)
c.Assert(err, IsNil)
c.Assert(tts, HasLen, 3)
verifyLastTasksetIsReRefresh(c, tts)
@@ -1487,13 +1519,13 @@
snapstate.ValidateRefreshes = validateRefreshes
// refresh all => no error
- updates, tts, err := snapstate.UpdateMany(context.TODO(), s.state, nil, 0, nil)
+ updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, 0, nil)
c.Assert(err, IsNil)
c.Check(tts, HasLen, 0)
c.Check(updates, HasLen, 0)
// refresh some-snap => report error
- updates, tts, err = snapstate.UpdateMany(context.TODO(), s.state, []string{"some-snap"}, 0, nil)
+ updates, tts, err = snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, 0, nil)
c.Assert(err, Equals, validateErr)
c.Check(tts, HasLen, 0)
c.Check(updates, HasLen, 0)
@@ -1534,6 +1566,7 @@
"setup-aliases",
"start-snap-services",
"run-hook[configure]",
+ "run-hook[check-health]",
})
}
@@ -1641,7 +1674,7 @@
})
_, err := snapstate.Switch(s.state, "kernel", &snapstate.RevisionOptions{Channel: "new-channel"})
- c.Assert(err, ErrorMatches, `cannot switch from kernel track "18" as specified for the \(device\) model to "new-channel/stable"`)
+ c.Assert(err, ErrorMatches, `cannot switch from kernel track "18" as specified for the \(device\) model to "new-channel"`)
}
func (s *snapmgrTestSuite) TestSwitchKernelTrackRiskOnlyIsOK(c *C) {
@@ -1698,7 +1731,7 @@
})
_, err := snapstate.Switch(s.state, "brand-gadget", &snapstate.RevisionOptions{Channel: "new-channel"})
- c.Assert(err, ErrorMatches, `cannot switch from gadget track "18" as specified for the \(device\) model to "new-channel/stable"`)
+ c.Assert(err, ErrorMatches, `cannot switch from gadget track "18" as specified for the \(device\) model to "new-channel"`)
}
func (s *snapmgrTestSuite) TestSwitchGadgetTrackRiskOnlyIsOK(c *C) {
@@ -1812,7 +1845,7 @@
snapstate.ReplaceStore(s.state, contentStore{fakeStore: s.fakeStore, state: s.state})
- ts, err := snapstate.Install(s.state, "snap-content-slot", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "snap-content-slot", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
var snapsup snapstate.SnapSetup
@@ -1871,7 +1904,7 @@
s.state.Lock()
defer s.state.Unlock()
- ts, err := snapstate.Install(s.state, "some-snap", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
var snapsup snapstate.SnapSetup
@@ -1886,7 +1919,7 @@
defer s.state.Unlock()
opts := &snapstate.RevisionOptions{Revision: snap.R(7)}
- ts, err := snapstate.Install(s.state, "some-snap", opts, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, 0, snapstate.Flags{})
c.Assert(err, IsNil)
var snapsup snapstate.SnapSetup
@@ -1902,7 +1935,7 @@
s.state.Set("seeded", nil)
- _, err := snapstate.Install(s.state, "some-snap", nil, 0, snapstate.Flags{})
+ _, err := snapstate.Install(context.Background(), s.state, "some-snap", nil, 0, snapstate.Flags{})
c.Check(err, FitsTypeOf, &snapstate.ChangeConflictError{})
c.Assert(err, ErrorMatches, `too early for operation, device not yet seeded or device model not acknowledged`)
}
@@ -1911,12 +1944,12 @@
s.state.Lock()
defer s.state.Unlock()
- ts, err := snapstate.Install(s.state, "some-snap", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
// need a change to make the tasks visible
s.state.NewChange("install", "...").AddAll(ts)
- _, err = snapstate.Install(s.state, "some-snap", nil, 0, snapstate.Flags{})
+ _, err = snapstate.Install(context.Background(), s.state, "some-snap", nil, 0, snapstate.Flags{})
c.Check(err, FitsTypeOf, &snapstate.ChangeConflictError{})
c.Assert(err, ErrorMatches, `snap "some-snap" has "install" change in progress`)
}
@@ -1937,7 +1970,7 @@
SnapType: "app",
})
- _, err := snapstate.Install(s.state, "foo", nil, 0, snapstate.Flags{})
+ _, err := snapstate.Install(context.Background(), s.state, "foo", nil, 0, snapstate.Flags{})
c.Assert(err, ErrorMatches, `snap "foo" command namespace conflicts with alias "foo\.bar" for "otherfoosnap" snap`)
}
@@ -1949,7 +1982,7 @@
defer s.state.Unlock()
opts := &snapstate.RevisionOptions{Channel: "channel-for-strict"}
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{Classic: true})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{Classic: true})
c.Assert(err, IsNil)
c.Assert(err, IsNil)
@@ -1998,7 +2031,7 @@
snapstate.ReplaceStore(s.state, sneakyStore{fakeStore: s.fakeStore, state: s.state})
- _, err := snapstate.Install(s.state, "some-snap", nil, 0, snapstate.Flags{})
+ _, err := snapstate.Install(context.Background(), s.state, "some-snap", nil, 0, snapstate.Flags{})
c.Check(err, FitsTypeOf, &snapstate.ChangeConflictError{})
c.Assert(err, ErrorMatches, `snap "some-snap" has changes in progress`)
}
@@ -2021,7 +2054,7 @@
s.state.Lock()
defer s.state.Unlock()
- ts, err := snapstate.Install(s.state, "some-snap", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
// need a change to make the tasks visible
s.state.NewChange("install", "...").AddAll(ts)
@@ -2106,19 +2139,19 @@
tr.Set("core", "experimental.parallel-instances", true)
tr.Commit()
- _, err := snapstate.Install(s.state, "core_foo", nil, 0, snapstate.Flags{})
+ _, err := snapstate.Install(context.Background(), s.state, "core_foo", nil, 0, snapstate.Flags{})
c.Check(err, ErrorMatches, `cannot install snap of type os as "core_foo"`)
- _, err = snapstate.Install(s.state, "some-base_foo", nil, 0, snapstate.Flags{})
+ _, err = snapstate.Install(context.Background(), s.state, "some-base_foo", nil, 0, snapstate.Flags{})
c.Check(err, ErrorMatches, `cannot install snap of type base as "some-base_foo"`)
- _, err = snapstate.Install(s.state, "some-gadget_foo", nil, 0, snapstate.Flags{})
+ _, err = snapstate.Install(context.Background(), s.state, "some-gadget_foo", nil, 0, snapstate.Flags{})
c.Check(err, ErrorMatches, `cannot install snap of type gadget as "some-gadget_foo"`)
- _, err = snapstate.Install(s.state, "some-kernel_foo", nil, 0, snapstate.Flags{})
+ _, err = snapstate.Install(context.Background(), s.state, "some-kernel_foo", nil, 0, snapstate.Flags{})
c.Check(err, ErrorMatches, `cannot install snap of type kernel as "some-kernel_foo"`)
- _, err = snapstate.Install(s.state, "some-snapd_foo", nil, 0, snapstate.Flags{})
+ _, err = snapstate.Install(context.Background(), s.state, "some-snapd_foo", nil, 0, snapstate.Flags{})
c.Check(err, ErrorMatches, `cannot install snap of type snapd as "some-snapd_foo"`)
}
@@ -2218,7 +2251,7 @@
// hook it up
snapstate.ValidateRefreshes = happyValidateRefreshes
- ts, err := snapstate.UpdateWithDeviceContext(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{}, deviceCtx)
+ ts, err := snapstate.UpdateWithDeviceContext(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{}, deviceCtx, "")
c.Assert(err, IsNil)
verifyUpdateTasks(c, unlinkBefore|cleanupAfter|doesReRefresh, 0, ts, s.state)
c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
@@ -2249,7 +2282,7 @@
})
opts := &snapstate.RevisionOptions{Channel: "some-channel", Revision: snap.R(11)}
- ts, err := snapstate.UpdateWithDeviceContext(s.state, "some-snap", opts, 0, snapstate.Flags{}, deviceCtx)
+ ts, err := snapstate.UpdateWithDeviceContext(s.state, "some-snap", opts, 0, snapstate.Flags{}, deviceCtx, "")
c.Assert(err, IsNil)
verifyUpdateTasks(c, unlinkBefore|cleanupAfter|doesReRefresh, 0, ts, s.state)
c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
@@ -2634,7 +2667,7 @@
chg := s.state.NewChange("install", "install a snap")
opts := &snapstate.RevisionOptions{Channel: "some-channel"}
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -2747,9 +2780,9 @@
c.Check(task.Summary(), Equals, `Download snap "some-snap" (11) from channel "some-channel"`)
// check link/start snap summary
- linkTask := ta[len(ta)-7]
+ linkTask := ta[len(ta)-8]
c.Check(linkTask.Summary(), Equals, `Make snap "some-snap" (11) available to the system`)
- startTask := ta[len(ta)-2]
+ startTask := ta[len(ta)-3]
c.Check(startTask.Summary(), Equals, `Start snap "some-snap" (11) services`)
// verify snap-setup in the task state
@@ -2805,7 +2838,7 @@
chg := s.state.NewChange("install", "install a snap")
opts := &snapstate.RevisionOptions{Channel: "some-channel"}
- ts, err := snapstate.Install(s.state, "some-snap_instance", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap_instance", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -2917,9 +2950,9 @@
c.Check(task.Summary(), Equals, `Download snap "some-snap_instance" (11) from channel "some-channel"`)
// check link/start snap summary
- linkTask := ta[len(ta)-7]
+ linkTask := ta[len(ta)-8]
c.Check(linkTask.Summary(), Equals, `Make snap "some-snap_instance" (11) available to the system`)
- startTask := ta[len(ta)-2]
+ startTask := ta[len(ta)-3]
c.Check(startTask.Summary(), Equals, `Start snap "some-snap_instance" (11) services`)
// verify snap-setup in the task state
@@ -2964,8 +2997,7 @@
c.Assert(snapst.InstanceKey, Equals, "instance")
runHooks := tasksWithKind(ts, "run-hook")
- // install and configure hooks
- c.Assert(runHooks, HasLen, 2)
+ c.Assert(taskKinds(runHooks), DeepEquals, []string{"run-hook[install]", "run-hook[configure]", "run-hook[check-health]"})
for _, hookTask := range runHooks {
c.Assert(hookTask.Kind(), Equals, "run-hook")
var hooksup hookstate.HookSetup
@@ -2981,7 +3013,7 @@
chg := s.state.NewChange("install", "install a snap")
opts := &snapstate.RevisionOptions{Channel: "some-channel"}
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -3130,7 +3162,7 @@
chg := s.state.NewChange("install", "install a snap")
opts := &snapstate.RevisionOptions{Channel: "some-channel", CohortKey: "scurries"}
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -3243,9 +3275,9 @@
c.Check(task.Summary(), Equals, `Download snap "some-snap" (666) from channel "some-channel"`)
// check link/start snap summary
- linkTask := ta[len(ta)-7]
+ linkTask := ta[len(ta)-8]
c.Check(linkTask.Summary(), Equals, `Make snap "some-snap" (666) available to the system`)
- startTask := ta[len(ta)-2]
+ startTask := ta[len(ta)-3]
c.Check(startTask.Summary(), Equals, `Start snap "some-snap" (666) services`)
// verify snap-setup in the task state
@@ -3296,7 +3328,7 @@
chg := s.state.NewChange("install", "install a snap")
opts := &snapstate.RevisionOptions{Channel: "some-channel", Revision: snap.R(42)}
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -3406,9 +3438,9 @@
c.Check(task.Summary(), Equals, `Download snap "some-snap" (42) from channel "some-channel"`)
// check link/start snap summary
- linkTask := ta[len(ta)-7]
+ linkTask := ta[len(ta)-8]
c.Check(linkTask.Summary(), Equals, `Make snap "some-snap" (42) available to the system`)
- startTask := ta[len(ta)-2]
+ startTask := ta[len(ta)-3]
c.Check(startTask.Summary(), Equals, `Start snap "some-snap" (42) services`)
// verify snap-setup in the task state
@@ -3456,7 +3488,7 @@
chg := s.state.NewChange("install", "install a snap")
opts := &snapstate.RevisionOptions{Channel: "some-channel"}
- ts, err := snapstate.Install(s.state, "services-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "services-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -3487,7 +3519,7 @@
chg := s.state.NewChange("install", "install a snap")
opts := &snapstate.RevisionOptions{Channel: "some-channel", Revision: snap.R(42)}
- ts, err := snapstate.Install(s.state, "some-snap", opts, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, 0, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -4462,7 +4494,7 @@
chg := s.state.NewChange("refresh", "refresh all snaps")
// no user is passed to use for UpdateMany
- updated, tts, err := snapstate.UpdateMany(context.TODO(), s.state, nil, 0, nil)
+ updated, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, 0, nil)
c.Assert(err, IsNil)
for _, ts := range tts {
chg.AddAll(ts)
@@ -4570,7 +4602,7 @@
chg := s.state.NewChange("refresh", "refresh all snaps")
// do UpdateMany using user 2 as fallback
- updated, tts, err := snapstate.UpdateMany(context.TODO(), s.state, nil, 2, nil)
+ updated, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, 2, nil)
c.Assert(err, IsNil)
for _, ts := range tts {
chg.AddAll(ts)
@@ -4697,7 +4729,7 @@
chg := s.state.NewChange("refresh", "refresh all snaps")
// no user is passed to use for UpdateMany
- updated, tts, err := snapstate.UpdateMany(context.TODO(), s.state, nil, 0, nil)
+ updated, tts, err := snapstate.UpdateMany(context.Background(), s.state, nil, 0, nil)
c.Assert(err, IsNil)
for _, ts := range tts {
chg.AddAll(ts)
@@ -5708,7 +5740,7 @@
s.fakeStore.refreshRevnos = map[string]snap.Revision{
"some-snap-id": snap.R(12),
}
- _, tts, err := snapstate.UpdateMany(context.TODO(), s.state, []string{"some-snap"}, s.user.ID, nil)
+ _, tts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, s.user.ID, nil)
c.Assert(err, IsNil)
c.Check(tts, HasLen, 2)
verifyLastTasksetIsReRefresh(c, tts)
@@ -5914,7 +5946,7 @@
s.fakeStore.refreshRevnos = map[string]snap.Revision{
"some-snap-id": snap.R(12),
}
- updates, tts, err := snapstate.UpdateMany(context.TODO(), s.state, []string{"some-snap", "some-snap_instance"}, s.user.ID, nil)
+ updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "some-snap_instance"}, s.user.ID, nil)
c.Assert(err, IsNil)
c.Check(tts, HasLen, 3)
verifyLastTasksetIsReRefresh(c, tts)
@@ -6136,7 +6168,7 @@
SnapType: "app",
})
- updates, _, err := snapstate.UpdateMany(context.TODO(), s.state, []string{"some-snap"}, s.user.ID, nil)
+ updates, _, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, s.user.ID, nil)
c.Assert(err, IsNil)
c.Check(updates, DeepEquals, []string{"some-snap"})
@@ -6176,7 +6208,7 @@
Current: si7.Revision,
})
- updates, _, err := snapstate.UpdateMany(context.TODO(), s.state, nil, s.user.ID, nil)
+ updates, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, s.user.ID, nil)
c.Check(err, IsNil)
c.Check(updates, HasLen, 0)
@@ -6281,7 +6313,7 @@
snapstate.Set(s.state, instanceName, &snapst)
}
- updates, tts, err := snapstate.UpdateMany(context.TODO(), s.state, scenario.names, s.user.ID, nil)
+ updates, tts, err := snapstate.UpdateMany(context.Background(), s.state, scenario.names, s.user.ID, nil)
c.Check(err, IsNil)
verifyLastTasksetIsReRefresh(c, tts)
@@ -6465,7 +6497,7 @@
}
if scenario.update {
first := tasks[j]
- j += 18
+ j += 19
c.Check(first.Kind(), Equals, "prerequisites")
wait := false
if expectedPruned["other-snap"]["aliasA"] {
@@ -6566,7 +6598,7 @@
// switching tracks is not ok
_, err := snapstate.Update(s.state, "kernel", &snapstate.RevisionOptions{Channel: "new-channel"}, s.user.ID, snapstate.Flags{})
- c.Assert(err, ErrorMatches, `cannot switch from kernel track "18" as specified for the \(device\) model to "new-channel/stable"`)
+ c.Assert(err, ErrorMatches, `cannot switch from kernel track "18" as specified for the \(device\) model to "new-channel"`)
// no change to the channel is ok
_, err = snapstate.Update(s.state, "kernel", nil, s.user.ID, snapstate.Flags{})
@@ -6602,7 +6634,7 @@
// switching tracks is not ok
_, err := snapstate.Update(s.state, "brand-gadget", &snapstate.RevisionOptions{Channel: "new-channel"}, s.user.ID, snapstate.Flags{})
- c.Assert(err, ErrorMatches, `cannot switch from gadget track "18" as specified for the \(device\) model to "new-channel/stable"`)
+ c.Assert(err, ErrorMatches, `cannot switch from gadget track "18" as specified for the \(device\) model to "new-channel"`)
// no change to the channel is ok
_, err = snapstate.Update(s.state, "brand-gadget", nil, s.user.ID, snapstate.Flags{})
@@ -6694,6 +6726,7 @@
},
}
+ // start with an easier-to-read error if this fails:
c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
c.Check(s.fakeBackend.ops, DeepEquals, expected)
@@ -6757,41 +6790,64 @@
s.settle(c)
s.state.Lock()
- ops := s.fakeBackend.ops
- // ensure only local install was run, i.e. first action is pseudo-action current
- c.Assert(ops.Ops(), HasLen, 11)
- c.Check(ops[0].op, Equals, "current")
- c.Check(ops[0].old, Equals, filepath.Join(dirs.SnapMountDir, "mock/x2"))
- // and setup-snap
- c.Check(ops[1].op, Equals, "setup-snap")
- c.Check(ops[1].name, Matches, "mock")
- c.Check(ops[1].path, Matches, `.*/mock_1.0_all.snap`)
- c.Check(ops[1].revno, Equals, snap.R("x3"))
- // and cleanup
- c.Check(ops[len(ops)-1], DeepEquals, fakeOp{
- op: "cleanup-trash",
- name: "mock",
- revno: snap.R("x3"),
- })
-
- c.Check(ops[3].op, Equals, "unlink-snap")
- c.Check(ops[3].path, Equals, filepath.Join(dirs.SnapMountDir, "mock/x2"))
-
- c.Check(ops[4].op, Equals, "copy-data")
- c.Check(ops[4].path, Equals, filepath.Join(dirs.SnapMountDir, "mock/x3"))
- c.Check(ops[4].old, Equals, filepath.Join(dirs.SnapMountDir, "mock/x2"))
-
- c.Check(ops[5].op, Equals, "setup-profiles:Doing")
- c.Check(ops[5].name, Equals, "mock")
- c.Check(ops[5].revno, Equals, snap.R(-3))
+ expected := fakeOps{
+ {
+ op: "current",
+ old: filepath.Join(dirs.SnapMountDir, "mock/x2"),
+ },
+ {
+ op: "setup-snap",
+ name: "mock",
+ path: mockSnap,
+ revno: snap.R("x3"),
+ },
+ {
+ op: "remove-snap-aliases",
+ name: "mock",
+ },
+ {
+ op: "unlink-snap",
+ path: filepath.Join(dirs.SnapMountDir, "mock/x2"),
+ },
+ {
+ op: "copy-data",
+ path: filepath.Join(dirs.SnapMountDir, "mock/x3"),
+ old: filepath.Join(dirs.SnapMountDir, "mock/x2"),
+ },
+ {
+ op: "setup-profiles:Doing",
+ name: "mock",
+ revno: snap.R(-3),
+ },
+ {
+ op: "candidate",
+ sinfo: snap.SideInfo{
+ RealName: "mock",
+ Revision: snap.R(-3),
+ },
+ },
+ {
+ op: "link-snap",
+ path: filepath.Join(dirs.SnapMountDir, "mock/x3"),
+ },
+ {
+ op: "auto-connect:Doing",
+ name: "mock",
+ revno: snap.R("x3"),
+ },
+ {
+ op: "update-aliases",
+ },
+ {
+ op: "cleanup-trash",
+ name: "mock",
+ revno: snap.R("x3"),
+ },
+ }
- c.Check(ops[6].op, Equals, "candidate")
- c.Check(ops[6].sinfo, DeepEquals, snap.SideInfo{
- RealName: "mock",
- Revision: snap.R(-3),
- })
- c.Check(ops[7].op, Equals, "link-snap")
- c.Check(ops[7].path, Equals, filepath.Join(dirs.SnapMountDir, "mock/x3"))
+ // start with an easier-to-read error if this fails:
+ c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
+ c.Check(s.fakeBackend.ops, DeepEquals, expected)
// verify snapSetup info
var snapsup snapstate.SnapSetup
@@ -6911,6 +6967,7 @@
revno: snap.R("x1"),
},
}
+ // start with an easier-to-read error if this fails:
c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, expected.Ops())
c.Check(s.fakeBackend.ops, DeepEquals, expected)
@@ -8020,6 +8077,61 @@
c.Assert(res, Equals, "100")
}
+func (s *snapmgrTestSuite) TestRefreshDoesntRestoreRevisionConfig(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)},
+ },
+ Current: snap.R(1),
+ SnapType: "app",
+ })
+
+ // set global configuration (affecting current snap)
+ tr := config.NewTransaction(s.state)
+ tr.Set("some-snap", "foo", "100")
+ tr.Commit()
+
+ // set per-revision config for the upcoming rev. 2, we don't expect it restored though
+ // since only revert restores revision configs.
+ s.state.Set("revision-config", map[string]interface{}{
+ "some-snap": map[string]interface{}{
+ "2": map[string]interface{}{"foo": "200"},
+ },
+ })
+
+ // simulate a refresh to rev. 2
+ chg := s.state.NewChange("update", "update some-snap")
+ ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel", Revision: snap.R(2)}, s.user.ID, snapstate.Flags{})
+ c.Assert(err, IsNil)
+ chg.AddAll(ts)
+
+ s.state.Unlock()
+ defer s.se.Stop()
+ s.settle(c)
+
+ s.state.Lock()
+ // config of rev. 1 has been stored in per-revision map
+ var cfgs map[string]interface{}
+ c.Assert(s.state.Get("revision-config", &cfgs), IsNil)
+ c.Assert(cfgs["some-snap"], DeepEquals, map[string]interface{}{
+ "1": map[string]interface{}{"foo": "100"},
+ "2": map[string]interface{}{"foo": "200"},
+ })
+
+ // config of rev. 2 hasn't been restored by refresh, old value returned
+ tr = config.NewTransaction(s.state)
+ var res string
+ c.Assert(tr.Get("some-snap", "foo", &res), IsNil)
+ c.Assert(res, Equals, "100")
+}
+
func (s *snapmgrTestSuite) TestUpdateDoesGC(c *C) {
s.state.Lock()
defer s.state.Unlock()
@@ -9370,7 +9482,7 @@
chg := s.state.NewChange("install", "install a snap")
opts := &snapstate.RevisionOptions{Channel: "some-channel"}
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -9568,7 +9680,7 @@
chg := s.state.NewChange("install", "install a snap")
opts := &snapstate.RevisionOptions{Channel: "some-channel"}
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
s.fakeBackend.linkSnapWaitCh = make(chan int)
@@ -9606,7 +9718,7 @@
chg := s.state.NewChange("install", "install a snap")
opts := &snapstate.RevisionOptions{Channel: "some-channel"}
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
s.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "some-snap/11")
@@ -10409,58 +10521,46 @@
c.Check(infos[1].Description(), Equals, "Lots of text")
}
-func (s *snapmgrQuerySuite) TestTypeInfo(c *C) {
+func (s *snapmgrQuerySuite) TestGadgetInfo(c *C) {
st := s.st
st.Lock()
defer st.Unlock()
- for _, x := range []struct {
- snapName string
- snapType snap.Type
- getInfo func(*state.State) (*snap.Info, error)
- }{
- {
- snapName: "gadget",
- snapType: snap.TypeGadget,
- getInfo: snapstate.GadgetInfo,
- },
- {
- snapName: "core",
- snapType: snap.TypeOS,
- getInfo: snapstate.CoreInfo,
- },
- {
- snapName: "kernel",
- snapType: snap.TypeKernel,
- getInfo: snapstate.KernelInfo,
- },
- } {
- _, err := x.getInfo(st)
- c.Assert(err, Equals, state.ErrNoState)
+ deviceCtxNoGadget := deviceWithoutGadgetContext()
+ deviceCtx := deviceWithGadgetContext("gadget")
- sideInfo := &snap.SideInfo{
- RealName: x.snapName,
- Revision: snap.R(2),
- }
- snaptest.MockSnap(c, fmt.Sprintf("name: %q\ntype: %q\nversion: %q\n", x.snapName, x.snapType, x.snapName), sideInfo)
- snapstate.Set(st, x.snapName, &snapstate.SnapState{
- SnapType: string(x.snapType),
- Active: true,
- Sequence: []*snap.SideInfo{sideInfo},
- Current: sideInfo.Revision,
- })
+ _, err := snapstate.GadgetInfo(st, deviceCtxNoGadget)
+ c.Assert(err, Equals, state.ErrNoState)
- info, err := x.getInfo(st)
- c.Assert(err, IsNil)
+ _, err = snapstate.GadgetInfo(st, deviceCtx)
+ c.Assert(err, Equals, state.ErrNoState)
- c.Check(info.InstanceName(), Equals, x.snapName)
- c.Check(info.Revision, Equals, snap.R(2))
- c.Check(info.Version, Equals, x.snapName)
- c.Check(info.GetType(), Equals, x.snapType)
+ sideInfo := &snap.SideInfo{
+ RealName: "gadget",
+ Revision: snap.R(2),
}
+ snaptest.MockSnap(c, `
+name: gadget
+type: gadget
+version: v1
+`, sideInfo)
+ snapstate.Set(st, "gadget", &snapstate.SnapState{
+ SnapType: "gadget",
+ Active: true,
+ Sequence: []*snap.SideInfo{sideInfo},
+ Current: sideInfo.Revision,
+ })
+
+ info, err := snapstate.GadgetInfo(st, deviceCtx)
+ c.Assert(err, IsNil)
+
+ c.Check(info.InstanceName(), Equals, "gadget")
+ c.Check(info.Revision, Equals, snap.R(2))
+ c.Check(info.Version, Equals, "v1")
+ c.Check(info.GetType(), Equals, snap.TypeGadget)
}
-func (s *snapmgrQuerySuite) TestTypeInfoCore(c *C) {
+func (s *snapmgrQuerySuite) TestCoreInfoInternal(c *C) {
st := s.st
st.Lock()
defer st.Unlock()
@@ -10508,7 +10608,7 @@
})
}
- info, err := snapstate.CoreInfo(st)
+ info, err := snapstate.CoreInfoInternal(st)
if t.errMatcher != "" {
c.Assert(err, ErrorMatches, t.errMatcher)
} else {
@@ -10756,6 +10856,7 @@
"run-hook[install]",
"start-snap-services",
"run-hook[configure]",
+ "run-hook[check-health]",
})
}
@@ -10932,7 +11033,7 @@
st *state.State
deviceCtx snapstate.DeviceContext
- bs bootloader.Bootloader
+ bootloader *bootloadertest.MockBootloader
}
var _ = Suite(&canRemoveSuite{})
@@ -10942,8 +11043,8 @@
s.st = state.New(nil)
s.deviceCtx = &snapstatetest.TrivialDeviceContext{DeviceModel: DefaultModel()}
- s.bs = boottest.NewMockBootloader("mock", c.MkDir())
- bootloader.Force(s.bs)
+ s.bootloader = bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(s.bootloader)
}
func (s *canRemoveSuite) TearDownTest(c *C) {
@@ -10995,9 +11096,7 @@
}
kernel.RealName = "kernel"
- s.bs.SetBootVars(map[string]string{
- "snap_kernel": fmt.Sprintf("%s_%s.snap", kernel.RealName, kernel.SideInfo.Revision),
- })
+ s.bootloader.SetBootKernel(fmt.Sprintf("%s_%s.snap", kernel.RealName, kernel.SideInfo.Revision))
removeAll := false
c.Check(snapstate.CanRemove(s.st, kernel, &snapstate.SnapState{}, removeAll, s.deviceCtx), Equals, false)
@@ -11012,9 +11111,7 @@
}
base.RealName = "core18"
- s.bs.SetBootVars(map[string]string{
- "snap_core": fmt.Sprintf("%s_%s.snap", base.RealName, base.SideInfo.Revision),
- })
+ s.bootloader.SetBootBase(fmt.Sprintf("%s_%s.snap", base.RealName, base.SideInfo.Revision))
removeAll := false
c.Check(snapstate.CanRemove(s.st, base, &snapstate.SnapState{}, removeAll, s.deviceCtx), Equals, false)
@@ -11038,9 +11135,7 @@
}
kernel.RealName = "other-non-model-kernel"
- s.bs.SetBootVars(map[string]string{
- "snap_kernel": fmt.Sprintf("%s_%s.snap", kernel.RealName, kernel.SideInfo.Revision),
- })
+ s.bootloader.SetBootKernel(fmt.Sprintf("%s_%s.snap", kernel.RealName, kernel.SideInfo.Revision))
c.Check(snapstate.CanRemove(s.st, kernel, &snapstate.SnapState{}, true, s.deviceCtx), Equals, false)
}
@@ -11533,13 +11628,13 @@
var snapsup snapstate.SnapSetup
tasks := ts.Tasks()
- i := len(tasks) - 7
+ i := len(tasks) - 8
c.Check(tasks[i].Kind(), Equals, "clear-snap")
err = tasks[i].Get("snap-setup", &snapsup)
c.Assert(err, IsNil)
c.Check(snapsup.Revision(), Equals, si3.Revision)
- i = len(tasks) - 5
+ i = len(tasks) - 6
c.Check(tasks[i].Kind(), Equals, "clear-snap")
err = tasks[i].Get("snap-setup", &snapsup)
c.Assert(err, IsNil)
@@ -11795,6 +11890,20 @@
})
}
+func deviceWithGadgetContext(gadgetName string) snapstate.DeviceContext {
+ return &snapstatetest.TrivialDeviceContext{
+ DeviceModel: MakeModel(map[string]interface{}{
+ "gadget": gadgetName,
+ }),
+ }
+}
+
+func deviceWithoutGadgetContext() snapstate.DeviceContext {
+ return &snapstatetest.TrivialDeviceContext{
+ DeviceModel: ClassicModel(),
+ }
+}
+
func (s *snapmgrTestSuite) TestConfigDefaults(c *C) {
r := release.MockOnClassic(false)
defer r()
@@ -11807,6 +11916,8 @@
s.prepareGadget(c)
+ deviceCtx := deviceWithGadgetContext("the-gadget")
+
snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
Active: true,
Sequence: []*snap.SideInfo{
@@ -11817,7 +11928,7 @@
})
makeInstalledMockCoreSnap(c)
- defls, err := snapstate.ConfigDefaults(s.state, "some-snap")
+ defls, err := snapstate.ConfigDefaults(s.state, deviceCtx, "some-snap")
c.Assert(err, IsNil)
c.Assert(defls, DeepEquals, map[string]interface{}{"key": "value"})
@@ -11829,7 +11940,33 @@
Current: snap.R(5),
SnapType: "app",
})
- _, err = snapstate.ConfigDefaults(s.state, "local-snap")
+ _, err = snapstate.ConfigDefaults(s.state, deviceCtx, "local-snap")
+ c.Assert(err, Equals, state.ErrNoState)
+}
+
+func (s *snapmgrTestSuite) TestConfigDefaultsNoGadget(c *C) {
+ r := release.MockOnClassic(false)
+ defer r()
+
+ // using MockSnap, we want to read the bits on disk
+ snapstate.MockSnapReadInfo(snap.ReadInfo)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ deviceCtxNoGadget := deviceWithoutGadgetContext()
+
+ snapstate.Set(s.state, "some-snap", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{
+ {RealName: "some-snap", Revision: snap.R(11), SnapID: "some-snap-ididididididididididid"},
+ },
+ Current: snap.R(11),
+ SnapType: "app",
+ })
+ makeInstalledMockCoreSnap(c)
+
+ _, err := snapstate.ConfigDefaults(s.state, deviceCtxNoGadget, "some-snap")
c.Assert(err, Equals, state.ErrNoState)
}
@@ -11849,9 +11986,11 @@
foo: bar
`)
+ deviceCtx := deviceWithGadgetContext("the-gadget")
+
makeInstalledMockCoreSnap(c)
- defls, err := snapstate.ConfigDefaults(s.state, "core")
+ defls, err := snapstate.ConfigDefaults(s.state, deviceCtx, "core")
c.Assert(err, IsNil)
c.Assert(defls, DeepEquals, map[string]interface{}{"foo": "bar"})
}
@@ -11875,6 +12014,8 @@
other-key: other-key-default
`)
+ deviceCtx := deviceWithGadgetContext("the-gadget")
+
snapstate.Set(s.state, "core", &snapstate.SnapState{
Active: true,
Sequence: []*snap.SideInfo{
@@ -11887,7 +12028,7 @@
makeInstalledMockCoreSnap(c)
// 'system' key defaults take precedence over snap-id ones
- defls, err := snapstate.ConfigDefaults(s.state, "core")
+ defls, err := snapstate.ConfigDefaults(s.state, deviceCtx, "core")
c.Assert(err, IsNil)
c.Assert(defls, DeepEquals, map[string]interface{}{"foo": "bar"})
}
@@ -11913,7 +12054,7 @@
err := ioutil.WriteFile(filepath.Join(info.MountDir(), "meta", "gadget.yaml"), mockGadgetYaml, 0644)
c.Assert(err, IsNil)
- gi, err := snap.ReadGadgetInfo(info, false)
+ gi, err := gadget.ReadInfo(info.MountDir(), false)
c.Assert(err, IsNil)
c.Assert(gi, NotNil)
@@ -11967,9 +12108,11 @@
var m map[string]interface{}
runHooks := tasksWithKind(ts, "run-hook")
- // two hooks expected - install and configure
- c.Assert(runHooks, HasLen, 2)
- c.Assert(runHooks[1].Kind(), Equals, "run-hook")
+ c.Assert(taskKinds(runHooks), DeepEquals, []string{
+ "run-hook[install]",
+ "run-hook[configure]",
+ "run-hook[check-health]",
+ })
err = runHooks[1].Get("hook-context", &m)
c.Assert(err, IsNil)
c.Assert(m, DeepEquals, map[string]interface{}{"use-defaults": true})
@@ -12523,13 +12666,240 @@
// other tasks block until the transition is done
opts := &snapstate.RevisionOptions{Channel: "stable"}
- _, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ _, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Check(err, FitsTypeOf, &snapstate.ChangeConflictError{})
c.Check(err, ErrorMatches, "ubuntu-core to core transition in progress, no other changes allowed until this is done")
// and when the transition is done, other tasks run
chg.SetStatus(state.DoneStatus)
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ c.Check(err, IsNil)
+ c.Check(ts, NotNil)
+}
+
+func (s *snapmgrTestSuite) TestTransitionSnapdSnapDoesNotRunWithoutSnaps(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "experimental.snapd-snap", true)
+ tr.Commit()
+
+ // no snaps installed on this system (e.g. fresh classic)
+ snapstate.Set(s.state, "core", nil)
+
+ s.state.Unlock()
+ defer s.se.Stop()
+ s.settle(c)
+ s.state.Lock()
+
+ c.Check(s.state.Changes(), HasLen, 0)
+}
+
+func (s *snapmgrTestSuite) TestTransitionSnapdSnapDoesRunWithAnySnap(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "experimental.snapd-snap", true)
+ tr.Commit()
+
+ // some snap installed on this system but no core
+ snapstate.Set(s.state, "core", nil)
+ snapstate.Set(s.state, "foo", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{{RealName: "foo", SnapID: "foo-id", Revision: snap.R(1), Channel: "beta"}},
+ Current: snap.R(1),
+ })
+
+ s.state.Unlock()
+ defer s.se.Stop()
+ s.settle(c)
+ s.state.Lock()
+
+ c.Check(s.state.Changes(), HasLen, 1)
+}
+
+func (s *snapmgrTestSuite) TestTransitionSnapdSnapDoesNotRunWhenNotEnabled(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "core", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{{RealName: "corecore", SnapID: "core-snap-id", Revision: snap.R(1), Channel: "beta"}},
+ Current: snap.R(1),
+ SnapType: "os",
+ })
+
+ s.state.Unlock()
+ defer s.se.Stop()
+ s.settle(c)
+ s.state.Lock()
+
+ c.Check(s.state.Changes(), HasLen, 0)
+}
+
+func (s *snapmgrTestSuite) TestTransitionSnapdSnapStartsAutomaticallyWhenEnabled(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "core", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{{RealName: "corecore", SnapID: "core-snap-id", Revision: snap.R(1), Channel: "beta"}},
+ Current: snap.R(1),
+ SnapType: "os",
+ })
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "experimental.snapd-snap", true)
+ tr.Commit()
+
+ s.state.Unlock()
+ defer s.se.Stop()
+ s.settle(c)
+ s.state.Lock()
+
+ c.Check(s.state.Changes(), HasLen, 1)
+ chg := s.state.Changes()[0]
+ c.Check(chg.Kind(), Equals, "transition-to-snapd-snap")
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.IsReady(), Equals, true)
+
+ // snapd snap is instaleld from the default channel
+ var snapst snapstate.SnapState
+ snapstate.Get(s.state, "snapd", &snapst)
+ c.Assert(snapst.Channel, Equals, "stable")
+}
+
+func (s *snapmgrTestSuite) TestTransitionSnapdSnapWithCoreRunthrough(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.Set(s.state, "core", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{{RealName: "corecore", SnapID: "core-snap-id", Revision: snap.R(1), Channel: "edge"}},
+ Current: snap.R(1),
+ SnapType: "os",
+ // TrackingChannel
+ Channel: "beta",
+ })
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "experimental.snapd-snap", true)
+ tr.Commit()
+
+ s.state.Unlock()
+ defer s.se.Stop()
+ s.settle(c)
+ s.state.Lock()
+
+ c.Assert(s.state.Changes(), HasLen, 1)
+ chg := s.state.Changes()[0]
+ c.Assert(chg.Kind(), Equals, "transition-to-snapd-snap")
+ c.Assert(chg.Err(), IsNil)
+ c.Assert(chg.IsReady(), Equals, true)
+ c.Check(s.fakeStore.downloads, HasLen, 1)
+ ts := state.NewTaskSet(chg.Tasks()...)
+ verifyInstallTasks(c, noConfigure, 0, ts, s.state)
+
+ // ensure preferences from the core snap got transferred over
+ var snapst snapstate.SnapState
+ snapstate.Get(s.state, "snapd", &snapst)
+ c.Assert(snapst.Channel, Equals, "beta")
+}
+
+func (s *snapmgrTestSuite) TestTransitionSnapdSnapTimeLimitWorks(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "experimental.snapd-snap", true)
+ tr.Commit()
+
+ // tried 3h ago, no retry
+ s.state.Set("snapd-transition-last-retry-time", time.Now().Add(-3*time.Hour))
+
+ s.state.Unlock()
+ defer s.se.Stop()
+ s.settle(c)
+ s.state.Lock()
+
+ c.Check(s.state.Changes(), HasLen, 0)
+
+ // tried 7h ago, retry
+ s.state.Set("snapd-transition-last-retry-time", time.Now().Add(-7*time.Hour))
+
+ s.state.Unlock()
+ defer s.se.Stop()
+ s.settle(c)
+ s.state.Lock()
+ c.Check(s.state.Changes(), HasLen, 1)
+
+ var t time.Time
+ s.state.Get("snapd-transition-last-retry-time", &t)
+ c.Assert(time.Now().Sub(t) < 2*time.Minute, Equals, true)
+}
+
+type unhappyStore struct {
+ *fakeStore
+}
+
+func (s unhappyStore) SnapAction(ctx context.Context, currentSnaps []*store.CurrentSnap, actions []*store.SnapAction, user *auth.UserState, opts *store.RefreshOptions) ([]*snap.Info, error) {
+
+ return nil, fmt.Errorf("a grumpy store")
+}
+
+func (s *snapmgrTestSuite) TestTransitionSnapdSnapError(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ snapstate.ReplaceStore(s.state, unhappyStore{fakeStore: s.fakeStore})
+
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "experimental.snapd-snap", true)
+ tr.Commit()
+
+ s.state.Unlock()
+ defer s.se.Stop()
+ err := s.o.Settle(5 * time.Second)
+ c.Assert(err, ErrorMatches, `state ensure errors: \[a grumpy store\]`)
+
+ s.state.Lock()
+ c.Check(s.state.Changes(), HasLen, 0)
+
+ // all the attempts were recorded
+ var t time.Time
+ s.state.Get("snapd-transition-last-retry-time", &t)
+ c.Assert(time.Now().Sub(t) < 2*time.Minute, Equals, true)
+
+ var cnt int
+ s.state.Get("snapd-transition-retry", &cnt)
+ c.Assert(cnt, Equals, 1)
+
+ // the transition is not tried again (because of retry time)
+ s.state.Unlock()
+ err = s.o.Settle(5 * time.Second)
+ c.Assert(err, IsNil)
+ s.state.Lock()
+
+ s.state.Get("snapd-transition-retry", &cnt)
+ c.Assert(cnt, Equals, 1)
+}
+
+func (s *snapmgrTestSuite) TestTransitionSnapdSnapBlocksOtherChanges(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // if we have a snapd transition
+ chg := s.state.NewChange("transition-to-snapd-snap", "...")
+ chg.SetStatus(state.DoStatus)
+
+ // other tasks block until the transition is done
+ _, err := snapstate.Install(context.Background(), s.state, "some-snap", &snapstate.RevisionOptions{Channel: "stable"}, s.user.ID, snapstate.Flags{})
+ c.Check(err, FitsTypeOf, &snapstate.ChangeConflictError{})
+ c.Check(err, ErrorMatches, "transition to snapd snap in progress, no other changes allowed until this is done")
+
+ // and when the transition is done, other tasks run
+ chg.SetStatus(state.DoneStatus)
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", &snapstate.RevisionOptions{Channel: "stable"}, s.user.ID, snapstate.Flags{})
c.Check(err, IsNil)
c.Check(ts, NotNil)
}
@@ -12858,7 +13228,7 @@
chg := s.state.NewChange("install", "install a snap on a system without core")
opts := &snapstate.RevisionOptions{Channel: "some-channel", Revision: snap.R(42)}
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -13080,13 +13450,13 @@
chg1 := s.state.NewChange("install", "install snap 1")
opts := &snapstate.RevisionOptions{Channel: "some-channel", Revision: snap.R(42)}
- ts1, err := snapstate.Install(s.state, "snap1", opts, s.user.ID, snapstate.Flags{})
+ ts1, err := snapstate.Install(context.Background(), s.state, "snap1", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg1.AddAll(ts1)
chg2 := s.state.NewChange("install", "install snap 2")
opts = &snapstate.RevisionOptions{Channel: "some-other-channel", Revision: snap.R(21)}
- ts2, err := snapstate.Install(s.state, "snap2", opts, s.user.ID, snapstate.Flags{})
+ ts2, err := snapstate.Install(context.Background(), s.state, "snap2", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg2.AddAll(ts2)
@@ -13103,15 +13473,11 @@
c.Assert(chg2.IsReady(), Equals, true)
// order in which the changes run is random
- len1 := len(chg1.Tasks())
- len2 := len(chg2.Tasks())
- if len1 > len2 {
- c.Assert(chg1.Tasks(), HasLen, 26)
- c.Assert(chg2.Tasks(), HasLen, 13)
- } else {
- c.Assert(chg1.Tasks(), HasLen, 13)
- c.Assert(chg2.Tasks(), HasLen, 26)
+ if len(chg1.Tasks()) < len(chg2.Tasks()) {
+ chg1, chg2 = chg2, chg1
}
+ c.Assert(taskKinds(chg1.Tasks()), HasLen, 28)
+ c.Assert(taskKinds(chg2.Tasks()), HasLen, 14)
// FIXME: add helpers and do a DeepEquals here for the operations
}
@@ -13143,7 +13509,7 @@
// chg1 has an error
chg1 := s.state.NewChange("install", "install snap 1")
opts := &snapstate.RevisionOptions{Channel: "some-channel", Revision: snap.R(42)}
- ts1, err := snapstate.Install(s.state, "snap1", opts, s.user.ID, snapstate.Flags{})
+ ts1, err := snapstate.Install(context.Background(), s.state, "snap1", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg1.AddAll(ts1)
@@ -13156,7 +13522,7 @@
// chg2 is good
chg2 := s.state.NewChange("install", "install snap 2")
opts = &snapstate.RevisionOptions{Channel: "some-other-channel", Revision: snap.R(21)}
- ts2, err := snapstate.Install(s.state, "snap2", opts, s.user.ID, snapstate.Flags{})
+ ts2, err := snapstate.Install(context.Background(), s.state, "snap2", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg2.AddAll(ts2)
@@ -13265,7 +13631,7 @@
// now install a snap that will pull in core
chg := s.state.NewChange("install", "install a snap on a system without core")
opts := &snapstate.RevisionOptions{Channel: "some-channel"}
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -13438,7 +13804,7 @@
chg := s.state.NewChange("install", "install a snap")
opts := &snapstate.RevisionOptions{Channel: "stable", Revision: snap.R(42)}
- ts, err := snapstate.Install(s.state, "snap-content-plug", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "snap-content-plug", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -13602,7 +13968,7 @@
chg := s.state.NewChange("install", "install a snap")
opts := &snapstate.RevisionOptions{Channel: "some-channel", Revision: snap.R(42)}
- ts, err := snapstate.Install(s.state, "snap-content-circular1", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "snap-content-circular1", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -13636,7 +14002,7 @@
chg := s.state.NewChange("install", "install a snap")
opts := &snapstate.RevisionOptions{Channel: "some-channel", Revision: snap.R(42)}
- ts, err := snapstate.Install(s.state, "snap-content-plug-compat", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "snap-content-plug-compat", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -13719,7 +14085,7 @@
s.state.Lock()
defer s.state.Unlock()
opts := &snapstate.RevisionOptions{Channel: "channel-for-paid"}
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg := s.state.NewChange("install", "install paid snap")
@@ -13742,7 +14108,7 @@
s.state.Lock()
defer s.state.Unlock()
opts := &snapstate.RevisionOptions{Channel: "channel-for-private"}
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg := s.state.NewChange("install", "install private snap")
@@ -13816,7 +14182,7 @@
Channel: "some-channel",
}
_, _, err := snapstate.InstallPath(s.state, si, someSnap, "", "some-channel", snapstate.Flags{Required: true})
- c.Assert(err, ErrorMatches, `cannot switch from kernel track "18" as specified for the \(device\) model to "some-channel/stable"`)
+ c.Assert(err, ErrorMatches, `cannot switch from kernel track "18" as specified for the \(device\) model to "some-channel"`)
}
func (s *snapmgrTestSuite) TestInstallPathWithMetadataChannelSwitchGadget(c *C) {
@@ -13847,7 +14213,7 @@
Channel: "some-channel",
}
_, _, err := snapstate.InstallPath(s.state, si, someSnap, "", "some-channel", snapstate.Flags{Required: true})
- c.Assert(err, ErrorMatches, `cannot switch from gadget track "18" as specified for the \(device\) model to "some-channel/stable"`)
+ c.Assert(err, ErrorMatches, `cannot switch from gadget track "18" as specified for the \(device\) model to "some-channel"`)
}
func (s *snapmgrTestSuite) TestInstallLayoutsChecksFeatureFlag(c *C) {
@@ -13856,35 +14222,35 @@
// Layouts are now enabled by default.
opts := &snapstate.RevisionOptions{Channel: "channel-for-layout"}
- _, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ _, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
// Layouts can be explicitly disabled.
tr := config.NewTransaction(s.state)
tr.Set("core", "experimental.layouts", false)
tr.Commit()
- _, err = snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ _, err = snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, ErrorMatches, "experimental feature disabled - test it by setting 'experimental.layouts' to true")
// Layouts can be explicitly enabled.
tr = config.NewTransaction(s.state)
tr.Set("core", "experimental.layouts", true)
tr.Commit()
- _, err = snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ _, err = snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
// The default empty value now means "enabled".
tr = config.NewTransaction(s.state)
tr.Set("core", "experimental.layouts", "")
tr.Commit()
- _, err = snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ _, err = snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
// Layouts are enabled when the controlling flag is reset to nil.
tr = config.NewTransaction(s.state)
tr.Set("core", "experimental.layouts", nil)
tr.Commit()
- _, err = snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ _, err = snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
}
@@ -13938,7 +14304,7 @@
SnapType: "app",
})
- _, _, err := snapstate.UpdateMany(context.TODO(), s.state, []string{"some-snap"}, s.user.ID, nil)
+ _, _, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, s.user.ID, nil)
c.Assert(err, ErrorMatches, "experimental feature disabled - test it by setting 'experimental.layouts' to true")
// When layouts are enabled we can refresh multiple snaps if one of them depends on the feature.
@@ -13946,7 +14312,7 @@
tr.Set("core", "experimental.layouts", true)
tr.Commit()
- _, _, err = snapstate.UpdateMany(context.TODO(), s.state, []string{"some-snap"}, s.user.ID, nil)
+ _, _, err = snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, s.user.ID, nil)
c.Assert(err, IsNil)
}
@@ -13969,7 +14335,7 @@
SnapType: "app",
})
- refreshes, _, err := snapstate.UpdateMany(context.TODO(), s.state, nil, s.user.ID, nil)
+ refreshes, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, s.user.ID, nil)
c.Assert(err, IsNil)
c.Assert(refreshes, HasLen, 0)
@@ -13978,7 +14344,7 @@
tr.Set("core", "experimental.layouts", true)
tr.Commit()
- refreshes, _, err = snapstate.UpdateMany(context.TODO(), s.state, nil, s.user.ID, nil)
+ refreshes, _, err = snapstate.UpdateMany(context.Background(), s.state, nil, s.user.ID, nil)
c.Assert(err, IsNil)
c.Assert(refreshes, DeepEquals, []string{"some-snap"})
}
@@ -14154,12 +14520,12 @@
// normal snaps get a configure task
opts := &snapstate.RevisionOptions{Channel: "some-channel"}
- ts, err := snapstate.Install(s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
c.Check(hasConfigureTask(ts), Equals, true)
// but bases do not for install
- ts, err = snapstate.Install(s.state, "some-base", opts, s.user.ID, snapstate.Flags{})
+ ts, err = snapstate.Install(context.Background(), s.state, "some-base", opts, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
c.Check(hasConfigureTask(ts), Equals, false)
@@ -14176,15 +14542,30 @@
c.Check(hasConfigureTask(ts), Equals, false)
}
-func (s *snapmgrTestSuite) TestNoSnapdSnapOnSystemsWithoutBaseOnUbuntuCore(c *C) {
- restore := release.MockOnClassic(false)
- defer restore()
+func (s *snapmgrTestSuite) TestNoSnapdSnapOnCoreWithoutBase(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ r := release.MockOnClassic(false)
+ defer r()
+
+ // but snapd do not for install
+ _, err := snapstate.Install(context.Background(), s.state, "snapd", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{})
+ c.Assert(err, ErrorMatches, "cannot install snapd snap on a model without a base snap yet")
+}
+func (s *snapmgrTestSuite) TestNoSnapdSnapOnSystemsWithoutBaseOnUbuntuCore(c *C) {
s.state.Lock()
defer s.state.Unlock()
+ r := release.MockOnClassic(false)
+ defer r()
+
+ // it is not possible to opt-into the snapd snap on core yet
+ tr := config.NewTransaction(s.state)
+ tr.Set("core", "experimental.snapd-snap", true)
+ tr.Commit()
// but snapd do not for install
- _, err := snapstate.Install(s.state, "snapd", nil, s.user.ID, snapstate.Flags{})
+ _, err := snapstate.Install(context.Background(), s.state, "snapd", nil, s.user.ID, snapstate.Flags{})
c.Assert(err, ErrorMatches, "cannot install snapd snap on a model without a base snap yet")
}
@@ -14196,7 +14577,7 @@
tr.Set("core", "experimental.snapd-snap", true)
tr.Commit()
- _, err := snapstate.Install(s.state, "snapd", nil, s.user.ID, snapstate.Flags{})
+ _, err := snapstate.Install(context.Background(), s.state, "snapd", nil, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
}
@@ -14204,12 +14585,15 @@
s.state.Lock()
defer s.state.Unlock()
+ restore := snap.MockSnapdSnapID("snapd-id")
+ defer restore()
+
// snapd cannot be installed unless the model uses a base snap
r := snapstatetest.MockDeviceModel(ModelWithBase("core18"))
defer r()
// but snapd do not for install
- ts, err := snapstate.Install(s.state, "snapd", nil, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "snapd", nil, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
c.Check(hasConfigureTask(ts), Equals, false)
@@ -14350,13 +14734,13 @@
// clear request-salt to have it generated
s.state.Set("refresh-privacy-key", nil)
- _, err := snapstate.Install(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
+ _, err := snapstate.Install(context.Background(), s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
c.Assert(err, ErrorMatches, "internal error: request salt is unset")
s.state.Set("refresh-privacy-key", "privacy-key")
chg := s.state.NewChange("install", "install a snap")
- ts, err := snapstate.Install(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", nil, s.user.ID, snapstate.Flags{})
c.Assert(err, IsNil)
chg.AddAll(ts)
@@ -14398,10 +14782,16 @@
// using MockSnap, we want to read the bits on disk
snapstate.MockSnapReadInfo(snap.ReadInfo)
+ deviceCtxNoGadget := deviceWithoutGadgetContext()
+ deviceCtx := deviceWithGadgetContext("the-gadget")
+
s.state.Lock()
defer s.state.Unlock()
- _, err := snapstate.GadgetConnections(s.state)
+ _, err := snapstate.GadgetConnections(s.state, deviceCtxNoGadget)
+ c.Assert(err, Equals, state.ErrNoState)
+
+ _, err = snapstate.GadgetConnections(s.state, deviceCtx)
c.Assert(err, Equals, state.ErrNoState)
s.prepareGadget(c, `
@@ -14410,7 +14800,7 @@
slot: snap2idididididididididididididi:slot
`)
- conns, err := snapstate.GadgetConnections(s.state)
+ conns, err := snapstate.GadgetConnections(s.state, deviceCtx)
c.Assert(err, IsNil)
c.Check(conns, DeepEquals, []gadget.Connection{
{Plug: gadget.ConnectionPlug{SnapID: "snap1idididididididididididididi", Plug: "plug"}, Slot: gadget.ConnectionSlot{SnapID: "snap2idididididididididididididi", Slot: "slot"}}})
@@ -14511,10 +14901,10 @@
s.state.Lock()
defer s.state.Unlock()
- _, err := snapstate.Install(s.state, "foo--invalid", nil, 0, snapstate.Flags{})
+ _, err := snapstate.Install(context.Background(), s.state, "foo--invalid", nil, 0, snapstate.Flags{})
c.Assert(err, ErrorMatches, `invalid instance name: invalid snap name: "foo--invalid"`)
- _, err = snapstate.Install(s.state, "foo_123_456", nil, 0, snapstate.Flags{})
+ _, err = snapstate.Install(context.Background(), s.state, "foo_123_456", nil, 0, snapstate.Flags{})
c.Assert(err, ErrorMatches, `invalid instance name: invalid instance key: "123_456"`)
_, _, err = snapstate.InstallMany(s.state, []string{"foo--invalid"}, 0)
@@ -14540,7 +14930,7 @@
defer s.state.Unlock()
// task added on install
- ts, err := snapstate.Install(s.state, "brand-gadget", nil, 0, snapstate.Flags{})
+ ts, err := snapstate.Install(context.Background(), s.state, "brand-gadget", nil, 0, snapstate.Flags{})
c.Assert(err, IsNil)
c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks()))
diff -Nru snapd-2.40/overlord/snapstate/storehelpers.go snapd-2.42.1/overlord/snapstate/storehelpers.go
--- snapd-2.40/overlord/snapstate/storehelpers.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/snapstate/storehelpers.go 2019-10-30 12:17:43.000000000 +0000
@@ -82,7 +82,7 @@
return &opts, nil
}
-func installInfo(st *state.State, name string, revOpts *RevisionOptions, userID int, deviceCtx DeviceContext) (*snap.Info, error) {
+func installInfo(ctx context.Context, st *state.State, name string, revOpts *RevisionOptions, userID int, deviceCtx DeviceContext) (*snap.Info, error) {
// TODO: support ignore-validation?
curSnaps, err := currentSnaps(st)
@@ -118,7 +118,7 @@
theStore := Store(st, deviceCtx)
st.Unlock() // calls to the store should be done without holding the state lock
- res, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, user, opts)
+ res, err := theStore.SnapAction(ctx, curSnaps, []*store.SnapAction{action}, user, opts)
st.Lock()
return singleActionResult(name, action.Action, res, err)
diff -Nru snapd-2.40/overlord/state/state.go snapd-2.42.1/overlord/state/state.go
--- snapd-2.40/overlord/state/state.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/state/state.go 2019-10-30 12:17:43.000000000 +0000
@@ -113,6 +113,7 @@
restarting RestartType
restartLck sync.Mutex
+ bootID string
}
// New returns a new empty state.
@@ -255,8 +256,15 @@
}
// RequestRestart asks for a restart of the managing process.
+// The state needs to be locked to request a RestartSystem.
func (s *State) RequestRestart(t RestartType) {
if s.backend != nil {
+ if t == RestartSystem {
+ if s.bootID == "" {
+ panic("internal error: cannot request a system restart if current boot ID was not provided via VerifyReboot")
+ }
+ s.Set("system-restart-from-boot-id", s.bootID)
+ }
s.restartLck.Lock()
s.restarting = t
s.restartLck.Unlock()
@@ -271,6 +279,37 @@
return s.restarting != RestartUnset, s.restarting
}
+var ErrExpectedReboot = errors.New("expected reboot did not happen")
+
+// VerifyReboot checks if the state rembers that a system restart was
+// requested and whether it succeeded based on the provided current
+// boot id. It returns ErrExpectedReboot if the expected reboot did
+// not happen yet. It must be called early in the usage of state and
+// before an RequestRestart with RestartSystem is attempted.
+// It must be called with the state lock held.
+func (s *State) VerifyReboot(curBootID string) error {
+ var fromBootID string
+ err := s.Get("system-restart-from-boot-id", &fromBootID)
+ if err != nil && err != ErrNoState {
+ return err
+ }
+ s.bootID = curBootID
+ if fromBootID == "" {
+ return nil
+ }
+ if fromBootID == curBootID {
+ return ErrExpectedReboot
+ }
+ // we rebooted alright
+ s.ClearReboot()
+ return nil
+}
+
+// ClearReboot clears state information about tracking requested reboots.
+func (s *State) ClearReboot() {
+ s.Set("system-restart-from-boot-id", nil)
+}
+
func MockRestarting(s *State, restarting RestartType) RestartType {
s.restartLck.Lock()
defer s.restartLck.Unlock()
diff -Nru snapd-2.40/overlord/state/state_test.go snapd-2.42.1/overlord/state/state_test.go
--- snapd-2.40/overlord/state/state_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/state/state_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -942,6 +942,49 @@
c.Check(t, Equals, state.RestartDaemon)
}
+func (ss *stateSuite) TestRequestRestartSystemAndVerifyReboot(c *C) {
+ b := new(fakeStateBackend)
+ st := state.New(b)
+
+ st.Lock()
+ err := st.VerifyReboot("boot-id-1")
+ st.Unlock()
+ c.Assert(err, IsNil)
+
+ ok, t := st.Restarting()
+ c.Check(ok, Equals, false)
+ c.Check(t, Equals, state.RestartUnset)
+
+ st.Lock()
+ st.RequestRestart(state.RestartSystem)
+ st.Unlock()
+
+ c.Check(b.restartRequested, Equals, true)
+
+ ok, t = st.Restarting()
+ c.Check(ok, Equals, true)
+ c.Check(t, Equals, state.RestartSystem)
+
+ var fromBootID string
+ st.Lock()
+ c.Check(st.Get("system-restart-from-boot-id", &fromBootID), IsNil)
+ st.Unlock()
+ c.Check(fromBootID, Equals, "boot-id-1")
+
+ st.Lock()
+ err = st.VerifyReboot("boot-id-1")
+ st.Unlock()
+ c.Check(err, Equals, state.ErrExpectedReboot)
+
+ st.Lock()
+ err = st.VerifyReboot("boot-id-2")
+ st.Unlock()
+ c.Assert(err, IsNil)
+ st.Lock()
+ c.Check(st.Get("system-restart-from-boot-id", &fromBootID), Equals, state.ErrNoState)
+ st.Unlock()
+}
+
func (ss *stateSuite) TestReadStateInitsCache(c *C) {
st, err := state.ReadState(nil, bytes.NewBufferString("{}"))
c.Assert(err, IsNil)
diff -Nru snapd-2.40/overlord/stateengine.go snapd-2.42.1/overlord/stateengine.go
--- snapd-2.40/overlord/stateengine.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/stateengine.go 2019-10-30 12:17:43.000000000 +0000
@@ -36,6 +36,13 @@
Ensure() error
}
+// StateStarterUp is optionally implemented by StateManager that have expensive
+// initialization to perform before the main Overlord loop.
+type StateStarterUp interface {
+ // StartUp asks manager to perform any expensive initialization.
+ StartUp() error
+}
+
// StateWaiter is optionally implemented by StateManagers that have running
// activities that can be waited.
type StateWaiter interface {
@@ -60,8 +67,9 @@
// cope with Ensure calls in any order, coordinating among themselves
// solely via the state.
type StateEngine struct {
- state *state.State
- stopped bool
+ state *state.State
+ stopped bool
+ startedUp bool
// managers in use
mgrLock sync.Mutex
managers []StateManager
@@ -79,6 +87,37 @@
return se.state
}
+type startupError struct {
+ errs []error
+}
+
+func (e *startupError) Error() string {
+ return fmt.Sprintf("state startup errors: %v", e.errs)
+}
+
+// StartUp asks all managers to perform any expensive initialization. It is a noop after the first invocation.
+func (se *StateEngine) StartUp() error {
+ se.mgrLock.Lock()
+ defer se.mgrLock.Unlock()
+ if se.startedUp {
+ return nil
+ }
+ se.startedUp = true
+ var errs []error
+ for _, m := range se.managers {
+ if starterUp, ok := m.(StateStarterUp); ok {
+ err := starterUp.StartUp()
+ if err != nil {
+ errs = append(errs, err)
+ }
+ }
+ }
+ if len(errs) != 0 {
+ return &startupError{errs}
+ }
+ return nil
+}
+
type ensureError struct {
errs []error
}
@@ -98,6 +137,9 @@
func (se *StateEngine) Ensure() error {
se.mgrLock.Lock()
defer se.mgrLock.Unlock()
+ if !se.startedUp {
+ return fmt.Errorf("state engine skipped startup")
+ }
if se.stopped {
return fmt.Errorf("state engine already stopped")
}
diff -Nru snapd-2.40/overlord/stateengine_test.go snapd-2.42.1/overlord/stateengine_test.go
--- snapd-2.40/overlord/stateengine_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/overlord/stateengine_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -40,9 +40,14 @@
}
type fakeManager struct {
- name string
- calls *[]string
- ensureError, stopError error
+ name string
+ calls *[]string
+ ensureError, startupError error
+}
+
+func (fm *fakeManager) StartUp() error {
+ *fm.calls = append(*fm.calls, "startup:"+fm.name)
+ return fm.startupError
}
func (fm *fakeManager) Ensure() error {
@@ -60,6 +65,48 @@
var _ overlord.StateManager = (*fakeManager)(nil)
+func (ses *stateEngineSuite) TestStartUp(c *C) {
+ s := state.New(nil)
+ se := overlord.NewStateEngine(s)
+
+ calls := []string{}
+
+ mgr1 := &fakeManager{name: "mgr1", calls: &calls}
+ mgr2 := &fakeManager{name: "mgr2", calls: &calls}
+
+ se.AddManager(mgr1)
+ se.AddManager(mgr2)
+
+ err := se.StartUp()
+ c.Assert(err, IsNil)
+ c.Check(calls, DeepEquals, []string{"startup:mgr1", "startup:mgr2"})
+
+ // noop
+ err = se.StartUp()
+ c.Assert(err, IsNil)
+ c.Check(calls, HasLen, 2)
+}
+
+func (ses *stateEngineSuite) TestStartUpError(c *C) {
+ s := state.New(nil)
+ se := overlord.NewStateEngine(s)
+
+ calls := []string{}
+
+ err1 := errors.New("boom1")
+ err2 := errors.New("boom2")
+
+ mgr1 := &fakeManager{name: "mgr1", calls: &calls, startupError: err1}
+ mgr2 := &fakeManager{name: "mgr2", calls: &calls, startupError: err2}
+
+ se.AddManager(mgr1)
+ se.AddManager(mgr2)
+
+ err := se.StartUp()
+ c.Check(err.Error(), DeepEquals, "state startup errors: [boom1 boom2]")
+ c.Check(calls, DeepEquals, []string{"startup:mgr1", "startup:mgr2"})
+}
+
func (ses *stateEngineSuite) TestEnsure(c *C) {
s := state.New(nil)
se := overlord.NewStateEngine(s)
@@ -73,6 +120,11 @@
se.AddManager(mgr2)
err := se.Ensure()
+ c.Check(err, ErrorMatches, "state engine skipped startup")
+ c.Assert(se.StartUp(), IsNil)
+ calls = []string{}
+
+ err = se.Ensure()
c.Assert(err, IsNil)
c.Check(calls, DeepEquals, []string{"ensure:mgr1", "ensure:mgr2"})
@@ -96,6 +148,9 @@
se.AddManager(mgr1)
se.AddManager(mgr2)
+ c.Assert(se.StartUp(), IsNil)
+ calls = []string{}
+
err := se.Ensure()
c.Check(err.Error(), DeepEquals, "state ensure errors: [boom1 boom2]")
c.Check(calls, DeepEquals, []string{"ensure:mgr1", "ensure:mgr2"})
@@ -113,6 +168,9 @@
se.AddManager(mgr1)
se.AddManager(mgr2)
+ c.Assert(se.StartUp(), IsNil)
+ calls = []string{}
+
se.Stop()
c.Check(calls, DeepEquals, []string{"stop:mgr1", "stop:mgr2"})
se.Stop()
diff -Nru snapd-2.40/packaging/amzn-2/snapd.spec snapd-2.42.1/packaging/amzn-2/snapd.spec
--- snapd-2.40/packaging/amzn-2/snapd.spec 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/amzn-2/snapd.spec 2019-10-30 12:17:43.000000000 +0000
@@ -60,9 +60,10 @@
%global import_path %{provider_prefix}
%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service snapd.seeded.service
+%global snappy_user_svcs snapd.session-agent.socket
# Until we have a way to add more extldflags to gobuild macro...
-%if 0%{?fedora}
+%if 0%{?fedora} || 0%{?rhel} >= 8
# buildmode PIE triggers external linker consumes -extldflags
%define gobuild_static(o:) go build -buildmode pie -compiler gc -tags=rpm_crashtraceback -ldflags "${LDFLAGS:-} -B 0x$(head -c20 /dev/urandom|od -An -tx1|tr -d ' \\n') -extldflags '%__global_ldflags -static'" -a -v -x %{?**};
%endif
@@ -91,7 +92,7 @@
%endif
Name: snapd
-Version: 2.40
+Version: 2.42.1
Release: 0%{?dist}
Summary: A transactional software package manager
License: GPLv3
@@ -146,7 +147,6 @@
%if ! 0%{?with_bundled}
BuildRequires: golang(github.com/boltdb/bolt)
-BuildRequires: golang(github.com/cheggaaa/pb)
BuildRequires: golang(github.com/coreos/go-systemd/activation)
BuildRequires: golang(github.com/godbus/dbus)
BuildRequires: golang(github.com/godbus/dbus/introspect)
@@ -156,8 +156,8 @@
BuildRequires: golang(github.com/kr/pretty)
BuildRequires: golang(github.com/kr/text)
BuildRequires: golang(github.com/mvo5/goconfigparser)
-BuildRequires: golang(github.com/ojii/gettext.go)
BuildRequires: golang(github.com/seccomp/libseccomp-golang)
+BuildRequires: golang(github.com/snapcore/go-gettext)
BuildRequires: golang(golang.org/x/crypto/openpgp/armor)
BuildRequires: golang(golang.org/x/crypto/openpgp/packet)
BuildRequires: golang(golang.org/x/crypto/sha3)
@@ -183,7 +183,6 @@
BuildRequires: gcc
BuildRequires: gettext
BuildRequires: gnupg
-BuildRequires: indent
BuildRequires: pkgconfig(glib-2.0)
BuildRequires: pkgconfig(libcap)
BuildRequires: pkgconfig(libseccomp)
@@ -243,7 +242,6 @@
%if ! 0%{?with_bundled}
Requires: golang(github.com/boltdb/bolt)
-Requires: golang(github.com/cheggaaa/pb)
Requires: golang(github.com/coreos/go-systemd/activation)
Requires: golang(github.com/godbus/dbus)
Requires: golang(github.com/godbus/dbus/introspect)
@@ -253,8 +251,8 @@
Requires: golang(github.com/kr/pretty)
Requires: golang(github.com/kr/text)
Requires: golang(github.com/mvo5/goconfigparser)
-Requires: golang(github.com/ojii/gettext.go)
Requires: golang(github.com/seccomp/libseccomp-golang)
+Requires: golang(github.com/snapcore/go-gettext)
Requires: golang(golang.org/x/crypto/openpgp/armor)
Requires: golang(golang.org/x/crypto/openpgp/packet)
Requires: golang(golang.org/x/crypto/sha3)
@@ -270,7 +268,6 @@
# the bundled tarball are unversioned (they go by git commit)
# *sigh*... I hate golang...
Provides: bundled(golang(github.com/snapcore/bolt))
-Provides: bundled(golang(github.com/cheggaaa/pb))
Provides: bundled(golang(github.com/coreos/go-systemd/activation))
Provides: bundled(golang(github.com/godbus/dbus))
Provides: bundled(golang(github.com/godbus/dbus/introspect))
@@ -281,7 +278,7 @@
Provides: bundled(golang(github.com/kr/text))
Provides: bundled(golang(github.com/mvo5/goconfigparser))
Provides: bundled(golang(github.com/mvo5/libseccomp-golang))
-Provides: bundled(golang(github.com/ojii/gettext.go))
+Provides: bundled(golang(github.com/snapcore/go-gettext))
Provides: bundled(golang(golang.org/x/crypto/openpgp/armor))
Provides: bundled(golang(golang.org/x/crypto/openpgp/packet))
Provides: bundled(golang(golang.org/x/crypto/sha3))
@@ -728,6 +725,8 @@
%{_unitdir}/snapd.autoimport.service
%{_unitdir}/snapd.failure.service
%{_unitdir}/snapd.seeded.service
+%{_userunitdir}/snapd.session-agent.service
+%{_userunitdir}/snapd.session-agent.socket
%{_datadir}/dbus-1/services/io.snapcraft.Launcher.service
%{_datadir}/dbus-1/services/io.snapcraft.Settings.service
%{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy
@@ -803,6 +802,7 @@
%sysctl_apply 99-snap.conf
%endif
%systemd_post %{snappy_svcs}
+%systemd_user_post %{snappy_user_svcs}
# If install, test if snapd socket and timer are enabled.
# If enabled, then attempt to start them. This will silently fail
# in chroots or other environments where services aren't expected
@@ -815,6 +815,7 @@
%preun
%systemd_preun %{snappy_svcs}
+%systemd_user_preun %{snappy_user_svcs}
# Remove all Snappy content if snapd is being fully uninstalled
if [ $1 -eq 0 ]; then
@@ -823,6 +824,7 @@
%postun
%systemd_postun_with_restart %{snappy_svcs}
+%systemd_user_postun %{snappy_user_svcs}
%if 0%{?with_selinux}
%triggerun -- snapd < 2.39
@@ -866,6 +868,453 @@
%changelog
+* Wed Oct 30 2019 Michael Vogt
+- New upstream release 2.42.1
+ - interfaces: de-duplicate emitted update-ns profiles
+ - packaging: tweak handling of usr.lib.snapd.snap-confine
+ - interfaces: allow introspecting network-manager on core
+ - tests/main/interfaces-contacts-service: disable on openSUSE
+ Tumbleweed
+ - tests/lib/lxd-snapfuse: restore mount changes introduced by LXD
+ - snap: fix default-provider in seed validation
+ - tests: update system-usernames test now that opensuse-15.1 works
+ - overlord: set fake sertial in TestRemodelSwitchToDifferentKernel
+ - gadget: rename "boot{select,img}" -> system-boot-{select,image}
+ - tests: listing test, make accepted snapd/core versions consistent
+
+* Tue Oct 01 2019 Michael Vogt
+- New upstream release 2.42
+ - tests: disable {contacts,calendar}-service tests on debian-sid
+ - tests/main/snap-run: disable strace test cases on Arch
+ - cmd/system-shutdown: include correct prototype for die
+ - snap/naming: add test for hook name connect-plug-i2c
+ - cmd/snap-confine: allow digits in hook names
+ - gadget: do not fail the update when old gadget snap is missing
+ bare content
+ - tests: disable {contacts,calendar}-service tests on Arch Linux
+ - tests: move "centos-7" to unstable systems
+ - interfaces/docker-support,kubernetes-support: misc updates for
+ strict k8s
+ - packaging: remove obsolete usr.lib.snapd.snap-confine in
+ postinst
+ - tests: add test that ensures our snapfuse binary actually works
+ - packaging: use snapfuse_ll to speed up snapfuse performance
+ - usersession/userd: make sure to export DBus interfaces before
+ requesting a name
+ - data/selinux: allow snapd to issue sigkill to journalctl
+ - store: download propagates options to delta download
+ - wrappers: allow snaps to install icon theme icons
+ - debug: state-inspect debugging utility
+ - sandbox/cgroup: introduce cgroup wrappers package
+ - snap-confine: fix return value checks for udev functions
+ - cmd/model: output tweaks, add'l tests
+ - wrappers/services: add ServicesEnableState + unit tests
+ - tests: fix newline and wrong test name pointed out in previous PRs
+ - tests: extend mount-ns test to handle mimics
+ - run-checks, tests/main/go: allow gofmt checks to be skipped on
+ 19.10
+ - tests/main/interfaces-{calendar,contacts}-service: disable on
+ 19.10
+ - tests: part3 making tests work on ubuntu-core-18
+ - tests: fix interfaces-timeserver-control on 19.10
+ - overlord/snapstate: config revision code cleanup and extra tests
+ - devicestate: allow remodel to different kernels
+ - overlord,daemon: adjust startup timeout via EXTEND_TIMEOUT_USEC
+ using an estimate
+ - tests/main/many: increase kill-timeout to 5m
+ - interfaces/kubernetes-support: allow systemd-run to ptrace read
+ unconfined
+ - snapstate: auto transition on experimental.snapd-snap=true
+ - tests: retry checking until the written file on desktop-portal-
+ filechooser
+ - tests: unit test for a refresh failing on configure hook
+ - tests: remove mount_id and parent_id from mount-ns test data
+ - tests: move classic-ubuntu-core-transition* to nightly
+ - tests/mountinfo-tool: proper formatting of opt_fields
+ - overlord/configstate: special-case "null" in transaction Changes()
+ - snap-confine: fallback gracefully on a cgroup v2 only system
+ - tests: debian sid now ships new seccomp, adjust tests
+ - tests: explicitly restore after using LXD
+ - snapstate: make progress reporting less granular
+ - bootloader: little kernel support
+ - fixme: rename ubuntu*architectures to dpkg*architectures
+ - tests: run dbus-launch inside a systemd unit
+ - channel: introduce Resolve and ResolveLocked
+ - tests: run failing tests on ubuntu eoan due to is now set as
+ unstable
+ - systemd: detach rather than unmount .mount units
+ - cmd/snap-confine: add unit tests for sc_invocation, cleanup memory
+ leaks in tests
+ - boot,dirs,image: introduce boot.MakeBootable, use it in image
+ instead of ad hoc code
+ - cmd/snap-update-ns: clarify sharing comment
+ - tests/overlord/snapstate: refactor for cleaner test failures
+ - cmd/snap-update-ns: don't propagate detaching changes
+ - interfaces: allow reading mutter Xauthority file
+ - cmd/snap-confine: fix /snap duplication in legacy mode
+ - tests: fix mountinfo-tool filtering when used with rewriting
+ - seed,image,o/devicestate: extract seed loading to seed/seed16.go
+ - many: pass the rootdir and options to bootloader.Find
+ - tests: part5 making tests work on ubuntu-core-18
+ - cmd/snap-confine: keep track of snap instance name and the snap
+ name
+ - cmd: unify die() across C programs
+ - tests: add functions to make an abstraction for the snaps
+ - packaging/fedora, tests/lib/prepare-restore: helper tool for
+ packing sources for RPM
+ - cmd/snap: improve help and error msg for snapshot commands
+ - hookstate/ctlcmd: fix snapctl set help message
+ - cmd/snap: don't append / to snap name just because a dir exists
+ - tests: support fastly-global.cdn.snapcraft.io url on proxy-no-core
+ test
+ - tests: add --quiet switch to retry-tool
+ - tests: add unstable stage for travis execution
+ - tests: disable interfaces-timeserver-control on 19.10
+ - tests: don't guess in is_classic_confinement_supported
+ - boot, etc: simplify BootParticipant (etc) usage
+ - tests: verify retry-tool not retrying missing commands
+ - tests: rewrite "retry" command as retry-tool
+ - tests: move debug section after restore
+ - cmd/libsnap-confine-private, cmd/s-c: use constants for
+ snap/instance name lengths
+ - tests: measure behavior of the device cgroup
+ - boot, bootloader, o/devicestate: boot env manip goes in boot
+ - tests: enabling ubuntu 19.10-64 on spread.yaml
+ - tests: fix ephemeral mount table in left over by prepare
+ - tests: add version-tool for comparing versions
+ - cmd/libsnap: make feature flag enum 1<
+- New upstream release 2.41
+ - overlord/snapstate: revert track-risk behavior
+ - tests: fix snap info test
+ - httputil: rework protocol error detection
+ - gadget: do not error on gadget refreshes with multiple volumes
+ - i18n, vendor, packaging: drop github.com/ojii/gettext.go, use
+ github.com/snapcore/go-gettext
+ - snapstate: validate all system-usernames before creating them
+ - mkversion.sh: fix version from git checkouts
+ - interfaces/network-{control,manager}: allow 'k' on
+ /run/resolvconf/**
+ - interfaces/wayland,x11: allow reading an Xwayland Xauth file
+ - interfaces: k8s worker node updates
+ - debian: re-enable systemd environment generator
+ - many: create system-usernames user/group if both don't exist
+ - packaging: fix symlink for snapd.session-agent.socket
+ - tests: change cgroups so that LXD doesn't have to
+ - interfaces/network-setup-control: allow dbus netplan apply
+ messages
+ - tests: add /var/cache/snapd to the snapd state to prevent error on
+ the store
+ - tests: add test for services disabled during refresh hook
+ - many: simpler access to snap-seccomp version-info
+ - snap: cleanup some tests, clarify some errorsThis is a follow up
+ from work on system usernames:
+ - osutil: add osutil.Find{Uid,Gid}
+ - tests: use a different archive based on the spread backend on go-
+ build test
+ - cmd/snap-update-ns: fix pair of bugs affecting refresh of snap
+ with layouts
+ - overlord/devicestate: detect clashing concurrent (ongoing, just
+ finished) remodels or changes
+ - interfaces/docker-support: declare controls-device-cgroup
+ - packaging: fix removal of old apparmor profile
+ - store: use track/risk for "channel" name when parsing store
+ details
+ - many: allow 'system-usernames' with libseccomp > 2.4 and golang-
+ seccomp > 0.9.0
+ - overlord/devicestate, tests: use gadget.Update() proper, spread
+ test
+ - overlord/configstate/configcore: allow setting start_x=1 to enable
+ CSI camera on RPi
+ - interfaces: remove BeforePrepareSlot from commonInterface
+ - many: support system-usernames for 'snap_daemon' user
+ - overlord/devicestate,o/snapstate: queue service commands before
+ mark-seeded and other final tasks
+ - interfaces/mount: discard mount ns on backend Remove
+ - packaging/fedora: build on RHEL8
+ - overlord/devicestate: support seeding a classic system with the
+ snapd snap and no core
+ - interfaces: fix test failure in gpio_control_test
+ - interfaces, policy: remove sanitize helpers and use minimal policy
+ check
+ - packaging: use %systemd_user_* macros to enable session agent
+ socket according to presets
+ - snapstate, store: handle 429s on catalog refresh a little bit
+ better
+ - tests: part4 making tests work on ubuntu-core-18
+ - many: drop snap.ReadGadgetInfo wrapper
+ - xdgopenproxy: update test API to match upstream
+ - tests: show why sbuild failed
+ - data/selinux: allow mandb_t to search /var/lib/snapd
+ - tests: be less verbose when checking service status
+ - tests: set sbuild test as manual
+ - overlord: DeviceCtx must find the remodel context for a remodel
+ change
+ - tests: use snap info --verbose to check for base
+ - sanity: unmount squashfs with --lazy
+ - overlord/snapstate: keep current track if only risk is specified
+ - interfaces/firewall-control: support nft routing expressions and
+ device groups
+ - gadget: support for writing symlinks
+ - tests: mountinfo-tool fail if there are no matches
+ - tests: sync journal log before start the test
+ - cmd/snap, data/completion: improve completion for 'snap debug'
+ - httputil: retry for http2 PROTOCOL_ERROR
+ - Errata commit: pulseaudio still auto-connects on classic
+ - interfaces/misc: updates for k8s 1.15 (and greengrass test)
+ - tests: set GOTRACEBACK=1 when running tests
+ - cmd/libsnap: don't leak memory in sc_die_on_error
+ - tests: improve how the system is restored when the upgrade-
+ from-2.15 test fails
+ - interfaces/bluetooth-control: add udev rules for BT_chrdev devices
+ - interfaces: add audio-playback/audio-record and make pulseaudio
+ manually connect
+ - tests: split the sbuild test in 2 depending on the type of build
+ - interfaces: add an interface granting access to AppStream metadata
+ - gadget: ensure filesystem labels are unique
+ - usersession/agent: use background context when stopping the agent
+ - HACKING.md: update spread section, other updates
+ - data/selinux: allow snap-confine to read entries on nsfs
+ - tests: respect SPREAD_DEBUG_EACH on the main suite
+ - packaging/debian-sid: set GOCACHE to a known writable location
+ - interfaces: add gpio-control interface
+ - cmd/snap: use showDone helper with 'snap switch'
+ - gadget: effective structure role fallback, extra tests
+ - many: fix unit tests getting stuck
+ - tests: remove installed snap on restore
+ - daemon: do not modify test data in user suite
+ - data/selinux: allow read on sysfs
+ - packaging/debian: don't md5sum absent files
+ - tests: remove test-snapd-curl
+ - tests: remove test-snapd-snapctl-core18 in restore
+ - tests: remove installed snap in the restore section
+ - tests: remove installed test snap
+ - tests: correctly escape mount unit path
+ - cmd/Makefile.am: support building with the go snap
+ - tests: work around classic snap affecting the host
+ - tests: fix typo "current"
+ - overlord/assertstate: add Batch.Precheck to check for the full
+ validity of the batch before Commit
+ - tests: restore cpuset clone_children clobbered by lxd
+ - usersession: move userd package to usersession/userd
+ - tests: reformat and fix markdown in snapd-state.md
+ - gadget: select the right updater for given structure
+ - tests: show stderr only if it exists
+ - sessionagent: add a REST interface with socket activation
+ - tests: remove locally installed core in more tests
+ - tests: remove local revision of core
+ - packaging/debian-sid: use correct apparmor Depends for Debian
+ - packaging/debian-sid: merge debian upload changes back into master
+ - cmd/snap-repair: make sure the goroutine doesn't stick around on
+ timeout
+ - packaging/fedora: github.com/cheggaaa/pb is no longer used
+ - configstate/config: fix crash in purgeNulls
+ - boot, o/snapst, o/devicest: limit knowledge of boot vars to boot
+ - client,cmd/snap: stop depending on status/status-code in the JSON
+ responses in client
+ - tests: unmount leftover /run/netns
+ - tests: switch mount-ns test to manual
+ - overlord,daemon,cmd/snapd: move expensive startup to dedicated
+ StartUp methods
+ - osutil: add EnsureTreeState helper
+ - tests: measure properties of various mount namespaces
+ - tests: part2 making tests work on ubuntu-core-18
+ - interfaces/policy: minimal policy check for replacing
+ sanitizeReservedFor helpers (1/2)
+ - interfaces: add an interface that grants access to the PackageKit
+ service
+ - overlord/devicestate: update gadget update handlers and mocks
+ - tests: add mountinfo-tool --ref-x1000
+ - tests: remove lxd / lxcfs if pre-installed
+ - tests: removing support for ubuntu cosmic on spread test suite
+ - tests: don't leak /run/netns mount
+ - image: clean up the validateSuite
+ - bootloader: remove "Dir()" from Bootloader interface
+ - many: retry to reboot if snapd gets restarted before expected
+ reboot
+ - overlord: implement re-registration remodeling
+ - cmd: revert PR#6933 (tweak of GOMAXPROCS)
+ - cmd/snap: add snap unset command
+ - many: add Client-User-Agent to "SnapAction" install API call
+ - tests: first part making tests run on ubuntu-core-18
+ - hookstate/ctlcmd: support hidden commands in snapctl
+ - many: replace snapd snap name checks with type checks (3/4)
+ - overlord: mostly stop needing Kernel/CoreInfo, make GadgetInfo
+ consider a DeviceContext
+ - snapctl: handle unsetting of config options with "!"
+ - tests: move core migration snaps to tests/lib/snaps dir
+ - cmd/snap: handle unsetting of config options with "!"
+ - cmd/snap, etc: add health to 'snap list' and 'snap info'
+ - gadget: use struct field names when intializing data in mounted
+ updater unit tests
+ - cmd/snap-confine: bring /lib/firmware from the host
+ - snap: set snapd snap type (1/4)
+ - snap: add checks in validate-seed for missing base/default-
+ provider
+ - daemon: replace shutdownServer with net/http's native shutdown
+ support
+ - interfaces/builtin: add exec "/bin/runc" to docker-support
+ - gadget: mounted filesystem updater
+ - overlord/patch: simplify conditions for re-applying sublevel
+ patches for level 6
+ - seccomp/compiler: adjust test case names and comment for later
+ changes
+ - tests: fix error doing snap pack running failover test
+ - tests: don't preserve size= when rewriting mount tables
+ - tests: allow reordering of rewrite operations
+ - gadget: main update routine
+ - overlord/config: normalize nulls to support config unsetting
+ semantics
+ - snap-userd-autostart: don't list as a startup application on the
+ GUI
+ - tests: renumber snap revisions as seen via writable
+ - tests: change allocation for mount options
+ - tests: re-enable ns-re-associate test
+ - tests: mountinfo-tool allow many --refs
+ - overlord/devicestate: implement reregRemodelContext with the
+ essential re-registration logic
+ - tests: replace various numeric mount options
+ - gadget: filesystem image writer
+ - tests: add more unit tests for mountinfo-tool
+ - tests: introduce mountinfo-tool --ref feature
+ - tests: refactor mountinfo-tool rewrite state
+ - tests: allow renumbering mount namespace identifiers
+ - snap: refactor and explain layout blacklisting
+ - tests: renumber snap revisions as seen via hostfs
+ - daemon, interfaces, travis: workaround build ID with Go 1.9, use
+ 1.9 for travis tests
+ - cmd/libsnap: add sc_error_init_{simple,api_misuse}
+ - gadget: make raw updater handle shifted structures
+ - tests/lib/nested: create WORK_DIR before accessing it
+ - cmd/libsnap: rename SC_LIBSNAP_ERROR to SC_LIBSNAP_DOMAIN
+ - cmd,tests: forcibly discard mount namespace when bases change
+ - many: introduce healthstate, run check-health
+ post-(install/refresh/try/revert)
+ - interfaces/optical-drive: add scsi-generic type 4 and 5 support
+ - cmd/snap-confine: exit from helper when parent dies
+
* Fri Jul 12 2019 Michael Vogt
- New upstream release 2.40
- overlord/patch: simplify conditions for re-applying sublevel
@@ -969,7 +1418,7 @@
- tests/lib/nested: fix multi argument copy_remote
- tests/lib/nested: have mkfs.ext4 use a rootdir instead of mounting
an image
- - packaging: fix permissons powerpc docs dir
+ - packaging: fix permissions powerpc docs dir
- overlord: mock store to avoid net requests
- debian: rework how we run autopkgtests
- interface: builtin: avahi-observe/control: allow slots
diff -Nru snapd-2.40/packaging/arch/PKGBUILD snapd-2.42.1/packaging/arch/PKGBUILD
--- snapd-2.40/packaging/arch/PKGBUILD 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/arch/PKGBUILD 2019-10-30 12:17:43.000000000 +0000
@@ -10,7 +10,7 @@
pkgdesc="Service and tools for management of snap packages."
depends=('squashfs-tools' 'libseccomp' 'libsystemd')
optdepends=('bash-completion: bash completion support')
-pkgver=2.40
+pkgver=2.42.1
pkgrel=1
arch=('x86_64')
url="https://github.com/snapcore/snapd"
diff -Nru snapd-2.40/packaging/centos-7/snapd.spec snapd-2.42.1/packaging/centos-7/snapd.spec
--- snapd-2.40/packaging/centos-7/snapd.spec 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/centos-7/snapd.spec 2019-10-30 12:17:43.000000000 +0000
@@ -60,9 +60,10 @@
%global import_path %{provider_prefix}
%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service snapd.seeded.service
+%global snappy_user_svcs snapd.session-agent.socket
# Until we have a way to add more extldflags to gobuild macro...
-%if 0%{?fedora}
+%if 0%{?fedora} || 0%{?rhel} >= 8
# buildmode PIE triggers external linker consumes -extldflags
%define gobuild_static(o:) go build -buildmode pie -compiler gc -tags=rpm_crashtraceback -ldflags "${LDFLAGS:-} -B 0x$(head -c20 /dev/urandom|od -An -tx1|tr -d ' \\n') -extldflags '%__global_ldflags -static'" -a -v -x %{?**};
%endif
@@ -91,7 +92,7 @@
%endif
Name: snapd
-Version: 2.40
+Version: 2.42.1
Release: 0%{?dist}
Summary: A transactional software package manager
License: GPLv3
@@ -146,7 +147,6 @@
%if ! 0%{?with_bundled}
BuildRequires: golang(github.com/boltdb/bolt)
-BuildRequires: golang(github.com/cheggaaa/pb)
BuildRequires: golang(github.com/coreos/go-systemd/activation)
BuildRequires: golang(github.com/godbus/dbus)
BuildRequires: golang(github.com/godbus/dbus/introspect)
@@ -156,8 +156,8 @@
BuildRequires: golang(github.com/kr/pretty)
BuildRequires: golang(github.com/kr/text)
BuildRequires: golang(github.com/mvo5/goconfigparser)
-BuildRequires: golang(github.com/ojii/gettext.go)
BuildRequires: golang(github.com/seccomp/libseccomp-golang)
+BuildRequires: golang(github.com/snapcore/go-gettext)
BuildRequires: golang(golang.org/x/crypto/openpgp/armor)
BuildRequires: golang(golang.org/x/crypto/openpgp/packet)
BuildRequires: golang(golang.org/x/crypto/sha3)
@@ -183,7 +183,6 @@
BuildRequires: gcc
BuildRequires: gettext
BuildRequires: gnupg
-BuildRequires: indent
BuildRequires: pkgconfig(glib-2.0)
BuildRequires: pkgconfig(libcap)
BuildRequires: pkgconfig(libseccomp)
@@ -243,7 +242,6 @@
%if ! 0%{?with_bundled}
Requires: golang(github.com/boltdb/bolt)
-Requires: golang(github.com/cheggaaa/pb)
Requires: golang(github.com/coreos/go-systemd/activation)
Requires: golang(github.com/godbus/dbus)
Requires: golang(github.com/godbus/dbus/introspect)
@@ -253,8 +251,8 @@
Requires: golang(github.com/kr/pretty)
Requires: golang(github.com/kr/text)
Requires: golang(github.com/mvo5/goconfigparser)
-Requires: golang(github.com/ojii/gettext.go)
Requires: golang(github.com/seccomp/libseccomp-golang)
+Requires: golang(github.com/snapcore/go-gettext)
Requires: golang(golang.org/x/crypto/openpgp/armor)
Requires: golang(golang.org/x/crypto/openpgp/packet)
Requires: golang(golang.org/x/crypto/sha3)
@@ -270,7 +268,6 @@
# the bundled tarball are unversioned (they go by git commit)
# *sigh*... I hate golang...
Provides: bundled(golang(github.com/snapcore/bolt))
-Provides: bundled(golang(github.com/cheggaaa/pb))
Provides: bundled(golang(github.com/coreos/go-systemd/activation))
Provides: bundled(golang(github.com/godbus/dbus))
Provides: bundled(golang(github.com/godbus/dbus/introspect))
@@ -281,7 +278,7 @@
Provides: bundled(golang(github.com/kr/text))
Provides: bundled(golang(github.com/mvo5/goconfigparser))
Provides: bundled(golang(github.com/mvo5/libseccomp-golang))
-Provides: bundled(golang(github.com/ojii/gettext.go))
+Provides: bundled(golang(github.com/snapcore/go-gettext))
Provides: bundled(golang(golang.org/x/crypto/openpgp/armor))
Provides: bundled(golang(golang.org/x/crypto/openpgp/packet))
Provides: bundled(golang(golang.org/x/crypto/sha3))
@@ -728,6 +725,8 @@
%{_unitdir}/snapd.autoimport.service
%{_unitdir}/snapd.failure.service
%{_unitdir}/snapd.seeded.service
+%{_userunitdir}/snapd.session-agent.service
+%{_userunitdir}/snapd.session-agent.socket
%{_datadir}/dbus-1/services/io.snapcraft.Launcher.service
%{_datadir}/dbus-1/services/io.snapcraft.Settings.service
%{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy
@@ -803,6 +802,7 @@
%sysctl_apply 99-snap.conf
%endif
%systemd_post %{snappy_svcs}
+%systemd_user_post %{snappy_user_svcs}
# If install, test if snapd socket and timer are enabled.
# If enabled, then attempt to start them. This will silently fail
# in chroots or other environments where services aren't expected
@@ -815,6 +815,7 @@
%preun
%systemd_preun %{snappy_svcs}
+%systemd_user_preun %{snappy_user_svcs}
# Remove all Snappy content if snapd is being fully uninstalled
if [ $1 -eq 0 ]; then
@@ -823,6 +824,7 @@
%postun
%systemd_postun_with_restart %{snappy_svcs}
+%systemd_user_postun %{snappy_user_svcs}
%if 0%{?with_selinux}
%triggerun -- snapd < 2.39
@@ -866,6 +868,453 @@
%changelog
+* Wed Oct 30 2019 Michael Vogt
+- New upstream release 2.42.1
+ - interfaces: de-duplicate emitted update-ns profiles
+ - packaging: tweak handling of usr.lib.snapd.snap-confine
+ - interfaces: allow introspecting network-manager on core
+ - tests/main/interfaces-contacts-service: disable on openSUSE
+ Tumbleweed
+ - tests/lib/lxd-snapfuse: restore mount changes introduced by LXD
+ - snap: fix default-provider in seed validation
+ - tests: update system-usernames test now that opensuse-15.1 works
+ - overlord: set fake sertial in TestRemodelSwitchToDifferentKernel
+ - gadget: rename "boot{select,img}" -> system-boot-{select,image}
+ - tests: listing test, make accepted snapd/core versions consistent
+
+* Tue Oct 01 2019 Michael Vogt
+- New upstream release 2.42
+ - tests: disable {contacts,calendar}-service tests on debian-sid
+ - tests/main/snap-run: disable strace test cases on Arch
+ - cmd/system-shutdown: include correct prototype for die
+ - snap/naming: add test for hook name connect-plug-i2c
+ - cmd/snap-confine: allow digits in hook names
+ - gadget: do not fail the update when old gadget snap is missing
+ bare content
+ - tests: disable {contacts,calendar}-service tests on Arch Linux
+ - tests: move "centos-7" to unstable systems
+ - interfaces/docker-support,kubernetes-support: misc updates for
+ strict k8s
+ - packaging: remove obsolete usr.lib.snapd.snap-confine in
+ postinst
+ - tests: add test that ensures our snapfuse binary actually works
+ - packaging: use snapfuse_ll to speed up snapfuse performance
+ - usersession/userd: make sure to export DBus interfaces before
+ requesting a name
+ - data/selinux: allow snapd to issue sigkill to journalctl
+ - store: download propagates options to delta download
+ - wrappers: allow snaps to install icon theme icons
+ - debug: state-inspect debugging utility
+ - sandbox/cgroup: introduce cgroup wrappers package
+ - snap-confine: fix return value checks for udev functions
+ - cmd/model: output tweaks, add'l tests
+ - wrappers/services: add ServicesEnableState + unit tests
+ - tests: fix newline and wrong test name pointed out in previous PRs
+ - tests: extend mount-ns test to handle mimics
+ - run-checks, tests/main/go: allow gofmt checks to be skipped on
+ 19.10
+ - tests/main/interfaces-{calendar,contacts}-service: disable on
+ 19.10
+ - tests: part3 making tests work on ubuntu-core-18
+ - tests: fix interfaces-timeserver-control on 19.10
+ - overlord/snapstate: config revision code cleanup and extra tests
+ - devicestate: allow remodel to different kernels
+ - overlord,daemon: adjust startup timeout via EXTEND_TIMEOUT_USEC
+ using an estimate
+ - tests/main/many: increase kill-timeout to 5m
+ - interfaces/kubernetes-support: allow systemd-run to ptrace read
+ unconfined
+ - snapstate: auto transition on experimental.snapd-snap=true
+ - tests: retry checking until the written file on desktop-portal-
+ filechooser
+ - tests: unit test for a refresh failing on configure hook
+ - tests: remove mount_id and parent_id from mount-ns test data
+ - tests: move classic-ubuntu-core-transition* to nightly
+ - tests/mountinfo-tool: proper formatting of opt_fields
+ - overlord/configstate: special-case "null" in transaction Changes()
+ - snap-confine: fallback gracefully on a cgroup v2 only system
+ - tests: debian sid now ships new seccomp, adjust tests
+ - tests: explicitly restore after using LXD
+ - snapstate: make progress reporting less granular
+ - bootloader: little kernel support
+ - fixme: rename ubuntu*architectures to dpkg*architectures
+ - tests: run dbus-launch inside a systemd unit
+ - channel: introduce Resolve and ResolveLocked
+ - tests: run failing tests on ubuntu eoan due to is now set as
+ unstable
+ - systemd: detach rather than unmount .mount units
+ - cmd/snap-confine: add unit tests for sc_invocation, cleanup memory
+ leaks in tests
+ - boot,dirs,image: introduce boot.MakeBootable, use it in image
+ instead of ad hoc code
+ - cmd/snap-update-ns: clarify sharing comment
+ - tests/overlord/snapstate: refactor for cleaner test failures
+ - cmd/snap-update-ns: don't propagate detaching changes
+ - interfaces: allow reading mutter Xauthority file
+ - cmd/snap-confine: fix /snap duplication in legacy mode
+ - tests: fix mountinfo-tool filtering when used with rewriting
+ - seed,image,o/devicestate: extract seed loading to seed/seed16.go
+ - many: pass the rootdir and options to bootloader.Find
+ - tests: part5 making tests work on ubuntu-core-18
+ - cmd/snap-confine: keep track of snap instance name and the snap
+ name
+ - cmd: unify die() across C programs
+ - tests: add functions to make an abstraction for the snaps
+ - packaging/fedora, tests/lib/prepare-restore: helper tool for
+ packing sources for RPM
+ - cmd/snap: improve help and error msg for snapshot commands
+ - hookstate/ctlcmd: fix snapctl set help message
+ - cmd/snap: don't append / to snap name just because a dir exists
+ - tests: support fastly-global.cdn.snapcraft.io url on proxy-no-core
+ test
+ - tests: add --quiet switch to retry-tool
+ - tests: add unstable stage for travis execution
+ - tests: disable interfaces-timeserver-control on 19.10
+ - tests: don't guess in is_classic_confinement_supported
+ - boot, etc: simplify BootParticipant (etc) usage
+ - tests: verify retry-tool not retrying missing commands
+ - tests: rewrite "retry" command as retry-tool
+ - tests: move debug section after restore
+ - cmd/libsnap-confine-private, cmd/s-c: use constants for
+ snap/instance name lengths
+ - tests: measure behavior of the device cgroup
+ - boot, bootloader, o/devicestate: boot env manip goes in boot
+ - tests: enabling ubuntu 19.10-64 on spread.yaml
+ - tests: fix ephemeral mount table in left over by prepare
+ - tests: add version-tool for comparing versions
+ - cmd/libsnap: make feature flag enum 1<
+- New upstream release 2.41
+ - overlord/snapstate: revert track-risk behavior
+ - tests: fix snap info test
+ - httputil: rework protocol error detection
+ - gadget: do not error on gadget refreshes with multiple volumes
+ - i18n, vendor, packaging: drop github.com/ojii/gettext.go, use
+ github.com/snapcore/go-gettext
+ - snapstate: validate all system-usernames before creating them
+ - mkversion.sh: fix version from git checkouts
+ - interfaces/network-{control,manager}: allow 'k' on
+ /run/resolvconf/**
+ - interfaces/wayland,x11: allow reading an Xwayland Xauth file
+ - interfaces: k8s worker node updates
+ - debian: re-enable systemd environment generator
+ - many: create system-usernames user/group if both don't exist
+ - packaging: fix symlink for snapd.session-agent.socket
+ - tests: change cgroups so that LXD doesn't have to
+ - interfaces/network-setup-control: allow dbus netplan apply
+ messages
+ - tests: add /var/cache/snapd to the snapd state to prevent error on
+ the store
+ - tests: add test for services disabled during refresh hook
+ - many: simpler access to snap-seccomp version-info
+ - snap: cleanup some tests, clarify some errorsThis is a follow up
+ from work on system usernames:
+ - osutil: add osutil.Find{Uid,Gid}
+ - tests: use a different archive based on the spread backend on go-
+ build test
+ - cmd/snap-update-ns: fix pair of bugs affecting refresh of snap
+ with layouts
+ - overlord/devicestate: detect clashing concurrent (ongoing, just
+ finished) remodels or changes
+ - interfaces/docker-support: declare controls-device-cgroup
+ - packaging: fix removal of old apparmor profile
+ - store: use track/risk for "channel" name when parsing store
+ details
+ - many: allow 'system-usernames' with libseccomp > 2.4 and golang-
+ seccomp > 0.9.0
+ - overlord/devicestate, tests: use gadget.Update() proper, spread
+ test
+ - overlord/configstate/configcore: allow setting start_x=1 to enable
+ CSI camera on RPi
+ - interfaces: remove BeforePrepareSlot from commonInterface
+ - many: support system-usernames for 'snap_daemon' user
+ - overlord/devicestate,o/snapstate: queue service commands before
+ mark-seeded and other final tasks
+ - interfaces/mount: discard mount ns on backend Remove
+ - packaging/fedora: build on RHEL8
+ - overlord/devicestate: support seeding a classic system with the
+ snapd snap and no core
+ - interfaces: fix test failure in gpio_control_test
+ - interfaces, policy: remove sanitize helpers and use minimal policy
+ check
+ - packaging: use %systemd_user_* macros to enable session agent
+ socket according to presets
+ - snapstate, store: handle 429s on catalog refresh a little bit
+ better
+ - tests: part4 making tests work on ubuntu-core-18
+ - many: drop snap.ReadGadgetInfo wrapper
+ - xdgopenproxy: update test API to match upstream
+ - tests: show why sbuild failed
+ - data/selinux: allow mandb_t to search /var/lib/snapd
+ - tests: be less verbose when checking service status
+ - tests: set sbuild test as manual
+ - overlord: DeviceCtx must find the remodel context for a remodel
+ change
+ - tests: use snap info --verbose to check for base
+ - sanity: unmount squashfs with --lazy
+ - overlord/snapstate: keep current track if only risk is specified
+ - interfaces/firewall-control: support nft routing expressions and
+ device groups
+ - gadget: support for writing symlinks
+ - tests: mountinfo-tool fail if there are no matches
+ - tests: sync journal log before start the test
+ - cmd/snap, data/completion: improve completion for 'snap debug'
+ - httputil: retry for http2 PROTOCOL_ERROR
+ - Errata commit: pulseaudio still auto-connects on classic
+ - interfaces/misc: updates for k8s 1.15 (and greengrass test)
+ - tests: set GOTRACEBACK=1 when running tests
+ - cmd/libsnap: don't leak memory in sc_die_on_error
+ - tests: improve how the system is restored when the upgrade-
+ from-2.15 test fails
+ - interfaces/bluetooth-control: add udev rules for BT_chrdev devices
+ - interfaces: add audio-playback/audio-record and make pulseaudio
+ manually connect
+ - tests: split the sbuild test in 2 depending on the type of build
+ - interfaces: add an interface granting access to AppStream metadata
+ - gadget: ensure filesystem labels are unique
+ - usersession/agent: use background context when stopping the agent
+ - HACKING.md: update spread section, other updates
+ - data/selinux: allow snap-confine to read entries on nsfs
+ - tests: respect SPREAD_DEBUG_EACH on the main suite
+ - packaging/debian-sid: set GOCACHE to a known writable location
+ - interfaces: add gpio-control interface
+ - cmd/snap: use showDone helper with 'snap switch'
+ - gadget: effective structure role fallback, extra tests
+ - many: fix unit tests getting stuck
+ - tests: remove installed snap on restore
+ - daemon: do not modify test data in user suite
+ - data/selinux: allow read on sysfs
+ - packaging/debian: don't md5sum absent files
+ - tests: remove test-snapd-curl
+ - tests: remove test-snapd-snapctl-core18 in restore
+ - tests: remove installed snap in the restore section
+ - tests: remove installed test snap
+ - tests: correctly escape mount unit path
+ - cmd/Makefile.am: support building with the go snap
+ - tests: work around classic snap affecting the host
+ - tests: fix typo "current"
+ - overlord/assertstate: add Batch.Precheck to check for the full
+ validity of the batch before Commit
+ - tests: restore cpuset clone_children clobbered by lxd
+ - usersession: move userd package to usersession/userd
+ - tests: reformat and fix markdown in snapd-state.md
+ - gadget: select the right updater for given structure
+ - tests: show stderr only if it exists
+ - sessionagent: add a REST interface with socket activation
+ - tests: remove locally installed core in more tests
+ - tests: remove local revision of core
+ - packaging/debian-sid: use correct apparmor Depends for Debian
+ - packaging/debian-sid: merge debian upload changes back into master
+ - cmd/snap-repair: make sure the goroutine doesn't stick around on
+ timeout
+ - packaging/fedora: github.com/cheggaaa/pb is no longer used
+ - configstate/config: fix crash in purgeNulls
+ - boot, o/snapst, o/devicest: limit knowledge of boot vars to boot
+ - client,cmd/snap: stop depending on status/status-code in the JSON
+ responses in client
+ - tests: unmount leftover /run/netns
+ - tests: switch mount-ns test to manual
+ - overlord,daemon,cmd/snapd: move expensive startup to dedicated
+ StartUp methods
+ - osutil: add EnsureTreeState helper
+ - tests: measure properties of various mount namespaces
+ - tests: part2 making tests work on ubuntu-core-18
+ - interfaces/policy: minimal policy check for replacing
+ sanitizeReservedFor helpers (1/2)
+ - interfaces: add an interface that grants access to the PackageKit
+ service
+ - overlord/devicestate: update gadget update handlers and mocks
+ - tests: add mountinfo-tool --ref-x1000
+ - tests: remove lxd / lxcfs if pre-installed
+ - tests: removing support for ubuntu cosmic on spread test suite
+ - tests: don't leak /run/netns mount
+ - image: clean up the validateSuite
+ - bootloader: remove "Dir()" from Bootloader interface
+ - many: retry to reboot if snapd gets restarted before expected
+ reboot
+ - overlord: implement re-registration remodeling
+ - cmd: revert PR#6933 (tweak of GOMAXPROCS)
+ - cmd/snap: add snap unset command
+ - many: add Client-User-Agent to "SnapAction" install API call
+ - tests: first part making tests run on ubuntu-core-18
+ - hookstate/ctlcmd: support hidden commands in snapctl
+ - many: replace snapd snap name checks with type checks (3/4)
+ - overlord: mostly stop needing Kernel/CoreInfo, make GadgetInfo
+ consider a DeviceContext
+ - snapctl: handle unsetting of config options with "!"
+ - tests: move core migration snaps to tests/lib/snaps dir
+ - cmd/snap: handle unsetting of config options with "!"
+ - cmd/snap, etc: add health to 'snap list' and 'snap info'
+ - gadget: use struct field names when intializing data in mounted
+ updater unit tests
+ - cmd/snap-confine: bring /lib/firmware from the host
+ - snap: set snapd snap type (1/4)
+ - snap: add checks in validate-seed for missing base/default-
+ provider
+ - daemon: replace shutdownServer with net/http's native shutdown
+ support
+ - interfaces/builtin: add exec "/bin/runc" to docker-support
+ - gadget: mounted filesystem updater
+ - overlord/patch: simplify conditions for re-applying sublevel
+ patches for level 6
+ - seccomp/compiler: adjust test case names and comment for later
+ changes
+ - tests: fix error doing snap pack running failover test
+ - tests: don't preserve size= when rewriting mount tables
+ - tests: allow reordering of rewrite operations
+ - gadget: main update routine
+ - overlord/config: normalize nulls to support config unsetting
+ semantics
+ - snap-userd-autostart: don't list as a startup application on the
+ GUI
+ - tests: renumber snap revisions as seen via writable
+ - tests: change allocation for mount options
+ - tests: re-enable ns-re-associate test
+ - tests: mountinfo-tool allow many --refs
+ - overlord/devicestate: implement reregRemodelContext with the
+ essential re-registration logic
+ - tests: replace various numeric mount options
+ - gadget: filesystem image writer
+ - tests: add more unit tests for mountinfo-tool
+ - tests: introduce mountinfo-tool --ref feature
+ - tests: refactor mountinfo-tool rewrite state
+ - tests: allow renumbering mount namespace identifiers
+ - snap: refactor and explain layout blacklisting
+ - tests: renumber snap revisions as seen via hostfs
+ - daemon, interfaces, travis: workaround build ID with Go 1.9, use
+ 1.9 for travis tests
+ - cmd/libsnap: add sc_error_init_{simple,api_misuse}
+ - gadget: make raw updater handle shifted structures
+ - tests/lib/nested: create WORK_DIR before accessing it
+ - cmd/libsnap: rename SC_LIBSNAP_ERROR to SC_LIBSNAP_DOMAIN
+ - cmd,tests: forcibly discard mount namespace when bases change
+ - many: introduce healthstate, run check-health
+ post-(install/refresh/try/revert)
+ - interfaces/optical-drive: add scsi-generic type 4 and 5 support
+ - cmd/snap-confine: exit from helper when parent dies
+
* Fri Jul 12 2019 Michael Vogt
- New upstream release 2.40
- overlord/patch: simplify conditions for re-applying sublevel
@@ -969,7 +1418,7 @@
- tests/lib/nested: fix multi argument copy_remote
- tests/lib/nested: have mkfs.ext4 use a rootdir instead of mounting
an image
- - packaging: fix permissons powerpc docs dir
+ - packaging: fix permissions powerpc docs dir
- overlord: mock store to avoid net requests
- debian: rework how we run autopkgtests
- interface: builtin: avahi-observe/control: allow slots
diff -Nru snapd-2.40/packaging/debian-sid/changelog snapd-2.42.1/packaging/debian-sid/changelog
--- snapd-2.40/packaging/debian-sid/changelog 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/debian-sid/changelog 2019-10-30 12:17:43.000000000 +0000
@@ -1,8 +1,43 @@
+snapd (2.42.1-1) unstable; urgency=medium
+
+ * New upstream release, LP: #1846181
+ - interfaces: de-duplicate emitted update-ns profiles
+ - packaging: tweak handling of usr.lib.snapd.snap-confine
+ - interfaces: allow introspecting network-manager on core
+ - tests/main/interfaces-contacts-service: disable on openSUSE
+ Tumbleweed
+ - tests/lib/lxd-snapfuse: restore mount changes introduced by LXD
+ - snap: fix default-provider in seed validation
+ - tests: update system-usernames test now that opensuse-15.1 works
+ - overlord: set fake sertial in TestRemodelSwitchToDifferentKernel
+ - gadget: rename "boot{select,img}" -> system-boot-{select,image}
+ - tests: listing test, make accepted snapd/core versions consistent
+
+ -- Michael Vogt Wed, 30 Oct 2019 13:17:43 +0100
+
+snapd (2.42-1) unstable; urgency=medium
+
+ * New upstream release
+
+ -- Michael Vogt Tue, 01 Oct 2019 11:40:58 +0200
+
+snapd (2.41-1) unstable; urgency=medium
+
+ [ Michael Vogt ]
+ * New upstream release, LP: #1840740
+
+ [ Jamie Strandboge ]
+ * debian/control: Depends on apparmor >= 2.10.95-5 instead of
+ 2.10.95-0ubuntu2.2 since 2.10.95-5 in Debian is the first version to have
+ all the patches that 2.10.95-0ubuntu2.2 in Ubuntu brought.
+
+ -- Michael Vogt Fri, 30 Aug 2019 08:53:57 +0200
+
snapd (2.40-1) unstable; urgency=medium
* New upstream release.
- -- Michael Vogt Fri, 12 Jul 2019 10:40:08 +0200
+ -- Michael Vogt Tue, 23 Jul 2019 15:38:36 +0200
snapd (2.39.3-1) unstable; urgency=medium
@@ -24,23 +59,39 @@
-- Michael Vogt Wed, 05 Jun 2019 08:46:14 +0200
+snapd (2.39-1) unstable; urgency=medium
+
+ * New upstream release
+ * d/patches0008-snap-squashsh-skip-TestBuildDate-on-Debian.patch: drop,
+ fixed upstream
+
+ -- Zygmunt Krynicki Thu, 28 Feb 2019 18:21:26 +0100
+
snapd (2.39.1-1) unstable; urgency=medium
* New upstream release
-- Michael Vogt Wed, 29 May 2019 12:08:43 +0200
-snapd (2.39-1) unstable; urgency=medium
+snapd (2.38-1) unstable; urgency=medium
+
+ * New upstream release
+
+ -- Michael Vogt Thu, 21 Mar 2019 11:02:04 +0100
+
+snapd (2.37.4-1) unstable; urgency=medium
* New upstream release
+ * d/patches0008-snap-squashsh-skip-TestBuildDate-on-Debian.patch: drop,
+ fixed upstream
- -- Michael Vogt Fri, 03 May 2019 11:55:23 +0200
+ -- Zygmunt Krynicki Thu, 28 Feb 2019 18:21:26 +0100
-snapd (2.38-1) unstable; urgency=medium
+snapd (2.37.3-1) unstable; urgency=medium
* New upstream release
- -- Michael Vogt Thu, 21 Mar 2019 11:02:04 +0100
+ -- Zygmunt Krynicki Tue, 19 Feb 2019 13:46:24 +0100
snapd (2.37.2-1) unstable; urgency=medium
diff -Nru snapd-2.40/packaging/debian-sid/control snapd-2.42.1/packaging/debian-sid/control
--- snapd-2.40/packaging/debian-sid/control 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/debian-sid/control 2019-10-30 12:17:43.000000000 +0000
@@ -83,7 +83,7 @@
Package: snapd
Architecture: any
Depends: adduser,
- apparmor (>= 2.10.95-0ubuntu2.2),
+ apparmor (>= 2.10.95-5),
ca-certificates,
gnupg1 | gnupg,
openssh-client,
diff -Nru snapd-2.40/packaging/debian-sid/gbp.conf snapd-2.42.1/packaging/debian-sid/gbp.conf
--- snapd-2.40/packaging/debian-sid/gbp.conf 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/debian-sid/gbp.conf 2019-10-30 12:17:43.000000000 +0000
@@ -1,3 +1,4 @@
[DEFAULT]
debian-branch = debian
export-dir = ../build-area
+upstream-tag = %(version)s
diff -Nru snapd-2.40/packaging/debian-sid/patches/0003-cmd-snap-seccomp-skip-tests-that-use-m32.patch snapd-2.42.1/packaging/debian-sid/patches/0003-cmd-snap-seccomp-skip-tests-that-use-m32.patch
--- snapd-2.40/packaging/debian-sid/patches/0003-cmd-snap-seccomp-skip-tests-that-use-m32.patch 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/debian-sid/patches/0003-cmd-snap-seccomp-skip-tests-that-use-m32.patch 2019-10-30 12:17:43.000000000 +0000
@@ -24,14 +24,14 @@
cmd/snap-seccomp/main_test.go | 8 ++++++++
1 file changed, 8 insertions(+)
-diff --git a/cmd/snap-seccomp/main_test.go b/cmd/snap-seccomp/main_test.go
-index d4ca193b2..8977385c2 100644
---- a/cmd/snap-seccomp/main_test.go
-+++ b/cmd/snap-seccomp/main_test.go
-@@ -185,6 +185,14 @@ func (s *snapSeccompSuite) SetUpSuite(c *C) {
+Index: snapd/cmd/snap-seccomp/main_test.go
+===================================================================
+--- snapd.orig/cmd/snap-seccomp/main_test.go
++++ snapd/cmd/snap-seccomp/main_test.go
+@@ -192,6 +192,14 @@ func (s *snapSeccompSuite) SetUpSuite(c
// Ideally we would build for ppc64el->powerpc and arm64->armhf but
// it seems tricky to find the right gcc-multilib for this.
- if arch.UbuntuArchitecture() == "amd64" && s.canCheckCompatArch {
+ if arch.DpkgArchitecture() == "amd64" && s.canCheckCompatArch {
+ // This test fails on Debian amd64
+ // cannot build multi-lib syscall runner: exit status 1
+ // In file included from /usr/include/errno.h:25,
@@ -43,6 +43,3 @@
cmd = exec.Command(cmd.Args[0], cmd.Args[1:]...)
cmd.Args = append(cmd.Args, "-m32")
for i, k := range cmd.Args {
---
-2.17.1
-
diff -Nru snapd-2.40/packaging/debian-sid/patches/0007-i18n-use-dummy-localizations-to-avoid-dependencies.patch snapd-2.42.1/packaging/debian-sid/patches/0007-i18n-use-dummy-localizations-to-avoid-dependencies.patch
--- snapd-2.40/packaging/debian-sid/patches/0007-i18n-use-dummy-localizations-to-avoid-dependencies.patch 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/debian-sid/patches/0007-i18n-use-dummy-localizations-to-avoid-dependencies.patch 2019-10-30 12:17:43.000000000 +0000
@@ -31,7 +31,7 @@
- "path/filepath"
- "strings"
-
-- "github.com/ojii/gettext.go"
+- "github.com/snapcore/go-gettext"
-
- "github.com/snapcore/snapd/dirs"
- "github.com/snapcore/snapd/osutil"
diff -Nru snapd-2.40/packaging/debian-sid/rules snapd-2.42.1/packaging/debian-sid/rules
--- snapd-2.40/packaging/debian-sid/rules 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/debian-sid/rules 2019-10-30 12:17:43.000000000 +0000
@@ -146,9 +146,9 @@
# Generate static snap-exec, snapctl and snap-update-ns - it somehow includes CGO so
# we must force a static build here. We need a static snap-{exec,update-ns}
# inside the core snap because not all bases will have a libc
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-exec)
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snapctl)
- (cd _build/bin && GOPATH=$$(pwd)/.. go build --ldflags '-extldflags "-static"' $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-update-ns)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_ENABLED=0 go build $(GCCGOFLAGS) -pkgdir=$$(pwd)/std $(DH_GOPKG)/cmd/snap-exec)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_ENABLED=0 go build $(GCCGOFLAGS) -pkgdir=$$(pwd)/std $(DH_GOPKG)/cmd/snapctl)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build go build --ldflags '-extldflags "-static"' $(GCCGOFLAGS) -pkgdir=$$(pwd)/std $(DH_GOPKG)/cmd/snap-update-ns)
# ensure we generated a static build
$(shell if ldd _build/bin/snap-exec; then false "need static build"; fi)
diff -Nru snapd-2.40/packaging/debian-sid/snapd.links snapd-2.42.1/packaging/debian-sid/snapd.links
--- snapd-2.40/packaging/debian-sid/snapd.links 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/debian-sid/snapd.links 2019-10-30 12:17:43.000000000 +0000
@@ -1,2 +1,6 @@
-../../usr/lib/snapd/snap-device-helper /lib/udev/snappy-app-dev
+/usr/lib/snapd/snap-device-helper /lib/udev/snappy-app-dev
usr/lib/snapd/snapctl usr/bin/snapctl
+
+# This should be removed once we can rely on debhelper >= 11.5:
+# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=764678
+/usr/lib/systemd/user/snapd.session-agent.socket /usr/lib/systemd/user/sockets.target.wants/snapd.session-agent.socket
diff -Nru snapd-2.40/packaging/debian-sid/snapd.maintscript snapd-2.42.1/packaging/debian-sid/snapd.maintscript
--- snapd-2.40/packaging/debian-sid/snapd.maintscript 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/debian-sid/snapd.maintscript 2019-10-30 12:17:43.000000000 +0000
@@ -2,4 +2,4 @@
# we used to ship a custom grub config that is no longer needed
rm_conffile /etc/grub.d/09_snappy 1.7.3ubuntu1
rm_conffile /etc/ld.so.conf.d/snappy.conf 2.0.7~
-rm_conffile /etc/apparmor.d/usr.lib.snapd.snap-confine 2.23.6~
+rm_conffile /etc/apparmor.d/usr.lib.snapd.snap-confine 2.23.6~ snap-confine
diff -Nru snapd-2.40/packaging/fedora/snapd.spec snapd-2.42.1/packaging/fedora/snapd.spec
--- snapd-2.40/packaging/fedora/snapd.spec 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/fedora/snapd.spec 2019-10-30 12:17:43.000000000 +0000
@@ -60,9 +60,10 @@
%global import_path %{provider_prefix}
%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service snapd.seeded.service
+%global snappy_user_svcs snapd.session-agent.socket
# Until we have a way to add more extldflags to gobuild macro...
-%if 0%{?fedora}
+%if 0%{?fedora} || 0%{?rhel} >= 8
# buildmode PIE triggers external linker consumes -extldflags
%define gobuild_static(o:) go build -buildmode pie -compiler gc -tags=rpm_crashtraceback -ldflags "${LDFLAGS:-} -B 0x$(head -c20 /dev/urandom|od -An -tx1|tr -d ' \\n') -extldflags '%__global_ldflags -static'" -a -v -x %{?**};
%endif
@@ -91,7 +92,7 @@
%endif
Name: snapd
-Version: 2.40
+Version: 2.42.1
Release: 0%{?dist}
Summary: A transactional software package manager
License: GPLv3
@@ -146,7 +147,6 @@
%if ! 0%{?with_bundled}
BuildRequires: golang(github.com/boltdb/bolt)
-BuildRequires: golang(github.com/cheggaaa/pb)
BuildRequires: golang(github.com/coreos/go-systemd/activation)
BuildRequires: golang(github.com/godbus/dbus)
BuildRequires: golang(github.com/godbus/dbus/introspect)
@@ -156,8 +156,8 @@
BuildRequires: golang(github.com/kr/pretty)
BuildRequires: golang(github.com/kr/text)
BuildRequires: golang(github.com/mvo5/goconfigparser)
-BuildRequires: golang(github.com/ojii/gettext.go)
BuildRequires: golang(github.com/seccomp/libseccomp-golang)
+BuildRequires: golang(github.com/snapcore/go-gettext)
BuildRequires: golang(golang.org/x/crypto/openpgp/armor)
BuildRequires: golang(golang.org/x/crypto/openpgp/packet)
BuildRequires: golang(golang.org/x/crypto/sha3)
@@ -183,7 +183,6 @@
BuildRequires: gcc
BuildRequires: gettext
BuildRequires: gnupg
-BuildRequires: indent
BuildRequires: pkgconfig(glib-2.0)
BuildRequires: pkgconfig(libcap)
BuildRequires: pkgconfig(libseccomp)
@@ -243,7 +242,6 @@
%if ! 0%{?with_bundled}
Requires: golang(github.com/boltdb/bolt)
-Requires: golang(github.com/cheggaaa/pb)
Requires: golang(github.com/coreos/go-systemd/activation)
Requires: golang(github.com/godbus/dbus)
Requires: golang(github.com/godbus/dbus/introspect)
@@ -253,8 +251,8 @@
Requires: golang(github.com/kr/pretty)
Requires: golang(github.com/kr/text)
Requires: golang(github.com/mvo5/goconfigparser)
-Requires: golang(github.com/ojii/gettext.go)
Requires: golang(github.com/seccomp/libseccomp-golang)
+Requires: golang(github.com/snapcore/go-gettext)
Requires: golang(golang.org/x/crypto/openpgp/armor)
Requires: golang(golang.org/x/crypto/openpgp/packet)
Requires: golang(golang.org/x/crypto/sha3)
@@ -270,7 +268,6 @@
# the bundled tarball are unversioned (they go by git commit)
# *sigh*... I hate golang...
Provides: bundled(golang(github.com/snapcore/bolt))
-Provides: bundled(golang(github.com/cheggaaa/pb))
Provides: bundled(golang(github.com/coreos/go-systemd/activation))
Provides: bundled(golang(github.com/godbus/dbus))
Provides: bundled(golang(github.com/godbus/dbus/introspect))
@@ -281,7 +278,7 @@
Provides: bundled(golang(github.com/kr/text))
Provides: bundled(golang(github.com/mvo5/goconfigparser))
Provides: bundled(golang(github.com/mvo5/libseccomp-golang))
-Provides: bundled(golang(github.com/ojii/gettext.go))
+Provides: bundled(golang(github.com/snapcore/go-gettext))
Provides: bundled(golang(golang.org/x/crypto/openpgp/armor))
Provides: bundled(golang(golang.org/x/crypto/openpgp/packet))
Provides: bundled(golang(golang.org/x/crypto/sha3))
@@ -728,6 +725,8 @@
%{_unitdir}/snapd.autoimport.service
%{_unitdir}/snapd.failure.service
%{_unitdir}/snapd.seeded.service
+%{_userunitdir}/snapd.session-agent.service
+%{_userunitdir}/snapd.session-agent.socket
%{_datadir}/dbus-1/services/io.snapcraft.Launcher.service
%{_datadir}/dbus-1/services/io.snapcraft.Settings.service
%{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy
@@ -803,6 +802,7 @@
%sysctl_apply 99-snap.conf
%endif
%systemd_post %{snappy_svcs}
+%systemd_user_post %{snappy_user_svcs}
# If install, test if snapd socket and timer are enabled.
# If enabled, then attempt to start them. This will silently fail
# in chroots or other environments where services aren't expected
@@ -815,6 +815,7 @@
%preun
%systemd_preun %{snappy_svcs}
+%systemd_user_preun %{snappy_user_svcs}
# Remove all Snappy content if snapd is being fully uninstalled
if [ $1 -eq 0 ]; then
@@ -823,6 +824,7 @@
%postun
%systemd_postun_with_restart %{snappy_svcs}
+%systemd_user_postun %{snappy_user_svcs}
%if 0%{?with_selinux}
%triggerun -- snapd < 2.39
@@ -866,6 +868,453 @@
%changelog
+* Wed Oct 30 2019 Michael Vogt
+- New upstream release 2.42.1
+ - interfaces: de-duplicate emitted update-ns profiles
+ - packaging: tweak handling of usr.lib.snapd.snap-confine
+ - interfaces: allow introspecting network-manager on core
+ - tests/main/interfaces-contacts-service: disable on openSUSE
+ Tumbleweed
+ - tests/lib/lxd-snapfuse: restore mount changes introduced by LXD
+ - snap: fix default-provider in seed validation
+ - tests: update system-usernames test now that opensuse-15.1 works
+ - overlord: set fake sertial in TestRemodelSwitchToDifferentKernel
+ - gadget: rename "boot{select,img}" -> system-boot-{select,image}
+ - tests: listing test, make accepted snapd/core versions consistent
+
+* Tue Oct 01 2019 Michael Vogt
+- New upstream release 2.42
+ - tests: disable {contacts,calendar}-service tests on debian-sid
+ - tests/main/snap-run: disable strace test cases on Arch
+ - cmd/system-shutdown: include correct prototype for die
+ - snap/naming: add test for hook name connect-plug-i2c
+ - cmd/snap-confine: allow digits in hook names
+ - gadget: do not fail the update when old gadget snap is missing
+ bare content
+ - tests: disable {contacts,calendar}-service tests on Arch Linux
+ - tests: move "centos-7" to unstable systems
+ - interfaces/docker-support,kubernetes-support: misc updates for
+ strict k8s
+ - packaging: remove obsolete usr.lib.snapd.snap-confine in
+ postinst
+ - tests: add test that ensures our snapfuse binary actually works
+ - packaging: use snapfuse_ll to speed up snapfuse performance
+ - usersession/userd: make sure to export DBus interfaces before
+ requesting a name
+ - data/selinux: allow snapd to issue sigkill to journalctl
+ - store: download propagates options to delta download
+ - wrappers: allow snaps to install icon theme icons
+ - debug: state-inspect debugging utility
+ - sandbox/cgroup: introduce cgroup wrappers package
+ - snap-confine: fix return value checks for udev functions
+ - cmd/model: output tweaks, add'l tests
+ - wrappers/services: add ServicesEnableState + unit tests
+ - tests: fix newline and wrong test name pointed out in previous PRs
+ - tests: extend mount-ns test to handle mimics
+ - run-checks, tests/main/go: allow gofmt checks to be skipped on
+ 19.10
+ - tests/main/interfaces-{calendar,contacts}-service: disable on
+ 19.10
+ - tests: part3 making tests work on ubuntu-core-18
+ - tests: fix interfaces-timeserver-control on 19.10
+ - overlord/snapstate: config revision code cleanup and extra tests
+ - devicestate: allow remodel to different kernels
+ - overlord,daemon: adjust startup timeout via EXTEND_TIMEOUT_USEC
+ using an estimate
+ - tests/main/many: increase kill-timeout to 5m
+ - interfaces/kubernetes-support: allow systemd-run to ptrace read
+ unconfined
+ - snapstate: auto transition on experimental.snapd-snap=true
+ - tests: retry checking until the written file on desktop-portal-
+ filechooser
+ - tests: unit test for a refresh failing on configure hook
+ - tests: remove mount_id and parent_id from mount-ns test data
+ - tests: move classic-ubuntu-core-transition* to nightly
+ - tests/mountinfo-tool: proper formatting of opt_fields
+ - overlord/configstate: special-case "null" in transaction Changes()
+ - snap-confine: fallback gracefully on a cgroup v2 only system
+ - tests: debian sid now ships new seccomp, adjust tests
+ - tests: explicitly restore after using LXD
+ - snapstate: make progress reporting less granular
+ - bootloader: little kernel support
+ - fixme: rename ubuntu*architectures to dpkg*architectures
+ - tests: run dbus-launch inside a systemd unit
+ - channel: introduce Resolve and ResolveLocked
+ - tests: run failing tests on ubuntu eoan due to is now set as
+ unstable
+ - systemd: detach rather than unmount .mount units
+ - cmd/snap-confine: add unit tests for sc_invocation, cleanup memory
+ leaks in tests
+ - boot,dirs,image: introduce boot.MakeBootable, use it in image
+ instead of ad hoc code
+ - cmd/snap-update-ns: clarify sharing comment
+ - tests/overlord/snapstate: refactor for cleaner test failures
+ - cmd/snap-update-ns: don't propagate detaching changes
+ - interfaces: allow reading mutter Xauthority file
+ - cmd/snap-confine: fix /snap duplication in legacy mode
+ - tests: fix mountinfo-tool filtering when used with rewriting
+ - seed,image,o/devicestate: extract seed loading to seed/seed16.go
+ - many: pass the rootdir and options to bootloader.Find
+ - tests: part5 making tests work on ubuntu-core-18
+ - cmd/snap-confine: keep track of snap instance name and the snap
+ name
+ - cmd: unify die() across C programs
+ - tests: add functions to make an abstraction for the snaps
+ - packaging/fedora, tests/lib/prepare-restore: helper tool for
+ packing sources for RPM
+ - cmd/snap: improve help and error msg for snapshot commands
+ - hookstate/ctlcmd: fix snapctl set help message
+ - cmd/snap: don't append / to snap name just because a dir exists
+ - tests: support fastly-global.cdn.snapcraft.io url on proxy-no-core
+ test
+ - tests: add --quiet switch to retry-tool
+ - tests: add unstable stage for travis execution
+ - tests: disable interfaces-timeserver-control on 19.10
+ - tests: don't guess in is_classic_confinement_supported
+ - boot, etc: simplify BootParticipant (etc) usage
+ - tests: verify retry-tool not retrying missing commands
+ - tests: rewrite "retry" command as retry-tool
+ - tests: move debug section after restore
+ - cmd/libsnap-confine-private, cmd/s-c: use constants for
+ snap/instance name lengths
+ - tests: measure behavior of the device cgroup
+ - boot, bootloader, o/devicestate: boot env manip goes in boot
+ - tests: enabling ubuntu 19.10-64 on spread.yaml
+ - tests: fix ephemeral mount table in left over by prepare
+ - tests: add version-tool for comparing versions
+ - cmd/libsnap: make feature flag enum 1<
+- New upstream release 2.41
+ - overlord/snapstate: revert track-risk behavior
+ - tests: fix snap info test
+ - httputil: rework protocol error detection
+ - gadget: do not error on gadget refreshes with multiple volumes
+ - i18n, vendor, packaging: drop github.com/ojii/gettext.go, use
+ github.com/snapcore/go-gettext
+ - snapstate: validate all system-usernames before creating them
+ - mkversion.sh: fix version from git checkouts
+ - interfaces/network-{control,manager}: allow 'k' on
+ /run/resolvconf/**
+ - interfaces/wayland,x11: allow reading an Xwayland Xauth file
+ - interfaces: k8s worker node updates
+ - debian: re-enable systemd environment generator
+ - many: create system-usernames user/group if both don't exist
+ - packaging: fix symlink for snapd.session-agent.socket
+ - tests: change cgroups so that LXD doesn't have to
+ - interfaces/network-setup-control: allow dbus netplan apply
+ messages
+ - tests: add /var/cache/snapd to the snapd state to prevent error on
+ the store
+ - tests: add test for services disabled during refresh hook
+ - many: simpler access to snap-seccomp version-info
+ - snap: cleanup some tests, clarify some errorsThis is a follow up
+ from work on system usernames:
+ - osutil: add osutil.Find{Uid,Gid}
+ - tests: use a different archive based on the spread backend on go-
+ build test
+ - cmd/snap-update-ns: fix pair of bugs affecting refresh of snap
+ with layouts
+ - overlord/devicestate: detect clashing concurrent (ongoing, just
+ finished) remodels or changes
+ - interfaces/docker-support: declare controls-device-cgroup
+ - packaging: fix removal of old apparmor profile
+ - store: use track/risk for "channel" name when parsing store
+ details
+ - many: allow 'system-usernames' with libseccomp > 2.4 and golang-
+ seccomp > 0.9.0
+ - overlord/devicestate, tests: use gadget.Update() proper, spread
+ test
+ - overlord/configstate/configcore: allow setting start_x=1 to enable
+ CSI camera on RPi
+ - interfaces: remove BeforePrepareSlot from commonInterface
+ - many: support system-usernames for 'snap_daemon' user
+ - overlord/devicestate,o/snapstate: queue service commands before
+ mark-seeded and other final tasks
+ - interfaces/mount: discard mount ns on backend Remove
+ - packaging/fedora: build on RHEL8
+ - overlord/devicestate: support seeding a classic system with the
+ snapd snap and no core
+ - interfaces: fix test failure in gpio_control_test
+ - interfaces, policy: remove sanitize helpers and use minimal policy
+ check
+ - packaging: use %systemd_user_* macros to enable session agent
+ socket according to presets
+ - snapstate, store: handle 429s on catalog refresh a little bit
+ better
+ - tests: part4 making tests work on ubuntu-core-18
+ - many: drop snap.ReadGadgetInfo wrapper
+ - xdgopenproxy: update test API to match upstream
+ - tests: show why sbuild failed
+ - data/selinux: allow mandb_t to search /var/lib/snapd
+ - tests: be less verbose when checking service status
+ - tests: set sbuild test as manual
+ - overlord: DeviceCtx must find the remodel context for a remodel
+ change
+ - tests: use snap info --verbose to check for base
+ - sanity: unmount squashfs with --lazy
+ - overlord/snapstate: keep current track if only risk is specified
+ - interfaces/firewall-control: support nft routing expressions and
+ device groups
+ - gadget: support for writing symlinks
+ - tests: mountinfo-tool fail if there are no matches
+ - tests: sync journal log before start the test
+ - cmd/snap, data/completion: improve completion for 'snap debug'
+ - httputil: retry for http2 PROTOCOL_ERROR
+ - Errata commit: pulseaudio still auto-connects on classic
+ - interfaces/misc: updates for k8s 1.15 (and greengrass test)
+ - tests: set GOTRACEBACK=1 when running tests
+ - cmd/libsnap: don't leak memory in sc_die_on_error
+ - tests: improve how the system is restored when the upgrade-
+ from-2.15 test fails
+ - interfaces/bluetooth-control: add udev rules for BT_chrdev devices
+ - interfaces: add audio-playback/audio-record and make pulseaudio
+ manually connect
+ - tests: split the sbuild test in 2 depending on the type of build
+ - interfaces: add an interface granting access to AppStream metadata
+ - gadget: ensure filesystem labels are unique
+ - usersession/agent: use background context when stopping the agent
+ - HACKING.md: update spread section, other updates
+ - data/selinux: allow snap-confine to read entries on nsfs
+ - tests: respect SPREAD_DEBUG_EACH on the main suite
+ - packaging/debian-sid: set GOCACHE to a known writable location
+ - interfaces: add gpio-control interface
+ - cmd/snap: use showDone helper with 'snap switch'
+ - gadget: effective structure role fallback, extra tests
+ - many: fix unit tests getting stuck
+ - tests: remove installed snap on restore
+ - daemon: do not modify test data in user suite
+ - data/selinux: allow read on sysfs
+ - packaging/debian: don't md5sum absent files
+ - tests: remove test-snapd-curl
+ - tests: remove test-snapd-snapctl-core18 in restore
+ - tests: remove installed snap in the restore section
+ - tests: remove installed test snap
+ - tests: correctly escape mount unit path
+ - cmd/Makefile.am: support building with the go snap
+ - tests: work around classic snap affecting the host
+ - tests: fix typo "current"
+ - overlord/assertstate: add Batch.Precheck to check for the full
+ validity of the batch before Commit
+ - tests: restore cpuset clone_children clobbered by lxd
+ - usersession: move userd package to usersession/userd
+ - tests: reformat and fix markdown in snapd-state.md
+ - gadget: select the right updater for given structure
+ - tests: show stderr only if it exists
+ - sessionagent: add a REST interface with socket activation
+ - tests: remove locally installed core in more tests
+ - tests: remove local revision of core
+ - packaging/debian-sid: use correct apparmor Depends for Debian
+ - packaging/debian-sid: merge debian upload changes back into master
+ - cmd/snap-repair: make sure the goroutine doesn't stick around on
+ timeout
+ - packaging/fedora: github.com/cheggaaa/pb is no longer used
+ - configstate/config: fix crash in purgeNulls
+ - boot, o/snapst, o/devicest: limit knowledge of boot vars to boot
+ - client,cmd/snap: stop depending on status/status-code in the JSON
+ responses in client
+ - tests: unmount leftover /run/netns
+ - tests: switch mount-ns test to manual
+ - overlord,daemon,cmd/snapd: move expensive startup to dedicated
+ StartUp methods
+ - osutil: add EnsureTreeState helper
+ - tests: measure properties of various mount namespaces
+ - tests: part2 making tests work on ubuntu-core-18
+ - interfaces/policy: minimal policy check for replacing
+ sanitizeReservedFor helpers (1/2)
+ - interfaces: add an interface that grants access to the PackageKit
+ service
+ - overlord/devicestate: update gadget update handlers and mocks
+ - tests: add mountinfo-tool --ref-x1000
+ - tests: remove lxd / lxcfs if pre-installed
+ - tests: removing support for ubuntu cosmic on spread test suite
+ - tests: don't leak /run/netns mount
+ - image: clean up the validateSuite
+ - bootloader: remove "Dir()" from Bootloader interface
+ - many: retry to reboot if snapd gets restarted before expected
+ reboot
+ - overlord: implement re-registration remodeling
+ - cmd: revert PR#6933 (tweak of GOMAXPROCS)
+ - cmd/snap: add snap unset command
+ - many: add Client-User-Agent to "SnapAction" install API call
+ - tests: first part making tests run on ubuntu-core-18
+ - hookstate/ctlcmd: support hidden commands in snapctl
+ - many: replace snapd snap name checks with type checks (3/4)
+ - overlord: mostly stop needing Kernel/CoreInfo, make GadgetInfo
+ consider a DeviceContext
+ - snapctl: handle unsetting of config options with "!"
+ - tests: move core migration snaps to tests/lib/snaps dir
+ - cmd/snap: handle unsetting of config options with "!"
+ - cmd/snap, etc: add health to 'snap list' and 'snap info'
+ - gadget: use struct field names when intializing data in mounted
+ updater unit tests
+ - cmd/snap-confine: bring /lib/firmware from the host
+ - snap: set snapd snap type (1/4)
+ - snap: add checks in validate-seed for missing base/default-
+ provider
+ - daemon: replace shutdownServer with net/http's native shutdown
+ support
+ - interfaces/builtin: add exec "/bin/runc" to docker-support
+ - gadget: mounted filesystem updater
+ - overlord/patch: simplify conditions for re-applying sublevel
+ patches for level 6
+ - seccomp/compiler: adjust test case names and comment for later
+ changes
+ - tests: fix error doing snap pack running failover test
+ - tests: don't preserve size= when rewriting mount tables
+ - tests: allow reordering of rewrite operations
+ - gadget: main update routine
+ - overlord/config: normalize nulls to support config unsetting
+ semantics
+ - snap-userd-autostart: don't list as a startup application on the
+ GUI
+ - tests: renumber snap revisions as seen via writable
+ - tests: change allocation for mount options
+ - tests: re-enable ns-re-associate test
+ - tests: mountinfo-tool allow many --refs
+ - overlord/devicestate: implement reregRemodelContext with the
+ essential re-registration logic
+ - tests: replace various numeric mount options
+ - gadget: filesystem image writer
+ - tests: add more unit tests for mountinfo-tool
+ - tests: introduce mountinfo-tool --ref feature
+ - tests: refactor mountinfo-tool rewrite state
+ - tests: allow renumbering mount namespace identifiers
+ - snap: refactor and explain layout blacklisting
+ - tests: renumber snap revisions as seen via hostfs
+ - daemon, interfaces, travis: workaround build ID with Go 1.9, use
+ 1.9 for travis tests
+ - cmd/libsnap: add sc_error_init_{simple,api_misuse}
+ - gadget: make raw updater handle shifted structures
+ - tests/lib/nested: create WORK_DIR before accessing it
+ - cmd/libsnap: rename SC_LIBSNAP_ERROR to SC_LIBSNAP_DOMAIN
+ - cmd,tests: forcibly discard mount namespace when bases change
+ - many: introduce healthstate, run check-health
+ post-(install/refresh/try/revert)
+ - interfaces/optical-drive: add scsi-generic type 4 and 5 support
+ - cmd/snap-confine: exit from helper when parent dies
+
* Fri Jul 12 2019 Michael Vogt
- New upstream release 2.40
- overlord/patch: simplify conditions for re-applying sublevel
@@ -969,7 +1418,7 @@
- tests/lib/nested: fix multi argument copy_remote
- tests/lib/nested: have mkfs.ext4 use a rootdir instead of mounting
an image
- - packaging: fix permissons powerpc docs dir
+ - packaging: fix permissions powerpc docs dir
- overlord: mock store to avoid net requests
- debian: rework how we run autopkgtests
- interface: builtin: avahi-observe/control: allow slots
diff -Nru snapd-2.40/packaging/fedora-29/snapd.spec snapd-2.42.1/packaging/fedora-29/snapd.spec
--- snapd-2.40/packaging/fedora-29/snapd.spec 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/fedora-29/snapd.spec 2019-10-30 12:17:43.000000000 +0000
@@ -60,9 +60,10 @@
%global import_path %{provider_prefix}
%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service snapd.seeded.service
+%global snappy_user_svcs snapd.session-agent.socket
# Until we have a way to add more extldflags to gobuild macro...
-%if 0%{?fedora}
+%if 0%{?fedora} || 0%{?rhel} >= 8
# buildmode PIE triggers external linker consumes -extldflags
%define gobuild_static(o:) go build -buildmode pie -compiler gc -tags=rpm_crashtraceback -ldflags "${LDFLAGS:-} -B 0x$(head -c20 /dev/urandom|od -An -tx1|tr -d ' \\n') -extldflags '%__global_ldflags -static'" -a -v -x %{?**};
%endif
@@ -91,7 +92,7 @@
%endif
Name: snapd
-Version: 2.40
+Version: 2.42.1
Release: 0%{?dist}
Summary: A transactional software package manager
License: GPLv3
@@ -146,7 +147,6 @@
%if ! 0%{?with_bundled}
BuildRequires: golang(github.com/boltdb/bolt)
-BuildRequires: golang(github.com/cheggaaa/pb)
BuildRequires: golang(github.com/coreos/go-systemd/activation)
BuildRequires: golang(github.com/godbus/dbus)
BuildRequires: golang(github.com/godbus/dbus/introspect)
@@ -156,8 +156,8 @@
BuildRequires: golang(github.com/kr/pretty)
BuildRequires: golang(github.com/kr/text)
BuildRequires: golang(github.com/mvo5/goconfigparser)
-BuildRequires: golang(github.com/ojii/gettext.go)
BuildRequires: golang(github.com/seccomp/libseccomp-golang)
+BuildRequires: golang(github.com/snapcore/go-gettext)
BuildRequires: golang(golang.org/x/crypto/openpgp/armor)
BuildRequires: golang(golang.org/x/crypto/openpgp/packet)
BuildRequires: golang(golang.org/x/crypto/sha3)
@@ -183,7 +183,6 @@
BuildRequires: gcc
BuildRequires: gettext
BuildRequires: gnupg
-BuildRequires: indent
BuildRequires: pkgconfig(glib-2.0)
BuildRequires: pkgconfig(libcap)
BuildRequires: pkgconfig(libseccomp)
@@ -243,7 +242,6 @@
%if ! 0%{?with_bundled}
Requires: golang(github.com/boltdb/bolt)
-Requires: golang(github.com/cheggaaa/pb)
Requires: golang(github.com/coreos/go-systemd/activation)
Requires: golang(github.com/godbus/dbus)
Requires: golang(github.com/godbus/dbus/introspect)
@@ -253,8 +251,8 @@
Requires: golang(github.com/kr/pretty)
Requires: golang(github.com/kr/text)
Requires: golang(github.com/mvo5/goconfigparser)
-Requires: golang(github.com/ojii/gettext.go)
Requires: golang(github.com/seccomp/libseccomp-golang)
+Requires: golang(github.com/snapcore/go-gettext)
Requires: golang(golang.org/x/crypto/openpgp/armor)
Requires: golang(golang.org/x/crypto/openpgp/packet)
Requires: golang(golang.org/x/crypto/sha3)
@@ -270,7 +268,6 @@
# the bundled tarball are unversioned (they go by git commit)
# *sigh*... I hate golang...
Provides: bundled(golang(github.com/snapcore/bolt))
-Provides: bundled(golang(github.com/cheggaaa/pb))
Provides: bundled(golang(github.com/coreos/go-systemd/activation))
Provides: bundled(golang(github.com/godbus/dbus))
Provides: bundled(golang(github.com/godbus/dbus/introspect))
@@ -281,7 +278,7 @@
Provides: bundled(golang(github.com/kr/text))
Provides: bundled(golang(github.com/mvo5/goconfigparser))
Provides: bundled(golang(github.com/mvo5/libseccomp-golang))
-Provides: bundled(golang(github.com/ojii/gettext.go))
+Provides: bundled(golang(github.com/snapcore/go-gettext))
Provides: bundled(golang(golang.org/x/crypto/openpgp/armor))
Provides: bundled(golang(golang.org/x/crypto/openpgp/packet))
Provides: bundled(golang(golang.org/x/crypto/sha3))
@@ -728,6 +725,8 @@
%{_unitdir}/snapd.autoimport.service
%{_unitdir}/snapd.failure.service
%{_unitdir}/snapd.seeded.service
+%{_userunitdir}/snapd.session-agent.service
+%{_userunitdir}/snapd.session-agent.socket
%{_datadir}/dbus-1/services/io.snapcraft.Launcher.service
%{_datadir}/dbus-1/services/io.snapcraft.Settings.service
%{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy
@@ -803,6 +802,7 @@
%sysctl_apply 99-snap.conf
%endif
%systemd_post %{snappy_svcs}
+%systemd_user_post %{snappy_user_svcs}
# If install, test if snapd socket and timer are enabled.
# If enabled, then attempt to start them. This will silently fail
# in chroots or other environments where services aren't expected
@@ -815,6 +815,7 @@
%preun
%systemd_preun %{snappy_svcs}
+%systemd_user_preun %{snappy_user_svcs}
# Remove all Snappy content if snapd is being fully uninstalled
if [ $1 -eq 0 ]; then
@@ -823,6 +824,7 @@
%postun
%systemd_postun_with_restart %{snappy_svcs}
+%systemd_user_postun %{snappy_user_svcs}
%if 0%{?with_selinux}
%triggerun -- snapd < 2.39
@@ -866,6 +868,453 @@
%changelog
+* Wed Oct 30 2019 Michael Vogt
+- New upstream release 2.42.1
+ - interfaces: de-duplicate emitted update-ns profiles
+ - packaging: tweak handling of usr.lib.snapd.snap-confine
+ - interfaces: allow introspecting network-manager on core
+ - tests/main/interfaces-contacts-service: disable on openSUSE
+ Tumbleweed
+ - tests/lib/lxd-snapfuse: restore mount changes introduced by LXD
+ - snap: fix default-provider in seed validation
+ - tests: update system-usernames test now that opensuse-15.1 works
+ - overlord: set fake sertial in TestRemodelSwitchToDifferentKernel
+ - gadget: rename "boot{select,img}" -> system-boot-{select,image}
+ - tests: listing test, make accepted snapd/core versions consistent
+
+* Tue Oct 01 2019 Michael Vogt
+- New upstream release 2.42
+ - tests: disable {contacts,calendar}-service tests on debian-sid
+ - tests/main/snap-run: disable strace test cases on Arch
+ - cmd/system-shutdown: include correct prototype for die
+ - snap/naming: add test for hook name connect-plug-i2c
+ - cmd/snap-confine: allow digits in hook names
+ - gadget: do not fail the update when old gadget snap is missing
+ bare content
+ - tests: disable {contacts,calendar}-service tests on Arch Linux
+ - tests: move "centos-7" to unstable systems
+ - interfaces/docker-support,kubernetes-support: misc updates for
+ strict k8s
+ - packaging: remove obsolete usr.lib.snapd.snap-confine in
+ postinst
+ - tests: add test that ensures our snapfuse binary actually works
+ - packaging: use snapfuse_ll to speed up snapfuse performance
+ - usersession/userd: make sure to export DBus interfaces before
+ requesting a name
+ - data/selinux: allow snapd to issue sigkill to journalctl
+ - store: download propagates options to delta download
+ - wrappers: allow snaps to install icon theme icons
+ - debug: state-inspect debugging utility
+ - sandbox/cgroup: introduce cgroup wrappers package
+ - snap-confine: fix return value checks for udev functions
+ - cmd/model: output tweaks, add'l tests
+ - wrappers/services: add ServicesEnableState + unit tests
+ - tests: fix newline and wrong test name pointed out in previous PRs
+ - tests: extend mount-ns test to handle mimics
+ - run-checks, tests/main/go: allow gofmt checks to be skipped on
+ 19.10
+ - tests/main/interfaces-{calendar,contacts}-service: disable on
+ 19.10
+ - tests: part3 making tests work on ubuntu-core-18
+ - tests: fix interfaces-timeserver-control on 19.10
+ - overlord/snapstate: config revision code cleanup and extra tests
+ - devicestate: allow remodel to different kernels
+ - overlord,daemon: adjust startup timeout via EXTEND_TIMEOUT_USEC
+ using an estimate
+ - tests/main/many: increase kill-timeout to 5m
+ - interfaces/kubernetes-support: allow systemd-run to ptrace read
+ unconfined
+ - snapstate: auto transition on experimental.snapd-snap=true
+ - tests: retry checking until the written file on desktop-portal-
+ filechooser
+ - tests: unit test for a refresh failing on configure hook
+ - tests: remove mount_id and parent_id from mount-ns test data
+ - tests: move classic-ubuntu-core-transition* to nightly
+ - tests/mountinfo-tool: proper formatting of opt_fields
+ - overlord/configstate: special-case "null" in transaction Changes()
+ - snap-confine: fallback gracefully on a cgroup v2 only system
+ - tests: debian sid now ships new seccomp, adjust tests
+ - tests: explicitly restore after using LXD
+ - snapstate: make progress reporting less granular
+ - bootloader: little kernel support
+ - fixme: rename ubuntu*architectures to dpkg*architectures
+ - tests: run dbus-launch inside a systemd unit
+ - channel: introduce Resolve and ResolveLocked
+ - tests: run failing tests on ubuntu eoan due to is now set as
+ unstable
+ - systemd: detach rather than unmount .mount units
+ - cmd/snap-confine: add unit tests for sc_invocation, cleanup memory
+ leaks in tests
+ - boot,dirs,image: introduce boot.MakeBootable, use it in image
+ instead of ad hoc code
+ - cmd/snap-update-ns: clarify sharing comment
+ - tests/overlord/snapstate: refactor for cleaner test failures
+ - cmd/snap-update-ns: don't propagate detaching changes
+ - interfaces: allow reading mutter Xauthority file
+ - cmd/snap-confine: fix /snap duplication in legacy mode
+ - tests: fix mountinfo-tool filtering when used with rewriting
+ - seed,image,o/devicestate: extract seed loading to seed/seed16.go
+ - many: pass the rootdir and options to bootloader.Find
+ - tests: part5 making tests work on ubuntu-core-18
+ - cmd/snap-confine: keep track of snap instance name and the snap
+ name
+ - cmd: unify die() across C programs
+ - tests: add functions to make an abstraction for the snaps
+ - packaging/fedora, tests/lib/prepare-restore: helper tool for
+ packing sources for RPM
+ - cmd/snap: improve help and error msg for snapshot commands
+ - hookstate/ctlcmd: fix snapctl set help message
+ - cmd/snap: don't append / to snap name just because a dir exists
+ - tests: support fastly-global.cdn.snapcraft.io url on proxy-no-core
+ test
+ - tests: add --quiet switch to retry-tool
+ - tests: add unstable stage for travis execution
+ - tests: disable interfaces-timeserver-control on 19.10
+ - tests: don't guess in is_classic_confinement_supported
+ - boot, etc: simplify BootParticipant (etc) usage
+ - tests: verify retry-tool not retrying missing commands
+ - tests: rewrite "retry" command as retry-tool
+ - tests: move debug section after restore
+ - cmd/libsnap-confine-private, cmd/s-c: use constants for
+ snap/instance name lengths
+ - tests: measure behavior of the device cgroup
+ - boot, bootloader, o/devicestate: boot env manip goes in boot
+ - tests: enabling ubuntu 19.10-64 on spread.yaml
+ - tests: fix ephemeral mount table in left over by prepare
+ - tests: add version-tool for comparing versions
+ - cmd/libsnap: make feature flag enum 1<
+- New upstream release 2.41
+ - overlord/snapstate: revert track-risk behavior
+ - tests: fix snap info test
+ - httputil: rework protocol error detection
+ - gadget: do not error on gadget refreshes with multiple volumes
+ - i18n, vendor, packaging: drop github.com/ojii/gettext.go, use
+ github.com/snapcore/go-gettext
+ - snapstate: validate all system-usernames before creating them
+ - mkversion.sh: fix version from git checkouts
+ - interfaces/network-{control,manager}: allow 'k' on
+ /run/resolvconf/**
+ - interfaces/wayland,x11: allow reading an Xwayland Xauth file
+ - interfaces: k8s worker node updates
+ - debian: re-enable systemd environment generator
+ - many: create system-usernames user/group if both don't exist
+ - packaging: fix symlink for snapd.session-agent.socket
+ - tests: change cgroups so that LXD doesn't have to
+ - interfaces/network-setup-control: allow dbus netplan apply
+ messages
+ - tests: add /var/cache/snapd to the snapd state to prevent error on
+ the store
+ - tests: add test for services disabled during refresh hook
+ - many: simpler access to snap-seccomp version-info
+ - snap: cleanup some tests, clarify some errorsThis is a follow up
+ from work on system usernames:
+ - osutil: add osutil.Find{Uid,Gid}
+ - tests: use a different archive based on the spread backend on go-
+ build test
+ - cmd/snap-update-ns: fix pair of bugs affecting refresh of snap
+ with layouts
+ - overlord/devicestate: detect clashing concurrent (ongoing, just
+ finished) remodels or changes
+ - interfaces/docker-support: declare controls-device-cgroup
+ - packaging: fix removal of old apparmor profile
+ - store: use track/risk for "channel" name when parsing store
+ details
+ - many: allow 'system-usernames' with libseccomp > 2.4 and golang-
+ seccomp > 0.9.0
+ - overlord/devicestate, tests: use gadget.Update() proper, spread
+ test
+ - overlord/configstate/configcore: allow setting start_x=1 to enable
+ CSI camera on RPi
+ - interfaces: remove BeforePrepareSlot from commonInterface
+ - many: support system-usernames for 'snap_daemon' user
+ - overlord/devicestate,o/snapstate: queue service commands before
+ mark-seeded and other final tasks
+ - interfaces/mount: discard mount ns on backend Remove
+ - packaging/fedora: build on RHEL8
+ - overlord/devicestate: support seeding a classic system with the
+ snapd snap and no core
+ - interfaces: fix test failure in gpio_control_test
+ - interfaces, policy: remove sanitize helpers and use minimal policy
+ check
+ - packaging: use %systemd_user_* macros to enable session agent
+ socket according to presets
+ - snapstate, store: handle 429s on catalog refresh a little bit
+ better
+ - tests: part4 making tests work on ubuntu-core-18
+ - many: drop snap.ReadGadgetInfo wrapper
+ - xdgopenproxy: update test API to match upstream
+ - tests: show why sbuild failed
+ - data/selinux: allow mandb_t to search /var/lib/snapd
+ - tests: be less verbose when checking service status
+ - tests: set sbuild test as manual
+ - overlord: DeviceCtx must find the remodel context for a remodel
+ change
+ - tests: use snap info --verbose to check for base
+ - sanity: unmount squashfs with --lazy
+ - overlord/snapstate: keep current track if only risk is specified
+ - interfaces/firewall-control: support nft routing expressions and
+ device groups
+ - gadget: support for writing symlinks
+ - tests: mountinfo-tool fail if there are no matches
+ - tests: sync journal log before start the test
+ - cmd/snap, data/completion: improve completion for 'snap debug'
+ - httputil: retry for http2 PROTOCOL_ERROR
+ - Errata commit: pulseaudio still auto-connects on classic
+ - interfaces/misc: updates for k8s 1.15 (and greengrass test)
+ - tests: set GOTRACEBACK=1 when running tests
+ - cmd/libsnap: don't leak memory in sc_die_on_error
+ - tests: improve how the system is restored when the upgrade-
+ from-2.15 test fails
+ - interfaces/bluetooth-control: add udev rules for BT_chrdev devices
+ - interfaces: add audio-playback/audio-record and make pulseaudio
+ manually connect
+ - tests: split the sbuild test in 2 depending on the type of build
+ - interfaces: add an interface granting access to AppStream metadata
+ - gadget: ensure filesystem labels are unique
+ - usersession/agent: use background context when stopping the agent
+ - HACKING.md: update spread section, other updates
+ - data/selinux: allow snap-confine to read entries on nsfs
+ - tests: respect SPREAD_DEBUG_EACH on the main suite
+ - packaging/debian-sid: set GOCACHE to a known writable location
+ - interfaces: add gpio-control interface
+ - cmd/snap: use showDone helper with 'snap switch'
+ - gadget: effective structure role fallback, extra tests
+ - many: fix unit tests getting stuck
+ - tests: remove installed snap on restore
+ - daemon: do not modify test data in user suite
+ - data/selinux: allow read on sysfs
+ - packaging/debian: don't md5sum absent files
+ - tests: remove test-snapd-curl
+ - tests: remove test-snapd-snapctl-core18 in restore
+ - tests: remove installed snap in the restore section
+ - tests: remove installed test snap
+ - tests: correctly escape mount unit path
+ - cmd/Makefile.am: support building with the go snap
+ - tests: work around classic snap affecting the host
+ - tests: fix typo "current"
+ - overlord/assertstate: add Batch.Precheck to check for the full
+ validity of the batch before Commit
+ - tests: restore cpuset clone_children clobbered by lxd
+ - usersession: move userd package to usersession/userd
+ - tests: reformat and fix markdown in snapd-state.md
+ - gadget: select the right updater for given structure
+ - tests: show stderr only if it exists
+ - sessionagent: add a REST interface with socket activation
+ - tests: remove locally installed core in more tests
+ - tests: remove local revision of core
+ - packaging/debian-sid: use correct apparmor Depends for Debian
+ - packaging/debian-sid: merge debian upload changes back into master
+ - cmd/snap-repair: make sure the goroutine doesn't stick around on
+ timeout
+ - packaging/fedora: github.com/cheggaaa/pb is no longer used
+ - configstate/config: fix crash in purgeNulls
+ - boot, o/snapst, o/devicest: limit knowledge of boot vars to boot
+ - client,cmd/snap: stop depending on status/status-code in the JSON
+ responses in client
+ - tests: unmount leftover /run/netns
+ - tests: switch mount-ns test to manual
+ - overlord,daemon,cmd/snapd: move expensive startup to dedicated
+ StartUp methods
+ - osutil: add EnsureTreeState helper
+ - tests: measure properties of various mount namespaces
+ - tests: part2 making tests work on ubuntu-core-18
+ - interfaces/policy: minimal policy check for replacing
+ sanitizeReservedFor helpers (1/2)
+ - interfaces: add an interface that grants access to the PackageKit
+ service
+ - overlord/devicestate: update gadget update handlers and mocks
+ - tests: add mountinfo-tool --ref-x1000
+ - tests: remove lxd / lxcfs if pre-installed
+ - tests: removing support for ubuntu cosmic on spread test suite
+ - tests: don't leak /run/netns mount
+ - image: clean up the validateSuite
+ - bootloader: remove "Dir()" from Bootloader interface
+ - many: retry to reboot if snapd gets restarted before expected
+ reboot
+ - overlord: implement re-registration remodeling
+ - cmd: revert PR#6933 (tweak of GOMAXPROCS)
+ - cmd/snap: add snap unset command
+ - many: add Client-User-Agent to "SnapAction" install API call
+ - tests: first part making tests run on ubuntu-core-18
+ - hookstate/ctlcmd: support hidden commands in snapctl
+ - many: replace snapd snap name checks with type checks (3/4)
+ - overlord: mostly stop needing Kernel/CoreInfo, make GadgetInfo
+ consider a DeviceContext
+ - snapctl: handle unsetting of config options with "!"
+ - tests: move core migration snaps to tests/lib/snaps dir
+ - cmd/snap: handle unsetting of config options with "!"
+ - cmd/snap, etc: add health to 'snap list' and 'snap info'
+ - gadget: use struct field names when intializing data in mounted
+ updater unit tests
+ - cmd/snap-confine: bring /lib/firmware from the host
+ - snap: set snapd snap type (1/4)
+ - snap: add checks in validate-seed for missing base/default-
+ provider
+ - daemon: replace shutdownServer with net/http's native shutdown
+ support
+ - interfaces/builtin: add exec "/bin/runc" to docker-support
+ - gadget: mounted filesystem updater
+ - overlord/patch: simplify conditions for re-applying sublevel
+ patches for level 6
+ - seccomp/compiler: adjust test case names and comment for later
+ changes
+ - tests: fix error doing snap pack running failover test
+ - tests: don't preserve size= when rewriting mount tables
+ - tests: allow reordering of rewrite operations
+ - gadget: main update routine
+ - overlord/config: normalize nulls to support config unsetting
+ semantics
+ - snap-userd-autostart: don't list as a startup application on the
+ GUI
+ - tests: renumber snap revisions as seen via writable
+ - tests: change allocation for mount options
+ - tests: re-enable ns-re-associate test
+ - tests: mountinfo-tool allow many --refs
+ - overlord/devicestate: implement reregRemodelContext with the
+ essential re-registration logic
+ - tests: replace various numeric mount options
+ - gadget: filesystem image writer
+ - tests: add more unit tests for mountinfo-tool
+ - tests: introduce mountinfo-tool --ref feature
+ - tests: refactor mountinfo-tool rewrite state
+ - tests: allow renumbering mount namespace identifiers
+ - snap: refactor and explain layout blacklisting
+ - tests: renumber snap revisions as seen via hostfs
+ - daemon, interfaces, travis: workaround build ID with Go 1.9, use
+ 1.9 for travis tests
+ - cmd/libsnap: add sc_error_init_{simple,api_misuse}
+ - gadget: make raw updater handle shifted structures
+ - tests/lib/nested: create WORK_DIR before accessing it
+ - cmd/libsnap: rename SC_LIBSNAP_ERROR to SC_LIBSNAP_DOMAIN
+ - cmd,tests: forcibly discard mount namespace when bases change
+ - many: introduce healthstate, run check-health
+ post-(install/refresh/try/revert)
+ - interfaces/optical-drive: add scsi-generic type 4 and 5 support
+ - cmd/snap-confine: exit from helper when parent dies
+
* Fri Jul 12 2019 Michael Vogt
- New upstream release 2.40
- overlord/patch: simplify conditions for re-applying sublevel
@@ -969,7 +1418,7 @@
- tests/lib/nested: fix multi argument copy_remote
- tests/lib/nested: have mkfs.ext4 use a rootdir instead of mounting
an image
- - packaging: fix permissons powerpc docs dir
+ - packaging: fix permissions powerpc docs dir
- overlord: mock store to avoid net requests
- debian: rework how we run autopkgtests
- interface: builtin: avahi-observe/control: allow slots
diff -Nru snapd-2.40/packaging/fedora-30/snapd.spec snapd-2.42.1/packaging/fedora-30/snapd.spec
--- snapd-2.40/packaging/fedora-30/snapd.spec 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/fedora-30/snapd.spec 2019-10-30 12:17:43.000000000 +0000
@@ -60,9 +60,10 @@
%global import_path %{provider_prefix}
%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service snapd.seeded.service
+%global snappy_user_svcs snapd.session-agent.socket
# Until we have a way to add more extldflags to gobuild macro...
-%if 0%{?fedora}
+%if 0%{?fedora} || 0%{?rhel} >= 8
# buildmode PIE triggers external linker consumes -extldflags
%define gobuild_static(o:) go build -buildmode pie -compiler gc -tags=rpm_crashtraceback -ldflags "${LDFLAGS:-} -B 0x$(head -c20 /dev/urandom|od -An -tx1|tr -d ' \\n') -extldflags '%__global_ldflags -static'" -a -v -x %{?**};
%endif
@@ -91,7 +92,7 @@
%endif
Name: snapd
-Version: 2.40
+Version: 2.42.1
Release: 0%{?dist}
Summary: A transactional software package manager
License: GPLv3
@@ -146,7 +147,6 @@
%if ! 0%{?with_bundled}
BuildRequires: golang(github.com/boltdb/bolt)
-BuildRequires: golang(github.com/cheggaaa/pb)
BuildRequires: golang(github.com/coreos/go-systemd/activation)
BuildRequires: golang(github.com/godbus/dbus)
BuildRequires: golang(github.com/godbus/dbus/introspect)
@@ -156,8 +156,8 @@
BuildRequires: golang(github.com/kr/pretty)
BuildRequires: golang(github.com/kr/text)
BuildRequires: golang(github.com/mvo5/goconfigparser)
-BuildRequires: golang(github.com/ojii/gettext.go)
BuildRequires: golang(github.com/seccomp/libseccomp-golang)
+BuildRequires: golang(github.com/snapcore/go-gettext)
BuildRequires: golang(golang.org/x/crypto/openpgp/armor)
BuildRequires: golang(golang.org/x/crypto/openpgp/packet)
BuildRequires: golang(golang.org/x/crypto/sha3)
@@ -183,7 +183,6 @@
BuildRequires: gcc
BuildRequires: gettext
BuildRequires: gnupg
-BuildRequires: indent
BuildRequires: pkgconfig(glib-2.0)
BuildRequires: pkgconfig(libcap)
BuildRequires: pkgconfig(libseccomp)
@@ -243,7 +242,6 @@
%if ! 0%{?with_bundled}
Requires: golang(github.com/boltdb/bolt)
-Requires: golang(github.com/cheggaaa/pb)
Requires: golang(github.com/coreos/go-systemd/activation)
Requires: golang(github.com/godbus/dbus)
Requires: golang(github.com/godbus/dbus/introspect)
@@ -253,8 +251,8 @@
Requires: golang(github.com/kr/pretty)
Requires: golang(github.com/kr/text)
Requires: golang(github.com/mvo5/goconfigparser)
-Requires: golang(github.com/ojii/gettext.go)
Requires: golang(github.com/seccomp/libseccomp-golang)
+Requires: golang(github.com/snapcore/go-gettext)
Requires: golang(golang.org/x/crypto/openpgp/armor)
Requires: golang(golang.org/x/crypto/openpgp/packet)
Requires: golang(golang.org/x/crypto/sha3)
@@ -270,7 +268,6 @@
# the bundled tarball are unversioned (they go by git commit)
# *sigh*... I hate golang...
Provides: bundled(golang(github.com/snapcore/bolt))
-Provides: bundled(golang(github.com/cheggaaa/pb))
Provides: bundled(golang(github.com/coreos/go-systemd/activation))
Provides: bundled(golang(github.com/godbus/dbus))
Provides: bundled(golang(github.com/godbus/dbus/introspect))
@@ -281,7 +278,7 @@
Provides: bundled(golang(github.com/kr/text))
Provides: bundled(golang(github.com/mvo5/goconfigparser))
Provides: bundled(golang(github.com/mvo5/libseccomp-golang))
-Provides: bundled(golang(github.com/ojii/gettext.go))
+Provides: bundled(golang(github.com/snapcore/go-gettext))
Provides: bundled(golang(golang.org/x/crypto/openpgp/armor))
Provides: bundled(golang(golang.org/x/crypto/openpgp/packet))
Provides: bundled(golang(golang.org/x/crypto/sha3))
@@ -728,6 +725,8 @@
%{_unitdir}/snapd.autoimport.service
%{_unitdir}/snapd.failure.service
%{_unitdir}/snapd.seeded.service
+%{_userunitdir}/snapd.session-agent.service
+%{_userunitdir}/snapd.session-agent.socket
%{_datadir}/dbus-1/services/io.snapcraft.Launcher.service
%{_datadir}/dbus-1/services/io.snapcraft.Settings.service
%{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy
@@ -803,6 +802,7 @@
%sysctl_apply 99-snap.conf
%endif
%systemd_post %{snappy_svcs}
+%systemd_user_post %{snappy_user_svcs}
# If install, test if snapd socket and timer are enabled.
# If enabled, then attempt to start them. This will silently fail
# in chroots or other environments where services aren't expected
@@ -815,6 +815,7 @@
%preun
%systemd_preun %{snappy_svcs}
+%systemd_user_preun %{snappy_user_svcs}
# Remove all Snappy content if snapd is being fully uninstalled
if [ $1 -eq 0 ]; then
@@ -823,6 +824,7 @@
%postun
%systemd_postun_with_restart %{snappy_svcs}
+%systemd_user_postun %{snappy_user_svcs}
%if 0%{?with_selinux}
%triggerun -- snapd < 2.39
@@ -866,6 +868,453 @@
%changelog
+* Wed Oct 30 2019 Michael Vogt
+- New upstream release 2.42.1
+ - interfaces: de-duplicate emitted update-ns profiles
+ - packaging: tweak handling of usr.lib.snapd.snap-confine
+ - interfaces: allow introspecting network-manager on core
+ - tests/main/interfaces-contacts-service: disable on openSUSE
+ Tumbleweed
+ - tests/lib/lxd-snapfuse: restore mount changes introduced by LXD
+ - snap: fix default-provider in seed validation
+ - tests: update system-usernames test now that opensuse-15.1 works
+ - overlord: set fake sertial in TestRemodelSwitchToDifferentKernel
+ - gadget: rename "boot{select,img}" -> system-boot-{select,image}
+ - tests: listing test, make accepted snapd/core versions consistent
+
+* Tue Oct 01 2019 Michael Vogt
+- New upstream release 2.42
+ - tests: disable {contacts,calendar}-service tests on debian-sid
+ - tests/main/snap-run: disable strace test cases on Arch
+ - cmd/system-shutdown: include correct prototype for die
+ - snap/naming: add test for hook name connect-plug-i2c
+ - cmd/snap-confine: allow digits in hook names
+ - gadget: do not fail the update when old gadget snap is missing
+ bare content
+ - tests: disable {contacts,calendar}-service tests on Arch Linux
+ - tests: move "centos-7" to unstable systems
+ - interfaces/docker-support,kubernetes-support: misc updates for
+ strict k8s
+ - packaging: remove obsolete usr.lib.snapd.snap-confine in
+ postinst
+ - tests: add test that ensures our snapfuse binary actually works
+ - packaging: use snapfuse_ll to speed up snapfuse performance
+ - usersession/userd: make sure to export DBus interfaces before
+ requesting a name
+ - data/selinux: allow snapd to issue sigkill to journalctl
+ - store: download propagates options to delta download
+ - wrappers: allow snaps to install icon theme icons
+ - debug: state-inspect debugging utility
+ - sandbox/cgroup: introduce cgroup wrappers package
+ - snap-confine: fix return value checks for udev functions
+ - cmd/model: output tweaks, add'l tests
+ - wrappers/services: add ServicesEnableState + unit tests
+ - tests: fix newline and wrong test name pointed out in previous PRs
+ - tests: extend mount-ns test to handle mimics
+ - run-checks, tests/main/go: allow gofmt checks to be skipped on
+ 19.10
+ - tests/main/interfaces-{calendar,contacts}-service: disable on
+ 19.10
+ - tests: part3 making tests work on ubuntu-core-18
+ - tests: fix interfaces-timeserver-control on 19.10
+ - overlord/snapstate: config revision code cleanup and extra tests
+ - devicestate: allow remodel to different kernels
+ - overlord,daemon: adjust startup timeout via EXTEND_TIMEOUT_USEC
+ using an estimate
+ - tests/main/many: increase kill-timeout to 5m
+ - interfaces/kubernetes-support: allow systemd-run to ptrace read
+ unconfined
+ - snapstate: auto transition on experimental.snapd-snap=true
+ - tests: retry checking until the written file on desktop-portal-
+ filechooser
+ - tests: unit test for a refresh failing on configure hook
+ - tests: remove mount_id and parent_id from mount-ns test data
+ - tests: move classic-ubuntu-core-transition* to nightly
+ - tests/mountinfo-tool: proper formatting of opt_fields
+ - overlord/configstate: special-case "null" in transaction Changes()
+ - snap-confine: fallback gracefully on a cgroup v2 only system
+ - tests: debian sid now ships new seccomp, adjust tests
+ - tests: explicitly restore after using LXD
+ - snapstate: make progress reporting less granular
+ - bootloader: little kernel support
+ - fixme: rename ubuntu*architectures to dpkg*architectures
+ - tests: run dbus-launch inside a systemd unit
+ - channel: introduce Resolve and ResolveLocked
+ - tests: run failing tests on ubuntu eoan due to is now set as
+ unstable
+ - systemd: detach rather than unmount .mount units
+ - cmd/snap-confine: add unit tests for sc_invocation, cleanup memory
+ leaks in tests
+ - boot,dirs,image: introduce boot.MakeBootable, use it in image
+ instead of ad hoc code
+ - cmd/snap-update-ns: clarify sharing comment
+ - tests/overlord/snapstate: refactor for cleaner test failures
+ - cmd/snap-update-ns: don't propagate detaching changes
+ - interfaces: allow reading mutter Xauthority file
+ - cmd/snap-confine: fix /snap duplication in legacy mode
+ - tests: fix mountinfo-tool filtering when used with rewriting
+ - seed,image,o/devicestate: extract seed loading to seed/seed16.go
+ - many: pass the rootdir and options to bootloader.Find
+ - tests: part5 making tests work on ubuntu-core-18
+ - cmd/snap-confine: keep track of snap instance name and the snap
+ name
+ - cmd: unify die() across C programs
+ - tests: add functions to make an abstraction for the snaps
+ - packaging/fedora, tests/lib/prepare-restore: helper tool for
+ packing sources for RPM
+ - cmd/snap: improve help and error msg for snapshot commands
+ - hookstate/ctlcmd: fix snapctl set help message
+ - cmd/snap: don't append / to snap name just because a dir exists
+ - tests: support fastly-global.cdn.snapcraft.io url on proxy-no-core
+ test
+ - tests: add --quiet switch to retry-tool
+ - tests: add unstable stage for travis execution
+ - tests: disable interfaces-timeserver-control on 19.10
+ - tests: don't guess in is_classic_confinement_supported
+ - boot, etc: simplify BootParticipant (etc) usage
+ - tests: verify retry-tool not retrying missing commands
+ - tests: rewrite "retry" command as retry-tool
+ - tests: move debug section after restore
+ - cmd/libsnap-confine-private, cmd/s-c: use constants for
+ snap/instance name lengths
+ - tests: measure behavior of the device cgroup
+ - boot, bootloader, o/devicestate: boot env manip goes in boot
+ - tests: enabling ubuntu 19.10-64 on spread.yaml
+ - tests: fix ephemeral mount table in left over by prepare
+ - tests: add version-tool for comparing versions
+ - cmd/libsnap: make feature flag enum 1<
+- New upstream release 2.41
+ - overlord/snapstate: revert track-risk behavior
+ - tests: fix snap info test
+ - httputil: rework protocol error detection
+ - gadget: do not error on gadget refreshes with multiple volumes
+ - i18n, vendor, packaging: drop github.com/ojii/gettext.go, use
+ github.com/snapcore/go-gettext
+ - snapstate: validate all system-usernames before creating them
+ - mkversion.sh: fix version from git checkouts
+ - interfaces/network-{control,manager}: allow 'k' on
+ /run/resolvconf/**
+ - interfaces/wayland,x11: allow reading an Xwayland Xauth file
+ - interfaces: k8s worker node updates
+ - debian: re-enable systemd environment generator
+ - many: create system-usernames user/group if both don't exist
+ - packaging: fix symlink for snapd.session-agent.socket
+ - tests: change cgroups so that LXD doesn't have to
+ - interfaces/network-setup-control: allow dbus netplan apply
+ messages
+ - tests: add /var/cache/snapd to the snapd state to prevent error on
+ the store
+ - tests: add test for services disabled during refresh hook
+ - many: simpler access to snap-seccomp version-info
+ - snap: cleanup some tests, clarify some errorsThis is a follow up
+ from work on system usernames:
+ - osutil: add osutil.Find{Uid,Gid}
+ - tests: use a different archive based on the spread backend on go-
+ build test
+ - cmd/snap-update-ns: fix pair of bugs affecting refresh of snap
+ with layouts
+ - overlord/devicestate: detect clashing concurrent (ongoing, just
+ finished) remodels or changes
+ - interfaces/docker-support: declare controls-device-cgroup
+ - packaging: fix removal of old apparmor profile
+ - store: use track/risk for "channel" name when parsing store
+ details
+ - many: allow 'system-usernames' with libseccomp > 2.4 and golang-
+ seccomp > 0.9.0
+ - overlord/devicestate, tests: use gadget.Update() proper, spread
+ test
+ - overlord/configstate/configcore: allow setting start_x=1 to enable
+ CSI camera on RPi
+ - interfaces: remove BeforePrepareSlot from commonInterface
+ - many: support system-usernames for 'snap_daemon' user
+ - overlord/devicestate,o/snapstate: queue service commands before
+ mark-seeded and other final tasks
+ - interfaces/mount: discard mount ns on backend Remove
+ - packaging/fedora: build on RHEL8
+ - overlord/devicestate: support seeding a classic system with the
+ snapd snap and no core
+ - interfaces: fix test failure in gpio_control_test
+ - interfaces, policy: remove sanitize helpers and use minimal policy
+ check
+ - packaging: use %systemd_user_* macros to enable session agent
+ socket according to presets
+ - snapstate, store: handle 429s on catalog refresh a little bit
+ better
+ - tests: part4 making tests work on ubuntu-core-18
+ - many: drop snap.ReadGadgetInfo wrapper
+ - xdgopenproxy: update test API to match upstream
+ - tests: show why sbuild failed
+ - data/selinux: allow mandb_t to search /var/lib/snapd
+ - tests: be less verbose when checking service status
+ - tests: set sbuild test as manual
+ - overlord: DeviceCtx must find the remodel context for a remodel
+ change
+ - tests: use snap info --verbose to check for base
+ - sanity: unmount squashfs with --lazy
+ - overlord/snapstate: keep current track if only risk is specified
+ - interfaces/firewall-control: support nft routing expressions and
+ device groups
+ - gadget: support for writing symlinks
+ - tests: mountinfo-tool fail if there are no matches
+ - tests: sync journal log before start the test
+ - cmd/snap, data/completion: improve completion for 'snap debug'
+ - httputil: retry for http2 PROTOCOL_ERROR
+ - Errata commit: pulseaudio still auto-connects on classic
+ - interfaces/misc: updates for k8s 1.15 (and greengrass test)
+ - tests: set GOTRACEBACK=1 when running tests
+ - cmd/libsnap: don't leak memory in sc_die_on_error
+ - tests: improve how the system is restored when the upgrade-
+ from-2.15 test fails
+ - interfaces/bluetooth-control: add udev rules for BT_chrdev devices
+ - interfaces: add audio-playback/audio-record and make pulseaudio
+ manually connect
+ - tests: split the sbuild test in 2 depending on the type of build
+ - interfaces: add an interface granting access to AppStream metadata
+ - gadget: ensure filesystem labels are unique
+ - usersession/agent: use background context when stopping the agent
+ - HACKING.md: update spread section, other updates
+ - data/selinux: allow snap-confine to read entries on nsfs
+ - tests: respect SPREAD_DEBUG_EACH on the main suite
+ - packaging/debian-sid: set GOCACHE to a known writable location
+ - interfaces: add gpio-control interface
+ - cmd/snap: use showDone helper with 'snap switch'
+ - gadget: effective structure role fallback, extra tests
+ - many: fix unit tests getting stuck
+ - tests: remove installed snap on restore
+ - daemon: do not modify test data in user suite
+ - data/selinux: allow read on sysfs
+ - packaging/debian: don't md5sum absent files
+ - tests: remove test-snapd-curl
+ - tests: remove test-snapd-snapctl-core18 in restore
+ - tests: remove installed snap in the restore section
+ - tests: remove installed test snap
+ - tests: correctly escape mount unit path
+ - cmd/Makefile.am: support building with the go snap
+ - tests: work around classic snap affecting the host
+ - tests: fix typo "current"
+ - overlord/assertstate: add Batch.Precheck to check for the full
+ validity of the batch before Commit
+ - tests: restore cpuset clone_children clobbered by lxd
+ - usersession: move userd package to usersession/userd
+ - tests: reformat and fix markdown in snapd-state.md
+ - gadget: select the right updater for given structure
+ - tests: show stderr only if it exists
+ - sessionagent: add a REST interface with socket activation
+ - tests: remove locally installed core in more tests
+ - tests: remove local revision of core
+ - packaging/debian-sid: use correct apparmor Depends for Debian
+ - packaging/debian-sid: merge debian upload changes back into master
+ - cmd/snap-repair: make sure the goroutine doesn't stick around on
+ timeout
+ - packaging/fedora: github.com/cheggaaa/pb is no longer used
+ - configstate/config: fix crash in purgeNulls
+ - boot, o/snapst, o/devicest: limit knowledge of boot vars to boot
+ - client,cmd/snap: stop depending on status/status-code in the JSON
+ responses in client
+ - tests: unmount leftover /run/netns
+ - tests: switch mount-ns test to manual
+ - overlord,daemon,cmd/snapd: move expensive startup to dedicated
+ StartUp methods
+ - osutil: add EnsureTreeState helper
+ - tests: measure properties of various mount namespaces
+ - tests: part2 making tests work on ubuntu-core-18
+ - interfaces/policy: minimal policy check for replacing
+ sanitizeReservedFor helpers (1/2)
+ - interfaces: add an interface that grants access to the PackageKit
+ service
+ - overlord/devicestate: update gadget update handlers and mocks
+ - tests: add mountinfo-tool --ref-x1000
+ - tests: remove lxd / lxcfs if pre-installed
+ - tests: removing support for ubuntu cosmic on spread test suite
+ - tests: don't leak /run/netns mount
+ - image: clean up the validateSuite
+ - bootloader: remove "Dir()" from Bootloader interface
+ - many: retry to reboot if snapd gets restarted before expected
+ reboot
+ - overlord: implement re-registration remodeling
+ - cmd: revert PR#6933 (tweak of GOMAXPROCS)
+ - cmd/snap: add snap unset command
+ - many: add Client-User-Agent to "SnapAction" install API call
+ - tests: first part making tests run on ubuntu-core-18
+ - hookstate/ctlcmd: support hidden commands in snapctl
+ - many: replace snapd snap name checks with type checks (3/4)
+ - overlord: mostly stop needing Kernel/CoreInfo, make GadgetInfo
+ consider a DeviceContext
+ - snapctl: handle unsetting of config options with "!"
+ - tests: move core migration snaps to tests/lib/snaps dir
+ - cmd/snap: handle unsetting of config options with "!"
+ - cmd/snap, etc: add health to 'snap list' and 'snap info'
+ - gadget: use struct field names when intializing data in mounted
+ updater unit tests
+ - cmd/snap-confine: bring /lib/firmware from the host
+ - snap: set snapd snap type (1/4)
+ - snap: add checks in validate-seed for missing base/default-
+ provider
+ - daemon: replace shutdownServer with net/http's native shutdown
+ support
+ - interfaces/builtin: add exec "/bin/runc" to docker-support
+ - gadget: mounted filesystem updater
+ - overlord/patch: simplify conditions for re-applying sublevel
+ patches for level 6
+ - seccomp/compiler: adjust test case names and comment for later
+ changes
+ - tests: fix error doing snap pack running failover test
+ - tests: don't preserve size= when rewriting mount tables
+ - tests: allow reordering of rewrite operations
+ - gadget: main update routine
+ - overlord/config: normalize nulls to support config unsetting
+ semantics
+ - snap-userd-autostart: don't list as a startup application on the
+ GUI
+ - tests: renumber snap revisions as seen via writable
+ - tests: change allocation for mount options
+ - tests: re-enable ns-re-associate test
+ - tests: mountinfo-tool allow many --refs
+ - overlord/devicestate: implement reregRemodelContext with the
+ essential re-registration logic
+ - tests: replace various numeric mount options
+ - gadget: filesystem image writer
+ - tests: add more unit tests for mountinfo-tool
+ - tests: introduce mountinfo-tool --ref feature
+ - tests: refactor mountinfo-tool rewrite state
+ - tests: allow renumbering mount namespace identifiers
+ - snap: refactor and explain layout blacklisting
+ - tests: renumber snap revisions as seen via hostfs
+ - daemon, interfaces, travis: workaround build ID with Go 1.9, use
+ 1.9 for travis tests
+ - cmd/libsnap: add sc_error_init_{simple,api_misuse}
+ - gadget: make raw updater handle shifted structures
+ - tests/lib/nested: create WORK_DIR before accessing it
+ - cmd/libsnap: rename SC_LIBSNAP_ERROR to SC_LIBSNAP_DOMAIN
+ - cmd,tests: forcibly discard mount namespace when bases change
+ - many: introduce healthstate, run check-health
+ post-(install/refresh/try/revert)
+ - interfaces/optical-drive: add scsi-generic type 4 and 5 support
+ - cmd/snap-confine: exit from helper when parent dies
+
* Fri Jul 12 2019 Michael Vogt
- New upstream release 2.40
- overlord/patch: simplify conditions for re-applying sublevel
@@ -969,7 +1418,7 @@
- tests/lib/nested: fix multi argument copy_remote
- tests/lib/nested: have mkfs.ext4 use a rootdir instead of mounting
an image
- - packaging: fix permissons powerpc docs dir
+ - packaging: fix permissions powerpc docs dir
- overlord: mock store to avoid net requests
- debian: rework how we run autopkgtests
- interface: builtin: avahi-observe/control: allow slots
diff -Nru snapd-2.40/packaging/fedora-rawhide/snapd.spec snapd-2.42.1/packaging/fedora-rawhide/snapd.spec
--- snapd-2.40/packaging/fedora-rawhide/snapd.spec 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/fedora-rawhide/snapd.spec 2019-10-30 12:17:43.000000000 +0000
@@ -60,9 +60,10 @@
%global import_path %{provider_prefix}
%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service snapd.seeded.service
+%global snappy_user_svcs snapd.session-agent.socket
# Until we have a way to add more extldflags to gobuild macro...
-%if 0%{?fedora}
+%if 0%{?fedora} || 0%{?rhel} >= 8
# buildmode PIE triggers external linker consumes -extldflags
%define gobuild_static(o:) go build -buildmode pie -compiler gc -tags=rpm_crashtraceback -ldflags "${LDFLAGS:-} -B 0x$(head -c20 /dev/urandom|od -An -tx1|tr -d ' \\n') -extldflags '%__global_ldflags -static'" -a -v -x %{?**};
%endif
@@ -91,7 +92,7 @@
%endif
Name: snapd
-Version: 2.40
+Version: 2.42.1
Release: 0%{?dist}
Summary: A transactional software package manager
License: GPLv3
@@ -146,7 +147,6 @@
%if ! 0%{?with_bundled}
BuildRequires: golang(github.com/boltdb/bolt)
-BuildRequires: golang(github.com/cheggaaa/pb)
BuildRequires: golang(github.com/coreos/go-systemd/activation)
BuildRequires: golang(github.com/godbus/dbus)
BuildRequires: golang(github.com/godbus/dbus/introspect)
@@ -156,8 +156,8 @@
BuildRequires: golang(github.com/kr/pretty)
BuildRequires: golang(github.com/kr/text)
BuildRequires: golang(github.com/mvo5/goconfigparser)
-BuildRequires: golang(github.com/ojii/gettext.go)
BuildRequires: golang(github.com/seccomp/libseccomp-golang)
+BuildRequires: golang(github.com/snapcore/go-gettext)
BuildRequires: golang(golang.org/x/crypto/openpgp/armor)
BuildRequires: golang(golang.org/x/crypto/openpgp/packet)
BuildRequires: golang(golang.org/x/crypto/sha3)
@@ -183,7 +183,6 @@
BuildRequires: gcc
BuildRequires: gettext
BuildRequires: gnupg
-BuildRequires: indent
BuildRequires: pkgconfig(glib-2.0)
BuildRequires: pkgconfig(libcap)
BuildRequires: pkgconfig(libseccomp)
@@ -243,7 +242,6 @@
%if ! 0%{?with_bundled}
Requires: golang(github.com/boltdb/bolt)
-Requires: golang(github.com/cheggaaa/pb)
Requires: golang(github.com/coreos/go-systemd/activation)
Requires: golang(github.com/godbus/dbus)
Requires: golang(github.com/godbus/dbus/introspect)
@@ -253,8 +251,8 @@
Requires: golang(github.com/kr/pretty)
Requires: golang(github.com/kr/text)
Requires: golang(github.com/mvo5/goconfigparser)
-Requires: golang(github.com/ojii/gettext.go)
Requires: golang(github.com/seccomp/libseccomp-golang)
+Requires: golang(github.com/snapcore/go-gettext)
Requires: golang(golang.org/x/crypto/openpgp/armor)
Requires: golang(golang.org/x/crypto/openpgp/packet)
Requires: golang(golang.org/x/crypto/sha3)
@@ -270,7 +268,6 @@
# the bundled tarball are unversioned (they go by git commit)
# *sigh*... I hate golang...
Provides: bundled(golang(github.com/snapcore/bolt))
-Provides: bundled(golang(github.com/cheggaaa/pb))
Provides: bundled(golang(github.com/coreos/go-systemd/activation))
Provides: bundled(golang(github.com/godbus/dbus))
Provides: bundled(golang(github.com/godbus/dbus/introspect))
@@ -281,7 +278,7 @@
Provides: bundled(golang(github.com/kr/text))
Provides: bundled(golang(github.com/mvo5/goconfigparser))
Provides: bundled(golang(github.com/mvo5/libseccomp-golang))
-Provides: bundled(golang(github.com/ojii/gettext.go))
+Provides: bundled(golang(github.com/snapcore/go-gettext))
Provides: bundled(golang(golang.org/x/crypto/openpgp/armor))
Provides: bundled(golang(golang.org/x/crypto/openpgp/packet))
Provides: bundled(golang(golang.org/x/crypto/sha3))
@@ -728,6 +725,8 @@
%{_unitdir}/snapd.autoimport.service
%{_unitdir}/snapd.failure.service
%{_unitdir}/snapd.seeded.service
+%{_userunitdir}/snapd.session-agent.service
+%{_userunitdir}/snapd.session-agent.socket
%{_datadir}/dbus-1/services/io.snapcraft.Launcher.service
%{_datadir}/dbus-1/services/io.snapcraft.Settings.service
%{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy
@@ -803,6 +802,7 @@
%sysctl_apply 99-snap.conf
%endif
%systemd_post %{snappy_svcs}
+%systemd_user_post %{snappy_user_svcs}
# If install, test if snapd socket and timer are enabled.
# If enabled, then attempt to start them. This will silently fail
# in chroots or other environments where services aren't expected
@@ -815,6 +815,7 @@
%preun
%systemd_preun %{snappy_svcs}
+%systemd_user_preun %{snappy_user_svcs}
# Remove all Snappy content if snapd is being fully uninstalled
if [ $1 -eq 0 ]; then
@@ -823,6 +824,7 @@
%postun
%systemd_postun_with_restart %{snappy_svcs}
+%systemd_user_postun %{snappy_user_svcs}
%if 0%{?with_selinux}
%triggerun -- snapd < 2.39
@@ -866,6 +868,453 @@
%changelog
+* Wed Oct 30 2019 Michael Vogt
+- New upstream release 2.42.1
+ - interfaces: de-duplicate emitted update-ns profiles
+ - packaging: tweak handling of usr.lib.snapd.snap-confine
+ - interfaces: allow introspecting network-manager on core
+ - tests/main/interfaces-contacts-service: disable on openSUSE
+ Tumbleweed
+ - tests/lib/lxd-snapfuse: restore mount changes introduced by LXD
+ - snap: fix default-provider in seed validation
+ - tests: update system-usernames test now that opensuse-15.1 works
+ - overlord: set fake sertial in TestRemodelSwitchToDifferentKernel
+ - gadget: rename "boot{select,img}" -> system-boot-{select,image}
+ - tests: listing test, make accepted snapd/core versions consistent
+
+* Tue Oct 01 2019 Michael Vogt
+- New upstream release 2.42
+ - tests: disable {contacts,calendar}-service tests on debian-sid
+ - tests/main/snap-run: disable strace test cases on Arch
+ - cmd/system-shutdown: include correct prototype for die
+ - snap/naming: add test for hook name connect-plug-i2c
+ - cmd/snap-confine: allow digits in hook names
+ - gadget: do not fail the update when old gadget snap is missing
+ bare content
+ - tests: disable {contacts,calendar}-service tests on Arch Linux
+ - tests: move "centos-7" to unstable systems
+ - interfaces/docker-support,kubernetes-support: misc updates for
+ strict k8s
+ - packaging: remove obsolete usr.lib.snapd.snap-confine in
+ postinst
+ - tests: add test that ensures our snapfuse binary actually works
+ - packaging: use snapfuse_ll to speed up snapfuse performance
+ - usersession/userd: make sure to export DBus interfaces before
+ requesting a name
+ - data/selinux: allow snapd to issue sigkill to journalctl
+ - store: download propagates options to delta download
+ - wrappers: allow snaps to install icon theme icons
+ - debug: state-inspect debugging utility
+ - sandbox/cgroup: introduce cgroup wrappers package
+ - snap-confine: fix return value checks for udev functions
+ - cmd/model: output tweaks, add'l tests
+ - wrappers/services: add ServicesEnableState + unit tests
+ - tests: fix newline and wrong test name pointed out in previous PRs
+ - tests: extend mount-ns test to handle mimics
+ - run-checks, tests/main/go: allow gofmt checks to be skipped on
+ 19.10
+ - tests/main/interfaces-{calendar,contacts}-service: disable on
+ 19.10
+ - tests: part3 making tests work on ubuntu-core-18
+ - tests: fix interfaces-timeserver-control on 19.10
+ - overlord/snapstate: config revision code cleanup and extra tests
+ - devicestate: allow remodel to different kernels
+ - overlord,daemon: adjust startup timeout via EXTEND_TIMEOUT_USEC
+ using an estimate
+ - tests/main/many: increase kill-timeout to 5m
+ - interfaces/kubernetes-support: allow systemd-run to ptrace read
+ unconfined
+ - snapstate: auto transition on experimental.snapd-snap=true
+ - tests: retry checking until the written file on desktop-portal-
+ filechooser
+ - tests: unit test for a refresh failing on configure hook
+ - tests: remove mount_id and parent_id from mount-ns test data
+ - tests: move classic-ubuntu-core-transition* to nightly
+ - tests/mountinfo-tool: proper formatting of opt_fields
+ - overlord/configstate: special-case "null" in transaction Changes()
+ - snap-confine: fallback gracefully on a cgroup v2 only system
+ - tests: debian sid now ships new seccomp, adjust tests
+ - tests: explicitly restore after using LXD
+ - snapstate: make progress reporting less granular
+ - bootloader: little kernel support
+ - fixme: rename ubuntu*architectures to dpkg*architectures
+ - tests: run dbus-launch inside a systemd unit
+ - channel: introduce Resolve and ResolveLocked
+ - tests: run failing tests on ubuntu eoan due to is now set as
+ unstable
+ - systemd: detach rather than unmount .mount units
+ - cmd/snap-confine: add unit tests for sc_invocation, cleanup memory
+ leaks in tests
+ - boot,dirs,image: introduce boot.MakeBootable, use it in image
+ instead of ad hoc code
+ - cmd/snap-update-ns: clarify sharing comment
+ - tests/overlord/snapstate: refactor for cleaner test failures
+ - cmd/snap-update-ns: don't propagate detaching changes
+ - interfaces: allow reading mutter Xauthority file
+ - cmd/snap-confine: fix /snap duplication in legacy mode
+ - tests: fix mountinfo-tool filtering when used with rewriting
+ - seed,image,o/devicestate: extract seed loading to seed/seed16.go
+ - many: pass the rootdir and options to bootloader.Find
+ - tests: part5 making tests work on ubuntu-core-18
+ - cmd/snap-confine: keep track of snap instance name and the snap
+ name
+ - cmd: unify die() across C programs
+ - tests: add functions to make an abstraction for the snaps
+ - packaging/fedora, tests/lib/prepare-restore: helper tool for
+ packing sources for RPM
+ - cmd/snap: improve help and error msg for snapshot commands
+ - hookstate/ctlcmd: fix snapctl set help message
+ - cmd/snap: don't append / to snap name just because a dir exists
+ - tests: support fastly-global.cdn.snapcraft.io url on proxy-no-core
+ test
+ - tests: add --quiet switch to retry-tool
+ - tests: add unstable stage for travis execution
+ - tests: disable interfaces-timeserver-control on 19.10
+ - tests: don't guess in is_classic_confinement_supported
+ - boot, etc: simplify BootParticipant (etc) usage
+ - tests: verify retry-tool not retrying missing commands
+ - tests: rewrite "retry" command as retry-tool
+ - tests: move debug section after restore
+ - cmd/libsnap-confine-private, cmd/s-c: use constants for
+ snap/instance name lengths
+ - tests: measure behavior of the device cgroup
+ - boot, bootloader, o/devicestate: boot env manip goes in boot
+ - tests: enabling ubuntu 19.10-64 on spread.yaml
+ - tests: fix ephemeral mount table in left over by prepare
+ - tests: add version-tool for comparing versions
+ - cmd/libsnap: make feature flag enum 1<
+- New upstream release 2.41
+ - overlord/snapstate: revert track-risk behavior
+ - tests: fix snap info test
+ - httputil: rework protocol error detection
+ - gadget: do not error on gadget refreshes with multiple volumes
+ - i18n, vendor, packaging: drop github.com/ojii/gettext.go, use
+ github.com/snapcore/go-gettext
+ - snapstate: validate all system-usernames before creating them
+ - mkversion.sh: fix version from git checkouts
+ - interfaces/network-{control,manager}: allow 'k' on
+ /run/resolvconf/**
+ - interfaces/wayland,x11: allow reading an Xwayland Xauth file
+ - interfaces: k8s worker node updates
+ - debian: re-enable systemd environment generator
+ - many: create system-usernames user/group if both don't exist
+ - packaging: fix symlink for snapd.session-agent.socket
+ - tests: change cgroups so that LXD doesn't have to
+ - interfaces/network-setup-control: allow dbus netplan apply
+ messages
+ - tests: add /var/cache/snapd to the snapd state to prevent error on
+ the store
+ - tests: add test for services disabled during refresh hook
+ - many: simpler access to snap-seccomp version-info
+ - snap: cleanup some tests, clarify some errorsThis is a follow up
+ from work on system usernames:
+ - osutil: add osutil.Find{Uid,Gid}
+ - tests: use a different archive based on the spread backend on go-
+ build test
+ - cmd/snap-update-ns: fix pair of bugs affecting refresh of snap
+ with layouts
+ - overlord/devicestate: detect clashing concurrent (ongoing, just
+ finished) remodels or changes
+ - interfaces/docker-support: declare controls-device-cgroup
+ - packaging: fix removal of old apparmor profile
+ - store: use track/risk for "channel" name when parsing store
+ details
+ - many: allow 'system-usernames' with libseccomp > 2.4 and golang-
+ seccomp > 0.9.0
+ - overlord/devicestate, tests: use gadget.Update() proper, spread
+ test
+ - overlord/configstate/configcore: allow setting start_x=1 to enable
+ CSI camera on RPi
+ - interfaces: remove BeforePrepareSlot from commonInterface
+ - many: support system-usernames for 'snap_daemon' user
+ - overlord/devicestate,o/snapstate: queue service commands before
+ mark-seeded and other final tasks
+ - interfaces/mount: discard mount ns on backend Remove
+ - packaging/fedora: build on RHEL8
+ - overlord/devicestate: support seeding a classic system with the
+ snapd snap and no core
+ - interfaces: fix test failure in gpio_control_test
+ - interfaces, policy: remove sanitize helpers and use minimal policy
+ check
+ - packaging: use %systemd_user_* macros to enable session agent
+ socket according to presets
+ - snapstate, store: handle 429s on catalog refresh a little bit
+ better
+ - tests: part4 making tests work on ubuntu-core-18
+ - many: drop snap.ReadGadgetInfo wrapper
+ - xdgopenproxy: update test API to match upstream
+ - tests: show why sbuild failed
+ - data/selinux: allow mandb_t to search /var/lib/snapd
+ - tests: be less verbose when checking service status
+ - tests: set sbuild test as manual
+ - overlord: DeviceCtx must find the remodel context for a remodel
+ change
+ - tests: use snap info --verbose to check for base
+ - sanity: unmount squashfs with --lazy
+ - overlord/snapstate: keep current track if only risk is specified
+ - interfaces/firewall-control: support nft routing expressions and
+ device groups
+ - gadget: support for writing symlinks
+ - tests: mountinfo-tool fail if there are no matches
+ - tests: sync journal log before start the test
+ - cmd/snap, data/completion: improve completion for 'snap debug'
+ - httputil: retry for http2 PROTOCOL_ERROR
+ - Errata commit: pulseaudio still auto-connects on classic
+ - interfaces/misc: updates for k8s 1.15 (and greengrass test)
+ - tests: set GOTRACEBACK=1 when running tests
+ - cmd/libsnap: don't leak memory in sc_die_on_error
+ - tests: improve how the system is restored when the upgrade-
+ from-2.15 test fails
+ - interfaces/bluetooth-control: add udev rules for BT_chrdev devices
+ - interfaces: add audio-playback/audio-record and make pulseaudio
+ manually connect
+ - tests: split the sbuild test in 2 depending on the type of build
+ - interfaces: add an interface granting access to AppStream metadata
+ - gadget: ensure filesystem labels are unique
+ - usersession/agent: use background context when stopping the agent
+ - HACKING.md: update spread section, other updates
+ - data/selinux: allow snap-confine to read entries on nsfs
+ - tests: respect SPREAD_DEBUG_EACH on the main suite
+ - packaging/debian-sid: set GOCACHE to a known writable location
+ - interfaces: add gpio-control interface
+ - cmd/snap: use showDone helper with 'snap switch'
+ - gadget: effective structure role fallback, extra tests
+ - many: fix unit tests getting stuck
+ - tests: remove installed snap on restore
+ - daemon: do not modify test data in user suite
+ - data/selinux: allow read on sysfs
+ - packaging/debian: don't md5sum absent files
+ - tests: remove test-snapd-curl
+ - tests: remove test-snapd-snapctl-core18 in restore
+ - tests: remove installed snap in the restore section
+ - tests: remove installed test snap
+ - tests: correctly escape mount unit path
+ - cmd/Makefile.am: support building with the go snap
+ - tests: work around classic snap affecting the host
+ - tests: fix typo "current"
+ - overlord/assertstate: add Batch.Precheck to check for the full
+ validity of the batch before Commit
+ - tests: restore cpuset clone_children clobbered by lxd
+ - usersession: move userd package to usersession/userd
+ - tests: reformat and fix markdown in snapd-state.md
+ - gadget: select the right updater for given structure
+ - tests: show stderr only if it exists
+ - sessionagent: add a REST interface with socket activation
+ - tests: remove locally installed core in more tests
+ - tests: remove local revision of core
+ - packaging/debian-sid: use correct apparmor Depends for Debian
+ - packaging/debian-sid: merge debian upload changes back into master
+ - cmd/snap-repair: make sure the goroutine doesn't stick around on
+ timeout
+ - packaging/fedora: github.com/cheggaaa/pb is no longer used
+ - configstate/config: fix crash in purgeNulls
+ - boot, o/snapst, o/devicest: limit knowledge of boot vars to boot
+ - client,cmd/snap: stop depending on status/status-code in the JSON
+ responses in client
+ - tests: unmount leftover /run/netns
+ - tests: switch mount-ns test to manual
+ - overlord,daemon,cmd/snapd: move expensive startup to dedicated
+ StartUp methods
+ - osutil: add EnsureTreeState helper
+ - tests: measure properties of various mount namespaces
+ - tests: part2 making tests work on ubuntu-core-18
+ - interfaces/policy: minimal policy check for replacing
+ sanitizeReservedFor helpers (1/2)
+ - interfaces: add an interface that grants access to the PackageKit
+ service
+ - overlord/devicestate: update gadget update handlers and mocks
+ - tests: add mountinfo-tool --ref-x1000
+ - tests: remove lxd / lxcfs if pre-installed
+ - tests: removing support for ubuntu cosmic on spread test suite
+ - tests: don't leak /run/netns mount
+ - image: clean up the validateSuite
+ - bootloader: remove "Dir()" from Bootloader interface
+ - many: retry to reboot if snapd gets restarted before expected
+ reboot
+ - overlord: implement re-registration remodeling
+ - cmd: revert PR#6933 (tweak of GOMAXPROCS)
+ - cmd/snap: add snap unset command
+ - many: add Client-User-Agent to "SnapAction" install API call
+ - tests: first part making tests run on ubuntu-core-18
+ - hookstate/ctlcmd: support hidden commands in snapctl
+ - many: replace snapd snap name checks with type checks (3/4)
+ - overlord: mostly stop needing Kernel/CoreInfo, make GadgetInfo
+ consider a DeviceContext
+ - snapctl: handle unsetting of config options with "!"
+ - tests: move core migration snaps to tests/lib/snaps dir
+ - cmd/snap: handle unsetting of config options with "!"
+ - cmd/snap, etc: add health to 'snap list' and 'snap info'
+ - gadget: use struct field names when intializing data in mounted
+ updater unit tests
+ - cmd/snap-confine: bring /lib/firmware from the host
+ - snap: set snapd snap type (1/4)
+ - snap: add checks in validate-seed for missing base/default-
+ provider
+ - daemon: replace shutdownServer with net/http's native shutdown
+ support
+ - interfaces/builtin: add exec "/bin/runc" to docker-support
+ - gadget: mounted filesystem updater
+ - overlord/patch: simplify conditions for re-applying sublevel
+ patches for level 6
+ - seccomp/compiler: adjust test case names and comment for later
+ changes
+ - tests: fix error doing snap pack running failover test
+ - tests: don't preserve size= when rewriting mount tables
+ - tests: allow reordering of rewrite operations
+ - gadget: main update routine
+ - overlord/config: normalize nulls to support config unsetting
+ semantics
+ - snap-userd-autostart: don't list as a startup application on the
+ GUI
+ - tests: renumber snap revisions as seen via writable
+ - tests: change allocation for mount options
+ - tests: re-enable ns-re-associate test
+ - tests: mountinfo-tool allow many --refs
+ - overlord/devicestate: implement reregRemodelContext with the
+ essential re-registration logic
+ - tests: replace various numeric mount options
+ - gadget: filesystem image writer
+ - tests: add more unit tests for mountinfo-tool
+ - tests: introduce mountinfo-tool --ref feature
+ - tests: refactor mountinfo-tool rewrite state
+ - tests: allow renumbering mount namespace identifiers
+ - snap: refactor and explain layout blacklisting
+ - tests: renumber snap revisions as seen via hostfs
+ - daemon, interfaces, travis: workaround build ID with Go 1.9, use
+ 1.9 for travis tests
+ - cmd/libsnap: add sc_error_init_{simple,api_misuse}
+ - gadget: make raw updater handle shifted structures
+ - tests/lib/nested: create WORK_DIR before accessing it
+ - cmd/libsnap: rename SC_LIBSNAP_ERROR to SC_LIBSNAP_DOMAIN
+ - cmd,tests: forcibly discard mount namespace when bases change
+ - many: introduce healthstate, run check-health
+ post-(install/refresh/try/revert)
+ - interfaces/optical-drive: add scsi-generic type 4 and 5 support
+ - cmd/snap-confine: exit from helper when parent dies
+
* Fri Jul 12 2019 Michael Vogt
- New upstream release 2.40
- overlord/patch: simplify conditions for re-applying sublevel
@@ -969,7 +1418,7 @@
- tests/lib/nested: fix multi argument copy_remote
- tests/lib/nested: have mkfs.ext4 use a rootdir instead of mounting
an image
- - packaging: fix permissons powerpc docs dir
+ - packaging: fix permissions powerpc docs dir
- overlord: mock store to avoid net requests
- debian: rework how we run autopkgtests
- interface: builtin: avahi-observe/control: allow slots
diff -Nru snapd-2.40/packaging/opensuse/snapd.changes snapd-2.42.1/packaging/opensuse/snapd.changes
--- snapd-2.40/packaging/opensuse/snapd.changes 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/opensuse/snapd.changes 2019-10-30 12:17:43.000000000 +0000
@@ -1,4 +1,19 @@
-------------------------------------------------------------------
+Wed Oct 30 11:17:43 UTC 2019 - mvo@ubuntu.com
+
+- Update to upstream release 2.42.1
+
+-------------------------------------------------------------------
+Tue Oct 01 09:42:48 UTC 2019 - mvo@ubuntu.com
+
+- Update to upstream release 2.42
+
+-------------------------------------------------------------------
+Fri Aug 30 06:55:43 UTC 2019 - mvo@ubuntu.com
+
+- Update to upstream release 2.41
+
+-------------------------------------------------------------------
Fri Jul 12 08:40:08 UTC 2019 - mvo@ubuntu.com
- Update to upstream release 2.40
diff -Nru snapd-2.40/packaging/opensuse/snapd.spec snapd-2.42.1/packaging/opensuse/snapd.spec
--- snapd-2.40/packaging/opensuse/snapd.spec 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/opensuse/snapd.spec 2019-10-30 12:17:43.000000000 +0000
@@ -29,6 +29,7 @@
# The list of systemd services we are expected to ship. Note that this does
# not include services that are only required on core systems.
%global systemd_services_list snapd.socket snapd.service snapd.seeded.service snapd.failure.service %{?with_apparmor:snapd.apparmor.service}
+%global systemd_user_services_list snapd.session-agent.socket
# Alternate snap mount directory: not used by openSUSE.
# If this spec file is integrated into Fedora then consider
@@ -38,6 +39,7 @@
# Compat macros
%{!?make_build: %global make_build %{__make} %{?_smp_mflags}}
%{?!_environmentdir: %global _environmentdir %{_prefix}/lib/environment.d}
+%{?!_userunitdir: %global _userunitdir %{_prefix}/lib/systemd/user}
# Define the variable for systemd generators, if missing.
%{?!_systemdgeneratordir: %global _systemdgeneratordir %{_prefix}/lib/systemd/system-generators}
@@ -75,7 +77,7 @@
Name: snapd
-Version: 2.40
+Version: 2.42.1
Release: 0
Summary: Tools enabling systems to work with .snap files
License: GPL-3.0
@@ -309,6 +311,7 @@
%apparmor_reload /etc/apparmor.d/usr.lib.snapd.snap-confine
%endif
%service_add_post %{systemd_services_list}
+%systemd_user_post %{systemd_user_services_list}
case ":$PATH:" in
*:/snap/bin:*)
;;
@@ -320,12 +323,14 @@
%preun
%service_del_preun %{systemd_services_list}
+%systemd_user_preun %{systemd_user_services_list}
if [ $1 -eq 0 ]; then
%{_libexecdir}/snapd/snap-mgmt --purge || :
fi
%postun
%service_del_postun %{systemd_services_list}
+%systemd_user_postun %{systemd_user_services_list}
%files
@@ -366,6 +371,7 @@
%dir %{_sharedstatedir}/snapd/snaps
%dir %{_systemd_system_env_generator_dir}
%dir %{_systemdgeneratordir}
+%dir %{_userunitdir}
%dir %{snap_mount_dir}
%dir %{snap_mount_dir}/bin
@@ -413,6 +419,8 @@
%{_unitdir}/snapd.seeded.service
%{_unitdir}/snapd.service
%{_unitdir}/snapd.socket
+%{_userunitdir}/snapd.session-agent.service
+%{_userunitdir}/snapd.session-agent.socket
# When apparmor is enabled there are some additional entries.
%if %{with apparmor}
diff -Nru snapd-2.40/packaging/opensuse-15.0/snapd.changes snapd-2.42.1/packaging/opensuse-15.0/snapd.changes
--- snapd-2.40/packaging/opensuse-15.0/snapd.changes 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/opensuse-15.0/snapd.changes 2019-10-30 12:17:43.000000000 +0000
@@ -1,4 +1,19 @@
-------------------------------------------------------------------
+Wed Oct 30 11:17:43 UTC 2019 - mvo@ubuntu.com
+
+- Update to upstream release 2.42.1
+
+-------------------------------------------------------------------
+Tue Oct 01 09:42:48 UTC 2019 - mvo@ubuntu.com
+
+- Update to upstream release 2.42
+
+-------------------------------------------------------------------
+Fri Aug 30 06:55:43 UTC 2019 - mvo@ubuntu.com
+
+- Update to upstream release 2.41
+
+-------------------------------------------------------------------
Fri Jul 12 08:40:08 UTC 2019 - mvo@ubuntu.com
- Update to upstream release 2.40
diff -Nru snapd-2.40/packaging/opensuse-15.0/snapd.spec snapd-2.42.1/packaging/opensuse-15.0/snapd.spec
--- snapd-2.40/packaging/opensuse-15.0/snapd.spec 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/opensuse-15.0/snapd.spec 2019-10-30 12:17:43.000000000 +0000
@@ -29,6 +29,7 @@
# The list of systemd services we are expected to ship. Note that this does
# not include services that are only required on core systems.
%global systemd_services_list snapd.socket snapd.service snapd.seeded.service snapd.failure.service %{?with_apparmor:snapd.apparmor.service}
+%global systemd_user_services_list snapd.session-agent.socket
# Alternate snap mount directory: not used by openSUSE.
# If this spec file is integrated into Fedora then consider
@@ -38,6 +39,7 @@
# Compat macros
%{!?make_build: %global make_build %{__make} %{?_smp_mflags}}
%{?!_environmentdir: %global _environmentdir %{_prefix}/lib/environment.d}
+%{?!_userunitdir: %global _userunitdir %{_prefix}/lib/systemd/user}
# Define the variable for systemd generators, if missing.
%{?!_systemdgeneratordir: %global _systemdgeneratordir %{_prefix}/lib/systemd/system-generators}
@@ -75,7 +77,7 @@
Name: snapd
-Version: 2.40
+Version: 2.42.1
Release: 0
Summary: Tools enabling systems to work with .snap files
License: GPL-3.0
@@ -309,6 +311,7 @@
%apparmor_reload /etc/apparmor.d/usr.lib.snapd.snap-confine
%endif
%service_add_post %{systemd_services_list}
+%systemd_user_post %{systemd_user_services_list}
case ":$PATH:" in
*:/snap/bin:*)
;;
@@ -320,12 +323,14 @@
%preun
%service_del_preun %{systemd_services_list}
+%systemd_user_preun %{systemd_user_services_list}
if [ $1 -eq 0 ]; then
%{_libexecdir}/snapd/snap-mgmt --purge || :
fi
%postun
%service_del_postun %{systemd_services_list}
+%systemd_user_postun %{systemd_user_services_list}
%files
@@ -366,6 +371,7 @@
%dir %{_sharedstatedir}/snapd/snaps
%dir %{_systemd_system_env_generator_dir}
%dir %{_systemdgeneratordir}
+%dir %{_userunitdir}
%dir %{snap_mount_dir}
%dir %{snap_mount_dir}/bin
@@ -413,6 +419,8 @@
%{_unitdir}/snapd.seeded.service
%{_unitdir}/snapd.service
%{_unitdir}/snapd.socket
+%{_userunitdir}/snapd.session-agent.service
+%{_userunitdir}/snapd.session-agent.socket
# When apparmor is enabled there are some additional entries.
%if %{with apparmor}
diff -Nru snapd-2.40/packaging/opensuse-15.1/snapd.changes snapd-2.42.1/packaging/opensuse-15.1/snapd.changes
--- snapd-2.40/packaging/opensuse-15.1/snapd.changes 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/opensuse-15.1/snapd.changes 2019-10-30 12:17:43.000000000 +0000
@@ -1,4 +1,19 @@
-------------------------------------------------------------------
+Wed Oct 30 11:17:43 UTC 2019 - mvo@ubuntu.com
+
+- Update to upstream release 2.42.1
+
+-------------------------------------------------------------------
+Tue Oct 01 09:42:48 UTC 2019 - mvo@ubuntu.com
+
+- Update to upstream release 2.42
+
+-------------------------------------------------------------------
+Fri Aug 30 06:55:43 UTC 2019 - mvo@ubuntu.com
+
+- Update to upstream release 2.41
+
+-------------------------------------------------------------------
Fri Jul 12 08:40:08 UTC 2019 - mvo@ubuntu.com
- Update to upstream release 2.40
diff -Nru snapd-2.40/packaging/opensuse-15.1/snapd.spec snapd-2.42.1/packaging/opensuse-15.1/snapd.spec
--- snapd-2.40/packaging/opensuse-15.1/snapd.spec 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/opensuse-15.1/snapd.spec 2019-10-30 12:17:43.000000000 +0000
@@ -29,6 +29,7 @@
# The list of systemd services we are expected to ship. Note that this does
# not include services that are only required on core systems.
%global systemd_services_list snapd.socket snapd.service snapd.seeded.service snapd.failure.service %{?with_apparmor:snapd.apparmor.service}
+%global systemd_user_services_list snapd.session-agent.socket
# Alternate snap mount directory: not used by openSUSE.
# If this spec file is integrated into Fedora then consider
@@ -38,6 +39,7 @@
# Compat macros
%{!?make_build: %global make_build %{__make} %{?_smp_mflags}}
%{?!_environmentdir: %global _environmentdir %{_prefix}/lib/environment.d}
+%{?!_userunitdir: %global _userunitdir %{_prefix}/lib/systemd/user}
# Define the variable for systemd generators, if missing.
%{?!_systemdgeneratordir: %global _systemdgeneratordir %{_prefix}/lib/systemd/system-generators}
@@ -75,7 +77,7 @@
Name: snapd
-Version: 2.40
+Version: 2.42.1
Release: 0
Summary: Tools enabling systems to work with .snap files
License: GPL-3.0
@@ -309,6 +311,7 @@
%apparmor_reload /etc/apparmor.d/usr.lib.snapd.snap-confine
%endif
%service_add_post %{systemd_services_list}
+%systemd_user_post %{systemd_user_services_list}
case ":$PATH:" in
*:/snap/bin:*)
;;
@@ -320,12 +323,14 @@
%preun
%service_del_preun %{systemd_services_list}
+%systemd_user_preun %{systemd_user_services_list}
if [ $1 -eq 0 ]; then
%{_libexecdir}/snapd/snap-mgmt --purge || :
fi
%postun
%service_del_postun %{systemd_services_list}
+%systemd_user_postun %{systemd_user_services_list}
%files
@@ -366,6 +371,7 @@
%dir %{_sharedstatedir}/snapd/snaps
%dir %{_systemd_system_env_generator_dir}
%dir %{_systemdgeneratordir}
+%dir %{_userunitdir}
%dir %{snap_mount_dir}
%dir %{snap_mount_dir}/bin
@@ -413,6 +419,8 @@
%{_unitdir}/snapd.seeded.service
%{_unitdir}/snapd.service
%{_unitdir}/snapd.socket
+%{_userunitdir}/snapd.session-agent.service
+%{_userunitdir}/snapd.session-agent.socket
# When apparmor is enabled there are some additional entries.
%if %{with apparmor}
diff -Nru snapd-2.40/packaging/opensuse-tumbleweed/snapd.changes snapd-2.42.1/packaging/opensuse-tumbleweed/snapd.changes
--- snapd-2.40/packaging/opensuse-tumbleweed/snapd.changes 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/opensuse-tumbleweed/snapd.changes 2019-10-30 12:17:43.000000000 +0000
@@ -1,4 +1,19 @@
-------------------------------------------------------------------
+Wed Oct 30 11:17:43 UTC 2019 - mvo@ubuntu.com
+
+- Update to upstream release 2.42.1
+
+-------------------------------------------------------------------
+Tue Oct 01 09:42:48 UTC 2019 - mvo@ubuntu.com
+
+- Update to upstream release 2.42
+
+-------------------------------------------------------------------
+Fri Aug 30 06:55:43 UTC 2019 - mvo@ubuntu.com
+
+- Update to upstream release 2.41
+
+-------------------------------------------------------------------
Fri Jul 12 08:40:08 UTC 2019 - mvo@ubuntu.com
- Update to upstream release 2.40
diff -Nru snapd-2.40/packaging/opensuse-tumbleweed/snapd.spec snapd-2.42.1/packaging/opensuse-tumbleweed/snapd.spec
--- snapd-2.40/packaging/opensuse-tumbleweed/snapd.spec 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/opensuse-tumbleweed/snapd.spec 2019-10-30 12:17:43.000000000 +0000
@@ -29,6 +29,7 @@
# The list of systemd services we are expected to ship. Note that this does
# not include services that are only required on core systems.
%global systemd_services_list snapd.socket snapd.service snapd.seeded.service snapd.failure.service %{?with_apparmor:snapd.apparmor.service}
+%global systemd_user_services_list snapd.session-agent.socket
# Alternate snap mount directory: not used by openSUSE.
# If this spec file is integrated into Fedora then consider
@@ -38,6 +39,7 @@
# Compat macros
%{!?make_build: %global make_build %{__make} %{?_smp_mflags}}
%{?!_environmentdir: %global _environmentdir %{_prefix}/lib/environment.d}
+%{?!_userunitdir: %global _userunitdir %{_prefix}/lib/systemd/user}
# Define the variable for systemd generators, if missing.
%{?!_systemdgeneratordir: %global _systemdgeneratordir %{_prefix}/lib/systemd/system-generators}
@@ -75,7 +77,7 @@
Name: snapd
-Version: 2.40
+Version: 2.42.1
Release: 0
Summary: Tools enabling systems to work with .snap files
License: GPL-3.0
@@ -309,6 +311,7 @@
%apparmor_reload /etc/apparmor.d/usr.lib.snapd.snap-confine
%endif
%service_add_post %{systemd_services_list}
+%systemd_user_post %{systemd_user_services_list}
case ":$PATH:" in
*:/snap/bin:*)
;;
@@ -320,12 +323,14 @@
%preun
%service_del_preun %{systemd_services_list}
+%systemd_user_preun %{systemd_user_services_list}
if [ $1 -eq 0 ]; then
%{_libexecdir}/snapd/snap-mgmt --purge || :
fi
%postun
%service_del_postun %{systemd_services_list}
+%systemd_user_postun %{systemd_user_services_list}
%files
@@ -366,6 +371,7 @@
%dir %{_sharedstatedir}/snapd/snaps
%dir %{_systemd_system_env_generator_dir}
%dir %{_systemdgeneratordir}
+%dir %{_userunitdir}
%dir %{snap_mount_dir}
%dir %{snap_mount_dir}/bin
@@ -413,6 +419,8 @@
%{_unitdir}/snapd.seeded.service
%{_unitdir}/snapd.service
%{_unitdir}/snapd.socket
+%{_userunitdir}/snapd.session-agent.service
+%{_userunitdir}/snapd.session-agent.socket
# When apparmor is enabled there are some additional entries.
%if %{with apparmor}
diff -Nru snapd-2.40/packaging/pack-source snapd-2.42.1/packaging/pack-source
--- snapd-2.40/packaging/pack-source 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/packaging/pack-source 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,63 @@
+#!/bin/bash
+
+set -e
+
+show_help() {
+ echo "usage: $(basename "$0") [-v ] [-o ] [-g] [-h]"
+ echo " -v set version"
+ echo " -o write source packages to this directory"
+ echo " -g use 'git describe' output as version string"
+ echo " -s use single source archive instead of no-vendor and"
+ echo " only-vendor packages"
+ echo " -h show help"
+ exit 1
+}
+
+if [ ! -e "packaging/$(basename "$0")" ]; then
+ echo "must be executed at the top of srcdir"
+ exit 1
+fi
+
+outdir=.
+single=0
+
+while getopts "v:o:gsh" arg; do
+ case "$arg" in
+ o)
+ outdir="$OPTARG"
+ ;;
+ v)
+ version="$OPTARG"
+ ;;
+ g)
+ version="$(git describe | tr '-' '.')"
+ ;;
+ s)
+ single=1
+ ;;
+ h|*)
+ show_help
+ ;;
+ esac
+done
+
+if [ -z "$version" ]; then
+ echo "error: version is unset"
+ exit 1
+fi
+
+set -x
+
+tmpdir=$(mktemp -d)
+trap 'rm -rf "$tmpdir"' EXIT
+
+if [[ "$single" == 0 ]]; then
+ tar -cJf "$tmpdir"/snapd_"$version".no-vendor.tar.xz --exclude='vendor/*' --exclude='.git/*' --transform "s#^#snapd-$version/#" .
+ tar -cJf "$tmpdir"/snapd_"$version".only-vendor.tar.xz --exclude='.git/*' --transform "s#^#snapd-$version/#" vendor
+
+ mv "$tmpdir"/snapd_"$version".no-vendor.tar.xz "$outdir"/
+ mv "$tmpdir"/snapd_"$version".only-vendor.tar.xz "$outdir"/
+else
+ tar -cJf "$tmpdir"/snapd_"$version".vendor.tar.xz --exclude='.git/*' --transform "s#^#snapd-$version/#" .
+ mv "$tmpdir"/snapd_"$version".vendor.tar.xz "$outdir"/
+fi
diff -Nru snapd-2.40/packaging/ubuntu-14.04/changelog snapd-2.42.1/packaging/ubuntu-14.04/changelog
--- snapd-2.40/packaging/ubuntu-14.04/changelog 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-14.04/changelog 2019-10-30 12:17:43.000000000 +0000
@@ -1,3 +1,459 @@
+snapd (2.42.1~14.04) trusty; urgency=medium
+
+ * New upstream release, LP: #1846181
+ - interfaces: de-duplicate emitted update-ns profiles
+ - packaging: tweak handling of usr.lib.snapd.snap-confine
+ - interfaces: allow introspecting network-manager on core
+ - tests/main/interfaces-contacts-service: disable on openSUSE
+ Tumbleweed
+ - tests/lib/lxd-snapfuse: restore mount changes introduced by LXD
+ - snap: fix default-provider in seed validation
+ - tests: update system-usernames test now that opensuse-15.1 works
+ - overlord: set fake sertial in TestRemodelSwitchToDifferentKernel
+ - gadget: rename "boot{select,img}" -> system-boot-{select,image}
+ - tests: listing test, make accepted snapd/core versions consistent
+
+ -- Michael Vogt Wed, 30 Oct 2019 13:17:43 +0100
+
+snapd (2.42~14.04) trusty; urgency=medium
+
+ * New upstream release, LP: #1846181
+ - tests: disable {contacts,calendar}-service tests on debian-sid
+ - tests/main/snap-run: disable strace test cases on Arch
+ - cmd/system-shutdown: include correct prototype for die
+ - snap/naming: add test for hook name connect-plug-i2c
+ - cmd/snap-confine: allow digits in hook names
+ - gadget: do not fail the update when old gadget snap is missing
+ bare content
+ - tests: disable {contacts,calendar}-service tests on Arch Linux
+ - tests: move "centos-7" to unstable systems
+ - interfaces/docker-support,kubernetes-support: misc updates for
+ strict k8s
+ - packaging: remove obsolete usr.lib.snapd.snap-confine in
+ postinst
+ - tests: add test that ensures our snapfuse binary actually works
+ - packaging: use snapfuse_ll to speed up snapfuse performance
+ - usersession/userd: make sure to export DBus interfaces before
+ requesting a name
+ - data/selinux: allow snapd to issue sigkill to journalctl
+ - store: download propagates options to delta download
+ - wrappers: allow snaps to install icon theme icons
+ - debug: state-inspect debugging utility
+ - sandbox/cgroup: introduce cgroup wrappers package
+ - snap-confine: fix return value checks for udev functions
+ - cmd/model: output tweaks, add'l tests
+ - wrappers/services: add ServicesEnableState + unit tests
+ - tests: fix newline and wrong test name pointed out in previous PRs
+ - tests: extend mount-ns test to handle mimics
+ - run-checks, tests/main/go: allow gofmt checks to be skipped on
+ 19.10
+ - tests/main/interfaces-{calendar,contacts}-service: disable on
+ 19.10
+ - tests: part3 making tests work on ubuntu-core-18
+ - tests: fix interfaces-timeserver-control on 19.10
+ - overlord/snapstate: config revision code cleanup and extra tests
+ - devicestate: allow remodel to different kernels
+ - overlord,daemon: adjust startup timeout via EXTEND_TIMEOUT_USEC
+ using an estimate
+ - tests/main/many: increase kill-timeout to 5m
+ - interfaces/kubernetes-support: allow systemd-run to ptrace read
+ unconfined
+ - snapstate: auto transition on experimental.snapd-snap=true
+ - tests: retry checking until the written file on desktop-portal-
+ filechooser
+ - tests: unit test for a refresh failing on configure hook
+ - tests: remove mount_id and parent_id from mount-ns test data
+ - tests: move classic-ubuntu-core-transition* to nightly
+ - tests/mountinfo-tool: proper formatting of opt_fields
+ - overlord/configstate: special-case "null" in transaction Changes()
+ - snap-confine: fallback gracefully on a cgroup v2 only system
+ - tests: debian sid now ships new seccomp, adjust tests
+ - tests: explicitly restore after using LXD
+ - snapstate: make progress reporting less granular
+ - bootloader: little kernel support
+ - fixme: rename ubuntu*architectures to dpkg*architectures
+ - tests: run dbus-launch inside a systemd unit
+ - channel: introduce Resolve and ResolveLocked
+ - tests: run failing tests on ubuntu eoan due to is now set as
+ unstable
+ - systemd: detach rather than unmount .mount units
+ - cmd/snap-confine: add unit tests for sc_invocation, cleanup memory
+ leaks in tests
+ - boot,dirs,image: introduce boot.MakeBootable, use it in image
+ instead of ad hoc code
+ - cmd/snap-update-ns: clarify sharing comment
+ - tests/overlord/snapstate: refactor for cleaner test failures
+ - cmd/snap-update-ns: don't propagate detaching changes
+ - interfaces: allow reading mutter Xauthority file
+ - cmd/snap-confine: fix /snap duplication in legacy mode
+ - tests: fix mountinfo-tool filtering when used with rewriting
+ - seed,image,o/devicestate: extract seed loading to seed/seed16.go
+ - many: pass the rootdir and options to bootloader.Find
+ - tests: part5 making tests work on ubuntu-core-18
+ - cmd/snap-confine: keep track of snap instance name and the snap
+ name
+ - cmd: unify die() across C programs
+ - tests: add functions to make an abstraction for the snaps
+ - packaging/fedora, tests/lib/prepare-restore: helper tool for
+ packing sources for RPM
+ - cmd/snap: improve help and error msg for snapshot commands
+ - hookstate/ctlcmd: fix snapctl set help message
+ - cmd/snap: don't append / to snap name just because a dir exists
+ - tests: support fastly-global.cdn.snapcraft.io url on proxy-no-core
+ test
+ - tests: add --quiet switch to retry-tool
+ - tests: add unstable stage for travis execution
+ - tests: disable interfaces-timeserver-control on 19.10
+ - tests: don't guess in is_classic_confinement_supported
+ - boot, etc: simplify BootParticipant (etc) usage
+ - tests: verify retry-tool not retrying missing commands
+ - tests: rewrite "retry" command as retry-tool
+ - tests: move debug section after restore
+ - cmd/libsnap-confine-private, cmd/s-c: use constants for
+ snap/instance name lengths
+ - tests: measure behavior of the device cgroup
+ - boot, bootloader, o/devicestate: boot env manip goes in boot
+ - tests: enabling ubuntu 19.10-64 on spread.yaml
+ - tests: fix ephemeral mount table in left over by prepare
+ - tests: add version-tool for comparing versions
+ - cmd/libsnap: make feature flag enum 1< Tue, 01 Oct 2019 11:40:34 +0200
+
+snapd (2.41~14.04) trusty; urgency=medium
+
+ * New upstream release, LP: #1840740
+ - overlord/snapstate: revert track-risk behavior
+ - tests: fix snap info test
+ - httputil: rework protocol error detection
+ - gadget: do not error on gadget refreshes with multiple volumes
+ - i18n, vendor, packaging: drop github.com/ojii/gettext.go, use
+ github.com/snapcore/go-gettext
+ - snapstate: validate all system-usernames before creating them
+ - mkversion.sh: fix version from git checkouts
+ - interfaces/network-{control,manager}: allow 'k' on
+ /run/resolvconf/**
+ - interfaces/wayland,x11: allow reading an Xwayland Xauth file
+ - interfaces: k8s worker node updates
+ - debian: re-enable systemd environment generator
+ - many: create system-usernames user/group if both don't exist
+ - packaging: fix symlink for snapd.session-agent.socket
+ - tests: change cgroups so that LXD doesn't have to
+ - interfaces/network-setup-control: allow dbus netplan apply
+ messages
+ - tests: add /var/cache/snapd to the snapd state to prevent error on
+ the store
+ - tests: add test for services disabled during refresh hook
+ - many: simpler access to snap-seccomp version-info
+ - snap: cleanup some tests, clarify some errorsThis is a follow up
+ from work on system usernames:
+ - osutil: add osutil.Find{Uid,Gid}
+ - tests: use a different archive based on the spread backend on go-
+ build test
+ - cmd/snap-update-ns: fix pair of bugs affecting refresh of snap
+ with layouts
+ - overlord/devicestate: detect clashing concurrent (ongoing, just
+ finished) remodels or changes
+ - interfaces/docker-support: declare controls-device-cgroup
+ - packaging: fix removal of old apparmor profile
+ - store: use track/risk for "channel" name when parsing store
+ details
+ - many: allow 'system-usernames' with libseccomp > 2.4 and golang-
+ seccomp > 0.9.0
+ - overlord/devicestate, tests: use gadget.Update() proper, spread
+ test
+ - overlord/configstate/configcore: allow setting start_x=1 to enable
+ CSI camera on RPi
+ - interfaces: remove BeforePrepareSlot from commonInterface
+ - many: support system-usernames for 'snap_daemon' user
+ - overlord/devicestate,o/snapstate: queue service commands before
+ mark-seeded and other final tasks
+ - interfaces/mount: discard mount ns on backend Remove
+ - packaging/fedora: build on RHEL8
+ - overlord/devicestate: support seeding a classic system with the
+ snapd snap and no core
+ - interfaces: fix test failure in gpio_control_test
+ - interfaces, policy: remove sanitize helpers and use minimal policy
+ check
+ - packaging: use %systemd_user_* macros to enable session agent
+ socket according to presets
+ - snapstate, store: handle 429s on catalog refresh a little bit
+ better
+ - tests: part4 making tests work on ubuntu-core-18
+ - many: drop snap.ReadGadgetInfo wrapper
+ - xdgopenproxy: update test API to match upstream
+ - tests: show why sbuild failed
+ - data/selinux: allow mandb_t to search /var/lib/snapd
+ - tests: be less verbose when checking service status
+ - tests: set sbuild test as manual
+ - overlord: DeviceCtx must find the remodel context for a remodel
+ change
+ - tests: use snap info --verbose to check for base
+ - sanity: unmount squashfs with --lazy
+ - overlord/snapstate: keep current track if only risk is specified
+ - interfaces/firewall-control: support nft routing expressions and
+ device groups
+ - gadget: support for writing symlinks
+ - tests: mountinfo-tool fail if there are no matches
+ - tests: sync journal log before start the test
+ - cmd/snap, data/completion: improve completion for 'snap debug'
+ - httputil: retry for http2 PROTOCOL_ERROR
+ - Errata commit: pulseaudio still auto-connects on classic
+ - interfaces/misc: updates for k8s 1.15 (and greengrass test)
+ - tests: set GOTRACEBACK=1 when running tests
+ - cmd/libsnap: don't leak memory in sc_die_on_error
+ - tests: improve how the system is restored when the upgrade-
+ from-2.15 test fails
+ - interfaces/bluetooth-control: add udev rules for BT_chrdev devices
+ - interfaces: add audio-playback/audio-record and make pulseaudio
+ manually connect
+ - tests: split the sbuild test in 2 depending on the type of build
+ - interfaces: add an interface granting access to AppStream metadata
+ - gadget: ensure filesystem labels are unique
+ - usersession/agent: use background context when stopping the agent
+ - HACKING.md: update spread section, other updates
+ - data/selinux: allow snap-confine to read entries on nsfs
+ - tests: respect SPREAD_DEBUG_EACH on the main suite
+ - packaging/debian-sid: set GOCACHE to a known writable location
+ - interfaces: add gpio-control interface
+ - cmd/snap: use showDone helper with 'snap switch'
+ - gadget: effective structure role fallback, extra tests
+ - many: fix unit tests getting stuck
+ - tests: remove installed snap on restore
+ - daemon: do not modify test data in user suite
+ - data/selinux: allow read on sysfs
+ - packaging/debian: don't md5sum absent files
+ - tests: remove test-snapd-curl
+ - tests: remove test-snapd-snapctl-core18 in restore
+ - tests: remove installed snap in the restore section
+ - tests: remove installed test snap
+ - tests: correctly escape mount unit path
+ - cmd/Makefile.am: support building with the go snap
+ - tests: work around classic snap affecting the host
+ - tests: fix typo "current"
+ - overlord/assertstate: add Batch.Precheck to check for the full
+ validity of the batch before Commit
+ - tests: restore cpuset clone_children clobbered by lxd
+ - usersession: move userd package to usersession/userd
+ - tests: reformat and fix markdown in snapd-state.md
+ - gadget: select the right updater for given structure
+ - tests: show stderr only if it exists
+ - sessionagent: add a REST interface with socket activation
+ - tests: remove locally installed core in more tests
+ - tests: remove local revision of core
+ - packaging/debian-sid: use correct apparmor Depends for Debian
+ - packaging/debian-sid: merge debian upload changes back into master
+ - cmd/snap-repair: make sure the goroutine doesn't stick around on
+ timeout
+ - packaging/fedora: github.com/cheggaaa/pb is no longer used
+ - configstate/config: fix crash in purgeNulls
+ - boot, o/snapst, o/devicest: limit knowledge of boot vars to boot
+ - client,cmd/snap: stop depending on status/status-code in the JSON
+ responses in client
+ - tests: unmount leftover /run/netns
+ - tests: switch mount-ns test to manual
+ - overlord,daemon,cmd/snapd: move expensive startup to dedicated
+ StartUp methods
+ - osutil: add EnsureTreeState helper
+ - tests: measure properties of various mount namespaces
+ - tests: part2 making tests work on ubuntu-core-18
+ - interfaces/policy: minimal policy check for replacing
+ sanitizeReservedFor helpers (1/2)
+ - interfaces: add an interface that grants access to the PackageKit
+ service
+ - overlord/devicestate: update gadget update handlers and mocks
+ - tests: add mountinfo-tool --ref-x1000
+ - tests: remove lxd / lxcfs if pre-installed
+ - tests: removing support for ubuntu cosmic on spread test suite
+ - tests: don't leak /run/netns mount
+ - image: clean up the validateSuite
+ - bootloader: remove "Dir()" from Bootloader interface
+ - many: retry to reboot if snapd gets restarted before expected
+ reboot
+ - overlord: implement re-registration remodeling
+ - cmd: revert PR#6933 (tweak of GOMAXPROCS)
+ - cmd/snap: add snap unset command
+ - many: add Client-User-Agent to "SnapAction" install API call
+ - tests: first part making tests run on ubuntu-core-18
+ - hookstate/ctlcmd: support hidden commands in snapctl
+ - many: replace snapd snap name checks with type checks (3/4)
+ - overlord: mostly stop needing Kernel/CoreInfo, make GadgetInfo
+ consider a DeviceContext
+ - snapctl: handle unsetting of config options with "!"
+ - tests: move core migration snaps to tests/lib/snaps dir
+ - cmd/snap: handle unsetting of config options with "!"
+ - cmd/snap, etc: add health to 'snap list' and 'snap info'
+ - gadget: use struct field names when intializing data in mounted
+ updater unit tests
+ - cmd/snap-confine: bring /lib/firmware from the host
+ - snap: set snapd snap type (1/4)
+ - snap: add checks in validate-seed for missing base/default-
+ provider
+ - daemon: replace shutdownServer with net/http's native shutdown
+ support
+ - interfaces/builtin: add exec "/bin/runc" to docker-support
+ - gadget: mounted filesystem updater
+ - overlord/patch: simplify conditions for re-applying sublevel
+ patches for level 6
+ - seccomp/compiler: adjust test case names and comment for later
+ changes
+ - tests: fix error doing snap pack running failover test
+ - tests: don't preserve size= when rewriting mount tables
+ - tests: allow reordering of rewrite operations
+ - gadget: main update routine
+ - overlord/config: normalize nulls to support config unsetting
+ semantics
+ - snap-userd-autostart: don't list as a startup application on the
+ GUI
+ - tests: renumber snap revisions as seen via writable
+ - tests: change allocation for mount options
+ - tests: re-enable ns-re-associate test
+ - tests: mountinfo-tool allow many --refs
+ - overlord/devicestate: implement reregRemodelContext with the
+ essential re-registration logic
+ - tests: replace various numeric mount options
+ - gadget: filesystem image writer
+ - tests: add more unit tests for mountinfo-tool
+ - tests: introduce mountinfo-tool --ref feature
+ - tests: refactor mountinfo-tool rewrite state
+ - tests: allow renumbering mount namespace identifiers
+ - snap: refactor and explain layout blacklisting
+ - tests: renumber snap revisions as seen via hostfs
+ - daemon, interfaces, travis: workaround build ID with Go 1.9, use
+ 1.9 for travis tests
+ - cmd/libsnap: add sc_error_init_{simple,api_misuse}
+ - gadget: make raw updater handle shifted structures
+ - tests/lib/nested: create WORK_DIR before accessing it
+ - cmd/libsnap: rename SC_LIBSNAP_ERROR to SC_LIBSNAP_DOMAIN
+ - cmd,tests: forcibly discard mount namespace when bases change
+ - many: introduce healthstate, run check-health
+ post-(install/refresh/try/revert)
+ - interfaces/optical-drive: add scsi-generic type 4 and 5 support
+ - cmd/snap-confine: exit from helper when parent dies
+
+ -- Michael Vogt Fri, 30 Aug 2019 08:56:32 +0200
+
snapd (2.40~14.04) trusty; urgency=medium
* New upstream release, LP: #1836327
@@ -102,7 +558,7 @@
- tests/lib/nested: fix multi argument copy_remote
- tests/lib/nested: have mkfs.ext4 use a rootdir instead of mounting
an image
- - packaging: fix permissons powerpc docs dir
+ - packaging: fix permissions powerpc docs dir
- overlord: mock store to avoid net requests
- debian: rework how we run autopkgtests
- interface: builtin: avahi-observe/control: allow slots
diff -Nru snapd-2.40/packaging/ubuntu-14.04/snapd.maintscript snapd-2.42.1/packaging/ubuntu-14.04/snapd.maintscript
--- snapd-2.40/packaging/ubuntu-14.04/snapd.maintscript 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-14.04/snapd.maintscript 2019-10-30 12:17:43.000000000 +0000
@@ -1,5 +1,5 @@
-# keep mount point busy
# we used to ship a custom grub config that is no longer needed
rm_conffile /etc/grub.d/09_snappy 1.7.3ubuntu1
rm_conffile /etc/ld.so.conf.d/snappy.conf 2.0.7~
-rm_conffile /etc/apparmor.d/usr.lib.snapd.snap-confine 2.23.6~
+# on trusty /etc/apparmor.d/usr.lib.snapd.snap-confine was never renamed
+# so we don't need to handle this file here (unlike in xenial+)
diff -Nru snapd-2.40/packaging/ubuntu-16.04/changelog snapd-2.42.1/packaging/ubuntu-16.04/changelog
--- snapd-2.40/packaging/ubuntu-16.04/changelog 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-16.04/changelog 2019-10-30 12:17:43.000000000 +0000
@@ -1,3 +1,459 @@
+snapd (2.42.1) xenial; urgency=medium
+
+ * New upstream release, LP: #1846181
+ - interfaces: de-duplicate emitted update-ns profiles
+ - packaging: tweak handling of usr.lib.snapd.snap-confine
+ - interfaces: allow introspecting network-manager on core
+ - tests/main/interfaces-contacts-service: disable on openSUSE
+ Tumbleweed
+ - tests/lib/lxd-snapfuse: restore mount changes introduced by LXD
+ - snap: fix default-provider in seed validation
+ - tests: update system-usernames test now that opensuse-15.1 works
+ - overlord: set fake sertial in TestRemodelSwitchToDifferentKernel
+ - gadget: rename "boot{select,img}" -> system-boot-{select,image}
+ - tests: listing test, make accepted snapd/core versions consistent
+
+ -- Michael Vogt Wed, 30 Oct 2019 13:17:43 +0100
+
+snapd (2.42) xenial; urgency=medium
+
+ * New upstream release, LP: #1846181
+ - tests: disable {contacts,calendar}-service tests on debian-sid
+ - tests/main/snap-run: disable strace test cases on Arch
+ - cmd/system-shutdown: include correct prototype for die
+ - snap/naming: add test for hook name connect-plug-i2c
+ - cmd/snap-confine: allow digits in hook names
+ - gadget: do not fail the update when old gadget snap is missing
+ bare content
+ - tests: disable {contacts,calendar}-service tests on Arch Linux
+ - tests: move "centos-7" to unstable systems
+ - interfaces/docker-support,kubernetes-support: misc updates for
+ strict k8s
+ - packaging: remove obsolete usr.lib.snapd.snap-confine in
+ postinst
+ - tests: add test that ensures our snapfuse binary actually works
+ - packaging: use snapfuse_ll to speed up snapfuse performance
+ - usersession/userd: make sure to export DBus interfaces before
+ requesting a name
+ - data/selinux: allow snapd to issue sigkill to journalctl
+ - store: download propagates options to delta download
+ - wrappers: allow snaps to install icon theme icons
+ - debug: state-inspect debugging utility
+ - sandbox/cgroup: introduce cgroup wrappers package
+ - snap-confine: fix return value checks for udev functions
+ - cmd/model: output tweaks, add'l tests
+ - wrappers/services: add ServicesEnableState + unit tests
+ - tests: fix newline and wrong test name pointed out in previous PRs
+ - tests: extend mount-ns test to handle mimics
+ - run-checks, tests/main/go: allow gofmt checks to be skipped on
+ 19.10
+ - tests/main/interfaces-{calendar,contacts}-service: disable on
+ 19.10
+ - tests: part3 making tests work on ubuntu-core-18
+ - tests: fix interfaces-timeserver-control on 19.10
+ - overlord/snapstate: config revision code cleanup and extra tests
+ - devicestate: allow remodel to different kernels
+ - overlord,daemon: adjust startup timeout via EXTEND_TIMEOUT_USEC
+ using an estimate
+ - tests/main/many: increase kill-timeout to 5m
+ - interfaces/kubernetes-support: allow systemd-run to ptrace read
+ unconfined
+ - snapstate: auto transition on experimental.snapd-snap=true
+ - tests: retry checking until the written file on desktop-portal-
+ filechooser
+ - tests: unit test for a refresh failing on configure hook
+ - tests: remove mount_id and parent_id from mount-ns test data
+ - tests: move classic-ubuntu-core-transition* to nightly
+ - tests/mountinfo-tool: proper formatting of opt_fields
+ - overlord/configstate: special-case "null" in transaction Changes()
+ - snap-confine: fallback gracefully on a cgroup v2 only system
+ - tests: debian sid now ships new seccomp, adjust tests
+ - tests: explicitly restore after using LXD
+ - snapstate: make progress reporting less granular
+ - bootloader: little kernel support
+ - fixme: rename ubuntu*architectures to dpkg*architectures
+ - tests: run dbus-launch inside a systemd unit
+ - channel: introduce Resolve and ResolveLocked
+ - tests: run failing tests on ubuntu eoan due to is now set as
+ unstable
+ - systemd: detach rather than unmount .mount units
+ - cmd/snap-confine: add unit tests for sc_invocation, cleanup memory
+ leaks in tests
+ - boot,dirs,image: introduce boot.MakeBootable, use it in image
+ instead of ad hoc code
+ - cmd/snap-update-ns: clarify sharing comment
+ - tests/overlord/snapstate: refactor for cleaner test failures
+ - cmd/snap-update-ns: don't propagate detaching changes
+ - interfaces: allow reading mutter Xauthority file
+ - cmd/snap-confine: fix /snap duplication in legacy mode
+ - tests: fix mountinfo-tool filtering when used with rewriting
+ - seed,image,o/devicestate: extract seed loading to seed/seed16.go
+ - many: pass the rootdir and options to bootloader.Find
+ - tests: part5 making tests work on ubuntu-core-18
+ - cmd/snap-confine: keep track of snap instance name and the snap
+ name
+ - cmd: unify die() across C programs
+ - tests: add functions to make an abstraction for the snaps
+ - packaging/fedora, tests/lib/prepare-restore: helper tool for
+ packing sources for RPM
+ - cmd/snap: improve help and error msg for snapshot commands
+ - hookstate/ctlcmd: fix snapctl set help message
+ - cmd/snap: don't append / to snap name just because a dir exists
+ - tests: support fastly-global.cdn.snapcraft.io url on proxy-no-core
+ test
+ - tests: add --quiet switch to retry-tool
+ - tests: add unstable stage for travis execution
+ - tests: disable interfaces-timeserver-control on 19.10
+ - tests: don't guess in is_classic_confinement_supported
+ - boot, etc: simplify BootParticipant (etc) usage
+ - tests: verify retry-tool not retrying missing commands
+ - tests: rewrite "retry" command as retry-tool
+ - tests: move debug section after restore
+ - cmd/libsnap-confine-private, cmd/s-c: use constants for
+ snap/instance name lengths
+ - tests: measure behavior of the device cgroup
+ - boot, bootloader, o/devicestate: boot env manip goes in boot
+ - tests: enabling ubuntu 19.10-64 on spread.yaml
+ - tests: fix ephemeral mount table in left over by prepare
+ - tests: add version-tool for comparing versions
+ - cmd/libsnap: make feature flag enum 1< Tue, 01 Oct 2019 11:24:41 +0200
+
+snapd (2.41) xenial; urgency=medium
+
+ * New upstream release, LP: #1840740
+ - overlord/snapstate: revert track-risk behavior
+ - tests: fix snap info test
+ - httputil: rework protocol error detection
+ - gadget: do not error on gadget refreshes with multiple volumes
+ - i18n, vendor, packaging: drop github.com/ojii/gettext.go, use
+ github.com/snapcore/go-gettext
+ - snapstate: validate all system-usernames before creating them
+ - mkversion.sh: fix version from git checkouts
+ - interfaces/network-{control,manager}: allow 'k' on
+ /run/resolvconf/**
+ - interfaces/wayland,x11: allow reading an Xwayland Xauth file
+ - interfaces: k8s worker node updates
+ - debian: re-enable systemd environment generator
+ - many: create system-usernames user/group if both don't exist
+ - packaging: fix symlink for snapd.session-agent.socket
+ - tests: change cgroups so that LXD doesn't have to
+ - interfaces/network-setup-control: allow dbus netplan apply
+ messages
+ - tests: add /var/cache/snapd to the snapd state to prevent error on
+ the store
+ - tests: add test for services disabled during refresh hook
+ - many: simpler access to snap-seccomp version-info
+ - snap: cleanup some tests, clarify some errorsThis is a follow up
+ from work on system usernames:
+ - osutil: add osutil.Find{Uid,Gid}
+ - tests: use a different archive based on the spread backend on go-
+ build test
+ - cmd/snap-update-ns: fix pair of bugs affecting refresh of snap
+ with layouts
+ - overlord/devicestate: detect clashing concurrent (ongoing, just
+ finished) remodels or changes
+ - interfaces/docker-support: declare controls-device-cgroup
+ - packaging: fix removal of old apparmor profile
+ - store: use track/risk for "channel" name when parsing store
+ details
+ - many: allow 'system-usernames' with libseccomp > 2.4 and golang-
+ seccomp > 0.9.0
+ - overlord/devicestate, tests: use gadget.Update() proper, spread
+ test
+ - overlord/configstate/configcore: allow setting start_x=1 to enable
+ CSI camera on RPi
+ - interfaces: remove BeforePrepareSlot from commonInterface
+ - many: support system-usernames for 'snap_daemon' user
+ - overlord/devicestate,o/snapstate: queue service commands before
+ mark-seeded and other final tasks
+ - interfaces/mount: discard mount ns on backend Remove
+ - packaging/fedora: build on RHEL8
+ - overlord/devicestate: support seeding a classic system with the
+ snapd snap and no core
+ - interfaces: fix test failure in gpio_control_test
+ - interfaces, policy: remove sanitize helpers and use minimal policy
+ check
+ - packaging: use %systemd_user_* macros to enable session agent
+ socket according to presets
+ - snapstate, store: handle 429s on catalog refresh a little bit
+ better
+ - tests: part4 making tests work on ubuntu-core-18
+ - many: drop snap.ReadGadgetInfo wrapper
+ - xdgopenproxy: update test API to match upstream
+ - tests: show why sbuild failed
+ - data/selinux: allow mandb_t to search /var/lib/snapd
+ - tests: be less verbose when checking service status
+ - tests: set sbuild test as manual
+ - overlord: DeviceCtx must find the remodel context for a remodel
+ change
+ - tests: use snap info --verbose to check for base
+ - sanity: unmount squashfs with --lazy
+ - overlord/snapstate: keep current track if only risk is specified
+ - interfaces/firewall-control: support nft routing expressions and
+ device groups
+ - gadget: support for writing symlinks
+ - tests: mountinfo-tool fail if there are no matches
+ - tests: sync journal log before start the test
+ - cmd/snap, data/completion: improve completion for 'snap debug'
+ - httputil: retry for http2 PROTOCOL_ERROR
+ - Errata commit: pulseaudio still auto-connects on classic
+ - interfaces/misc: updates for k8s 1.15 (and greengrass test)
+ - tests: set GOTRACEBACK=1 when running tests
+ - cmd/libsnap: don't leak memory in sc_die_on_error
+ - tests: improve how the system is restored when the upgrade-
+ from-2.15 test fails
+ - interfaces/bluetooth-control: add udev rules for BT_chrdev devices
+ - interfaces: add audio-playback/audio-record and make pulseaudio
+ manually connect
+ - tests: split the sbuild test in 2 depending on the type of build
+ - interfaces: add an interface granting access to AppStream metadata
+ - gadget: ensure filesystem labels are unique
+ - usersession/agent: use background context when stopping the agent
+ - HACKING.md: update spread section, other updates
+ - data/selinux: allow snap-confine to read entries on nsfs
+ - tests: respect SPREAD_DEBUG_EACH on the main suite
+ - packaging/debian-sid: set GOCACHE to a known writable location
+ - interfaces: add gpio-control interface
+ - cmd/snap: use showDone helper with 'snap switch'
+ - gadget: effective structure role fallback, extra tests
+ - many: fix unit tests getting stuck
+ - tests: remove installed snap on restore
+ - daemon: do not modify test data in user suite
+ - data/selinux: allow read on sysfs
+ - packaging/debian: don't md5sum absent files
+ - tests: remove test-snapd-curl
+ - tests: remove test-snapd-snapctl-core18 in restore
+ - tests: remove installed snap in the restore section
+ - tests: remove installed test snap
+ - tests: correctly escape mount unit path
+ - cmd/Makefile.am: support building with the go snap
+ - tests: work around classic snap affecting the host
+ - tests: fix typo "current"
+ - overlord/assertstate: add Batch.Precheck to check for the full
+ validity of the batch before Commit
+ - tests: restore cpuset clone_children clobbered by lxd
+ - usersession: move userd package to usersession/userd
+ - tests: reformat and fix markdown in snapd-state.md
+ - gadget: select the right updater for given structure
+ - tests: show stderr only if it exists
+ - sessionagent: add a REST interface with socket activation
+ - tests: remove locally installed core in more tests
+ - tests: remove local revision of core
+ - packaging/debian-sid: use correct apparmor Depends for Debian
+ - packaging/debian-sid: merge debian upload changes back into master
+ - cmd/snap-repair: make sure the goroutine doesn't stick around on
+ timeout
+ - packaging/fedora: github.com/cheggaaa/pb is no longer used
+ - configstate/config: fix crash in purgeNulls
+ - boot, o/snapst, o/devicest: limit knowledge of boot vars to boot
+ - client,cmd/snap: stop depending on status/status-code in the JSON
+ responses in client
+ - tests: unmount leftover /run/netns
+ - tests: switch mount-ns test to manual
+ - overlord,daemon,cmd/snapd: move expensive startup to dedicated
+ StartUp methods
+ - osutil: add EnsureTreeState helper
+ - tests: measure properties of various mount namespaces
+ - tests: part2 making tests work on ubuntu-core-18
+ - interfaces/policy: minimal policy check for replacing
+ sanitizeReservedFor helpers (1/2)
+ - interfaces: add an interface that grants access to the PackageKit
+ service
+ - overlord/devicestate: update gadget update handlers and mocks
+ - tests: add mountinfo-tool --ref-x1000
+ - tests: remove lxd / lxcfs if pre-installed
+ - tests: removing support for ubuntu cosmic on spread test suite
+ - tests: don't leak /run/netns mount
+ - image: clean up the validateSuite
+ - bootloader: remove "Dir()" from Bootloader interface
+ - many: retry to reboot if snapd gets restarted before expected
+ reboot
+ - overlord: implement re-registration remodeling
+ - cmd: revert PR#6933 (tweak of GOMAXPROCS)
+ - cmd/snap: add snap unset command
+ - many: add Client-User-Agent to "SnapAction" install API call
+ - tests: first part making tests run on ubuntu-core-18
+ - hookstate/ctlcmd: support hidden commands in snapctl
+ - many: replace snapd snap name checks with type checks (3/4)
+ - overlord: mostly stop needing Kernel/CoreInfo, make GadgetInfo
+ consider a DeviceContext
+ - snapctl: handle unsetting of config options with "!"
+ - tests: move core migration snaps to tests/lib/snaps dir
+ - cmd/snap: handle unsetting of config options with "!"
+ - cmd/snap, etc: add health to 'snap list' and 'snap info'
+ - gadget: use struct field names when intializing data in mounted
+ updater unit tests
+ - cmd/snap-confine: bring /lib/firmware from the host
+ - snap: set snapd snap type (1/4)
+ - snap: add checks in validate-seed for missing base/default-
+ provider
+ - daemon: replace shutdownServer with net/http's native shutdown
+ support
+ - interfaces/builtin: add exec "/bin/runc" to docker-support
+ - gadget: mounted filesystem updater
+ - overlord/patch: simplify conditions for re-applying sublevel
+ patches for level 6
+ - seccomp/compiler: adjust test case names and comment for later
+ changes
+ - tests: fix error doing snap pack running failover test
+ - tests: don't preserve size= when rewriting mount tables
+ - tests: allow reordering of rewrite operations
+ - gadget: main update routine
+ - overlord/config: normalize nulls to support config unsetting
+ semantics
+ - snap-userd-autostart: don't list as a startup application on the
+ GUI
+ - tests: renumber snap revisions as seen via writable
+ - tests: change allocation for mount options
+ - tests: re-enable ns-re-associate test
+ - tests: mountinfo-tool allow many --refs
+ - overlord/devicestate: implement reregRemodelContext with the
+ essential re-registration logic
+ - tests: replace various numeric mount options
+ - gadget: filesystem image writer
+ - tests: add more unit tests for mountinfo-tool
+ - tests: introduce mountinfo-tool --ref feature
+ - tests: refactor mountinfo-tool rewrite state
+ - tests: allow renumbering mount namespace identifiers
+ - snap: refactor and explain layout blacklisting
+ - tests: renumber snap revisions as seen via hostfs
+ - daemon, interfaces, travis: workaround build ID with Go 1.9, use
+ 1.9 for travis tests
+ - cmd/libsnap: add sc_error_init_{simple,api_misuse}
+ - gadget: make raw updater handle shifted structures
+ - tests/lib/nested: create WORK_DIR before accessing it
+ - cmd/libsnap: rename SC_LIBSNAP_ERROR to SC_LIBSNAP_DOMAIN
+ - cmd,tests: forcibly discard mount namespace when bases change
+ - many: introduce healthstate, run check-health
+ post-(install/refresh/try/revert)
+ - interfaces/optical-drive: add scsi-generic type 4 and 5 support
+ - cmd/snap-confine: exit from helper when parent dies
+
+ -- Michael Vogt Fri, 30 Aug 2019 08:56:16 +0200
+
snapd (2.40) xenial; urgency=medium
* New upstream release, LP: #1836327
@@ -102,7 +558,7 @@
- tests/lib/nested: fix multi argument copy_remote
- tests/lib/nested: have mkfs.ext4 use a rootdir instead of mounting
an image
- - packaging: fix permissons powerpc docs dir
+ - packaging: fix permissions powerpc docs dir
- overlord: mock store to avoid net requests
- debian: rework how we run autopkgtests
- interface: builtin: avahi-observe/control: allow slots
diff -Nru snapd-2.40/packaging/ubuntu-16.04/rules snapd-2.42.1/packaging/ubuntu-16.04/rules
--- snapd-2.40/packaging/ubuntu-16.04/rules 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-16.04/rules 2019-10-30 12:17:43.000000000 +0000
@@ -26,7 +26,7 @@
# problem on "apt purge snapd". To ensure this won't happen add the
# right dependency on 18.04.
ifeq (${VERSION_ID},"18.04")
- SUBSTVARS = -Vsnapd:Breaks="apt (<< 1.6.3)"
+ SUBSTVARS = -Vsnapd:Breaks="systemd (<< 237-3ubuntu10.24), apt (<< 1.6.3)"
endif
# Same as above for 18.10 just a different version.
ifeq (${VERSION_ID},"18.10")
@@ -168,9 +168,9 @@
# Generate static snap-exec, snapctl and snap-update-ns - it somehow includes CGO so
# we must force a static build here. We need a static snap-{exec,update-ns}/snapctl
# inside the core snap because not all bases will have a libc
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-exec)
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snapctl)
- (cd _build/bin && GOPATH=$$(pwd)/.. go build --ldflags '-extldflags "-static"' $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-update-ns)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-exec)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snapctl)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build go build --ldflags '-extldflags "-static"' $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-update-ns)
# ensure we generated a static build
$(shell if ldd _build/bin/snap-exec; then false "need static build"; fi)
@@ -180,7 +180,7 @@
# ensure snap-seccomp is build with a static libseccomp on Ubuntu
ifeq ($(shell dpkg-vendor --query Vendor),Ubuntu)
sed -i "s|#cgo LDFLAGS:|#cgo LDFLAGS: /usr/lib/$(shell dpkg-architecture -qDEB_TARGET_MULTIARCH)/libseccomp.a|" _build/src/$(DH_GOPKG)/cmd/snap-seccomp/main.go
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_LDFLAGS_ALLOW="/.*/libseccomp.a" go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-seccomp)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_LDFLAGS_ALLOW="/.*/libseccomp.a" go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-seccomp)
# ensure that libseccomp is not dynamically linked
ldd _build/bin/snap-seccomp
test "$$(ldd _build/bin/snap-seccomp | grep libseccomp)" = ""
@@ -197,7 +197,7 @@
$(MAKE) -C data all
# build squashfuse and rename to snapfuse
- (cd vendor/github.com/snapcore/squashfuse/src && mkdir -p autom4te.cache && ./autogen.sh --disable-demo && ./configure --disable-demo && make && mv squashfuse snapfuse)
+ (cd vendor/github.com/snapcore/squashfuse/src && mkdir -p autom4te.cache && ./autogen.sh --disable-demo && ./configure --disable-demo && make && mv squashfuse_ll snapfuse)
override_dh_auto_test:
dh_auto_test -- $(GCCGOFLAGS)
@@ -246,12 +246,6 @@
$(MAKE) -C cmd install DESTDIR=$(CURDIR)/debian/tmp
- # We have to disable the systemd generator on bionic because of
- # the systemd bug LP: 1771858. Enabling it caused bug #1811233
-ifeq (${VERSION_ID},"18.04")
- chmod -x ${CURDIR}/debian/tmp/usr/lib/systemd/system-environment-generators/snapd-env-generator
-endif
-
# Rename the apparmor profile, see dh_apparmor call above for an explanation.
mv $(CURDIR)/debian/tmp/etc/apparmor.d/usr.lib.snapd.snap-confine $(CURDIR)/debian/tmp/etc/apparmor.d/usr.lib.snapd.snap-confine.real
diff -Nru snapd-2.40/packaging/ubuntu-16.04/snapd.links snapd-2.42.1/packaging/ubuntu-16.04/snapd.links
--- snapd-2.40/packaging/ubuntu-16.04/snapd.links 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-16.04/snapd.links 2019-10-30 12:17:43.000000000 +0000
@@ -1,2 +1,6 @@
-../../usr/lib/snapd/snap-device-helper /lib/udev/snappy-app-dev
+/usr/lib/snapd/snap-device-helper /lib/udev/snappy-app-dev
usr/lib/snapd/snapctl usr/bin/snapctl
+
+# This should be removed once we can rely on debhelper >= 11.5:
+# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=764678
+/usr/lib/systemd/user/snapd.session-agent.socket /usr/lib/systemd/user/sockets.target.wants/snapd.session-agent.socket
diff -Nru snapd-2.40/packaging/ubuntu-16.04/snapd.postinst snapd-2.42.1/packaging/ubuntu-16.04/snapd.postinst
--- snapd-2.40/packaging/ubuntu-16.04/snapd.postinst 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-16.04/snapd.postinst 2019-10-30 12:17:43.000000000 +0000
@@ -41,12 +41,12 @@
# In commit 0dce4704a5d (2017-03-28, snapd v2.23.6) we renamed
# /etc/apparmor.d/usr.lib.snap-confine to usr.lib.snap-confine.real
- # to fix LP: #1673247 - however some people (upgrades?) still have
+ # to fix LP: #1673247 - however some people (developers?) still have
# the old usr.lib.snap-confine file. This seems to be loaded instead
# of the correct usr.lib.snap-confine.real profile. To fix this we
- # use the rather blunt approach to remove the file forcefully.
- if test "$(md5sum /etc/apparmor.d/usr.lib.snapd.snap-confine | cut -f1 -d' ' 2>/dev/null)" = "2a38d40fe662f46fedd0aefbe78f23e9"; then
- rm -f /etc/apparmor.d/usr.lib.snapd.snap-confine
+ # use the rather blunt approach to rename the file forcefully.
+ if test -f /etc/apparmor.d/usr.lib.snapd.snap-confine && test -f /etc/apparmor.d/usr.lib.snapd.snap-confine.real; then
+ mv /etc/apparmor.d/usr.lib.snapd.snap-confine /etc/apparmor.d/usr.lib.snapd.snap-confine.dpkg-bak
fi
# Ensure that the void directory has correct permissions.
diff -Nru snapd-2.40/packaging/ubuntu-16.10/changelog snapd-2.42.1/packaging/ubuntu-16.10/changelog
--- snapd-2.40/packaging/ubuntu-16.10/changelog 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-16.10/changelog 2019-10-30 12:17:43.000000000 +0000
@@ -1,3 +1,459 @@
+snapd (2.42.1) xenial; urgency=medium
+
+ * New upstream release, LP: #1846181
+ - interfaces: de-duplicate emitted update-ns profiles
+ - packaging: tweak handling of usr.lib.snapd.snap-confine
+ - interfaces: allow introspecting network-manager on core
+ - tests/main/interfaces-contacts-service: disable on openSUSE
+ Tumbleweed
+ - tests/lib/lxd-snapfuse: restore mount changes introduced by LXD
+ - snap: fix default-provider in seed validation
+ - tests: update system-usernames test now that opensuse-15.1 works
+ - overlord: set fake sertial in TestRemodelSwitchToDifferentKernel
+ - gadget: rename "boot{select,img}" -> system-boot-{select,image}
+ - tests: listing test, make accepted snapd/core versions consistent
+
+ -- Michael Vogt Wed, 30 Oct 2019 13:17:43 +0100
+
+snapd (2.42) xenial; urgency=medium
+
+ * New upstream release, LP: #1846181
+ - tests: disable {contacts,calendar}-service tests on debian-sid
+ - tests/main/snap-run: disable strace test cases on Arch
+ - cmd/system-shutdown: include correct prototype for die
+ - snap/naming: add test for hook name connect-plug-i2c
+ - cmd/snap-confine: allow digits in hook names
+ - gadget: do not fail the update when old gadget snap is missing
+ bare content
+ - tests: disable {contacts,calendar}-service tests on Arch Linux
+ - tests: move "centos-7" to unstable systems
+ - interfaces/docker-support,kubernetes-support: misc updates for
+ strict k8s
+ - packaging: remove obsolete usr.lib.snapd.snap-confine in
+ postinst
+ - tests: add test that ensures our snapfuse binary actually works
+ - packaging: use snapfuse_ll to speed up snapfuse performance
+ - usersession/userd: make sure to export DBus interfaces before
+ requesting a name
+ - data/selinux: allow snapd to issue sigkill to journalctl
+ - store: download propagates options to delta download
+ - wrappers: allow snaps to install icon theme icons
+ - debug: state-inspect debugging utility
+ - sandbox/cgroup: introduce cgroup wrappers package
+ - snap-confine: fix return value checks for udev functions
+ - cmd/model: output tweaks, add'l tests
+ - wrappers/services: add ServicesEnableState + unit tests
+ - tests: fix newline and wrong test name pointed out in previous PRs
+ - tests: extend mount-ns test to handle mimics
+ - run-checks, tests/main/go: allow gofmt checks to be skipped on
+ 19.10
+ - tests/main/interfaces-{calendar,contacts}-service: disable on
+ 19.10
+ - tests: part3 making tests work on ubuntu-core-18
+ - tests: fix interfaces-timeserver-control on 19.10
+ - overlord/snapstate: config revision code cleanup and extra tests
+ - devicestate: allow remodel to different kernels
+ - overlord,daemon: adjust startup timeout via EXTEND_TIMEOUT_USEC
+ using an estimate
+ - tests/main/many: increase kill-timeout to 5m
+ - interfaces/kubernetes-support: allow systemd-run to ptrace read
+ unconfined
+ - snapstate: auto transition on experimental.snapd-snap=true
+ - tests: retry checking until the written file on desktop-portal-
+ filechooser
+ - tests: unit test for a refresh failing on configure hook
+ - tests: remove mount_id and parent_id from mount-ns test data
+ - tests: move classic-ubuntu-core-transition* to nightly
+ - tests/mountinfo-tool: proper formatting of opt_fields
+ - overlord/configstate: special-case "null" in transaction Changes()
+ - snap-confine: fallback gracefully on a cgroup v2 only system
+ - tests: debian sid now ships new seccomp, adjust tests
+ - tests: explicitly restore after using LXD
+ - snapstate: make progress reporting less granular
+ - bootloader: little kernel support
+ - fixme: rename ubuntu*architectures to dpkg*architectures
+ - tests: run dbus-launch inside a systemd unit
+ - channel: introduce Resolve and ResolveLocked
+ - tests: run failing tests on ubuntu eoan due to is now set as
+ unstable
+ - systemd: detach rather than unmount .mount units
+ - cmd/snap-confine: add unit tests for sc_invocation, cleanup memory
+ leaks in tests
+ - boot,dirs,image: introduce boot.MakeBootable, use it in image
+ instead of ad hoc code
+ - cmd/snap-update-ns: clarify sharing comment
+ - tests/overlord/snapstate: refactor for cleaner test failures
+ - cmd/snap-update-ns: don't propagate detaching changes
+ - interfaces: allow reading mutter Xauthority file
+ - cmd/snap-confine: fix /snap duplication in legacy mode
+ - tests: fix mountinfo-tool filtering when used with rewriting
+ - seed,image,o/devicestate: extract seed loading to seed/seed16.go
+ - many: pass the rootdir and options to bootloader.Find
+ - tests: part5 making tests work on ubuntu-core-18
+ - cmd/snap-confine: keep track of snap instance name and the snap
+ name
+ - cmd: unify die() across C programs
+ - tests: add functions to make an abstraction for the snaps
+ - packaging/fedora, tests/lib/prepare-restore: helper tool for
+ packing sources for RPM
+ - cmd/snap: improve help and error msg for snapshot commands
+ - hookstate/ctlcmd: fix snapctl set help message
+ - cmd/snap: don't append / to snap name just because a dir exists
+ - tests: support fastly-global.cdn.snapcraft.io url on proxy-no-core
+ test
+ - tests: add --quiet switch to retry-tool
+ - tests: add unstable stage for travis execution
+ - tests: disable interfaces-timeserver-control on 19.10
+ - tests: don't guess in is_classic_confinement_supported
+ - boot, etc: simplify BootParticipant (etc) usage
+ - tests: verify retry-tool not retrying missing commands
+ - tests: rewrite "retry" command as retry-tool
+ - tests: move debug section after restore
+ - cmd/libsnap-confine-private, cmd/s-c: use constants for
+ snap/instance name lengths
+ - tests: measure behavior of the device cgroup
+ - boot, bootloader, o/devicestate: boot env manip goes in boot
+ - tests: enabling ubuntu 19.10-64 on spread.yaml
+ - tests: fix ephemeral mount table in left over by prepare
+ - tests: add version-tool for comparing versions
+ - cmd/libsnap: make feature flag enum 1< Tue, 01 Oct 2019 11:24:41 +0200
+
+snapd (2.41) xenial; urgency=medium
+
+ * New upstream release, LP: #1840740
+ - overlord/snapstate: revert track-risk behavior
+ - tests: fix snap info test
+ - httputil: rework protocol error detection
+ - gadget: do not error on gadget refreshes with multiple volumes
+ - i18n, vendor, packaging: drop github.com/ojii/gettext.go, use
+ github.com/snapcore/go-gettext
+ - snapstate: validate all system-usernames before creating them
+ - mkversion.sh: fix version from git checkouts
+ - interfaces/network-{control,manager}: allow 'k' on
+ /run/resolvconf/**
+ - interfaces/wayland,x11: allow reading an Xwayland Xauth file
+ - interfaces: k8s worker node updates
+ - debian: re-enable systemd environment generator
+ - many: create system-usernames user/group if both don't exist
+ - packaging: fix symlink for snapd.session-agent.socket
+ - tests: change cgroups so that LXD doesn't have to
+ - interfaces/network-setup-control: allow dbus netplan apply
+ messages
+ - tests: add /var/cache/snapd to the snapd state to prevent error on
+ the store
+ - tests: add test for services disabled during refresh hook
+ - many: simpler access to snap-seccomp version-info
+ - snap: cleanup some tests, clarify some errorsThis is a follow up
+ from work on system usernames:
+ - osutil: add osutil.Find{Uid,Gid}
+ - tests: use a different archive based on the spread backend on go-
+ build test
+ - cmd/snap-update-ns: fix pair of bugs affecting refresh of snap
+ with layouts
+ - overlord/devicestate: detect clashing concurrent (ongoing, just
+ finished) remodels or changes
+ - interfaces/docker-support: declare controls-device-cgroup
+ - packaging: fix removal of old apparmor profile
+ - store: use track/risk for "channel" name when parsing store
+ details
+ - many: allow 'system-usernames' with libseccomp > 2.4 and golang-
+ seccomp > 0.9.0
+ - overlord/devicestate, tests: use gadget.Update() proper, spread
+ test
+ - overlord/configstate/configcore: allow setting start_x=1 to enable
+ CSI camera on RPi
+ - interfaces: remove BeforePrepareSlot from commonInterface
+ - many: support system-usernames for 'snap_daemon' user
+ - overlord/devicestate,o/snapstate: queue service commands before
+ mark-seeded and other final tasks
+ - interfaces/mount: discard mount ns on backend Remove
+ - packaging/fedora: build on RHEL8
+ - overlord/devicestate: support seeding a classic system with the
+ snapd snap and no core
+ - interfaces: fix test failure in gpio_control_test
+ - interfaces, policy: remove sanitize helpers and use minimal policy
+ check
+ - packaging: use %systemd_user_* macros to enable session agent
+ socket according to presets
+ - snapstate, store: handle 429s on catalog refresh a little bit
+ better
+ - tests: part4 making tests work on ubuntu-core-18
+ - many: drop snap.ReadGadgetInfo wrapper
+ - xdgopenproxy: update test API to match upstream
+ - tests: show why sbuild failed
+ - data/selinux: allow mandb_t to search /var/lib/snapd
+ - tests: be less verbose when checking service status
+ - tests: set sbuild test as manual
+ - overlord: DeviceCtx must find the remodel context for a remodel
+ change
+ - tests: use snap info --verbose to check for base
+ - sanity: unmount squashfs with --lazy
+ - overlord/snapstate: keep current track if only risk is specified
+ - interfaces/firewall-control: support nft routing expressions and
+ device groups
+ - gadget: support for writing symlinks
+ - tests: mountinfo-tool fail if there are no matches
+ - tests: sync journal log before start the test
+ - cmd/snap, data/completion: improve completion for 'snap debug'
+ - httputil: retry for http2 PROTOCOL_ERROR
+ - Errata commit: pulseaudio still auto-connects on classic
+ - interfaces/misc: updates for k8s 1.15 (and greengrass test)
+ - tests: set GOTRACEBACK=1 when running tests
+ - cmd/libsnap: don't leak memory in sc_die_on_error
+ - tests: improve how the system is restored when the upgrade-
+ from-2.15 test fails
+ - interfaces/bluetooth-control: add udev rules for BT_chrdev devices
+ - interfaces: add audio-playback/audio-record and make pulseaudio
+ manually connect
+ - tests: split the sbuild test in 2 depending on the type of build
+ - interfaces: add an interface granting access to AppStream metadata
+ - gadget: ensure filesystem labels are unique
+ - usersession/agent: use background context when stopping the agent
+ - HACKING.md: update spread section, other updates
+ - data/selinux: allow snap-confine to read entries on nsfs
+ - tests: respect SPREAD_DEBUG_EACH on the main suite
+ - packaging/debian-sid: set GOCACHE to a known writable location
+ - interfaces: add gpio-control interface
+ - cmd/snap: use showDone helper with 'snap switch'
+ - gadget: effective structure role fallback, extra tests
+ - many: fix unit tests getting stuck
+ - tests: remove installed snap on restore
+ - daemon: do not modify test data in user suite
+ - data/selinux: allow read on sysfs
+ - packaging/debian: don't md5sum absent files
+ - tests: remove test-snapd-curl
+ - tests: remove test-snapd-snapctl-core18 in restore
+ - tests: remove installed snap in the restore section
+ - tests: remove installed test snap
+ - tests: correctly escape mount unit path
+ - cmd/Makefile.am: support building with the go snap
+ - tests: work around classic snap affecting the host
+ - tests: fix typo "current"
+ - overlord/assertstate: add Batch.Precheck to check for the full
+ validity of the batch before Commit
+ - tests: restore cpuset clone_children clobbered by lxd
+ - usersession: move userd package to usersession/userd
+ - tests: reformat and fix markdown in snapd-state.md
+ - gadget: select the right updater for given structure
+ - tests: show stderr only if it exists
+ - sessionagent: add a REST interface with socket activation
+ - tests: remove locally installed core in more tests
+ - tests: remove local revision of core
+ - packaging/debian-sid: use correct apparmor Depends for Debian
+ - packaging/debian-sid: merge debian upload changes back into master
+ - cmd/snap-repair: make sure the goroutine doesn't stick around on
+ timeout
+ - packaging/fedora: github.com/cheggaaa/pb is no longer used
+ - configstate/config: fix crash in purgeNulls
+ - boot, o/snapst, o/devicest: limit knowledge of boot vars to boot
+ - client,cmd/snap: stop depending on status/status-code in the JSON
+ responses in client
+ - tests: unmount leftover /run/netns
+ - tests: switch mount-ns test to manual
+ - overlord,daemon,cmd/snapd: move expensive startup to dedicated
+ StartUp methods
+ - osutil: add EnsureTreeState helper
+ - tests: measure properties of various mount namespaces
+ - tests: part2 making tests work on ubuntu-core-18
+ - interfaces/policy: minimal policy check for replacing
+ sanitizeReservedFor helpers (1/2)
+ - interfaces: add an interface that grants access to the PackageKit
+ service
+ - overlord/devicestate: update gadget update handlers and mocks
+ - tests: add mountinfo-tool --ref-x1000
+ - tests: remove lxd / lxcfs if pre-installed
+ - tests: removing support for ubuntu cosmic on spread test suite
+ - tests: don't leak /run/netns mount
+ - image: clean up the validateSuite
+ - bootloader: remove "Dir()" from Bootloader interface
+ - many: retry to reboot if snapd gets restarted before expected
+ reboot
+ - overlord: implement re-registration remodeling
+ - cmd: revert PR#6933 (tweak of GOMAXPROCS)
+ - cmd/snap: add snap unset command
+ - many: add Client-User-Agent to "SnapAction" install API call
+ - tests: first part making tests run on ubuntu-core-18
+ - hookstate/ctlcmd: support hidden commands in snapctl
+ - many: replace snapd snap name checks with type checks (3/4)
+ - overlord: mostly stop needing Kernel/CoreInfo, make GadgetInfo
+ consider a DeviceContext
+ - snapctl: handle unsetting of config options with "!"
+ - tests: move core migration snaps to tests/lib/snaps dir
+ - cmd/snap: handle unsetting of config options with "!"
+ - cmd/snap, etc: add health to 'snap list' and 'snap info'
+ - gadget: use struct field names when intializing data in mounted
+ updater unit tests
+ - cmd/snap-confine: bring /lib/firmware from the host
+ - snap: set snapd snap type (1/4)
+ - snap: add checks in validate-seed for missing base/default-
+ provider
+ - daemon: replace shutdownServer with net/http's native shutdown
+ support
+ - interfaces/builtin: add exec "/bin/runc" to docker-support
+ - gadget: mounted filesystem updater
+ - overlord/patch: simplify conditions for re-applying sublevel
+ patches for level 6
+ - seccomp/compiler: adjust test case names and comment for later
+ changes
+ - tests: fix error doing snap pack running failover test
+ - tests: don't preserve size= when rewriting mount tables
+ - tests: allow reordering of rewrite operations
+ - gadget: main update routine
+ - overlord/config: normalize nulls to support config unsetting
+ semantics
+ - snap-userd-autostart: don't list as a startup application on the
+ GUI
+ - tests: renumber snap revisions as seen via writable
+ - tests: change allocation for mount options
+ - tests: re-enable ns-re-associate test
+ - tests: mountinfo-tool allow many --refs
+ - overlord/devicestate: implement reregRemodelContext with the
+ essential re-registration logic
+ - tests: replace various numeric mount options
+ - gadget: filesystem image writer
+ - tests: add more unit tests for mountinfo-tool
+ - tests: introduce mountinfo-tool --ref feature
+ - tests: refactor mountinfo-tool rewrite state
+ - tests: allow renumbering mount namespace identifiers
+ - snap: refactor and explain layout blacklisting
+ - tests: renumber snap revisions as seen via hostfs
+ - daemon, interfaces, travis: workaround build ID with Go 1.9, use
+ 1.9 for travis tests
+ - cmd/libsnap: add sc_error_init_{simple,api_misuse}
+ - gadget: make raw updater handle shifted structures
+ - tests/lib/nested: create WORK_DIR before accessing it
+ - cmd/libsnap: rename SC_LIBSNAP_ERROR to SC_LIBSNAP_DOMAIN
+ - cmd,tests: forcibly discard mount namespace when bases change
+ - many: introduce healthstate, run check-health
+ post-(install/refresh/try/revert)
+ - interfaces/optical-drive: add scsi-generic type 4 and 5 support
+ - cmd/snap-confine: exit from helper when parent dies
+
+ -- Michael Vogt Fri, 30 Aug 2019 08:56:16 +0200
+
snapd (2.40) xenial; urgency=medium
* New upstream release, LP: #1836327
@@ -102,7 +558,7 @@
- tests/lib/nested: fix multi argument copy_remote
- tests/lib/nested: have mkfs.ext4 use a rootdir instead of mounting
an image
- - packaging: fix permissons powerpc docs dir
+ - packaging: fix permissions powerpc docs dir
- overlord: mock store to avoid net requests
- debian: rework how we run autopkgtests
- interface: builtin: avahi-observe/control: allow slots
diff -Nru snapd-2.40/packaging/ubuntu-16.10/rules snapd-2.42.1/packaging/ubuntu-16.10/rules
--- snapd-2.40/packaging/ubuntu-16.10/rules 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-16.10/rules 2019-10-30 12:17:43.000000000 +0000
@@ -26,7 +26,7 @@
# problem on "apt purge snapd". To ensure this won't happen add the
# right dependency on 18.04.
ifeq (${VERSION_ID},"18.04")
- SUBSTVARS = -Vsnapd:Breaks="apt (<< 1.6.3)"
+ SUBSTVARS = -Vsnapd:Breaks="systemd (<< 237-3ubuntu10.24), apt (<< 1.6.3)"
endif
# Same as above for 18.10 just a different version.
ifeq (${VERSION_ID},"18.10")
@@ -168,9 +168,9 @@
# Generate static snap-exec, snapctl and snap-update-ns - it somehow includes CGO so
# we must force a static build here. We need a static snap-{exec,update-ns}/snapctl
# inside the core snap because not all bases will have a libc
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-exec)
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snapctl)
- (cd _build/bin && GOPATH=$$(pwd)/.. go build --ldflags '-extldflags "-static"' $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-update-ns)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-exec)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snapctl)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build go build --ldflags '-extldflags "-static"' $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-update-ns)
# ensure we generated a static build
$(shell if ldd _build/bin/snap-exec; then false "need static build"; fi)
@@ -180,7 +180,7 @@
# ensure snap-seccomp is build with a static libseccomp on Ubuntu
ifeq ($(shell dpkg-vendor --query Vendor),Ubuntu)
sed -i "s|#cgo LDFLAGS:|#cgo LDFLAGS: /usr/lib/$(shell dpkg-architecture -qDEB_TARGET_MULTIARCH)/libseccomp.a|" _build/src/$(DH_GOPKG)/cmd/snap-seccomp/main.go
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_LDFLAGS_ALLOW="/.*/libseccomp.a" go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-seccomp)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_LDFLAGS_ALLOW="/.*/libseccomp.a" go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-seccomp)
# ensure that libseccomp is not dynamically linked
ldd _build/bin/snap-seccomp
test "$$(ldd _build/bin/snap-seccomp | grep libseccomp)" = ""
@@ -197,7 +197,7 @@
$(MAKE) -C data all
# build squashfuse and rename to snapfuse
- (cd vendor/github.com/snapcore/squashfuse/src && mkdir -p autom4te.cache && ./autogen.sh --disable-demo && ./configure --disable-demo && make && mv squashfuse snapfuse)
+ (cd vendor/github.com/snapcore/squashfuse/src && mkdir -p autom4te.cache && ./autogen.sh --disable-demo && ./configure --disable-demo && make && mv squashfuse_ll snapfuse)
override_dh_auto_test:
dh_auto_test -- $(GCCGOFLAGS)
@@ -246,12 +246,6 @@
$(MAKE) -C cmd install DESTDIR=$(CURDIR)/debian/tmp
- # We have to disable the systemd generator on bionic because of
- # the systemd bug LP: 1771858. Enabling it caused bug #1811233
-ifeq (${VERSION_ID},"18.04")
- chmod -x ${CURDIR}/debian/tmp/usr/lib/systemd/system-environment-generators/snapd-env-generator
-endif
-
# Rename the apparmor profile, see dh_apparmor call above for an explanation.
mv $(CURDIR)/debian/tmp/etc/apparmor.d/usr.lib.snapd.snap-confine $(CURDIR)/debian/tmp/etc/apparmor.d/usr.lib.snapd.snap-confine.real
diff -Nru snapd-2.40/packaging/ubuntu-16.10/snapd.links snapd-2.42.1/packaging/ubuntu-16.10/snapd.links
--- snapd-2.40/packaging/ubuntu-16.10/snapd.links 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-16.10/snapd.links 2019-10-30 12:17:43.000000000 +0000
@@ -1,2 +1,6 @@
-../../usr/lib/snapd/snap-device-helper /lib/udev/snappy-app-dev
+/usr/lib/snapd/snap-device-helper /lib/udev/snappy-app-dev
usr/lib/snapd/snapctl usr/bin/snapctl
+
+# This should be removed once we can rely on debhelper >= 11.5:
+# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=764678
+/usr/lib/systemd/user/snapd.session-agent.socket /usr/lib/systemd/user/sockets.target.wants/snapd.session-agent.socket
diff -Nru snapd-2.40/packaging/ubuntu-16.10/snapd.postinst snapd-2.42.1/packaging/ubuntu-16.10/snapd.postinst
--- snapd-2.40/packaging/ubuntu-16.10/snapd.postinst 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-16.10/snapd.postinst 2019-10-30 12:17:43.000000000 +0000
@@ -41,12 +41,12 @@
# In commit 0dce4704a5d (2017-03-28, snapd v2.23.6) we renamed
# /etc/apparmor.d/usr.lib.snap-confine to usr.lib.snap-confine.real
- # to fix LP: #1673247 - however some people (upgrades?) still have
+ # to fix LP: #1673247 - however some people (developers?) still have
# the old usr.lib.snap-confine file. This seems to be loaded instead
# of the correct usr.lib.snap-confine.real profile. To fix this we
- # use the rather blunt approach to remove the file forcefully.
- if test "$(md5sum /etc/apparmor.d/usr.lib.snapd.snap-confine | cut -f1 -d' ' 2>/dev/null)" = "2a38d40fe662f46fedd0aefbe78f23e9"; then
- rm -f /etc/apparmor.d/usr.lib.snapd.snap-confine
+ # use the rather blunt approach to rename the file forcefully.
+ if test -f /etc/apparmor.d/usr.lib.snapd.snap-confine && test -f /etc/apparmor.d/usr.lib.snapd.snap-confine.real; then
+ mv /etc/apparmor.d/usr.lib.snapd.snap-confine /etc/apparmor.d/usr.lib.snapd.snap-confine.dpkg-bak
fi
# Ensure that the void directory has correct permissions.
diff -Nru snapd-2.40/packaging/ubuntu-17.04/changelog snapd-2.42.1/packaging/ubuntu-17.04/changelog
--- snapd-2.40/packaging/ubuntu-17.04/changelog 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-17.04/changelog 2019-10-30 12:17:43.000000000 +0000
@@ -1,3 +1,459 @@
+snapd (2.42.1) xenial; urgency=medium
+
+ * New upstream release, LP: #1846181
+ - interfaces: de-duplicate emitted update-ns profiles
+ - packaging: tweak handling of usr.lib.snapd.snap-confine
+ - interfaces: allow introspecting network-manager on core
+ - tests/main/interfaces-contacts-service: disable on openSUSE
+ Tumbleweed
+ - tests/lib/lxd-snapfuse: restore mount changes introduced by LXD
+ - snap: fix default-provider in seed validation
+ - tests: update system-usernames test now that opensuse-15.1 works
+ - overlord: set fake sertial in TestRemodelSwitchToDifferentKernel
+ - gadget: rename "boot{select,img}" -> system-boot-{select,image}
+ - tests: listing test, make accepted snapd/core versions consistent
+
+ -- Michael Vogt Wed, 30 Oct 2019 13:17:43 +0100
+
+snapd (2.42) xenial; urgency=medium
+
+ * New upstream release, LP: #1846181
+ - tests: disable {contacts,calendar}-service tests on debian-sid
+ - tests/main/snap-run: disable strace test cases on Arch
+ - cmd/system-shutdown: include correct prototype for die
+ - snap/naming: add test for hook name connect-plug-i2c
+ - cmd/snap-confine: allow digits in hook names
+ - gadget: do not fail the update when old gadget snap is missing
+ bare content
+ - tests: disable {contacts,calendar}-service tests on Arch Linux
+ - tests: move "centos-7" to unstable systems
+ - interfaces/docker-support,kubernetes-support: misc updates for
+ strict k8s
+ - packaging: remove obsolete usr.lib.snapd.snap-confine in
+ postinst
+ - tests: add test that ensures our snapfuse binary actually works
+ - packaging: use snapfuse_ll to speed up snapfuse performance
+ - usersession/userd: make sure to export DBus interfaces before
+ requesting a name
+ - data/selinux: allow snapd to issue sigkill to journalctl
+ - store: download propagates options to delta download
+ - wrappers: allow snaps to install icon theme icons
+ - debug: state-inspect debugging utility
+ - sandbox/cgroup: introduce cgroup wrappers package
+ - snap-confine: fix return value checks for udev functions
+ - cmd/model: output tweaks, add'l tests
+ - wrappers/services: add ServicesEnableState + unit tests
+ - tests: fix newline and wrong test name pointed out in previous PRs
+ - tests: extend mount-ns test to handle mimics
+ - run-checks, tests/main/go: allow gofmt checks to be skipped on
+ 19.10
+ - tests/main/interfaces-{calendar,contacts}-service: disable on
+ 19.10
+ - tests: part3 making tests work on ubuntu-core-18
+ - tests: fix interfaces-timeserver-control on 19.10
+ - overlord/snapstate: config revision code cleanup and extra tests
+ - devicestate: allow remodel to different kernels
+ - overlord,daemon: adjust startup timeout via EXTEND_TIMEOUT_USEC
+ using an estimate
+ - tests/main/many: increase kill-timeout to 5m
+ - interfaces/kubernetes-support: allow systemd-run to ptrace read
+ unconfined
+ - snapstate: auto transition on experimental.snapd-snap=true
+ - tests: retry checking until the written file on desktop-portal-
+ filechooser
+ - tests: unit test for a refresh failing on configure hook
+ - tests: remove mount_id and parent_id from mount-ns test data
+ - tests: move classic-ubuntu-core-transition* to nightly
+ - tests/mountinfo-tool: proper formatting of opt_fields
+ - overlord/configstate: special-case "null" in transaction Changes()
+ - snap-confine: fallback gracefully on a cgroup v2 only system
+ - tests: debian sid now ships new seccomp, adjust tests
+ - tests: explicitly restore after using LXD
+ - snapstate: make progress reporting less granular
+ - bootloader: little kernel support
+ - fixme: rename ubuntu*architectures to dpkg*architectures
+ - tests: run dbus-launch inside a systemd unit
+ - channel: introduce Resolve and ResolveLocked
+ - tests: run failing tests on ubuntu eoan due to is now set as
+ unstable
+ - systemd: detach rather than unmount .mount units
+ - cmd/snap-confine: add unit tests for sc_invocation, cleanup memory
+ leaks in tests
+ - boot,dirs,image: introduce boot.MakeBootable, use it in image
+ instead of ad hoc code
+ - cmd/snap-update-ns: clarify sharing comment
+ - tests/overlord/snapstate: refactor for cleaner test failures
+ - cmd/snap-update-ns: don't propagate detaching changes
+ - interfaces: allow reading mutter Xauthority file
+ - cmd/snap-confine: fix /snap duplication in legacy mode
+ - tests: fix mountinfo-tool filtering when used with rewriting
+ - seed,image,o/devicestate: extract seed loading to seed/seed16.go
+ - many: pass the rootdir and options to bootloader.Find
+ - tests: part5 making tests work on ubuntu-core-18
+ - cmd/snap-confine: keep track of snap instance name and the snap
+ name
+ - cmd: unify die() across C programs
+ - tests: add functions to make an abstraction for the snaps
+ - packaging/fedora, tests/lib/prepare-restore: helper tool for
+ packing sources for RPM
+ - cmd/snap: improve help and error msg for snapshot commands
+ - hookstate/ctlcmd: fix snapctl set help message
+ - cmd/snap: don't append / to snap name just because a dir exists
+ - tests: support fastly-global.cdn.snapcraft.io url on proxy-no-core
+ test
+ - tests: add --quiet switch to retry-tool
+ - tests: add unstable stage for travis execution
+ - tests: disable interfaces-timeserver-control on 19.10
+ - tests: don't guess in is_classic_confinement_supported
+ - boot, etc: simplify BootParticipant (etc) usage
+ - tests: verify retry-tool not retrying missing commands
+ - tests: rewrite "retry" command as retry-tool
+ - tests: move debug section after restore
+ - cmd/libsnap-confine-private, cmd/s-c: use constants for
+ snap/instance name lengths
+ - tests: measure behavior of the device cgroup
+ - boot, bootloader, o/devicestate: boot env manip goes in boot
+ - tests: enabling ubuntu 19.10-64 on spread.yaml
+ - tests: fix ephemeral mount table in left over by prepare
+ - tests: add version-tool for comparing versions
+ - cmd/libsnap: make feature flag enum 1< Tue, 01 Oct 2019 11:24:41 +0200
+
+snapd (2.41) xenial; urgency=medium
+
+ * New upstream release, LP: #1840740
+ - overlord/snapstate: revert track-risk behavior
+ - tests: fix snap info test
+ - httputil: rework protocol error detection
+ - gadget: do not error on gadget refreshes with multiple volumes
+ - i18n, vendor, packaging: drop github.com/ojii/gettext.go, use
+ github.com/snapcore/go-gettext
+ - snapstate: validate all system-usernames before creating them
+ - mkversion.sh: fix version from git checkouts
+ - interfaces/network-{control,manager}: allow 'k' on
+ /run/resolvconf/**
+ - interfaces/wayland,x11: allow reading an Xwayland Xauth file
+ - interfaces: k8s worker node updates
+ - debian: re-enable systemd environment generator
+ - many: create system-usernames user/group if both don't exist
+ - packaging: fix symlink for snapd.session-agent.socket
+ - tests: change cgroups so that LXD doesn't have to
+ - interfaces/network-setup-control: allow dbus netplan apply
+ messages
+ - tests: add /var/cache/snapd to the snapd state to prevent error on
+ the store
+ - tests: add test for services disabled during refresh hook
+ - many: simpler access to snap-seccomp version-info
+ - snap: cleanup some tests, clarify some errorsThis is a follow up
+ from work on system usernames:
+ - osutil: add osutil.Find{Uid,Gid}
+ - tests: use a different archive based on the spread backend on go-
+ build test
+ - cmd/snap-update-ns: fix pair of bugs affecting refresh of snap
+ with layouts
+ - overlord/devicestate: detect clashing concurrent (ongoing, just
+ finished) remodels or changes
+ - interfaces/docker-support: declare controls-device-cgroup
+ - packaging: fix removal of old apparmor profile
+ - store: use track/risk for "channel" name when parsing store
+ details
+ - many: allow 'system-usernames' with libseccomp > 2.4 and golang-
+ seccomp > 0.9.0
+ - overlord/devicestate, tests: use gadget.Update() proper, spread
+ test
+ - overlord/configstate/configcore: allow setting start_x=1 to enable
+ CSI camera on RPi
+ - interfaces: remove BeforePrepareSlot from commonInterface
+ - many: support system-usernames for 'snap_daemon' user
+ - overlord/devicestate,o/snapstate: queue service commands before
+ mark-seeded and other final tasks
+ - interfaces/mount: discard mount ns on backend Remove
+ - packaging/fedora: build on RHEL8
+ - overlord/devicestate: support seeding a classic system with the
+ snapd snap and no core
+ - interfaces: fix test failure in gpio_control_test
+ - interfaces, policy: remove sanitize helpers and use minimal policy
+ check
+ - packaging: use %systemd_user_* macros to enable session agent
+ socket according to presets
+ - snapstate, store: handle 429s on catalog refresh a little bit
+ better
+ - tests: part4 making tests work on ubuntu-core-18
+ - many: drop snap.ReadGadgetInfo wrapper
+ - xdgopenproxy: update test API to match upstream
+ - tests: show why sbuild failed
+ - data/selinux: allow mandb_t to search /var/lib/snapd
+ - tests: be less verbose when checking service status
+ - tests: set sbuild test as manual
+ - overlord: DeviceCtx must find the remodel context for a remodel
+ change
+ - tests: use snap info --verbose to check for base
+ - sanity: unmount squashfs with --lazy
+ - overlord/snapstate: keep current track if only risk is specified
+ - interfaces/firewall-control: support nft routing expressions and
+ device groups
+ - gadget: support for writing symlinks
+ - tests: mountinfo-tool fail if there are no matches
+ - tests: sync journal log before start the test
+ - cmd/snap, data/completion: improve completion for 'snap debug'
+ - httputil: retry for http2 PROTOCOL_ERROR
+ - Errata commit: pulseaudio still auto-connects on classic
+ - interfaces/misc: updates for k8s 1.15 (and greengrass test)
+ - tests: set GOTRACEBACK=1 when running tests
+ - cmd/libsnap: don't leak memory in sc_die_on_error
+ - tests: improve how the system is restored when the upgrade-
+ from-2.15 test fails
+ - interfaces/bluetooth-control: add udev rules for BT_chrdev devices
+ - interfaces: add audio-playback/audio-record and make pulseaudio
+ manually connect
+ - tests: split the sbuild test in 2 depending on the type of build
+ - interfaces: add an interface granting access to AppStream metadata
+ - gadget: ensure filesystem labels are unique
+ - usersession/agent: use background context when stopping the agent
+ - HACKING.md: update spread section, other updates
+ - data/selinux: allow snap-confine to read entries on nsfs
+ - tests: respect SPREAD_DEBUG_EACH on the main suite
+ - packaging/debian-sid: set GOCACHE to a known writable location
+ - interfaces: add gpio-control interface
+ - cmd/snap: use showDone helper with 'snap switch'
+ - gadget: effective structure role fallback, extra tests
+ - many: fix unit tests getting stuck
+ - tests: remove installed snap on restore
+ - daemon: do not modify test data in user suite
+ - data/selinux: allow read on sysfs
+ - packaging/debian: don't md5sum absent files
+ - tests: remove test-snapd-curl
+ - tests: remove test-snapd-snapctl-core18 in restore
+ - tests: remove installed snap in the restore section
+ - tests: remove installed test snap
+ - tests: correctly escape mount unit path
+ - cmd/Makefile.am: support building with the go snap
+ - tests: work around classic snap affecting the host
+ - tests: fix typo "current"
+ - overlord/assertstate: add Batch.Precheck to check for the full
+ validity of the batch before Commit
+ - tests: restore cpuset clone_children clobbered by lxd
+ - usersession: move userd package to usersession/userd
+ - tests: reformat and fix markdown in snapd-state.md
+ - gadget: select the right updater for given structure
+ - tests: show stderr only if it exists
+ - sessionagent: add a REST interface with socket activation
+ - tests: remove locally installed core in more tests
+ - tests: remove local revision of core
+ - packaging/debian-sid: use correct apparmor Depends for Debian
+ - packaging/debian-sid: merge debian upload changes back into master
+ - cmd/snap-repair: make sure the goroutine doesn't stick around on
+ timeout
+ - packaging/fedora: github.com/cheggaaa/pb is no longer used
+ - configstate/config: fix crash in purgeNulls
+ - boot, o/snapst, o/devicest: limit knowledge of boot vars to boot
+ - client,cmd/snap: stop depending on status/status-code in the JSON
+ responses in client
+ - tests: unmount leftover /run/netns
+ - tests: switch mount-ns test to manual
+ - overlord,daemon,cmd/snapd: move expensive startup to dedicated
+ StartUp methods
+ - osutil: add EnsureTreeState helper
+ - tests: measure properties of various mount namespaces
+ - tests: part2 making tests work on ubuntu-core-18
+ - interfaces/policy: minimal policy check for replacing
+ sanitizeReservedFor helpers (1/2)
+ - interfaces: add an interface that grants access to the PackageKit
+ service
+ - overlord/devicestate: update gadget update handlers and mocks
+ - tests: add mountinfo-tool --ref-x1000
+ - tests: remove lxd / lxcfs if pre-installed
+ - tests: removing support for ubuntu cosmic on spread test suite
+ - tests: don't leak /run/netns mount
+ - image: clean up the validateSuite
+ - bootloader: remove "Dir()" from Bootloader interface
+ - many: retry to reboot if snapd gets restarted before expected
+ reboot
+ - overlord: implement re-registration remodeling
+ - cmd: revert PR#6933 (tweak of GOMAXPROCS)
+ - cmd/snap: add snap unset command
+ - many: add Client-User-Agent to "SnapAction" install API call
+ - tests: first part making tests run on ubuntu-core-18
+ - hookstate/ctlcmd: support hidden commands in snapctl
+ - many: replace snapd snap name checks with type checks (3/4)
+ - overlord: mostly stop needing Kernel/CoreInfo, make GadgetInfo
+ consider a DeviceContext
+ - snapctl: handle unsetting of config options with "!"
+ - tests: move core migration snaps to tests/lib/snaps dir
+ - cmd/snap: handle unsetting of config options with "!"
+ - cmd/snap, etc: add health to 'snap list' and 'snap info'
+ - gadget: use struct field names when intializing data in mounted
+ updater unit tests
+ - cmd/snap-confine: bring /lib/firmware from the host
+ - snap: set snapd snap type (1/4)
+ - snap: add checks in validate-seed for missing base/default-
+ provider
+ - daemon: replace shutdownServer with net/http's native shutdown
+ support
+ - interfaces/builtin: add exec "/bin/runc" to docker-support
+ - gadget: mounted filesystem updater
+ - overlord/patch: simplify conditions for re-applying sublevel
+ patches for level 6
+ - seccomp/compiler: adjust test case names and comment for later
+ changes
+ - tests: fix error doing snap pack running failover test
+ - tests: don't preserve size= when rewriting mount tables
+ - tests: allow reordering of rewrite operations
+ - gadget: main update routine
+ - overlord/config: normalize nulls to support config unsetting
+ semantics
+ - snap-userd-autostart: don't list as a startup application on the
+ GUI
+ - tests: renumber snap revisions as seen via writable
+ - tests: change allocation for mount options
+ - tests: re-enable ns-re-associate test
+ - tests: mountinfo-tool allow many --refs
+ - overlord/devicestate: implement reregRemodelContext with the
+ essential re-registration logic
+ - tests: replace various numeric mount options
+ - gadget: filesystem image writer
+ - tests: add more unit tests for mountinfo-tool
+ - tests: introduce mountinfo-tool --ref feature
+ - tests: refactor mountinfo-tool rewrite state
+ - tests: allow renumbering mount namespace identifiers
+ - snap: refactor and explain layout blacklisting
+ - tests: renumber snap revisions as seen via hostfs
+ - daemon, interfaces, travis: workaround build ID with Go 1.9, use
+ 1.9 for travis tests
+ - cmd/libsnap: add sc_error_init_{simple,api_misuse}
+ - gadget: make raw updater handle shifted structures
+ - tests/lib/nested: create WORK_DIR before accessing it
+ - cmd/libsnap: rename SC_LIBSNAP_ERROR to SC_LIBSNAP_DOMAIN
+ - cmd,tests: forcibly discard mount namespace when bases change
+ - many: introduce healthstate, run check-health
+ post-(install/refresh/try/revert)
+ - interfaces/optical-drive: add scsi-generic type 4 and 5 support
+ - cmd/snap-confine: exit from helper when parent dies
+
+ -- Michael Vogt Fri, 30 Aug 2019 08:56:16 +0200
+
snapd (2.40) xenial; urgency=medium
* New upstream release, LP: #1836327
@@ -102,7 +558,7 @@
- tests/lib/nested: fix multi argument copy_remote
- tests/lib/nested: have mkfs.ext4 use a rootdir instead of mounting
an image
- - packaging: fix permissons powerpc docs dir
+ - packaging: fix permissions powerpc docs dir
- overlord: mock store to avoid net requests
- debian: rework how we run autopkgtests
- interface: builtin: avahi-observe/control: allow slots
diff -Nru snapd-2.40/packaging/ubuntu-17.04/rules snapd-2.42.1/packaging/ubuntu-17.04/rules
--- snapd-2.40/packaging/ubuntu-17.04/rules 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-17.04/rules 2019-10-30 12:17:43.000000000 +0000
@@ -26,7 +26,7 @@
# problem on "apt purge snapd". To ensure this won't happen add the
# right dependency on 18.04.
ifeq (${VERSION_ID},"18.04")
- SUBSTVARS = -Vsnapd:Breaks="apt (<< 1.6.3)"
+ SUBSTVARS = -Vsnapd:Breaks="systemd (<< 237-3ubuntu10.24), apt (<< 1.6.3)"
endif
# Same as above for 18.10 just a different version.
ifeq (${VERSION_ID},"18.10")
@@ -168,9 +168,9 @@
# Generate static snap-exec, snapctl and snap-update-ns - it somehow includes CGO so
# we must force a static build here. We need a static snap-{exec,update-ns}/snapctl
# inside the core snap because not all bases will have a libc
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-exec)
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snapctl)
- (cd _build/bin && GOPATH=$$(pwd)/.. go build --ldflags '-extldflags "-static"' $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-update-ns)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-exec)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_ENABLED=0 go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snapctl)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build go build --ldflags '-extldflags "-static"' $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-update-ns)
# ensure we generated a static build
$(shell if ldd _build/bin/snap-exec; then false "need static build"; fi)
@@ -180,7 +180,7 @@
# ensure snap-seccomp is build with a static libseccomp on Ubuntu
ifeq ($(shell dpkg-vendor --query Vendor),Ubuntu)
sed -i "s|#cgo LDFLAGS:|#cgo LDFLAGS: /usr/lib/$(shell dpkg-architecture -qDEB_TARGET_MULTIARCH)/libseccomp.a|" _build/src/$(DH_GOPKG)/cmd/snap-seccomp/main.go
- (cd _build/bin && GOPATH=$$(pwd)/.. CGO_LDFLAGS_ALLOW="/.*/libseccomp.a" go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-seccomp)
+ (cd _build/bin && GOPATH=$$(pwd)/.. GOCACHE=/tmp/go-build CGO_LDFLAGS_ALLOW="/.*/libseccomp.a" go build $(GCCGOFLAGS) $(DH_GOPKG)/cmd/snap-seccomp)
# ensure that libseccomp is not dynamically linked
ldd _build/bin/snap-seccomp
test "$$(ldd _build/bin/snap-seccomp | grep libseccomp)" = ""
@@ -197,7 +197,7 @@
$(MAKE) -C data all
# build squashfuse and rename to snapfuse
- (cd vendor/github.com/snapcore/squashfuse/src && mkdir -p autom4te.cache && ./autogen.sh --disable-demo && ./configure --disable-demo && make && mv squashfuse snapfuse)
+ (cd vendor/github.com/snapcore/squashfuse/src && mkdir -p autom4te.cache && ./autogen.sh --disable-demo && ./configure --disable-demo && make && mv squashfuse_ll snapfuse)
override_dh_auto_test:
dh_auto_test -- $(GCCGOFLAGS)
@@ -246,12 +246,6 @@
$(MAKE) -C cmd install DESTDIR=$(CURDIR)/debian/tmp
- # We have to disable the systemd generator on bionic because of
- # the systemd bug LP: 1771858. Enabling it caused bug #1811233
-ifeq (${VERSION_ID},"18.04")
- chmod -x ${CURDIR}/debian/tmp/usr/lib/systemd/system-environment-generators/snapd-env-generator
-endif
-
# Rename the apparmor profile, see dh_apparmor call above for an explanation.
mv $(CURDIR)/debian/tmp/etc/apparmor.d/usr.lib.snapd.snap-confine $(CURDIR)/debian/tmp/etc/apparmor.d/usr.lib.snapd.snap-confine.real
diff -Nru snapd-2.40/packaging/ubuntu-17.04/snapd.links snapd-2.42.1/packaging/ubuntu-17.04/snapd.links
--- snapd-2.40/packaging/ubuntu-17.04/snapd.links 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-17.04/snapd.links 2019-10-30 12:17:43.000000000 +0000
@@ -1,2 +1,6 @@
-../../usr/lib/snapd/snap-device-helper /lib/udev/snappy-app-dev
+/usr/lib/snapd/snap-device-helper /lib/udev/snappy-app-dev
usr/lib/snapd/snapctl usr/bin/snapctl
+
+# This should be removed once we can rely on debhelper >= 11.5:
+# https://bugs.debian.org/cgi-bin/bugreport.cgi?bug=764678
+/usr/lib/systemd/user/snapd.session-agent.socket /usr/lib/systemd/user/sockets.target.wants/snapd.session-agent.socket
diff -Nru snapd-2.40/packaging/ubuntu-17.04/snapd.postinst snapd-2.42.1/packaging/ubuntu-17.04/snapd.postinst
--- snapd-2.40/packaging/ubuntu-17.04/snapd.postinst 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/packaging/ubuntu-17.04/snapd.postinst 2019-10-30 12:17:43.000000000 +0000
@@ -41,12 +41,12 @@
# In commit 0dce4704a5d (2017-03-28, snapd v2.23.6) we renamed
# /etc/apparmor.d/usr.lib.snap-confine to usr.lib.snap-confine.real
- # to fix LP: #1673247 - however some people (upgrades?) still have
+ # to fix LP: #1673247 - however some people (developers?) still have
# the old usr.lib.snap-confine file. This seems to be loaded instead
# of the correct usr.lib.snap-confine.real profile. To fix this we
- # use the rather blunt approach to remove the file forcefully.
- if test "$(md5sum /etc/apparmor.d/usr.lib.snapd.snap-confine | cut -f1 -d' ' 2>/dev/null)" = "2a38d40fe662f46fedd0aefbe78f23e9"; then
- rm -f /etc/apparmor.d/usr.lib.snapd.snap-confine
+ # use the rather blunt approach to rename the file forcefully.
+ if test -f /etc/apparmor.d/usr.lib.snapd.snap-confine && test -f /etc/apparmor.d/usr.lib.snapd.snap-confine.real; then
+ mv /etc/apparmor.d/usr.lib.snapd.snap-confine /etc/apparmor.d/usr.lib.snapd.snap-confine.dpkg-bak
fi
# Ensure that the void directory has correct permissions.
diff -Nru snapd-2.40/parts/plugins/x_builddeb.py snapd-2.42.1/parts/plugins/x_builddeb.py
--- snapd-2.40/parts/plugins/x_builddeb.py 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/parts/plugins/x_builddeb.py 2019-10-30 12:17:43.000000000 +0000
@@ -56,6 +56,6 @@
# run the real build
self.run(["dpkg-buildpackage"], env=env)
# and "install" into the right place
- snapd_deb = glob.glob("parts/snapd/snapd_*.deb")[0]
+ snapd_deb = glob.glob(os.path.join(self.partdir, "snapd_*.deb"))[0]
self.run(["dpkg-deb", "-x", os.path.abspath(snapd_deb), self.installdir])
diff -Nru snapd-2.40/README.md snapd-2.42.1/README.md
--- snapd-2.40/README.md 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/README.md 2019-10-30 12:17:43.000000000 +0000
@@ -12,6 +12,8 @@
Head over to [snapcraft.io](https://snapcraft.io) to get started.
+[![Get it from the Snap Store](https://snapcraft.io/static/images/badges/en/snap-store-black.svg)](https://snapcraft.io/snapd)
+
## Development
To get started with development off the snapd code itself, please check
diff -Nru snapd-2.40/run-checks snapd-2.42.1/run-checks
--- snapd-2.40/run-checks 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/run-checks 2019-10-30 12:17:43.000000000 +0000
@@ -63,6 +63,9 @@
--spread-no-ubuntu)
SPREAD=no-ubuntu
;;
+ --spread-unstable)
+ SPREAD=unstable
+ ;;
*)
echo "Wrong flag ${1}. To run a single suite use --static, --unit, --spread."
exit 1
@@ -164,7 +167,11 @@
if [ -n "$fmt" ]; then
echo "Formatting wrong in following files:"
echo "$fmt" | sed -e 's/\\n/\n/g'
- exit 1
+ if [ -z "${SKIP_GOFMT:-}" ]; then
+ exit 1
+ else
+ echo "Ignoring gofmt errors as requested"
+ fi
fi
# go vet
@@ -247,7 +254,7 @@
echo Running tests from "$PWD"
if [ "$short" = 1 ]; then
# shellcheck disable=SC2046
- $goctest -short -v $(go list ./... | grep -v '/vendor/' )
+ GOTRACEBACK=1 $goctest -short -v -timeout 5m $(go list ./... | grep -v '/vendor/' )
else
# Prepare the coverage output profile.
rm -rf .coverage
@@ -256,11 +263,11 @@
if dpkg --compare-versions "$(go version | awk '$3 ~ /^go[0-9]/ {print substr($3, 3)}')" ge 1.10; then
# shellcheck disable=SC2046
- $goctest -v -coverprofile=.coverage/coverage.out -covermode="$COVERMODE" $(go list ./... | grep -v '/vendor/' )
+ GOTRACEBACK=1 $goctest -v -timeout 5m -coverprofile=.coverage/coverage.out -covermode="$COVERMODE" $(go list ./... | grep -v '/vendor/' )
else
for pkg in $(go list ./... | grep -v '/vendor/' ); do
- go test -i "$pkg"
- $goctest -v -coverprofile=.coverage/profile.out -covermode="$COVERMODE" "$pkg"
+ GOTRACEBACK=1 go test -timeout 5m -i "$pkg"
+ GOTRACEBACK=1 $goctest -v -timeout 5m -coverprofile=.coverage/profile.out -covermode="$COVERMODE" "$pkg"
append_coverage .coverage/profile.out
done
fi
@@ -269,6 +276,12 @@
curl -s https://codecov.io/bash | bash /dev/stdin -f .coverage/coverage.out
fi
fi
+
+ # python unit test for mountinfo-tool and version-tool
+ command -v python2 && python2 ./tests/lib/bin/mountinfo-tool --run-unit-tests
+ command -v python3 && python3 ./tests/lib/bin/mountinfo-tool --run-unit-tests
+ command -v python2 && python2 ./tests/lib/bin/version-tool --run-unit-tests
+ command -v python3 && python3 ./tests/lib/bin/version-tool --run-unit-tests
fi
if [ -n "$SPREAD" ]; then
@@ -288,6 +301,9 @@
no-ubuntu)
spread "google:[^u]...:tests/..."
;;
+ unstable)
+ spread "google-unstable:"
+ ;;
*)
echo "Spread parameter $SPREAD not supported"
exit 1
diff -Nru snapd-2.40/sandbox/cgroup/cgroup.go snapd-2.42.1/sandbox/cgroup/cgroup.go
--- snapd-2.40/sandbox/cgroup/cgroup.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/sandbox/cgroup/cgroup.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,110 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package cgroup
+
+import (
+ "fmt"
+ "path/filepath"
+ "syscall"
+
+ "github.com/snapcore/snapd/dirs"
+)
+
+const (
+ // From golang.org/x/sys/unix
+ cgroup2SuperMagic = 0x63677270
+
+ // The only cgroup path we expect, for v2 this is where the unified
+ // hierarchy is mounted, for v1 this is usually a tmpfs mount, under
+ // which the controller-hierarchies are mounted
+ expectedMountPoint = "/sys/fs/cgroup"
+)
+
+const (
+ // Separate block, because iota is fun
+ Unknown = iota
+ V1
+ V2
+)
+
+var (
+ probeVersion = Unknown
+ probeErr error = nil
+)
+
+func init() {
+ probeVersion, probeErr = probeCgroupVersion()
+}
+
+var fsTypeForPath = fsTypeForPathImpl
+
+func fsTypeForPathImpl(path string) (int64, error) {
+ var statfs syscall.Statfs_t
+ if err := syscall.Statfs(path, &statfs); err != nil {
+ return 0, fmt.Errorf("cannot statfs path: %v", err)
+ }
+ // Typs is int32 on 386, use explicit conversion to keep the code
+ // working for both
+ return int64(statfs.Type), nil
+}
+
+// ProcPidPath returns the path to the cgroup file under /proc for the given
+// process id.
+func ProcPidPath(pid int) string {
+ return filepath.Join(dirs.GlobalRootDir, fmt.Sprintf("proc/%v/cgroup", pid))
+}
+
+// ControllerPathV1 returns the path to given controller assuming cgroup v1
+// hierarchy
+func ControllerPathV1(controller string) string {
+ return filepath.Join(dirs.GlobalRootDir, expectedMountPoint, controller)
+}
+
+func probeCgroupVersion() (version int, err error) {
+ cgroupMount := filepath.Join(dirs.GlobalRootDir, expectedMountPoint)
+ typ, err := fsTypeForPath(cgroupMount)
+ if err != nil {
+ return Unknown, fmt.Errorf("cannot determine filesystem type: %v", err)
+ }
+ if typ == cgroup2SuperMagic {
+ return V2, nil
+ }
+ return V1, nil
+}
+
+// IsUnified returns true when a unified cgroup hierarchy is in use
+func IsUnified() bool {
+ version, _ := Version()
+ return version == V2
+}
+
+// Version returns the detected cgroup version
+func Version() (int, error) {
+ return probeVersion, probeErr
+}
+
+// MockVersion sets the reported version of cgroup support. For use in testing only
+func MockVersion(mockVersion int, mockErr error) (restore func()) {
+ oldVersion, oldErr := probeVersion, probeErr
+ probeVersion, probeErr = mockVersion, mockErr
+ return func() {
+ probeVersion, probeErr = oldVersion, oldErr
+ }
+}
diff -Nru snapd-2.40/sandbox/cgroup/cgroup_test.go snapd-2.42.1/sandbox/cgroup/cgroup_test.go
--- snapd-2.40/sandbox/cgroup/cgroup_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/sandbox/cgroup/cgroup_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,124 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package cgroup_test
+
+import (
+ "errors"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/dirs"
+ "github.com/snapcore/snapd/sandbox/cgroup"
+)
+
+type cgroupSuite struct{}
+
+var _ = Suite(&cgroupSuite{})
+
+func TestCgroup(t *testing.T) { TestingT(t) }
+
+func (s *cgroupSuite) TearDownTest(c *C) {
+ dirs.SetRootDir("/")
+}
+
+func (s *cgroupSuite) TestIsUnified(c *C) {
+ restore := cgroup.MockVersion(cgroup.V2, nil)
+ defer restore()
+ c.Assert(cgroup.IsUnified(), Equals, true)
+
+ restore = cgroup.MockVersion(cgroup.V1, nil)
+ defer restore()
+ c.Assert(cgroup.IsUnified(), Equals, false)
+
+ restore = cgroup.MockVersion(cgroup.Unknown, nil)
+ defer restore()
+ c.Assert(cgroup.IsUnified(), Equals, false)
+}
+
+func (s *cgroupSuite) TestProbeVersion2(c *C) {
+ restore := cgroup.MockFsTypeForPath(func(p string) (int64, error) {
+ c.Assert(p, Equals, filepath.Join(dirs.GlobalRootDir, "/sys/fs/cgroup"))
+ return int64(cgroup.Cgroup2SuperMagic), nil
+ })
+ defer restore()
+ v, err := cgroup.ProbeCgroupVersion()
+ c.Assert(err, IsNil)
+ c.Assert(v, Equals, cgroup.V2)
+}
+
+func (s *cgroupSuite) TestProbeVersion1(c *C) {
+ const TMPFS_MAGIC = 0x1021994
+ restore := cgroup.MockFsTypeForPath(func(p string) (int64, error) {
+ c.Assert(p, Equals, filepath.Join(dirs.GlobalRootDir, "/sys/fs/cgroup"))
+ return TMPFS_MAGIC, nil
+ })
+ defer restore()
+ v, err := cgroup.ProbeCgroupVersion()
+ c.Assert(err, IsNil)
+ c.Assert(v, Equals, cgroup.V1)
+}
+
+func (s *cgroupSuite) TestProbeVersionUnhappy(c *C) {
+ restore := cgroup.MockFsTypeForPath(func(p string) (int64, error) {
+ c.Assert(p, Equals, filepath.Join(dirs.GlobalRootDir, "/sys/fs/cgroup"))
+ return 0, errors.New("statfs fail")
+ })
+ defer restore()
+ v, err := cgroup.ProbeCgroupVersion()
+ c.Assert(err, ErrorMatches, "cannot determine filesystem type: statfs fail")
+ c.Assert(v, Equals, cgroup.Unknown)
+}
+
+func (s *cgroupSuite) TestVersion(c *C) {
+ restore := cgroup.MockVersion(cgroup.V2, nil)
+ defer restore()
+ v, err := cgroup.Version()
+ c.Assert(v, Equals, cgroup.V2)
+ c.Assert(err, IsNil)
+
+ restore = cgroup.MockVersion(cgroup.V1, nil)
+ defer restore()
+ v, err = cgroup.Version()
+ c.Assert(v, Equals, cgroup.V1)
+ c.Assert(err, IsNil)
+
+ restore = cgroup.MockVersion(cgroup.Unknown, nil)
+ defer restore()
+ v, err = cgroup.Version()
+ c.Assert(v, Equals, cgroup.Unknown)
+ c.Assert(err, IsNil)
+
+ restore = cgroup.MockVersion(cgroup.Unknown, errors.New("foo"))
+ defer restore()
+ v, err = cgroup.Version()
+ c.Assert(v, Equals, cgroup.Unknown)
+ c.Assert(err, ErrorMatches, "foo")
+}
+
+func (s *cgroupSuite) TestProcPidPath(c *C) {
+ c.Assert(cgroup.ProcPidPath(1), Equals, filepath.Join(dirs.GlobalRootDir, "/proc/1/cgroup"))
+ c.Assert(cgroup.ProcPidPath(1234), Equals, filepath.Join(dirs.GlobalRootDir, "/proc/1234/cgroup"))
+}
+
+func (s *cgroupSuite) TestControllerPathV1(c *C) {
+ c.Assert(cgroup.ControllerPathV1("freezer"), Equals, filepath.Join(dirs.GlobalRootDir, "/sys/fs/cgroup/freezer"))
+ c.Assert(cgroup.ControllerPathV1("memory"), Equals, filepath.Join(dirs.GlobalRootDir, "/sys/fs/cgroup/memory"))
+}
diff -Nru snapd-2.40/sandbox/cgroup/export_test.go snapd-2.42.1/sandbox/cgroup/export_test.go
--- snapd-2.40/sandbox/cgroup/export_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/sandbox/cgroup/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,32 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+package cgroup
+
+var (
+ Cgroup2SuperMagic = cgroup2SuperMagic
+ ProbeCgroupVersion = probeCgroupVersion
+)
+
+func MockFsTypeForPath(mock func(string) (int64, error)) (restore func()) {
+ old := fsTypeForPath
+ fsTypeForPath = mock
+ return func() {
+ fsTypeForPath = old
+ }
+}
diff -Nru snapd-2.40/sandbox/seccomp/compiler.go snapd-2.42.1/sandbox/seccomp/compiler.go
--- snapd-2.40/sandbox/seccomp/compiler.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/sandbox/seccomp/compiler.go 2019-10-30 12:17:43.000000000 +0000
@@ -20,9 +20,11 @@
import (
"bytes"
+ "errors"
"fmt"
"os/exec"
"regexp"
+ "strconv"
"strings"
"github.com/snapcore/snapd/osutil"
@@ -62,7 +64,7 @@
// version information is: .
// Where, the hash is calculated over all syscall names supported by the
// libseccomp library.
-func (c *Compiler) VersionInfo() (string, error) {
+func (c *Compiler) VersionInfo() (VersionInfo, error) {
cmd := exec.Command(c.snapSeccomp, "version-info")
output, err := cmd.CombinedOutput()
if err != nil {
@@ -75,15 +77,44 @@
return "", fmt.Errorf("invalid format of version-info: %q", raw)
}
- return string(raw), nil
+ return VersionInfo(raw), nil
}
+var compilerVersionInfoImpl = func(lookupTool func(name string) (string, error)) (VersionInfo, error) {
+ c, err := New(lookupTool)
+ if err != nil {
+ return VersionInfo(""), err
+ }
+ return c.VersionInfo()
+}
+
+// CompilerVersionInfo returns the version information of snap-seccomp
+// looked up via lookupTool.
+func CompilerVersionInfo(lookupTool func(name string) (string, error)) (VersionInfo, error) {
+ return compilerVersionInfoImpl(lookupTool)
+}
+
+// MockCompilerVersionInfo mocks the return value of CompilerVersionInfo.
+func MockCompilerVersionInfo(versionInfo string) (restore func()) {
+ old := compilerVersionInfoImpl
+ compilerVersionInfoImpl = func(_ func(name string) (string, error)) (VersionInfo, error) {
+ return VersionInfo(versionInfo), nil
+ }
+ return func() {
+ compilerVersionInfoImpl = old
+ }
+}
+
+var errEmptyVersionInfo = errors.New("empty version-info")
+
// VersionInfo represents information about the seccomp compilter
type VersionInfo string
-// LibseccompVersion parses VersionInfo and provides the
-// libseccomp version
+// LibseccompVersion parses VersionInfo and provides the libseccomp version
func (vi VersionInfo) LibseccompVersion() (string, error) {
+ if vi == "" {
+ return "", errEmptyVersionInfo
+ }
if match := validVersionInfo.Match([]byte(vi)); !match {
return "", fmt.Errorf("invalid format of version-info: %q", vi)
}
@@ -93,13 +124,16 @@
// Features parses the output of VersionInfo and provides the
// golang seccomp features
func (vi VersionInfo) Features() (string, error) {
+ if vi == "" {
+ return "", errEmptyVersionInfo
+ }
if match := validVersionInfo.Match([]byte(vi)); !match {
return "", fmt.Errorf("invalid format of version-info: %q", vi)
}
return strings.Split(string(vi), " ")[3], nil
}
-// HasGoSeccompFeature parses the output of VersionInfo and answers whether or
+// HasFeature parses the output of VersionInfo and answers whether or
// not golang-seccomp supports the feature
func (vi VersionInfo) HasFeature(feature string) (bool, error) {
features, err := vi.Features()
@@ -114,6 +148,74 @@
return false, nil
}
+// BuildTimeRequirementError represents the error case of a feature
+// that cannot be supported because of unfulfilled build time
+// requirements.
+type BuildTimeRequirementError struct {
+ Feature string
+ Requirements []string
+}
+
+func (e *BuildTimeRequirementError) RequirementsString() string {
+ return strings.Join(e.Requirements, ", ")
+}
+
+func (e *BuildTimeRequirementError) Error() string {
+ return fmt.Sprintf("%s requires a snapd built against %s", e.Feature, e.RequirementsString())
+}
+
+// SupportsRobustArgumentFiltering parses the output of VersionInfo and
+// determines if libseccomp and golang-seccomp are new enough to support robust
+// argument filtering
+func (vi VersionInfo) SupportsRobustArgumentFiltering() error {
+ libseccompVersion, err := vi.LibseccompVersion()
+ if err != nil {
+ return err
+ }
+
+ // Parse
+ tmp := strings.Split(libseccompVersion, ".")
+ maj, err := strconv.Atoi(tmp[0])
+ if err != nil {
+ return fmt.Errorf("cannot obtain seccomp compiler information: %v", err)
+ }
+ min, err := strconv.Atoi(tmp[1])
+ if err != nil {
+ return fmt.Errorf("cannot obtain seccomp compiler information: %v", err)
+ }
+
+ var unfulfilledReqs []string
+
+ // libseccomp < 2.4 has significant argument filtering bugs that we
+ // cannot reliably work around with this feature.
+ if maj < 2 || (maj == 2 && min < 4) {
+ unfulfilledReqs = append(unfulfilledReqs, "libseccomp >= 2.4")
+ }
+
+ // Due to https://github.com/seccomp/libseccomp-golang/issues/22,
+ // golang-seccomp <= 0.9.0 cannot create correct BPFs for this feature.
+ // The package does not contain any version information, but we know
+ // that ActLog was implemented in the library after this issue was
+ // fixed, so base the decision on that. ActLog is first available in
+ // 0.9.1.
+ res, err := vi.HasFeature("bpf-actlog")
+ if err != nil {
+ return err
+ }
+ if !res {
+ unfulfilledReqs = append(unfulfilledReqs, "golang-seccomp >= 0.9.1")
+ }
+
+ if len(unfulfilledReqs) != 0 {
+ return &BuildTimeRequirementError{
+ Feature: "robust argument filtering",
+ Requirements: unfulfilledReqs,
+ }
+ }
+
+ return nil
+}
+
// Compile compiles given source profile and saves the result to the out
// location.
func (c *Compiler) Compile(in, out string) error {
diff -Nru snapd-2.40/sandbox/seccomp/compiler_test.go snapd-2.42.1/sandbox/seccomp/compiler_test.go
--- snapd-2.40/sandbox/seccomp/compiler_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/sandbox/seccomp/compiler_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -86,10 +86,10 @@
v, err := compiler.VersionInfo()
if tc.err != "" {
c.Check(err, ErrorMatches, tc.err)
- c.Check(v, Equals, "")
+ c.Check(v, Equals, seccomp.VersionInfo(""))
} else {
c.Check(err, IsNil)
- c.Check(v, Equals, tc.exp)
+ c.Check(v, Equals, seccomp.VersionInfo(tc.exp))
_, err := seccomp.VersionInfo(v).LibseccompVersion()
c.Check(err, IsNil)
_, err = seccomp.VersionInfo(v).Features()
@@ -103,6 +103,25 @@
}
+func (s *compilerSuite) TestCompilerVersionInfo(c *C) {
+ const vi = "7ac348ac9c934269214b00d1692dfa50d5d4a157 2.3.3 03e996919907bc7163bc83b95bca0ecab31300f20dfa365ea14047c698340e7c bpf-actlog"
+ cmd := testutil.MockCommand(c, "snap-seccomp", fmt.Sprintf(`echo "%s"`, vi))
+
+ vi1, err := seccomp.CompilerVersionInfo(fromCmd(c, cmd))
+ c.Check(err, IsNil)
+ c.Check(vi1, Equals, seccomp.VersionInfo(vi))
+}
+
+func (s *compilerSuite) TestEmptyVersionInfo(c *C) {
+ vi := seccomp.VersionInfo("")
+
+ _, err := vi.LibseccompVersion()
+ c.Check(err, ErrorMatches, "empty version-info")
+
+ _, err = vi.Features()
+ c.Check(err, ErrorMatches, "empty version-info")
+}
+
func (s *compilerSuite) TestVersionInfoUnhappy(c *C) {
cmd := testutil.MockCommand(c, "snap-seccomp", `
if [ "$1" = "version-info" ]; then echo "unknown command version-info"; exit 1; fi
@@ -159,7 +178,7 @@
c.Assert(func() { seccomp.New(nil) }, PanicMatches, "lookup tool func not provided")
}
-func (s *compilerSuite) TestGetLibseccompVersion(c *C) {
+func (s *compilerSuite) TestLibseccompVersion(c *C) {
v, err := seccomp.VersionInfo("a 2.4.1 b -").LibseccompVersion()
c.Assert(err, IsNil)
c.Check(v, Equals, "2.4.1")
@@ -193,7 +212,7 @@
}
}
-func (s *compilerSuite) TestHasGoSeccompFeature(c *C) {
+func (s *compilerSuite) TestHasFeature(c *C) {
for _, tc := range []struct {
v string
f string
@@ -220,3 +239,30 @@
}
}
}
+
+func (s *compilerSuite) TestSupportsRobustArgumentFiltering(c *C) {
+ for _, tc := range []struct {
+ v string
+ err string
+ }{
+ // libseccomp < 2.3.3 and golang-seccomp < 0.9.1
+ {"a 2.3.3 b -", "robust argument filtering requires a snapd built against libseccomp >= 2.4, golang-seccomp >= 0.9.1"},
+ // libseccomp < 2.3.3
+ {"a 2.3.3 b bpf-actlog", "robust argument filtering requires a snapd built against libseccomp >= 2.4"},
+ // golang-seccomp < 0.9.1
+ {"a 2.4.1 b -", "robust argument filtering requires a snapd built against golang-seccomp >= 0.9.1"},
+ {"a 2.4.1 b bpf-other", "robust argument filtering requires a snapd built against golang-seccomp >= 0.9.1"},
+ // libseccomp >= 2.4.1 and golang-seccomp >= 0.9.1
+ {"a 2.4.1 b bpf-actlog", ""},
+ {"a 3.0.0 b bpf-actlog", ""},
+ // invalid
+ {"a 1.2.3 b b@rf", "invalid format of version-info: .*"},
+ } {
+ err := seccomp.VersionInfo(tc.v).SupportsRobustArgumentFiltering()
+ if tc.err == "" {
+ c.Assert(err, IsNil)
+ } else {
+ c.Assert(err, ErrorMatches, tc.err)
+ }
+ }
+}
diff -Nru snapd-2.40/sanity/export_test.go snapd-2.42.1/sanity/export_test.go
--- snapd-2.40/sanity/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/sanity/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -24,6 +24,8 @@
CheckKernelVersion = checkKernelVersion
CheckApparmorUsable = checkApparmorUsable
CheckWSL = checkWSL
+
+ CheckFuse = firstCheckFuse
)
func Checks() []func() error {
@@ -45,3 +47,11 @@
apparmorProfilesPath = old
}
}
+
+func MockFuseBinary(new string) (restore func()) {
+ oldFuseBinary := fuseBinary
+ fuseBinary = new
+ return func() {
+ fuseBinary = oldFuseBinary
+ }
+}
diff -Nru snapd-2.40/sanity/squashfs.go snapd-2.42.1/sanity/squashfs.go
--- snapd-2.40/sanity/squashfs.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/sanity/squashfs.go 2019-10-30 12:17:43.000000000 +0000
@@ -70,7 +70,22 @@
bAEA+f+YuAAQAAA=
`)
+var fuseBinary = "mount.fuse"
+
+func firstCheckFuse() error {
+ if squashfs.NeedsFuse() {
+ if _, err := exec.LookPath(fuseBinary); err != nil {
+ return fmt.Errorf(`The "fuse" filesystem is required on this system but not available. Please try to install the fuse package.`)
+ }
+ }
+ return nil
+}
+
func checkSquashfsMount() error {
+ if err := firstCheckFuse(); err != nil {
+ return err
+ }
+
tmpSquashfsFile, err := ioutil.TempFile("", "sanity-squashfs-")
if err != nil {
return err
@@ -112,7 +127,7 @@
}
defer func() {
- if output, err := exec.Command("umount", tmpMountDir).CombinedOutput(); err != nil {
+ if output, err := exec.Command("umount", "-l", tmpMountDir).CombinedOutput(); err != nil {
// os.RemoveAll(tmpMountDir) will fail here if umount fails
logger.Noticef("cannot unmount sanity check squashfs image: %v", osutil.OutputErr(output, err))
}
diff -Nru snapd-2.40/sanity/squashfs_test.go snapd-2.42.1/sanity/squashfs_test.go
--- snapd-2.40/sanity/squashfs_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/sanity/squashfs_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -29,7 +29,7 @@
)
func (s *sanitySuite) TestCheckSquashfsMountHappy(c *C) {
- restore := squashfs.MockUseFuse(false)
+ restore := squashfs.MockNeedsFuse(false)
defer restore()
// we create a canary.txt with the same prefix as the real one
@@ -51,12 +51,12 @@
{"mount", "-t", "squashfs", squashfsFile, mountPoint},
})
c.Check(mockUmount.Calls(), DeepEquals, [][]string{
- {"umount", mountPoint},
+ {"umount", "-l", mountPoint},
})
}
func (s *sanitySuite) TestCheckSquashfsMountNotHappy(c *C) {
- restore := squashfs.MockUseFuse(false)
+ restore := squashfs.MockNeedsFuse(false)
defer restore()
mockMount := testutil.MockCommand(c, "mount", "echo iz-broken;false")
@@ -79,7 +79,7 @@
}
func (s *sanitySuite) TestCheckSquashfsMountWrongContent(c *C) {
- restore := squashfs.MockUseFuse(false)
+ restore := squashfs.MockNeedsFuse(false)
defer restore()
mockMount := testutil.MockCommand(c, "mount", `echo 'wrong content' > "$4"/canary.txt`)
@@ -96,7 +96,7 @@
}
func (s *sanitySuite) TestCheckSquashfsMountSELinuxContext(c *C) {
- restore := squashfs.MockUseFuse(false)
+ restore := squashfs.MockNeedsFuse(false)
defer restore()
mockMount := testutil.MockCommand(c, "mount", "echo 'mock ran'")
@@ -120,3 +120,30 @@
{"mount", "-t", "squashfs", "-o", "context=system_u:object_r:snappy_snap_t:s0", squashfsFile, mountPoint},
})
}
+
+func (s *sanitySuite) TestCheckFuseNoFuseHappy(c *C) {
+ restore := squashfs.MockNeedsFuse(false)
+ defer restore()
+
+ c.Assert(sanity.CheckFuse(), IsNil)
+}
+
+func (s *sanitySuite) TestCheckFuseNeedsFuseAndHasFuse(c *C) {
+ restore := squashfs.MockNeedsFuse(true)
+ defer restore()
+
+ restore = sanity.MockFuseBinary("true")
+ defer restore()
+
+ c.Assert(sanity.CheckFuse(), IsNil)
+}
+
+func (s *sanitySuite) TestCheckFuseNoDevFuseUnhappy(c *C) {
+ restore := squashfs.MockNeedsFuse(true)
+ defer restore()
+
+ restore = sanity.MockFuseBinary("/it/does/not/exist")
+ defer restore()
+
+ c.Assert(sanity.CheckFuse(), ErrorMatches, `The "fuse" filesystem is required on this system but not available. Please try to install the fuse package.`)
+}
diff -Nru snapd-2.40/seed/export_test.go snapd-2.42.1/seed/export_test.go
--- snapd-2.40/seed/export_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/seed/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,24 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package seed
+
+var (
+ LoadAssertions = loadAssertions
+)
diff -Nru snapd-2.40/seed/helpers.go snapd-2.42.1/seed/helpers.go
--- snapd-2.40/seed/helpers.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/seed/helpers.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,93 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2016-2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package seed
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/sysdb"
+)
+
+var trusted = sysdb.Trusted()
+
+func MockTrusted(mockTrusted []asserts.Assertion) (restore func()) {
+ prevTrusted := trusted
+ trusted = mockTrusted
+ return func() {
+ trusted = prevTrusted
+ }
+}
+
+func newMemAssertionsDB() (db asserts.RODatabase, commitTo func(*asserts.Batch) error, err error) {
+ memDB, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: trusted,
+ })
+ if err != nil {
+ return nil, nil, err
+ }
+
+ commitTo = func(b *asserts.Batch) error {
+ return b.CommitTo(memDB, nil)
+ }
+
+ return memDB, commitTo, nil
+}
+
+func loadAssertions(assertsDir string, loadedFunc func(*asserts.Ref) error) (*asserts.Batch, error) {
+ dc, err := ioutil.ReadDir(assertsDir)
+ if err != nil {
+ if os.IsNotExist(err) {
+ return nil, ErrNoAssertions
+ }
+ return nil, fmt.Errorf("cannot read assertions dir: %s", err)
+ }
+
+ batch := asserts.NewBatch(nil)
+ for _, fi := range dc {
+ fn := filepath.Join(assertsDir, fi.Name())
+ refs, err := readAsserts(batch, fn)
+ if err != nil {
+ return nil, fmt.Errorf("cannot read assertions: %s", err)
+ }
+ if loadedFunc != nil {
+ for _, ref := range refs {
+ if err := loadedFunc(ref); err != nil {
+ return nil, err
+ }
+ }
+ }
+ }
+
+ return batch, nil
+}
+
+func readAsserts(batch *asserts.Batch, fn string) ([]*asserts.Ref, error) {
+ f, err := os.Open(fn)
+ if err != nil {
+ return nil, err
+ }
+ defer f.Close()
+ return batch.AddStream(f)
+}
diff -Nru snapd-2.40/seed/helpers_test.go snapd-2.42.1/seed/helpers_test.go
--- snapd-2.40/seed/helpers_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/seed/helpers_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,163 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package seed_test
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/seed"
+ "github.com/snapcore/snapd/seed/seedtest"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type helpersSuite struct {
+ testutil.BaseTest
+
+ *seedtest.TestingSeed
+ devAcct *asserts.Account
+}
+
+var _ = Suite(&helpersSuite{})
+
+func (s *helpersSuite) SetUpTest(c *C) {
+ s.BaseTest.SetUpTest(c)
+ s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
+
+ s.TestingSeed = &seedtest.TestingSeed{}
+ s.SetupAssertSigning("canonical", s)
+
+ dir := c.MkDir()
+
+ s.SnapsDir = filepath.Join(dir, "snaps")
+ s.AssertsDir = filepath.Join(dir, "assertions")
+ err := os.MkdirAll(s.SnapsDir, 0755)
+ c.Assert(err, IsNil)
+ err = os.MkdirAll(s.AssertsDir, 0755)
+ c.Assert(err, IsNil)
+
+ s.devAcct = assertstest.NewAccount(s.StoreSigning, "developer", map[string]interface{}{
+ "account-id": "developerid",
+ }, "")
+
+}
+
+const fooSnap = `type: app
+name: foo
+version: 1.0
+`
+
+const barSnap = `type: app
+name: bar
+version: 2.0
+`
+
+func (s *helpersSuite) TestLoadAssertionsNoAssertions(c *C) {
+ os.Remove(s.AssertsDir)
+
+ b, err := seed.LoadAssertions(s.AssertsDir, nil)
+ c.Check(err, Equals, seed.ErrNoAssertions)
+ c.Check(b, IsNil)
+}
+
+func (s *helpersSuite) TestLoadAssertions(c *C) {
+ _, fooDecl, fooRev := s.MakeAssertedSnap(c, fooSnap, nil, snap.R(1), "developerid")
+ _, barDecl, barRev := s.MakeAssertedSnap(c, barSnap, nil, snap.R(2), "developerid")
+
+ s.WriteAssertions("ground.asserts", s.StoreSigning.StoreAccountKey(""))
+ s.WriteAssertions("foo.asserts", s.devAcct, fooDecl, fooRev)
+ s.WriteAssertions("bar.asserts", barDecl, barRev)
+
+ b, err := seed.LoadAssertions(s.AssertsDir, nil)
+ c.Assert(err, IsNil)
+
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: s.StoreSigning.Trusted,
+ })
+ c.Assert(err, IsNil)
+
+ err = b.CommitTo(db, nil)
+ c.Assert(err, IsNil)
+
+ _, err = db.Find(asserts.SnapRevisionType, map[string]string{
+ "snap-sha3-384": fooRev.SnapSHA3_384(),
+ })
+ c.Check(err, IsNil)
+
+ _, err = db.Find(asserts.SnapRevisionType, map[string]string{
+ "snap-sha3-384": barRev.SnapSHA3_384(),
+ })
+ c.Check(err, IsNil)
+}
+
+func (s *helpersSuite) TestLoadAssertionsLoadedCallback(c *C) {
+ _, fooDecl, fooRev := s.MakeAssertedSnap(c, fooSnap, nil, snap.R(1), "developerid")
+ _, barDecl, barRev := s.MakeAssertedSnap(c, barSnap, nil, snap.R(2), "developerid")
+
+ s.WriteAssertions("ground.asserts", s.StoreSigning.StoreAccountKey(""))
+ s.WriteAssertions("foo.asserts", s.devAcct, fooDecl, fooRev)
+ s.WriteAssertions("bar.asserts", barDecl, barRev)
+
+ counts := make(map[string]int)
+ seen := make(map[string]bool)
+
+ loaded := func(ref *asserts.Ref) error {
+ if ref.Type == asserts.SnapDeclarationType {
+ seen[ref.PrimaryKey[1]] = true
+ }
+ counts[ref.Type.Name]++
+ return nil
+ }
+
+ _, err := seed.LoadAssertions(s.AssertsDir, loaded)
+ c.Assert(err, IsNil)
+
+ c.Check(seen, DeepEquals, map[string]bool{
+ "bardidididididididididididididid": true,
+ "foodidididididididididididididid": true,
+ })
+
+ // overall
+ c.Check(counts, DeepEquals, map[string]int{
+ "account": 1,
+ "account-key": 1,
+ "snap-declaration": 2,
+ "snap-revision": 2,
+ })
+}
+
+func (s *helpersSuite) TestLoadAssertionsLoadedCallbackError(c *C) {
+ s.WriteAssertions("ground.asserts", s.StoreSigning.StoreAccountKey(""))
+
+ loaded := func(ref *asserts.Ref) error {
+ return fmt.Errorf("boom")
+
+ }
+
+ _, err := seed.LoadAssertions(s.AssertsDir, loaded)
+ c.Assert(err, ErrorMatches, "boom")
+}
diff -Nru snapd-2.40/seed/seed16.go snapd-2.42.1/seed/seed16.go
--- snapd-2.40/seed/seed16.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/seed/seed16.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,294 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package seed
+
+/* ATTN this should *not* use:
+
+* dirs package: it is passed an explicit directory to work on
+
+* release.OnClassic: it assumes classic based on the model classic
+ option; consistency between system and model can/must be enforced
+ elsewhere
+
+*/
+
+import (
+ "fmt"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/snapasserts"
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/naming"
+ "github.com/snapcore/snapd/timings"
+)
+
+type seed16 struct {
+ seedDir string
+
+ db asserts.RODatabase
+
+ model *asserts.Model
+
+ snaps []*Snap
+ essentialSnapsNum int
+
+ usesSnapdSnap bool
+}
+
+func (s *seed16) LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Batch) error) error {
+ if db == nil {
+ // a db was not provided, create an internal temporary one
+ var err error
+ db, commitTo, err = newMemAssertionsDB()
+ if err != nil {
+ return err
+ }
+ }
+
+ assertSeedDir := filepath.Join(s.seedDir, "assertions")
+ // collect assertions and find model assertion
+ var modelRef *asserts.Ref
+ checkForModel := func(ref *asserts.Ref) error {
+ if ref.Type == asserts.ModelType {
+ if modelRef != nil && modelRef.Unique() != ref.Unique() {
+ return fmt.Errorf("cannot have multiple model assertions in seed")
+ }
+ modelRef = ref
+ }
+ return nil
+ }
+
+ batch, err := loadAssertions(assertSeedDir, checkForModel)
+ if err != nil {
+ return err
+ }
+
+ // verify we have one model assertion
+ if modelRef == nil {
+ return fmt.Errorf("seed must have a model assertion")
+ }
+
+ if err := commitTo(batch); err != nil {
+ return err
+ }
+
+ a, err := modelRef.Resolve(db.Find)
+ if err != nil {
+ return fmt.Errorf("internal error: cannot find just added assertion %v: %v", modelRef, err)
+ }
+
+ // remember db for later use
+ s.db = db
+ s.model = a.(*asserts.Model)
+
+ return nil
+}
+
+func (s *seed16) Model() (*asserts.Model, error) {
+ if s.model == nil {
+ return nil, fmt.Errorf("internal error: model assertion unset")
+ }
+ return s.model, nil
+}
+
+func (s *seed16) addSnap(sn *Snap16, tm timings.Measurer) (*Snap, error) {
+ path := filepath.Join(s.seedDir, "snaps", sn.File)
+ seedSnap := &Snap{
+ Path: path,
+ // TODO|XXX: make sure channel is right for pinned tracks
+ Channel: sn.Channel,
+ Classic: sn.Classic,
+ DevMode: sn.DevMode,
+ }
+
+ var sideInfo snap.SideInfo
+ if sn.Unasserted {
+ sideInfo.RealName = sn.Name
+ } else {
+ var si *snap.SideInfo
+ var err error
+ timings.Run(tm, "derive-side-info", fmt.Sprintf("hash and derive side info for snap %q", sn.Name), func(nested timings.Measurer) {
+ si, err = snapasserts.DeriveSideInfo(path, s.db)
+ })
+ if asserts.IsNotFound(err) {
+ return nil, fmt.Errorf("cannot find signatures with metadata for snap %q (%q)", sn.Name, path)
+ }
+ if err != nil {
+ return nil, err
+ }
+ sideInfo = *si
+ sideInfo.Private = sn.Private
+ sideInfo.Contact = sn.Contact
+ }
+
+ seedSnap.SideInfo = &sideInfo
+
+ s.snaps = append(s.snaps, seedSnap)
+
+ return seedSnap, nil
+}
+
+func (s *seed16) LoadMeta(tm timings.Measurer) error {
+ model, err := s.Model()
+ if err != nil {
+ return err
+ }
+
+ seedYamlFile := filepath.Join(s.seedDir, "seed.yaml")
+ if !osutil.FileExists(seedYamlFile) {
+ return ErrNoMeta
+ }
+
+ seedYaml, err := ReadYaml(seedYamlFile)
+ if err != nil {
+ return err
+ }
+ yamlSnaps := seedYaml.Snaps
+
+ required := naming.NewSnapSet(model.RequiredWithEssentialSnaps())
+ seeding := make(map[string]*Snap16, len(yamlSnaps))
+ for _, sn := range yamlSnaps {
+ seeding[sn.Name] = sn
+ }
+ added := make(map[string]bool, 3)
+ classic := model.Classic()
+ _, s.usesSnapdSnap = seeding["snapd"]
+
+ baseSnap := "core"
+ classicWithSnapd := false
+ if model.Base() != "" {
+ baseSnap = model.Base()
+ }
+ if classic && s.usesSnapdSnap {
+ classicWithSnapd = true
+ // there is no system-wide base as such
+ // if there is a gadget we will install its base first though
+ baseSnap = ""
+ }
+
+ // add the essential snaps
+ addEssential := func(snapName string) (*Snap, error) {
+ // be idempotent
+ if added[snapName] {
+ return nil, nil
+ }
+ yamlSnap := seeding[snapName]
+ if yamlSnap == nil {
+ return nil, fmt.Errorf("essential snap %q required by the model is missing in the seed", snapName)
+ }
+
+ seedSnap, err := s.addSnap(yamlSnap, tm)
+ if err != nil {
+ return nil, err
+ }
+
+ seedSnap.Essential = true
+ seedSnap.Required = true
+ added[snapName] = true
+
+ return seedSnap, nil
+ }
+
+ // if there are snaps to seed, core/base needs to be seeded too
+ if len(yamlSnaps) != 0 {
+ // ensure "snapd" snap is installed first
+ if model.Base() != "" || classicWithSnapd {
+ if _, err := addEssential("snapd"); err != nil {
+ return err
+ }
+ }
+ if !classicWithSnapd {
+ if _, err := addEssential(baseSnap); err != nil {
+ return err
+ }
+ }
+ }
+
+ if kernelName := model.Kernel(); kernelName != "" {
+ if _, err := addEssential(kernelName); err != nil {
+ return err
+ }
+ }
+
+ if gadgetName := model.Gadget(); gadgetName != "" {
+ gadget, err := addEssential(gadgetName)
+ if err != nil {
+ return err
+ }
+
+ // always make sure the base of gadget is installed first
+ snapf, err := snap.Open(gadget.Path)
+ if err != nil {
+ return err
+ }
+ info, err := snap.ReadInfoFromSnapFile(snapf, gadget.SideInfo)
+ if err != nil {
+ return err
+ }
+ gadgetBase := info.Base
+ if gadgetBase == "" {
+ gadgetBase = "core"
+ }
+ // Sanity check
+ // TODO: do we want to relax this? the new logic would allow
+ // but it might just be confusing for now
+ if baseSnap != "" && gadgetBase != baseSnap {
+ return fmt.Errorf("cannot use gadget snap because its base %q is different from model base %q", gadgetBase, model.Base())
+ }
+ if _, err = addEssential(gadgetBase); err != nil {
+ return err
+ }
+ }
+
+ s.essentialSnapsNum = len(s.snaps)
+
+ // the rest of the snaps
+ for _, sn := range yamlSnaps {
+ if added[sn.Name] {
+ continue
+ }
+ seedSnap, err := s.addSnap(sn, tm)
+ if err != nil {
+ return err
+ }
+ if required.Contains(seedSnap) {
+ seedSnap.Required = true
+ }
+ }
+
+ return nil
+}
+
+func (s *seed16) UsesSnapdSnap() bool {
+ return s.usesSnapdSnap
+}
+
+func (s *seed16) EssentialSnaps() []*Snap {
+ return s.snaps[:s.essentialSnapsNum]
+}
+
+func (s *seed16) ModeSnaps(mode string) ([]*Snap, error) {
+ if mode != "run" {
+ return nil, fmt.Errorf("internal error: Core 16/18 have only run mode, got: %s", mode)
+ }
+ return s.snaps[s.essentialSnapsNum:], nil
+}
diff -Nru snapd-2.40/seed/seed16_test.go snapd-2.42.1/seed/seed16_test.go
--- snapd-2.40/seed/seed16_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/seed/seed16_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,1058 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package seed_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+ "gopkg.in/yaml.v2"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/seed"
+ "github.com/snapcore/snapd/seed/seedtest"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/testutil"
+ "github.com/snapcore/snapd/timings"
+)
+
+type seed16Suite struct {
+ testutil.BaseTest
+
+ *seedtest.TestingSeed
+ devAcct *asserts.Account
+
+ seedDir string
+
+ seed16 seed.Seed
+
+ db *asserts.Database
+
+ perfTimings timings.Measurer
+}
+
+var _ = Suite(&seed16Suite{})
+
+var (
+ brandPrivKey, _ = assertstest.GenerateKey(752)
+)
+
+func (s *seed16Suite) SetUpTest(c *C) {
+ s.BaseTest.SetUpTest(c)
+ s.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
+
+ s.TestingSeed = &seedtest.TestingSeed{}
+ s.SetupAssertSigning("canonical", s)
+ s.Brands.Register("my-brand", brandPrivKey, map[string]interface{}{
+ "verification": "verified",
+ })
+
+ s.seedDir = c.MkDir()
+
+ s.SnapsDir = filepath.Join(s.seedDir, "snaps")
+ s.AssertsDir = filepath.Join(s.seedDir, "assertions")
+
+ s.devAcct = assertstest.NewAccount(s.StoreSigning, "developer", map[string]interface{}{
+ "account-id": "developerid",
+ }, "")
+ assertstest.AddMany(s.StoreSigning, s.devAcct)
+
+ seed16, err := seed.Open(s.seedDir)
+ c.Assert(err, IsNil)
+ s.seed16 = seed16
+
+ db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{
+ Backstore: asserts.NewMemoryBackstore(),
+ Trusted: s.StoreSigning.Trusted,
+ })
+ c.Assert(err, IsNil)
+ s.db = db
+
+ s.perfTimings = timings.New(nil)
+}
+
+func (s *seed16Suite) commitTo(b *asserts.Batch) error {
+ return b.CommitTo(s.db, nil)
+}
+
+func (s *seed16Suite) TestLoadAssertionsNoAssertions(c *C) {
+ c.Check(s.seed16.LoadAssertions(s.db, s.commitTo), Equals, seed.ErrNoAssertions)
+}
+
+func (s *seed16Suite) TestLoadAssertionsNoModelAssertion(c *C) {
+ err := os.Mkdir(s.AssertsDir, 0755)
+ c.Assert(err, IsNil)
+
+ c.Check(s.seed16.LoadAssertions(s.db, s.commitTo), ErrorMatches, "seed must have a model assertion")
+}
+
+func (s *seed16Suite) TestLoadAssertionsTwoModelAssertionsError(c *C) {
+ err := os.Mkdir(s.AssertsDir, 0755)
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ }
+ modelChain := s.MakeModelAssertionChain("my-brand", "my-model", headers)
+ s.WriteAssertions("model.asserts", modelChain...)
+ modelChain = s.MakeModelAssertionChain("my-brand", "my-model-2", headers)
+ s.WriteAssertions("model2.asserts", modelChain...)
+
+ c.Check(s.seed16.LoadAssertions(s.db, s.commitTo), ErrorMatches, "cannot have multiple model assertions in seed")
+}
+
+func (s *seed16Suite) TestLoadAssertionsConsistencyError(c *C) {
+ err := os.Mkdir(s.AssertsDir, 0755)
+ c.Assert(err, IsNil)
+
+ // write out only the model assertion
+ headers := map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ }
+ model := s.Brands.Model("my-brand", "my-model", headers)
+ s.WriteAssertions("model.asserts", model)
+
+ c.Check(s.seed16.LoadAssertions(s.db, s.commitTo), ErrorMatches, "cannot resolve prerequisite assertion: account-key .*")
+}
+
+func (s *seed16Suite) TestLoadAssertionsModelHappy(c *C) {
+ err := os.Mkdir(s.AssertsDir, 0755)
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ }
+ modelChain := s.MakeModelAssertionChain("my-brand", "my-model", headers)
+ s.WriteAssertions("model.asserts", modelChain...)
+
+ err = s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ model, err := s.seed16.Model()
+ c.Assert(err, IsNil)
+ c.Check(model.Model(), Equals, "my-model")
+
+ _, err = s.db.Find(asserts.ModelType, map[string]string{
+ "series": "16",
+ "brand-id": "my-brand",
+ "model": "my-model",
+ })
+ c.Assert(err, IsNil)
+}
+
+func (s *seed16Suite) TestLoadAssertionsModelTempDBHappy(c *C) {
+ r := seed.MockTrusted(s.StoreSigning.Trusted)
+ defer r()
+
+ err := os.Mkdir(s.AssertsDir, 0755)
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ }
+ modelChain := s.MakeModelAssertionChain("my-brand", "my-model", headers)
+ s.WriteAssertions("model.asserts", modelChain...)
+
+ err = s.seed16.LoadAssertions(nil, nil)
+ c.Assert(err, IsNil)
+
+ model, err := s.seed16.Model()
+ c.Assert(err, IsNil)
+ c.Check(model.Model(), Equals, "my-model")
+}
+
+func (s *seed16Suite) TestSkippedLoadAssertion(c *C) {
+ _, err := s.seed16.Model()
+ c.Check(err, ErrorMatches, "internal error: model assertion unset")
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Check(err, ErrorMatches, "internal error: model assertion unset")
+}
+
+func (s *seed16Suite) TestLoadMetaNoMeta(c *C) {
+ err := os.Mkdir(s.AssertsDir, 0755)
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ }
+ modelChain := s.MakeModelAssertionChain("my-brand", "my-model", headers)
+ s.WriteAssertions("model.asserts", modelChain...)
+
+ err = s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Check(err, Equals, seed.ErrNoMeta)
+}
+
+func (s *seed16Suite) TestLoadMetaInvalidSeedYaml(c *C) {
+ err := os.Mkdir(s.AssertsDir, 0755)
+ c.Assert(err, IsNil)
+
+ headers := map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ }
+ modelChain := s.MakeModelAssertionChain("my-brand", "my-model", headers)
+ s.WriteAssertions("model.asserts", modelChain...)
+
+ err = s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ // create a seed.yaml
+ content, err := yaml.Marshal(map[string]interface{}{
+ "snaps": []*seed.Snap16{{
+ Name: "core",
+ Channel: "track/not-a-risk",
+ }},
+ })
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(s.seedDir, "seed.yaml"), content, 0644)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Check(err, ErrorMatches, `cannot read seed yaml: invalid risk in channel name: track/not-a-risk`)
+}
+
+var snapYaml = map[string]string{
+ "core": `name: core
+type: os
+version: 1.0
+`,
+ "pc-kernel": `name: pc-kernel
+type: kernel
+version: 1.0
+`,
+ "pc": `name: pc
+type: gadget
+version: 1.0
+`,
+ "required": `name: required
+type: app
+version: 1.0
+`,
+ "snapd": `name: snapd
+type: snapd
+version: 1.0
+`,
+ "core18": `name: core18
+type: base
+version: 1.0
+`,
+ "pc-kernel=18": `name: pc-kernel
+type: kernel
+version: 1.0
+`,
+ "pc=18": `name: pc
+type: gadget
+base: core18
+version: 1.0
+`,
+ "required18": `name: required18
+type: app
+base: core18
+version: 1.0
+`,
+ "classic-snap": `name: classic-snap
+type: app
+confinement: classic
+version: 1.0
+`,
+ "classic-gadget": `name: classic-gadget
+type: gadget
+version: 1.0
+`,
+ "classic-gadget18": `name: classic-gadget18
+type: gadget
+base: core18
+version: 1.0
+`,
+ "private-snap": `name: private-snap
+base: core18
+version: 1.0
+`,
+ "contactable-snap": `name: contactable-snap
+base: core18
+version: 1.0
+`,
+}
+
+var snapPublishers = map[string]string{
+ "required": "developerid",
+}
+
+var (
+ coreSeed = &seed.Snap16{
+ Name: "core",
+ Channel: "stable",
+ }
+ kernelSeed = &seed.Snap16{
+ Name: "pc-kernel",
+ Channel: "stable",
+ }
+ gadgetSeed = &seed.Snap16{
+ Name: "pc",
+ Channel: "stable",
+ }
+ requiredSeed = &seed.Snap16{
+ Name: "required",
+ Channel: "stable",
+ }
+ // Core 18
+ snapdSeed = &seed.Snap16{
+ Name: "snapd",
+ Channel: "stable",
+ }
+ core18Seed = &seed.Snap16{
+ Name: "core18",
+ Channel: "stable",
+ }
+ kernel18Seed = &seed.Snap16{
+ Name: "pc-kernel",
+ Channel: "18",
+ }
+ gadget18Seed = &seed.Snap16{
+ Name: "pc",
+ Channel: "18",
+ }
+ required18Seed = &seed.Snap16{
+ Name: "required18",
+ Channel: "stable",
+ }
+ classicSnapSeed = &seed.Snap16{
+ Name: "classic-snap",
+ Channel: "stable",
+ Classic: true,
+ }
+ classicGadgetSeed = &seed.Snap16{
+ Name: "classic-gadget",
+ Channel: "stable",
+ }
+ classicGadget18Seed = &seed.Snap16{
+ Name: "classic-gadget18",
+ Channel: "stable",
+ }
+ privateSnapSeed = &seed.Snap16{
+ Name: "private-snap",
+ Channel: "stable",
+ Private: true,
+ }
+ contactableSnapSeed = &seed.Snap16{
+ Name: "contactable-snap",
+ Channel: "stable",
+ Contact: "author@example.com",
+ }
+)
+
+func (s *seed16Suite) makeSeed(c *C, modelHeaders map[string]interface{}, seedSnaps ...*seed.Snap16) []*seed.Snap16 {
+ coreHeaders := map[string]interface{}{
+ "architecture": "amd64",
+ }
+
+ if _, ok := modelHeaders["classic"]; !ok {
+ coreHeaders["kernel"] = "pc-kernel"
+ coreHeaders["gadget"] = "pc"
+ }
+
+ err := os.Mkdir(s.AssertsDir, 0755)
+ c.Assert(err, IsNil)
+
+ modelChain := s.MakeModelAssertionChain("my-brand", "my-model", coreHeaders, modelHeaders)
+ s.WriteAssertions("model.asserts", modelChain...)
+
+ err = os.Mkdir(s.SnapsDir, 0755)
+ c.Assert(err, IsNil)
+
+ var completeSeedSnaps []*seed.Snap16
+ for _, seedSnap := range seedSnaps {
+ completeSeedSnap := *seedSnap
+ var snapFname string
+ if seedSnap.Unasserted {
+ mockSnapFile := snaptest.MakeTestSnapWithFiles(c, snapYaml[seedSnap.Name], nil)
+ snapFname = filepath.Base(mockSnapFile)
+ err := os.Rename(mockSnapFile, filepath.Join(s.seedDir, "snaps", snapFname))
+ c.Assert(err, IsNil)
+ } else {
+ publisher := snapPublishers[seedSnap.Name]
+ if publisher == "" {
+ publisher = "canonical"
+ }
+ whichYaml := seedSnap.Name
+ if seedSnap.Channel != "stable" {
+ whichYaml = whichYaml + "=" + seedSnap.Channel
+ }
+ fname, decl, rev := s.MakeAssertedSnap(c, snapYaml[whichYaml], nil, snap.R(1), publisher)
+ acct, err := s.StoreSigning.Find(asserts.AccountType, map[string]string{"account-id": publisher})
+ c.Assert(err, IsNil)
+ s.WriteAssertions(fmt.Sprintf("%s.asserts", seedSnap.Name), rev, decl, acct)
+ snapFname = fname
+ }
+ completeSeedSnap.File = snapFname
+ completeSeedSnaps = append(completeSeedSnaps, &completeSeedSnap)
+ }
+
+ // create a seed.yaml
+ content, err := yaml.Marshal(map[string]interface{}{
+ "snaps": completeSeedSnaps,
+ })
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(s.seedDir, "seed.yaml"), content, 0644)
+ c.Assert(err, IsNil)
+
+ return completeSeedSnaps
+}
+
+func (s *seed16Suite) expectedPath(snapName string) string {
+ return filepath.Join(s.seedDir, "snaps", filepath.Base(s.AssertedSnap(snapName)))
+}
+
+func (s *seed16Suite) TestLoadMetaCore16Minimal(c *C) {
+ s.makeSeed(c, nil, coreSeed, kernelSeed, gadgetSeed)
+
+ err := s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Assert(err, IsNil)
+
+ c.Check(s.seed16.UsesSnapdSnap(), Equals, false)
+
+ essSnaps := s.seed16.EssentialSnaps()
+ c.Check(essSnaps, HasLen, 3)
+
+ c.Check(essSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("core"),
+ SideInfo: &s.AssertedSnapInfo("core").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("pc-kernel"),
+ SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("pc"),
+ SideInfo: &s.AssertedSnapInfo("pc").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ },
+ })
+
+ runSnaps, err := s.seed16.ModeSnaps("run")
+ c.Assert(err, IsNil)
+ c.Check(runSnaps, HasLen, 0)
+}
+
+func (s *seed16Suite) TestLoadMetaCore16(c *C) {
+ s.makeSeed(c, map[string]interface{}{
+ "required-snaps": []interface{}{"required"},
+ }, coreSeed, kernelSeed, gadgetSeed, requiredSeed)
+
+ err := s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Assert(err, IsNil)
+
+ essSnaps := s.seed16.EssentialSnaps()
+ c.Check(essSnaps, HasLen, 3)
+
+ runSnaps, err := s.seed16.ModeSnaps("run")
+ c.Assert(err, IsNil)
+ c.Check(runSnaps, HasLen, 1)
+
+ c.Check(runSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("required"),
+ SideInfo: &s.AssertedSnapInfo("required").SideInfo,
+ Required: true,
+ Channel: "stable",
+ },
+ })
+}
+
+func (s *seed16Suite) TestLoadMetaCore18Minimal(c *C) {
+ s.makeSeed(c, map[string]interface{}{
+ "base": "core18",
+ "kernel": "pc-kernel=18",
+ "gadget": "pc=18",
+ }, snapdSeed, core18Seed, kernel18Seed, gadget18Seed)
+
+ err := s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Assert(err, IsNil)
+
+ c.Check(s.seed16.UsesSnapdSnap(), Equals, true)
+
+ essSnaps := s.seed16.EssentialSnaps()
+ c.Check(essSnaps, HasLen, 4)
+
+ c.Check(essSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("snapd"),
+ SideInfo: &s.AssertedSnapInfo("snapd").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("core18"),
+ SideInfo: &s.AssertedSnapInfo("core18").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("pc-kernel"),
+ SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "18",
+ }, {
+ Path: s.expectedPath("pc"),
+ SideInfo: &s.AssertedSnapInfo("pc").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "18",
+ },
+ })
+
+ runSnaps, err := s.seed16.ModeSnaps("run")
+ c.Assert(err, IsNil)
+ c.Check(runSnaps, HasLen, 0)
+}
+
+func (s *seed16Suite) TestLoadMetaCore18(c *C) {
+ s.makeSeed(c, map[string]interface{}{
+ "base": "core18",
+ "kernel": "pc-kernel=18",
+ "gadget": "pc=18",
+ "required-snaps": []interface{}{"core", "required", "required18"},
+ }, snapdSeed, core18Seed, kernel18Seed, gadget18Seed, requiredSeed, coreSeed, required18Seed)
+
+ err := s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Assert(err, IsNil)
+
+ essSnaps := s.seed16.EssentialSnaps()
+ c.Check(essSnaps, HasLen, 4)
+
+ c.Check(essSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("snapd"),
+ SideInfo: &s.AssertedSnapInfo("snapd").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("core18"),
+ SideInfo: &s.AssertedSnapInfo("core18").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("pc-kernel"),
+ SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "18",
+ }, {
+ Path: s.expectedPath("pc"),
+ SideInfo: &s.AssertedSnapInfo("pc").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "18",
+ },
+ })
+
+ runSnaps, err := s.seed16.ModeSnaps("run")
+ c.Assert(err, IsNil)
+ c.Check(runSnaps, HasLen, 3)
+
+ // these are not sorted by type, firstboot will do that
+ c.Check(runSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("required"),
+ SideInfo: &s.AssertedSnapInfo("required").SideInfo,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("core"),
+ SideInfo: &s.AssertedSnapInfo("core").SideInfo,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("required18"),
+ SideInfo: &s.AssertedSnapInfo("required18").SideInfo,
+ Required: true,
+ Channel: "stable",
+ },
+ })
+}
+
+func (s *seed16Suite) TestLoadMetaClassicNothing(c *C) {
+ s.makeSeed(c, map[string]interface{}{
+ "classic": "true",
+ })
+
+ err := s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Assert(err, IsNil)
+
+ c.Check(s.seed16.UsesSnapdSnap(), Equals, false)
+
+ essSnaps := s.seed16.EssentialSnaps()
+ c.Check(essSnaps, HasLen, 0)
+
+ runSnaps, err := s.seed16.ModeSnaps("run")
+ c.Assert(err, IsNil)
+ c.Check(runSnaps, HasLen, 0)
+}
+
+func (s *seed16Suite) TestLoadMetaClassicCore(c *C) {
+ s.makeSeed(c, map[string]interface{}{
+ "classic": "true",
+ }, coreSeed, classicSnapSeed)
+
+ err := s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Assert(err, IsNil)
+
+ c.Check(s.seed16.UsesSnapdSnap(), Equals, false)
+
+ essSnaps := s.seed16.EssentialSnaps()
+ c.Check(essSnaps, HasLen, 1)
+ c.Check(essSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("core"),
+ SideInfo: &s.AssertedSnapInfo("core").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ },
+ })
+
+ // classic-snap is not required, just an extra snap
+ runSnaps, err := s.seed16.ModeSnaps("run")
+ c.Assert(err, IsNil)
+ c.Check(runSnaps, HasLen, 1)
+ c.Check(runSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("classic-snap"),
+ SideInfo: &s.AssertedSnapInfo("classic-snap").SideInfo,
+ Channel: "stable",
+ Classic: true,
+ },
+ })
+}
+
+func (s *seed16Suite) TestLoadMetaClassicCoreWithGadget(c *C) {
+ s.makeSeed(c, map[string]interface{}{
+ "classic": "true",
+ "gadget": "classic-gadget",
+ }, coreSeed, classicGadgetSeed)
+
+ err := s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Assert(err, IsNil)
+
+ c.Check(s.seed16.UsesSnapdSnap(), Equals, false)
+
+ essSnaps := s.seed16.EssentialSnaps()
+ c.Check(essSnaps, HasLen, 2)
+ c.Check(essSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("core"),
+ SideInfo: &s.AssertedSnapInfo("core").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ },
+ {
+ Path: s.expectedPath("classic-gadget"),
+ SideInfo: &s.AssertedSnapInfo("classic-gadget").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ },
+ })
+
+ runSnaps, err := s.seed16.ModeSnaps("run")
+ c.Assert(err, IsNil)
+ c.Check(runSnaps, HasLen, 0)
+}
+
+func (s *seed16Suite) TestLoadMetaClassicSnapd(c *C) {
+ s.makeSeed(c, map[string]interface{}{
+ "classic": "true",
+ "required-snaps": []interface{}{"core18", "required18"},
+ }, snapdSeed, core18Seed, required18Seed)
+
+ err := s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Assert(err, IsNil)
+
+ c.Check(s.seed16.UsesSnapdSnap(), Equals, true)
+
+ essSnaps := s.seed16.EssentialSnaps()
+ c.Check(essSnaps, HasLen, 1)
+ c.Check(essSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("snapd"),
+ SideInfo: &s.AssertedSnapInfo("snapd").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ },
+ })
+
+ runSnaps, err := s.seed16.ModeSnaps("run")
+ c.Assert(err, IsNil)
+ c.Check(runSnaps, HasLen, 2)
+ c.Check(runSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("core18"),
+ SideInfo: &s.AssertedSnapInfo("core18").SideInfo,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("required18"),
+ SideInfo: &s.AssertedSnapInfo("required18").SideInfo,
+ Required: true,
+ Channel: "stable",
+ },
+ })
+}
+
+func (s *seed16Suite) TestLoadMetaClassicSnapdWithGadget(c *C) {
+ s.makeSeed(c, map[string]interface{}{
+ "classic": "true",
+ "gadget": "classic-gadget",
+ }, snapdSeed, classicGadgetSeed, coreSeed)
+
+ err := s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Assert(err, IsNil)
+
+ c.Check(s.seed16.UsesSnapdSnap(), Equals, true)
+
+ essSnaps := s.seed16.EssentialSnaps()
+ c.Check(essSnaps, HasLen, 3)
+ c.Check(essSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("snapd"),
+ SideInfo: &s.AssertedSnapInfo("snapd").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("classic-gadget"),
+ SideInfo: &s.AssertedSnapInfo("classic-gadget").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("core"),
+ SideInfo: &s.AssertedSnapInfo("core").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ },
+ })
+
+ runSnaps, err := s.seed16.ModeSnaps("run")
+ c.Assert(err, IsNil)
+ c.Check(runSnaps, HasLen, 0)
+}
+
+func (s *seed16Suite) TestLoadMetaClassicSnapdWithGadget18(c *C) {
+ s.makeSeed(c, map[string]interface{}{
+ "classic": "true",
+ "gadget": "classic-gadget18",
+ "required-snaps": []interface{}{"core", "required"},
+ }, snapdSeed, coreSeed, requiredSeed, classicGadget18Seed, core18Seed)
+
+ err := s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Assert(err, IsNil)
+
+ c.Check(s.seed16.UsesSnapdSnap(), Equals, true)
+
+ essSnaps := s.seed16.EssentialSnaps()
+ c.Check(essSnaps, HasLen, 3)
+ c.Check(essSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("snapd"),
+ SideInfo: &s.AssertedSnapInfo("snapd").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("classic-gadget18"),
+ SideInfo: &s.AssertedSnapInfo("classic-gadget18").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("core18"),
+ SideInfo: &s.AssertedSnapInfo("core18").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ },
+ })
+
+ runSnaps, err := s.seed16.ModeSnaps("run")
+ c.Assert(err, IsNil)
+ c.Check(runSnaps, HasLen, 2)
+ c.Check(runSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("core"),
+ SideInfo: &s.AssertedSnapInfo("core").SideInfo,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("required"),
+ SideInfo: &s.AssertedSnapInfo("required").SideInfo,
+ Required: true,
+ Channel: "stable",
+ },
+ })
+}
+
+func (s *seed16Suite) TestLoadMetaCore18Local(c *C) {
+ localRequired18Seed := &seed.Snap16{
+ Name: "required18",
+ Unasserted: true,
+ DevMode: true,
+ }
+ s.makeSeed(c, map[string]interface{}{
+ "base": "core18",
+ "kernel": "pc-kernel=18",
+ "gadget": "pc=18",
+ "required-snaps": []interface{}{"core", "required18"},
+ }, snapdSeed, core18Seed, kernel18Seed, gadget18Seed, localRequired18Seed)
+
+ err := s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Assert(err, IsNil)
+
+ essSnaps := s.seed16.EssentialSnaps()
+ c.Check(essSnaps, HasLen, 4)
+
+ c.Check(essSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("snapd"),
+ SideInfo: &s.AssertedSnapInfo("snapd").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("core18"),
+ SideInfo: &s.AssertedSnapInfo("core18").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("pc-kernel"),
+ SideInfo: &s.AssertedSnapInfo("pc-kernel").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "18",
+ }, {
+ Path: s.expectedPath("pc"),
+ SideInfo: &s.AssertedSnapInfo("pc").SideInfo,
+ Essential: true,
+ Required: true,
+ Channel: "18",
+ },
+ })
+
+ runSnaps, err := s.seed16.ModeSnaps("run")
+ c.Assert(err, IsNil)
+ c.Check(runSnaps, HasLen, 1)
+
+ c.Check(runSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: filepath.Join(s.seedDir, "snaps", "required18_1.0_all.snap"),
+ SideInfo: &snap.SideInfo{RealName: "required18"},
+ Required: true,
+ DevMode: true,
+ },
+ })
+}
+
+func (s *seed16Suite) TestLoadMetaCore18StoreInfo(c *C) {
+ s.makeSeed(c, map[string]interface{}{
+ "base": "core18",
+ "kernel": "pc-kernel=18",
+ "gadget": "pc=18",
+ }, snapdSeed, core18Seed, kernel18Seed, gadget18Seed, privateSnapSeed, contactableSnapSeed)
+
+ err := s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = s.seed16.LoadMeta(s.perfTimings)
+ c.Assert(err, IsNil)
+
+ essSnaps := s.seed16.EssentialSnaps()
+ c.Check(essSnaps, HasLen, 4)
+
+ runSnaps, err := s.seed16.ModeSnaps("run")
+ c.Assert(err, IsNil)
+ c.Check(runSnaps, HasLen, 2)
+
+ privateSnapSideInfo := s.AssertedSnapInfo("private-snap").SideInfo
+ privateSnapSideInfo.Private = true
+ contactableSnapSideInfo := s.AssertedSnapInfo("contactable-snap").SideInfo
+ contactableSnapSideInfo.Contact = "author@example.com"
+
+ // these are not sorted by type, firstboot will do that
+ c.Check(runSnaps, DeepEquals, []*seed.Snap{
+ {
+ Path: s.expectedPath("private-snap"),
+ SideInfo: &privateSnapSideInfo,
+ Channel: "stable",
+ }, {
+ Path: s.expectedPath("contactable-snap"),
+ SideInfo: &contactableSnapSideInfo,
+ Channel: "stable",
+ },
+ })
+}
+
+func (s *seed16Suite) TestLoadMetaBrokenSeed(c *C) {
+ seedSnap16s := s.makeSeed(c, map[string]interface{}{
+ "base": "core18",
+ "kernel": "pc-kernel=18",
+ "gadget": "pc=18",
+ "required-snaps": []interface{}{"required18"},
+ }, snapdSeed, core18Seed, kernel18Seed, gadget18Seed, required18Seed)
+
+ otherSnapFile := snaptest.MakeTestSnapWithFiles(c, `name: other
+version: other`, nil)
+ otherFname := filepath.Base(otherSnapFile)
+ err := os.Rename(otherSnapFile, filepath.Join(s.seedDir, "snaps", otherFname))
+ c.Assert(err, IsNil)
+
+ const otherBaseGadget = `name: pc
+type: gadget
+base: other-base
+version: other-base
+`
+ otherBaseGadgetFname, obgDecl, obgRev := s.MakeAssertedSnap(c, otherBaseGadget, nil, snap.R(3), "canonical")
+ s.WriteAssertions("other-gadget.asserts", obgDecl, obgRev)
+
+ err = s.seed16.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ omit := func(which int) func([]*seed.Snap16) []*seed.Snap16 {
+ return func(snaps []*seed.Snap16) []*seed.Snap16 {
+ broken := make([]*seed.Snap16, 0, len(snaps)-1)
+ for i, sn := range snaps {
+ if i == which {
+ continue
+ }
+ broken = append(broken, sn)
+ }
+ return broken
+ }
+ }
+ replaceFile := func(snapName, fname string) func([]*seed.Snap16) []*seed.Snap16 {
+ return func(snaps []*seed.Snap16) []*seed.Snap16 {
+ for i := range snaps {
+ if snaps[i].Name != snapName {
+ continue
+ }
+ sn := *snaps[i]
+ sn.File = fname
+ snaps[i] = &sn
+ }
+ return snaps
+ }
+ }
+
+ tests := []struct {
+ breakSeed func([]*seed.Snap16) []*seed.Snap16
+ err string
+ }{
+ {omit(0), `essential snap "snapd" required by the model is missing in the seed`},
+ {omit(1), `essential snap "core18" required by the model is missing in the seed`},
+ {omit(2), `essential snap "pc-kernel" required by the model is missing in the seed`},
+ {omit(3), `essential snap "pc" required by the model is missing in the seed`},
+ // omitting "required18" currently doesn't error in any way
+ {replaceFile("core18", otherFname), `cannot find signatures with metadata for snap "core18".*`},
+ {replaceFile("required18", otherFname), `cannot find signatures with metadata for snap "required18".*`},
+ {replaceFile("core18", "not-existent"), `cannot compute snap .* digest: .*`},
+ {replaceFile("pc", otherBaseGadgetFname), `cannot use gadget snap because its base "other-base" is different from model base "core18"`},
+ }
+
+ for _, t := range tests {
+ testSeedSnap16s := make([]*seed.Snap16, 5)
+ copy(testSeedSnap16s, seedSnap16s)
+
+ testSeedSnap16s = t.breakSeed(testSeedSnap16s)
+ content, err := yaml.Marshal(map[string]interface{}{
+ "snaps": testSeedSnap16s,
+ })
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(filepath.Join(s.seedDir, "seed.yaml"), content, 0644)
+ c.Assert(err, IsNil)
+
+ c.Check(s.seed16.LoadMeta(s.perfTimings), ErrorMatches, t.err)
+ }
+}
diff -Nru snapd-2.40/seed/seed.go snapd-2.42.1/seed/seed.go
--- snapd-2.40/seed/seed.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/seed/seed.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,98 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+// Package seed implements loading and validating of seed data.
+package seed
+
+import (
+ "errors"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/timings"
+)
+
+var (
+ ErrNoAssertions = errors.New("no seed assertions")
+ ErrNoMeta = errors.New("no seed metadata")
+)
+
+// Snap holds the details of a snap in a seed.
+type Snap struct {
+ Path string
+
+ SideInfo *snap.SideInfo
+
+ Essential bool
+ Required bool
+
+ // options
+ Channel string
+ DevMode bool
+ Classic bool
+}
+
+func (s *Snap) SnapName() string {
+ return s.SideInfo.RealName
+}
+
+func (s *Snap) ID() string {
+ return s.SideInfo.SnapID
+}
+
+// Seed supports loading assertions and seed snaps' metadata.
+type Seed interface {
+ // LoadAssertions loads all assertions from the seed with
+ // cross-checks. A read-only view on an assertions database
+ // can be passed in together with a commitTo function which
+ // will be used to commit the assertions to the underlying
+ // database. If db is nil an internal temporary database will
+ // be setup instead. ErrNoAssertions will be returned if there
+ // is no assertions directory in the seed, this is legitimate
+ // only on classic.
+ LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Batch) error) error
+
+ // Model returns the seed provided model assertion. It is an
+ // error to call Model before LoadAssertions.
+ Model() (*asserts.Model, error)
+
+ // LoadMeta loads the seed and seed's snaps metadata. It can
+ // return ErrNoMeta if there is no metadata nor snaps in the
+ // seed, this is legitimate only on classic. It is an error to
+ // call LoadMeta before LoadAssertions.
+ LoadMeta(tm timings.Measurer) error
+
+ // UsesSnapdSnap returns whether the system as defined by the
+ // seed will use the snapd snap, after LoadMeta.
+ UsesSnapdSnap() bool
+
+ // EssentialSnaps returns the essential snaps as defined by
+ // the seed, after LoadMeta.
+ EssentialSnaps() []*Snap
+
+ // ModeSnaps returns the snaps that should be available
+ // in the given mode as defined by the seed, after LoadMeta.
+ ModeSnaps(mode string) ([]*Snap, error)
+}
+
+// Open returns a Seed implementation for the seed at seedDir.
+// TODO: more parameters for the Core20 case
+func Open(seedDir string) (Seed, error) {
+ return &seed16{seedDir: seedDir}, nil
+}
diff -Nru snapd-2.40/seed/seedtest/seedtest.go snapd-2.42.1/seed/seedtest/seedtest.go
--- snapd-2.40/seed/seedtest/seedtest.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/seed/seedtest/seedtest.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,177 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2015-2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package seedtest
+
+import (
+ "fmt"
+ "os"
+ "path/filepath"
+ "strings"
+ "time"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/asserts"
+ "github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/asserts/sysdb"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+)
+
+// SeedSnaps helps creating snaps for a seed.
+type SeedSnaps struct {
+ StoreSigning *assertstest.StoreStack
+ Brands *assertstest.SigningAccounts
+
+ // DB will be populated with snap assertions if not nil
+ DB *asserts.Database
+
+ snaps map[string]string
+ infos map[string]*snap.Info
+}
+
+type Cleaner interface {
+ AddCleanup(func())
+}
+
+// SetupAssertSigning initializes StoreSigning for storeBrandID and Brands.
+func (ss *SeedSnaps) SetupAssertSigning(storeBrandID string, cleaner Cleaner) {
+ ss.StoreSigning = assertstest.NewStoreStack(storeBrandID, nil)
+ cleaner.AddCleanup(sysdb.InjectTrusted(ss.StoreSigning.Trusted))
+
+ ss.Brands = assertstest.NewSigningAccounts(ss.StoreSigning)
+}
+
+func (ss *SeedSnaps) AssertedSnapID(snapName string) string {
+ cleanedName := strings.Replace(snapName, "-", "", -1)
+ return (cleanedName + strings.Repeat("id", 16)[len(cleanedName):])
+}
+
+func (ss *SeedSnaps) MakeAssertedSnap(c *C, snapYaml string, files [][]string, revision snap.Revision, developerID string) (*asserts.SnapDeclaration, *asserts.SnapRevision) {
+ info, err := snap.InfoFromSnapYaml([]byte(snapYaml))
+ c.Assert(err, IsNil)
+ snapName := info.SnapName()
+
+ snapFile := snaptest.MakeTestSnapWithFiles(c, snapYaml, files)
+
+ snapID := ss.AssertedSnapID(snapName)
+ declA, err := ss.StoreSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": snapID,
+ "publisher-id": developerID,
+ "snap-name": snapName,
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ sha3_384, size, err := asserts.SnapFileSHA3_384(snapFile)
+ c.Assert(err, IsNil)
+
+ revA, err := ss.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
+ "snap-sha3-384": sha3_384,
+ "snap-size": fmt.Sprintf("%d", size),
+ "snap-id": snapID,
+ "developer-id": developerID,
+ "snap-revision": revision.String(),
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ if !revision.Unset() {
+ info.SnapID = snapID
+ info.Revision = revision
+ }
+
+ if ss.DB != nil {
+ err := ss.DB.Add(declA)
+ c.Assert(err, IsNil)
+ err = ss.DB.Add(revA)
+ c.Assert(err, IsNil)
+ }
+
+ if ss.snaps == nil {
+ ss.snaps = make(map[string]string)
+ ss.infos = make(map[string]*snap.Info)
+ }
+
+ ss.snaps[snapName] = snapFile
+ info.SideInfo.RealName = snapName
+ ss.infos[snapName] = info
+
+ return declA.(*asserts.SnapDeclaration), revA.(*asserts.SnapRevision)
+}
+
+func (ss *SeedSnaps) AssertedSnap(snapName string) (snapFile string) {
+ return ss.snaps[snapName]
+}
+
+func (ss *SeedSnaps) AssertedSnapInfo(snapName string) *snap.Info {
+ return ss.infos[snapName]
+}
+
+// TestingSeed helps setting up a populated testing seed.
+type TestingSeed struct {
+ SeedSnaps
+
+ SnapsDir string
+ AssertsDir string
+}
+
+func (s *TestingSeed) MakeAssertedSnap(c *C, snapYaml string, files [][]string, revision snap.Revision, developerID string) (snapFname string, snapDecl *asserts.SnapDeclaration, snapRev *asserts.SnapRevision) {
+ decl, rev := s.SeedSnaps.MakeAssertedSnap(c, snapYaml, files, revision, developerID)
+
+ snapFile := s.snaps[decl.SnapName()]
+
+ snapFname = filepath.Base(snapFile)
+ targetFile := filepath.Join(s.SnapsDir, snapFname)
+ err := os.Rename(snapFile, targetFile)
+ c.Assert(err, IsNil)
+
+ return snapFname, decl, rev
+}
+
+func (s *TestingSeed) MakeModelAssertionChain(brandID, model string, extras ...map[string]interface{}) []asserts.Assertion {
+ assertChain := []asserts.Assertion{}
+ modelA := s.Brands.Model(brandID, model, extras...)
+
+ assertChain = append(assertChain, s.Brands.Account(modelA.BrandID()))
+ assertChain = append(assertChain, s.Brands.AccountKey(modelA.BrandID()))
+ assertChain = append(assertChain, modelA)
+
+ storeAccountKey := s.StoreSigning.StoreAccountKey("")
+ assertChain = append(assertChain, storeAccountKey)
+ return assertChain
+}
+
+func (s *TestingSeed) WriteAssertions(fn string, assertions ...asserts.Assertion) {
+ multifn := filepath.Join(s.AssertsDir, fn)
+ f, err := os.OpenFile(multifn, os.O_CREATE|os.O_WRONLY, 0644)
+ if err != nil {
+ panic(err)
+ }
+ defer f.Close()
+ enc := asserts.NewEncoder(f)
+ for _, a := range assertions {
+ err := enc.Encode(a)
+ if err != nil {
+ panic(err)
+ }
+ }
+}
diff -Nru snapd-2.40/seed/seed_yaml.go snapd-2.42.1/seed/seed_yaml.go
--- snapd-2.40/seed/seed_yaml.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/seed/seed_yaml.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,120 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package seed
+
+import (
+ "fmt"
+ "io/ioutil"
+ "strings"
+
+ "gopkg.in/yaml.v2"
+
+ "github.com/snapcore/snapd/osutil"
+ "github.com/snapcore/snapd/snap/channel"
+ "github.com/snapcore/snapd/snap/naming"
+)
+
+// Snap points to a snap in the seed to install, together with
+// assertions (or alone if unasserted is true) it will be used to
+// drive the installation and ultimately set SideInfo/SnapState for it.
+// TODO: make this internal
+type Snap16 struct {
+ Name string `yaml:"name"`
+
+ // cross-reference/audit
+ SnapID string `yaml:"snap-id,omitempty"`
+
+ // bits that are orthongonal/not in assertions
+ Channel string `yaml:"channel,omitempty"`
+ DevMode bool `yaml:"devmode,omitempty"`
+ Classic bool `yaml:"classic,omitempty"`
+
+ Private bool `yaml:"private,omitempty"`
+
+ Contact string `yaml:"contact,omitempty"`
+
+ // no assertions are available in the seed for this snap
+ Unasserted bool `yaml:"unasserted,omitempty"`
+
+ File string `yaml:"file"`
+}
+
+// TODO: make all of this internal only
+
+type Seed16 struct {
+ Snaps []*Snap16 `yaml:"snaps"`
+}
+
+func ReadYaml(fn string) (*Seed16, error) {
+ errPrefix := "cannot read seed yaml"
+
+ yamlData, err := ioutil.ReadFile(fn)
+ if err != nil {
+ return nil, fmt.Errorf("%s: %v", errPrefix, err)
+ }
+
+ var seed Seed16
+ if err := yaml.Unmarshal(yamlData, &seed); err != nil {
+ return nil, fmt.Errorf("%s: cannot unmarshal %q: %s", errPrefix, yamlData, err)
+ }
+
+ seenNames := make(map[string]bool, len(seed.Snaps))
+ // validate
+ for _, sn := range seed.Snaps {
+ if sn == nil {
+ return nil, fmt.Errorf("%s: empty element in seed", errPrefix)
+ }
+ // TODO: check if it's a parallel install explicitly,
+ // need to move *Instance* helpers from snap to naming
+ if err := naming.ValidateSnap(sn.Name); err != nil {
+ return nil, fmt.Errorf("%s: %v", errPrefix, err)
+ }
+ if sn.Channel != "" {
+ if _, err := channel.Parse(sn.Channel, ""); err != nil {
+ return nil, fmt.Errorf("%s: %v", errPrefix, err)
+ }
+ }
+ if sn.File == "" {
+ return nil, fmt.Errorf(`%s: "file" attribute for %q cannot be empty`, errPrefix, sn.Name)
+ }
+ if strings.Contains(sn.File, "/") {
+ return nil, fmt.Errorf("%s: %q must be a filename, not a path", errPrefix, sn.File)
+ }
+
+ // make sure names and file names are unique
+ if seenNames[sn.Name] {
+ return nil, fmt.Errorf("%s: snap name %q must be unique", errPrefix, sn.Name)
+ }
+ seenNames[sn.Name] = true
+ }
+
+ return &seed, nil
+}
+
+func (seed *Seed16) Write(seedFn string) error {
+ data, err := yaml.Marshal(&seed)
+ if err != nil {
+ return err
+ }
+ if err := osutil.AtomicWriteFile(seedFn, data, 0644, 0); err != nil {
+ return err
+ }
+ return nil
+}
diff -Nru snapd-2.40/seed/seed_yaml_test.go snapd-2.42.1/seed/seed_yaml_test.go
--- snapd-2.40/seed/seed_yaml_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/seed/seed_yaml_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,166 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2016 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package seed_test
+
+import (
+ "io/ioutil"
+ "path/filepath"
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/seed"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type seedYamlTestSuite struct{}
+
+var _ = Suite(&seedYamlTestSuite{})
+
+var mockSeedYaml = []byte(`
+snaps:
+ - name: foo
+ snap-id: snapidsnapidsnapid
+ channel: stable
+ devmode: true
+ file: foo_1.0_all.snap
+ - name: local
+ unasserted: true
+ file: local.snap
+`)
+
+func (s *seedYamlTestSuite) TestSimple(c *C) {
+ fn := filepath.Join(c.MkDir(), "seed.yaml")
+ err := ioutil.WriteFile(fn, mockSeedYaml, 0644)
+ c.Assert(err, IsNil)
+
+ seedYaml, err := seed.ReadYaml(fn)
+ c.Assert(err, IsNil)
+ c.Assert(seedYaml.Snaps, HasLen, 2)
+ c.Assert(seedYaml.Snaps[0], DeepEquals, &seed.Snap16{
+ File: "foo_1.0_all.snap",
+ Name: "foo",
+ SnapID: "snapidsnapidsnapid",
+
+ Channel: "stable",
+ DevMode: true,
+ })
+ c.Assert(seedYaml.Snaps[1], DeepEquals, &seed.Snap16{
+ File: "local.snap",
+ Name: "local",
+ Unasserted: true,
+ })
+}
+
+var badMockSeedYaml = []byte(`
+snaps:
+ - name: foo
+ file: foo/bar.snap
+`)
+
+func (s *seedYamlTestSuite) TestNoPathAllowed(c *C) {
+ fn := filepath.Join(c.MkDir(), "seed.yaml")
+ err := ioutil.WriteFile(fn, badMockSeedYaml, 0644)
+ c.Assert(err, IsNil)
+
+ _, err = seed.ReadYaml(fn)
+ c.Assert(err, ErrorMatches, `cannot read seed yaml: "foo/bar.snap" must be a filename, not a path`)
+}
+
+func (s *seedYamlTestSuite) TestDuplicatedSnapName(c *C) {
+ fn := filepath.Join(c.MkDir(), "seed.yaml")
+ err := ioutil.WriteFile(fn, []byte(`
+snaps:
+ - name: foo
+ channel: stable
+ file: foo_1.0_all.snap
+ - name: foo
+ channel: edge
+ file: bar_1.0_all.snap
+`), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = seed.ReadYaml(fn)
+ c.Assert(err, ErrorMatches, `cannot read seed yaml: snap name "foo" must be unique`)
+}
+
+func (s *seedYamlTestSuite) TestValidateChannelUnhappy(c *C) {
+ fn := filepath.Join(c.MkDir(), "seed.yaml")
+ err := ioutil.WriteFile(fn, []byte(`
+snaps:
+ - name: foo
+ channel: invalid/channel/
+`), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = seed.ReadYaml(fn)
+ c.Assert(err, ErrorMatches, `cannot read seed yaml: invalid risk in channel name: invalid/channel/`)
+}
+
+func (s *seedYamlTestSuite) TestValidateNameUnhappy(c *C) {
+ fn := filepath.Join(c.MkDir(), "seed.yaml")
+ err := ioutil.WriteFile(fn, []byte(`
+snaps:
+ - name: invalid--name
+ file: ./foo.snap
+`), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = seed.ReadYaml(fn)
+ c.Assert(err, ErrorMatches, `cannot read seed yaml: invalid snap name: "invalid--name"`)
+}
+
+func (s *seedYamlTestSuite) TestValidateNameInstanceUnsupported(c *C) {
+ fn := filepath.Join(c.MkDir(), "seed.yaml")
+ err := ioutil.WriteFile(fn, []byte(`
+snaps:
+ - name: foo_1
+ file: ./foo.snap
+`), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = seed.ReadYaml(fn)
+ c.Assert(err, ErrorMatches, `cannot read seed yaml: invalid snap name: "foo_1"`)
+}
+
+func (s *seedYamlTestSuite) TestValidateNameMissing(c *C) {
+ fn := filepath.Join(c.MkDir(), "seed.yaml")
+ err := ioutil.WriteFile(fn, []byte(`
+snaps:
+ - file: ./foo.snap
+`), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = seed.ReadYaml(fn)
+ c.Assert(err, ErrorMatches, `cannot read seed yaml: invalid snap name: ""`)
+}
+
+func (s *seedYamlTestSuite) TestValidateFileMissing(c *C) {
+ fn := filepath.Join(c.MkDir(), "seed.yaml")
+ err := ioutil.WriteFile(fn, []byte(`
+snaps:
+ - name: foo
+`), 0644)
+ c.Assert(err, IsNil)
+
+ _, err = seed.ReadYaml(fn)
+ c.Assert(err, ErrorMatches, `cannot read seed yaml: "file" attribute for "foo" cannot be empty`)
+}
diff -Nru snapd-2.40/seed/validate.go snapd-2.42.1/seed/validate.go
--- snapd-2.40/seed/validate.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/seed/validate.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,74 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2014-2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package seed
+
+import (
+ "bytes"
+ "fmt"
+ "path/filepath"
+
+ "github.com/snapcore/snapd/snap"
+)
+
+// ValidateFromYaml validates the given seed.yaml file and surrounding seed.
+func ValidateFromYaml(seedYamlFile string) error {
+ seed, err := ReadYaml(seedYamlFile)
+ if err != nil {
+ return err
+ }
+
+ var errs []error
+ // read the snaps info
+ snapInfos := make(map[string]*snap.Info)
+ for _, seedSnap := range seed.Snaps {
+ fn := filepath.Join(filepath.Dir(seedYamlFile), "snaps", seedSnap.File)
+ snapf, err := snap.Open(fn)
+ if err != nil {
+ errs = append(errs, err)
+ } else {
+ info, err := snap.ReadInfoFromSnapFile(snapf, nil)
+ if err != nil {
+ errs = append(errs, fmt.Errorf("cannot use snap %s: %v", fn, err))
+ } else {
+ snapInfos[info.InstanceName()] = info
+ }
+ }
+ }
+
+ // ensure we have either "core" or "snapd"
+ _, haveCore := snapInfos["core"]
+ _, haveSnapd := snapInfos["snapd"]
+ if !(haveCore || haveSnapd) {
+ errs = append(errs, fmt.Errorf("the core or snapd snap must be part of the seed"))
+ }
+
+ if errs2 := snap.ValidateBasesAndProviders(snapInfos); errs2 != nil {
+ errs = append(errs, errs2...)
+ }
+ if errs != nil {
+ var buf bytes.Buffer
+ for _, err := range errs {
+ fmt.Fprintf(&buf, "\n- %s", err)
+ }
+ return fmt.Errorf("cannot validate seed:%s", buf.Bytes())
+ }
+
+ return nil
+}
diff -Nru snapd-2.40/seed/validate_test.go snapd-2.42.1/seed/validate_test.go
--- snapd-2.40/seed/validate_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/seed/validate_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,264 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package seed_test
+
+import (
+ "fmt"
+ "io/ioutil"
+ "os"
+ "path/filepath"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/seed"
+ "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/snaptest"
+ "github.com/snapcore/snapd/snap/squashfs"
+ "github.com/snapcore/snapd/testutil"
+)
+
+type validateSuite struct {
+ testutil.BaseTest
+ root string
+}
+
+var _ = Suite(&validateSuite{})
+
+var coreYaml = `name: core
+version: 1.0
+type: os
+`
+
+const snapdYaml = `name: snapd
+version: 1.0
+type: snapd
+`
+
+const packageCore18 = `
+name: core18
+version: 18.04
+type: base
+`
+
+func (s *validateSuite) SetUpTest(c *C) {
+ s.BaseTest.SetUpTest(c)
+ s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}))
+
+ s.root = c.MkDir()
+
+ err := os.MkdirAll(filepath.Join(s.root, "snaps"), 0755)
+ c.Assert(err, IsNil)
+}
+
+func (s *validateSuite) makeSnapInSeed(c *C, snapYaml string) {
+ info, err := snap.InfoFromSnapYaml([]byte(snapYaml))
+ c.Assert(err, IsNil)
+
+ src := snaptest.MakeTestSnapWithFiles(c, snapYaml, nil)
+ dst := filepath.Join(s.root, "snaps", fmt.Sprintf("%s_%s.snap", info.SnapName(), snap.R(1)))
+ c.Assert(os.Rename(src, dst), IsNil)
+}
+
+func (s *validateSuite) makeSeedYaml(c *C, seedYaml string) string {
+ tmpf := filepath.Join(s.root, "seed.yaml")
+ err := ioutil.WriteFile(tmpf, []byte(seedYaml), 0644)
+ c.Assert(err, IsNil)
+ return tmpf
+}
+
+func (s *validateSuite) TestValidateFromYamlSnapHappy(c *C) {
+ s.makeSnapInSeed(c, coreYaml)
+ s.makeSnapInSeed(c, `name: gtk-common-themes
+version: 19.04`)
+ seedFn := s.makeSeedYaml(c, `
+snaps:
+ - name: core
+ channel: stable
+ file: core_1.snap
+ - name: gtk-common-themes
+ channel: stable/ubuntu-19.04
+ file: gtk-common-themes_1.snap
+`)
+
+ err := seed.ValidateFromYaml(seedFn)
+ c.Assert(err, IsNil)
+}
+
+func (s *validateSuite) TestValidateFromYamlSnapMissingBase(c *C) {
+ s.makeSnapInSeed(c, `name: need-base
+base: some-base
+version: 1.0`)
+ s.makeSnapInSeed(c, coreYaml)
+ seedFn := s.makeSeedYaml(c, `
+snaps:
+ - name: core
+ file: core_1.snap
+ - name: need-base
+ file: need-base_1.snap
+`)
+
+ err := seed.ValidateFromYaml(seedFn)
+ c.Assert(err, ErrorMatches, `cannot validate seed:
+- cannot use snap "need-base": base "some-base" is missing`)
+}
+
+func (s *validateSuite) TestValidateFromYamlSnapMissingDefaultProvider(c *C) {
+ s.makeSnapInSeed(c, coreYaml)
+ s.makeSnapInSeed(c, `name: need-df
+version: 1.0
+plugs:
+ gtk-3-themes:
+ interface: content
+ default-provider: gtk-common-themes
+`)
+ seedFn := s.makeSeedYaml(c, `
+snaps:
+ - name: core
+ file: core_1.snap
+ - name: need-df
+ file: need-df_1.snap
+`)
+
+ err := seed.ValidateFromYaml(seedFn)
+ c.Assert(err, ErrorMatches, `cannot validate seed:
+- cannot use snap "need-df": default provider "gtk-common-themes" is missing`)
+}
+
+func (s *validateSuite) TestValidateFromYamlSnapSnapdHappy(c *C) {
+ s.makeSnapInSeed(c, snapdYaml)
+ s.makeSnapInSeed(c, packageCore18)
+ s.makeSnapInSeed(c, `name: some-snap
+version: 1.0
+base: core18
+`)
+ seedFn := s.makeSeedYaml(c, `
+snaps:
+ - name: snapd
+ file: snapd_1.snap
+ - name: some-snap
+ file: some-snap_1.snap
+ - name: core18
+ file: core18_1.snap
+`)
+
+ err := seed.ValidateFromYaml(seedFn)
+ c.Assert(err, IsNil)
+}
+
+func (s *validateSuite) TestValidateFromYamlSnapMissingCore(c *C) {
+ s.makeSnapInSeed(c, snapdYaml)
+ s.makeSnapInSeed(c, `name: some-snap
+version: 1.0`)
+ seedFn := s.makeSeedYaml(c, `
+snaps:
+ - name: snapd
+ file: snapd_1.snap
+ - name: some-snap
+ file: some-snap_1.snap
+`)
+
+ err := seed.ValidateFromYaml(seedFn)
+ c.Assert(err, ErrorMatches, `cannot validate seed:
+- cannot use snap "some-snap": required snap "core" missing`)
+}
+
+func (s *validateSuite) TestValidateFromYamlSnapMissingSnapdAndCore(c *C) {
+ s.makeSnapInSeed(c, packageCore18)
+ s.makeSnapInSeed(c, `name: some-snap
+version: 1.0
+base: core18`)
+ seedFn := s.makeSeedYaml(c, `
+snaps:
+ - name: some-snap
+ file: some-snap_1.snap
+ - name: core18
+ file: core18_1.snap
+`)
+
+ err := seed.ValidateFromYaml(seedFn)
+ c.Assert(err, ErrorMatches, `cannot validate seed:
+- the core or snapd snap must be part of the seed`)
+}
+
+func (s *validateSuite) TestValidateFromYamlSnapMultipleErrors(c *C) {
+ s.makeSnapInSeed(c, `name: some-snap
+version: 1.0`)
+ seedFn := s.makeSeedYaml(c, `
+snaps:
+ - name: some-snap
+ file: some-snap_1.snap
+`)
+
+ err := seed.ValidateFromYaml(seedFn)
+ c.Assert(err, ErrorMatches, `cannot validate seed:
+- the core or snapd snap must be part of the seed
+- cannot use snap "some-snap": required snap "core" missing`)
+}
+
+func (s *validateSuite) TestValidateFromYamlSnapSnapMissing(c *C) {
+ s.makeSnapInSeed(c, coreYaml)
+ seedFn := s.makeSeedYaml(c, `
+snaps:
+ - name: core
+ file: core_1.snap
+ - name: some-snap
+ file: some-snap_1.snap
+`)
+
+ err := seed.ValidateFromYaml(seedFn)
+ c.Assert(err, ErrorMatches, `cannot validate seed:
+- cannot open snap: open /.*/snaps/some-snap_1.snap: no such file or directory`)
+}
+
+func (s *validateSuite) TestValidateFromYamlSnapSnapInvalid(c *C) {
+ s.makeSnapInSeed(c, coreYaml)
+
+ // "version" is missing in this yaml
+ snapBuildDir := c.MkDir()
+ snapYaml := `name: some-snap-invalid-yaml`
+ metaSnapYaml := filepath.Join(snapBuildDir, "meta", "snap.yaml")
+ err := os.MkdirAll(filepath.Dir(metaSnapYaml), 0755)
+ c.Assert(err, IsNil)
+ err = ioutil.WriteFile(metaSnapYaml, []byte(snapYaml), 0644)
+ c.Assert(err, IsNil)
+
+ // need to build the snap "manually" pack.Snap() will do validation
+ snapFilePath := filepath.Join(c.MkDir(), "some-snap-invalid-yaml_1.snap")
+ d := squashfs.New(snapFilePath)
+ err = d.Build(snapBuildDir, "app")
+ c.Assert(err, IsNil)
+
+ // put the broken snap in place
+ dst := filepath.Join(s.root, "snaps", "some-snap-invalid-yaml_1.snap")
+ err = os.Rename(snapFilePath, dst)
+ c.Assert(err, IsNil)
+
+ seedFn := s.makeSeedYaml(c, `
+snaps:
+ - name: core
+ file: core_1.snap
+ - name: some-snap-invalid-yaml
+ file: some-snap-invalid-yaml_1.snap
+`)
+
+ err = seed.ValidateFromYaml(seedFn)
+ c.Assert(err, ErrorMatches, `cannot validate seed:
+- cannot use snap /.*/snaps/some-snap-invalid-yaml_1.snap: invalid snap version: cannot be empty`)
+}
diff -Nru snapd-2.40/snap/channel/channel.go snapd-2.42.1/snap/channel/channel.go
--- snapd-2.40/snap/channel/channel.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/snap/channel/channel.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,264 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018-2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package channel
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+
+ "github.com/snapcore/snapd/arch"
+ "github.com/snapcore/snapd/strutil"
+)
+
+var channelRisks = []string{"stable", "candidate", "beta", "edge"}
+
+// Channel identifies and describes completely a store channel.
+type Channel struct {
+ Architecture string `json:"architecture"`
+ Name string `json:"name"`
+ Track string `json:"track"`
+ Risk string `json:"risk"`
+ Branch string `json:"branch,omitempty"`
+}
+
+// ParseVerbatim parses a string representing a store channel and
+// includes the given architecture, if architecture is "" the system
+// architecture is included. The channel representation is not normalized.
+// Parse() should be used in most cases.
+func ParseVerbatim(s string, architecture string) (Channel, error) {
+ if s == "" {
+ return Channel{}, fmt.Errorf("channel name cannot be empty")
+ }
+ p := strings.Split(s, "/")
+ var risk, track, branch *string
+ switch len(p) {
+ default:
+ return Channel{}, fmt.Errorf("channel name has too many components: %s", s)
+ case 3:
+ track, risk, branch = &p[0], &p[1], &p[2]
+ case 2:
+ if strutil.ListContains(channelRisks, p[0]) {
+ risk, branch = &p[0], &p[1]
+ } else {
+ track, risk = &p[0], &p[1]
+ }
+ case 1:
+ if strutil.ListContains(channelRisks, p[0]) {
+ risk = &p[0]
+ } else {
+ track = &p[0]
+ }
+ }
+
+ if architecture == "" {
+ architecture = arch.DpkgArchitecture()
+ }
+
+ ch := Channel{
+ Architecture: architecture,
+ }
+
+ if risk != nil {
+ if !strutil.ListContains(channelRisks, *risk) {
+ return Channel{}, fmt.Errorf("invalid risk in channel name: %s", s)
+ }
+ ch.Risk = *risk
+ }
+ if track != nil {
+ if *track == "" {
+ return Channel{}, fmt.Errorf("invalid track in channel name: %s", s)
+ }
+ ch.Track = *track
+ }
+ if branch != nil {
+ if *branch == "" {
+ return Channel{}, fmt.Errorf("invalid branch in channel name: %s", s)
+ }
+ ch.Branch = *branch
+ }
+
+ return ch, nil
+}
+
+// Parse parses a string representing a store channel and includes given
+// architecture, , if architecture is "" the system architecture is included.
+// The returned channel's track, risk and name are normalized.
+func Parse(s string, architecture string) (Channel, error) {
+ channel, err := ParseVerbatim(s, architecture)
+ if err != nil {
+ return Channel{}, err
+ }
+ return channel.Clean(), nil
+}
+
+// Clean returns a Channel with a normalized track, risk and name.
+func (c Channel) Clean() Channel {
+ track := c.Track
+ risk := c.Risk
+
+ if track == "latest" {
+ track = ""
+ }
+ if risk == "" {
+ risk = "stable"
+ }
+
+ // normalized name
+ name := risk
+ if track != "" {
+ name = track + "/" + name
+ }
+ if c.Branch != "" {
+ name = name + "/" + c.Branch
+ }
+
+ return Channel{
+ Architecture: c.Architecture,
+ Name: name,
+ Track: track,
+ Risk: risk,
+ Branch: c.Branch,
+ }
+}
+
+func (c Channel) String() string {
+ return c.Name
+}
+
+// Full returns the full name of the channel, inclusive the default track "latest".
+func (c *Channel) Full() string {
+ if c.Track == "" {
+ return "latest/" + c.Name
+ }
+ return c.String()
+}
+
+// VerbatimTrackOnly returns whether the channel represents a track only.
+func (c *Channel) VerbatimTrackOnly() bool {
+ return c.Track != "" && c.Risk == "" && c.Branch == ""
+}
+
+// VerbatimRiskOnly returns whether the channel represents a risk only.
+func (c *Channel) VerbatimRiskOnly() bool {
+ return c.Track == "" && c.Risk != "" && c.Branch == ""
+}
+
+func riskLevel(risk string) int {
+ for i, r := range channelRisks {
+ if r == risk {
+ return i
+ }
+ }
+ return -1
+}
+
+// ChannelMatch represents on which fields two channels are matching.
+type ChannelMatch struct {
+ Architecture bool
+ Track bool
+ Risk bool
+}
+
+// String returns the string represantion of the match, results can be:
+// "architecture:track:risk"
+// "architecture:track"
+// "architecture:risk"
+// "track:risk"
+// "architecture"
+// "track"
+// "risk"
+// ""
+func (cm ChannelMatch) String() string {
+ matching := []string{}
+ if cm.Architecture {
+ matching = append(matching, "architecture")
+ }
+ if cm.Track {
+ matching = append(matching, "track")
+ }
+ if cm.Risk {
+ matching = append(matching, "risk")
+ }
+ return strings.Join(matching, ":")
+
+}
+
+// Match returns a ChannelMatch of which fields among architecture,track,risk match between c and c1 store channels, risk is matched taking channel inheritance into account and considering c the requested channel.
+func (c *Channel) Match(c1 *Channel) ChannelMatch {
+ requestedRiskLevel := riskLevel(c.Risk)
+ rl1 := riskLevel(c1.Risk)
+ return ChannelMatch{
+ Architecture: c.Architecture == c1.Architecture,
+ Track: c.Track == c1.Track,
+ Risk: requestedRiskLevel >= rl1,
+ }
+}
+
+// Resolve resolves newChannel wrt channel, this means if newChannel
+// is risk/branch only it will preserve the track of channel. It
+// assumes that if both are not empty, channel is parseable.
+func Resolve(channel, newChannel string) (string, error) {
+ if newChannel == "" {
+ return channel, nil
+ }
+ if channel == "" {
+ return newChannel, nil
+ }
+ ch, err := ParseVerbatim(channel, "-")
+ if err != nil {
+ return "", err
+ }
+ p := strings.Split(newChannel, "/")
+ if strutil.ListContains(channelRisks, p[0]) && ch.Track != "" {
+ // risk/branch inherits the track if any
+ return ch.Track + "/" + newChannel, nil
+ }
+ return newChannel, nil
+}
+
+var ErrLockedTrackSwitch = errors.New("cannot switch locked track")
+
+// ResolveLocked resolves newChannel wrt a locked track, newChannel
+// can only be risk/branch-only or have the same track, otherwise
+// ErrLockedTrackSwitch is returned.
+func ResolveLocked(track, newChannel string) (string, error) {
+ if track == "" {
+ return newChannel, nil
+ }
+ ch, err := ParseVerbatim(track, "-")
+ if err != nil || !ch.VerbatimTrackOnly() {
+ return "", fmt.Errorf("invalid locked track: %s", track)
+ }
+ if newChannel == "" {
+ return track, nil
+ }
+ trackPrefix := ch.Track + "/"
+ p := strings.Split(newChannel, "/")
+ if strutil.ListContains(channelRisks, p[0]) && ch.Track != "" {
+ // risk/branch inherits the track if any
+ return trackPrefix + newChannel, nil
+ }
+ if newChannel != track && !strings.HasPrefix(newChannel, trackPrefix) {
+ // the track is locked/pinned
+ return "", ErrLockedTrackSwitch
+ }
+ return newChannel, nil
+}
diff -Nru snapd-2.40/snap/channel/channel_test.go snapd-2.42.1/snap/channel/channel_test.go
--- snapd-2.40/snap/channel/channel_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/snap/channel/channel_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,355 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2018 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package channel_test
+
+import (
+ "testing"
+
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/arch"
+ "github.com/snapcore/snapd/snap/channel"
+)
+
+func Test(t *testing.T) { TestingT(t) }
+
+type storeChannelSuite struct{}
+
+var _ = Suite(&storeChannelSuite{})
+
+func (s storeChannelSuite) TestParse(c *C) {
+ ch, err := channel.Parse("stable", "")
+ c.Assert(err, IsNil)
+ c.Check(ch, DeepEquals, channel.Channel{
+ Architecture: arch.DpkgArchitecture(),
+ Name: "stable",
+ Track: "",
+ Risk: "stable",
+ Branch: "",
+ })
+
+ ch, err = channel.Parse("latest/stable", "")
+ c.Assert(err, IsNil)
+ c.Check(ch, DeepEquals, channel.Channel{
+ Architecture: arch.DpkgArchitecture(),
+ Name: "stable",
+ Track: "",
+ Risk: "stable",
+ Branch: "",
+ })
+
+ ch, err = channel.Parse("1.0/edge", "")
+ c.Assert(err, IsNil)
+ c.Check(ch, DeepEquals, channel.Channel{
+ Architecture: arch.DpkgArchitecture(),
+ Name: "1.0/edge",
+ Track: "1.0",
+ Risk: "edge",
+ Branch: "",
+ })
+
+ ch, err = channel.Parse("1.0", "")
+ c.Assert(err, IsNil)
+ c.Check(ch, DeepEquals, channel.Channel{
+ Architecture: arch.DpkgArchitecture(),
+ Name: "1.0/stable",
+ Track: "1.0",
+ Risk: "stable",
+ Branch: "",
+ })
+
+ ch, err = channel.Parse("1.0/beta/foo", "")
+ c.Assert(err, IsNil)
+ c.Check(ch, DeepEquals, channel.Channel{
+ Architecture: arch.DpkgArchitecture(),
+ Name: "1.0/beta/foo",
+ Track: "1.0",
+ Risk: "beta",
+ Branch: "foo",
+ })
+
+ ch, err = channel.Parse("candidate/foo", "")
+ c.Assert(err, IsNil)
+ c.Check(ch, DeepEquals, channel.Channel{
+ Architecture: arch.DpkgArchitecture(),
+ Name: "candidate/foo",
+ Track: "",
+ Risk: "candidate",
+ Branch: "foo",
+ })
+
+ ch, err = channel.Parse("candidate/foo", "other-arch")
+ c.Assert(err, IsNil)
+ c.Check(ch, DeepEquals, channel.Channel{
+ Architecture: "other-arch",
+ Name: "candidate/foo",
+ Track: "",
+ Risk: "candidate",
+ Branch: "foo",
+ })
+}
+
+func mustParse(c *C, channelStr string) channel.Channel {
+ ch, err := channel.Parse(channelStr, "")
+ c.Assert(err, IsNil)
+ return ch
+}
+
+func (s storeChannelSuite) TestParseVerbatim(c *C) {
+ ch, err := channel.ParseVerbatim("sometrack", "")
+ c.Assert(err, IsNil)
+ c.Check(ch, DeepEquals, channel.Channel{
+ Architecture: arch.DpkgArchitecture(),
+ Track: "sometrack",
+ })
+ c.Check(ch.VerbatimTrackOnly(), Equals, true)
+ c.Check(ch.VerbatimRiskOnly(), Equals, false)
+ c.Check(mustParse(c, "sometrack"), DeepEquals, ch.Clean())
+
+ ch, err = channel.ParseVerbatim("latest", "")
+ c.Assert(err, IsNil)
+ c.Check(ch, DeepEquals, channel.Channel{
+ Architecture: arch.DpkgArchitecture(),
+ Track: "latest",
+ })
+ c.Check(ch.VerbatimTrackOnly(), Equals, true)
+ c.Check(ch.VerbatimRiskOnly(), Equals, false)
+ c.Check(mustParse(c, "latest"), DeepEquals, ch.Clean())
+
+ ch, err = channel.ParseVerbatim("edge", "")
+ c.Assert(err, IsNil)
+ c.Check(ch, DeepEquals, channel.Channel{
+ Architecture: arch.DpkgArchitecture(),
+ Risk: "edge",
+ })
+ c.Check(ch.VerbatimTrackOnly(), Equals, false)
+ c.Check(ch.VerbatimRiskOnly(), Equals, true)
+ c.Check(mustParse(c, "edge"), DeepEquals, ch.Clean())
+
+ ch, err = channel.ParseVerbatim("latest/stable", "")
+ c.Assert(err, IsNil)
+ c.Check(ch, DeepEquals, channel.Channel{
+ Architecture: arch.DpkgArchitecture(),
+ Track: "latest",
+ Risk: "stable",
+ })
+ c.Check(ch.VerbatimTrackOnly(), Equals, false)
+ c.Check(ch.VerbatimRiskOnly(), Equals, false)
+ c.Check(mustParse(c, "latest/stable"), DeepEquals, ch.Clean())
+
+ ch, err = channel.ParseVerbatim("latest/stable/foo", "")
+ c.Assert(err, IsNil)
+ c.Check(ch, DeepEquals, channel.Channel{
+ Architecture: arch.DpkgArchitecture(),
+ Track: "latest",
+ Risk: "stable",
+ Branch: "foo",
+ })
+ c.Check(ch.VerbatimTrackOnly(), Equals, false)
+ c.Check(ch.VerbatimRiskOnly(), Equals, false)
+ c.Check(mustParse(c, "latest/stable/foo"), DeepEquals, ch.Clean())
+}
+
+func (s storeChannelSuite) TestClean(c *C) {
+ ch := channel.Channel{
+ Architecture: "arm64",
+ Track: "latest",
+ Name: "latest/stable",
+ Risk: "stable",
+ }
+
+ cleanedCh := ch.Clean()
+ c.Check(cleanedCh, Not(DeepEquals), c)
+ c.Check(cleanedCh, DeepEquals, channel.Channel{
+ Architecture: "arm64",
+ Track: "",
+ Name: "stable",
+ Risk: "stable",
+ })
+}
+
+func (s storeChannelSuite) TestParseErrors(c *C) {
+ for _, tc := range []struct {
+ channel string
+ err string
+ }{
+ {"", "channel name cannot be empty"},
+ {"1.0////", "channel name has too many components: 1.0////"},
+ {"1.0/cand", "invalid risk in channel name: 1.0/cand"},
+ {"fix//hotfix", "invalid risk in channel name: fix//hotfix"},
+ {"/stable/", "invalid track in channel name: /stable/"},
+ {"//stable", "invalid risk in channel name: //stable"},
+ {"stable/", "invalid branch in channel name: stable/"},
+ {"/stable", "invalid track in channel name: /stable"},
+ } {
+ _, err := channel.Parse(tc.channel, "")
+ c.Check(err, ErrorMatches, tc.err)
+ _, err = channel.ParseVerbatim(tc.channel, "")
+ c.Check(err, ErrorMatches, tc.err)
+ }
+}
+
+func (s *storeChannelSuite) TestString(c *C) {
+ tests := []struct {
+ channel string
+ str string
+ }{
+ {"stable", "stable"},
+ {"latest/stable", "stable"},
+ {"1.0/edge", "1.0/edge"},
+ {"1.0/beta/foo", "1.0/beta/foo"},
+ {"1.0", "1.0/stable"},
+ {"candidate/foo", "candidate/foo"},
+ }
+
+ for _, t := range tests {
+ ch, err := channel.Parse(t.channel, "")
+ c.Assert(err, IsNil)
+
+ c.Check(ch.String(), Equals, t.str)
+ }
+}
+
+func (s *storeChannelSuite) TestFull(c *C) {
+ tests := []struct {
+ channel string
+ str string
+ }{
+ {"stable", "latest/stable"},
+ {"latest/stable", "latest/stable"},
+ {"1.0/edge", "1.0/edge"},
+ {"1.0/beta/foo", "1.0/beta/foo"},
+ {"1.0", "1.0/stable"},
+ {"candidate/foo", "latest/candidate/foo"},
+ }
+
+ for _, t := range tests {
+ ch, err := channel.Parse(t.channel, "")
+ c.Assert(err, IsNil)
+
+ c.Check(ch.Full(), Equals, t.str)
+ }
+}
+
+func (s *storeChannelSuite) TestMatch(c *C) {
+ tests := []struct {
+ req string
+ c1 string
+ sameArch bool
+ res string
+ }{
+ {"stable", "stable", true, "architecture:track:risk"},
+ {"stable", "beta", true, "architecture:track"},
+ {"beta", "stable", true, "architecture:track:risk"},
+ {"stable", "edge", false, "track"},
+ {"edge", "stable", false, "track:risk"},
+ {"1.0/stable", "1.0/edge", true, "architecture:track"},
+ {"1.0/edge", "stable", true, "architecture:risk"},
+ {"1.0/edge", "stable", false, "risk"},
+ {"1.0/stable", "stable", false, "risk"},
+ {"1.0/stable", "beta", false, ""},
+ {"1.0/stable", "2.0/beta", false, ""},
+ {"2.0/stable", "2.0/beta", false, "track"},
+ {"1.0/stable", "2.0/beta", true, "architecture"},
+ }
+
+ for _, t := range tests {
+ reqArch := "amd64"
+ c1Arch := "amd64"
+ if !t.sameArch {
+ c1Arch = "arm64"
+ }
+ req, err := channel.Parse(t.req, reqArch)
+ c.Assert(err, IsNil)
+ c1, err := channel.Parse(t.c1, c1Arch)
+ c.Assert(err, IsNil)
+
+ c.Check(req.Match(&c1).String(), Equals, t.res)
+ }
+}
+
+func (s *storeChannelSuite) TestResolve(c *C) {
+ tests := []struct {
+ channel string
+ new string
+ result string
+ expErr string
+ }{
+ {"", "", "", ""},
+ {"", "edge", "edge", ""},
+ {"track/foo", "", "track/foo", ""},
+ {"stable", "", "stable", ""},
+ {"stable", "edge", "edge", ""},
+ {"stable/branch1", "edge/branch2", "edge/branch2", ""},
+ {"track", "track", "track", ""},
+ {"track", "beta", "track/beta", ""},
+ {"track/stable", "beta", "track/beta", ""},
+ {"track/stable", "stable/branch", "track/stable/branch", ""},
+ {"track/stable", "track/edge/branch", "track/edge/branch", ""},
+ {"track/stable", "track/candidate", "track/candidate", ""},
+ {"track/stable", "track/stable/branch", "track/stable/branch", ""},
+ {"track1/stable", "track2/stable", "track2/stable", ""},
+ {"track1/stable", "track2/stable/branch", "track2/stable/branch", ""},
+ {"track/foo", "track/stable/branch", "", "invalid risk in channel name: track/foo"},
+ }
+
+ for _, t := range tests {
+ r, err := channel.Resolve(t.channel, t.new)
+ tcomm := Commentf("%#v", t)
+ if t.expErr == "" {
+ c.Assert(err, IsNil, tcomm)
+ c.Check(r, Equals, t.result, tcomm)
+ } else {
+ c.Assert(err, ErrorMatches, t.expErr, tcomm)
+ }
+ }
+}
+
+func (s *storeChannelSuite) TestResolveLocked(c *C) {
+ tests := []struct {
+ track string
+ new string
+ result string
+ expErr string
+ }{
+ {"", "", "", ""},
+ {"", "anytrack/stable", "anytrack/stable", ""},
+ {"track/foo", "", "", "invalid locked track: track/foo"},
+ {"track", "", "track", ""},
+ {"track", "track", "track", ""},
+ {"track", "beta", "track/beta", ""},
+ {"track", "stable/branch", "track/stable/branch", ""},
+ {"track", "track/edge/branch", "track/edge/branch", ""},
+ {"track", "track/candidate", "track/candidate", ""},
+ {"track", "track/stable/branch", "track/stable/branch", ""},
+ {"track1", "track2/stable", "track2/stable", "cannot switch locked track"},
+ {"track1", "track2/stable/branch", "track2/stable/branch", "cannot switch locked track"},
+ }
+ for _, t := range tests {
+ r, err := channel.ResolveLocked(t.track, t.new)
+ tcomm := Commentf("%#v", t)
+ if t.expErr == "" {
+ c.Assert(err, IsNil, tcomm)
+ c.Check(r, Equals, t.result, tcomm)
+ } else {
+ c.Assert(err, ErrorMatches, t.expErr, tcomm)
+ }
+ }
+}
diff -Nru snapd-2.40/snap/channel.go snapd-2.42.1/snap/channel.go
--- snapd-2.40/snap/channel.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/channel.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,202 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2018 Canonical Ltd
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package snap
-
-import (
- "fmt"
- "strings"
-
- "github.com/snapcore/snapd/arch"
- "github.com/snapcore/snapd/strutil"
-)
-
-var channelRisks = []string{"stable", "candidate", "beta", "edge"}
-
-// Channel identifies and describes completely a store channel.
-type Channel struct {
- Architecture string `json:"architecture"`
- Name string `json:"name"`
- Track string `json:"track"`
- Risk string `json:"risk"`
- Branch string `json:"branch,omitempty"`
-}
-
-// ParseChannelVerbatim parses a string representing a store channel and
-// includes the given architecture, if architecture is "" the system
-// architecture is included. The channel representation is not normalized.
-// ParseChannel() should be used in most cases.
-func ParseChannelVerbatim(s string, architecture string) (Channel, error) {
- if s == "" {
- return Channel{}, fmt.Errorf("channel name cannot be empty")
- }
- p := strings.Split(s, "/")
- var risk, track, branch *string
- switch len(p) {
- default:
- return Channel{}, fmt.Errorf("channel name has too many components: %s", s)
- case 3:
- track, risk, branch = &p[0], &p[1], &p[2]
- case 2:
- if strutil.ListContains(channelRisks, p[0]) {
- risk, branch = &p[0], &p[1]
- } else {
- track, risk = &p[0], &p[1]
- }
- case 1:
- if strutil.ListContains(channelRisks, p[0]) {
- risk = &p[0]
- } else {
- track = &p[0]
- }
- }
-
- if architecture == "" {
- architecture = arch.UbuntuArchitecture()
- }
-
- ch := Channel{
- Architecture: architecture,
- }
-
- if risk != nil {
- if !strutil.ListContains(channelRisks, *risk) {
- return Channel{}, fmt.Errorf("invalid risk in channel name: %s", s)
- }
- ch.Risk = *risk
- }
- if track != nil {
- if *track == "" {
- return Channel{}, fmt.Errorf("invalid track in channel name: %s", s)
- }
- ch.Track = *track
- }
- if branch != nil {
- if *branch == "" {
- return Channel{}, fmt.Errorf("invalid branch in channel name: %s", s)
- }
- ch.Branch = *branch
- }
-
- return ch, nil
-}
-
-// ParseChannel parses a string representing a store channel and includes given
-// architecture, , if architecture is "" the system architecture is included.
-// The returned channel's track, risk and name are normalized.
-func ParseChannel(s string, architecture string) (Channel, error) {
- channel, err := ParseChannelVerbatim(s, architecture)
- if err != nil {
- return Channel{}, err
- }
- return channel.Clean(), nil
-}
-
-// Clean returns a Channel with a normalized track, risk and name.
-func (c Channel) Clean() Channel {
- track := c.Track
- risk := c.Risk
-
- if track == "latest" {
- track = ""
- }
- if risk == "" {
- risk = "stable"
- }
-
- // normalized name
- name := risk
- if track != "" {
- name = track + "/" + name
- }
- if c.Branch != "" {
- name = name + "/" + c.Branch
- }
-
- return Channel{
- Architecture: c.Architecture,
- Name: name,
- Track: track,
- Risk: risk,
- Branch: c.Branch,
- }
-}
-
-func (c Channel) String() string {
- return c.Name
-}
-
-// Full returns the full name of the channel, inclusive the default track "latest".
-func (c *Channel) Full() string {
- if c.Track == "" {
- return "latest/" + c.Name
- }
- return c.String()
-}
-
-func riskLevel(risk string) int {
- for i, r := range channelRisks {
- if r == risk {
- return i
- }
- }
- return -1
-}
-
-// ChannelMatch represents on which fields two channels are matching.
-type ChannelMatch struct {
- Architecture bool
- Track bool
- Risk bool
-}
-
-// String returns the string represantion of the match, results can be:
-// "architecture:track:risk"
-// "architecture:track"
-// "architecture:risk"
-// "track:risk"
-// "architecture"
-// "track"
-// "risk"
-// ""
-func (cm ChannelMatch) String() string {
- matching := []string{}
- if cm.Architecture {
- matching = append(matching, "architecture")
- }
- if cm.Track {
- matching = append(matching, "track")
- }
- if cm.Risk {
- matching = append(matching, "risk")
- }
- return strings.Join(matching, ":")
-
-}
-
-// Match returns a ChannelMatch of which fields among architecture,track,risk match between c and c1 store channels, risk is matched taking channel inheritance into account and considering c the requested channel.
-func (c *Channel) Match(c1 *Channel) ChannelMatch {
- requestedRiskLevel := riskLevel(c.Risk)
- rl1 := riskLevel(c1.Risk)
- return ChannelMatch{
- Architecture: c.Architecture == c1.Architecture,
- Track: c.Track == c1.Track,
- Risk: requestedRiskLevel >= rl1,
- }
-}
diff -Nru snapd-2.40/snap/channel_test.go snapd-2.42.1/snap/channel_test.go
--- snapd-2.40/snap/channel_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/channel_test.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,264 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2018 Canonical Ltd
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package snap_test
-
-import (
- . "gopkg.in/check.v1"
-
- "github.com/snapcore/snapd/arch"
- "github.com/snapcore/snapd/snap"
-)
-
-type storeChannelSuite struct{}
-
-var _ = Suite(&storeChannelSuite{})
-
-func (s storeChannelSuite) TestParseChannel(c *C) {
- ch, err := snap.ParseChannel("stable", "")
- c.Assert(err, IsNil)
- c.Check(ch, DeepEquals, snap.Channel{
- Architecture: arch.UbuntuArchitecture(),
- Name: "stable",
- Track: "",
- Risk: "stable",
- Branch: "",
- })
-
- ch, err = snap.ParseChannel("latest/stable", "")
- c.Assert(err, IsNil)
- c.Check(ch, DeepEquals, snap.Channel{
- Architecture: arch.UbuntuArchitecture(),
- Name: "stable",
- Track: "",
- Risk: "stable",
- Branch: "",
- })
-
- ch, err = snap.ParseChannel("1.0/edge", "")
- c.Assert(err, IsNil)
- c.Check(ch, DeepEquals, snap.Channel{
- Architecture: arch.UbuntuArchitecture(),
- Name: "1.0/edge",
- Track: "1.0",
- Risk: "edge",
- Branch: "",
- })
-
- ch, err = snap.ParseChannel("1.0", "")
- c.Assert(err, IsNil)
- c.Check(ch, DeepEquals, snap.Channel{
- Architecture: arch.UbuntuArchitecture(),
- Name: "1.0/stable",
- Track: "1.0",
- Risk: "stable",
- Branch: "",
- })
-
- ch, err = snap.ParseChannel("1.0/beta/foo", "")
- c.Assert(err, IsNil)
- c.Check(ch, DeepEquals, snap.Channel{
- Architecture: arch.UbuntuArchitecture(),
- Name: "1.0/beta/foo",
- Track: "1.0",
- Risk: "beta",
- Branch: "foo",
- })
-
- ch, err = snap.ParseChannel("candidate/foo", "")
- c.Assert(err, IsNil)
- c.Check(ch, DeepEquals, snap.Channel{
- Architecture: arch.UbuntuArchitecture(),
- Name: "candidate/foo",
- Track: "",
- Risk: "candidate",
- Branch: "foo",
- })
-
- ch, err = snap.ParseChannel("candidate/foo", "other-arch")
- c.Assert(err, IsNil)
- c.Check(ch, DeepEquals, snap.Channel{
- Architecture: "other-arch",
- Name: "candidate/foo",
- Track: "",
- Risk: "candidate",
- Branch: "foo",
- })
-}
-
-func mustParseChannel(c *C, channel string) snap.Channel {
- ch, err := snap.ParseChannel(channel, "")
- c.Assert(err, IsNil)
- return ch
-}
-
-func (s storeChannelSuite) TestParseChannelVerbatim(c *C) {
- ch, err := snap.ParseChannelVerbatim("sometrack", "")
- c.Assert(err, IsNil)
- c.Check(ch, DeepEquals, snap.Channel{
- Architecture: arch.UbuntuArchitecture(),
- Track: "sometrack",
- })
- c.Check(mustParseChannel(c, "sometrack"), DeepEquals, ch.Clean())
-
- ch, err = snap.ParseChannelVerbatim("latest", "")
- c.Assert(err, IsNil)
- c.Check(ch, DeepEquals, snap.Channel{
- Architecture: arch.UbuntuArchitecture(),
- Track: "latest",
- })
- c.Check(mustParseChannel(c, "latest"), DeepEquals, ch.Clean())
-
- ch, err = snap.ParseChannelVerbatim("latest/stable", "")
- c.Assert(err, IsNil)
- c.Check(ch, DeepEquals, snap.Channel{
- Architecture: arch.UbuntuArchitecture(),
- Track: "latest",
- Risk: "stable",
- })
- c.Check(mustParseChannel(c, "latest/stable"), DeepEquals, ch.Clean())
-
- ch, err = snap.ParseChannelVerbatim("latest/stable/foo", "")
- c.Assert(err, IsNil)
- c.Check(ch, DeepEquals, snap.Channel{
- Architecture: arch.UbuntuArchitecture(),
- Track: "latest",
- Risk: "stable",
- Branch: "foo",
- })
- c.Check(mustParseChannel(c, "latest/stable/foo"), DeepEquals, ch.Clean())
-}
-
-func (s storeChannelSuite) TestClean(c *C) {
- ch := snap.Channel{
- Architecture: "arm64",
- Track: "latest",
- Name: "latest/stable",
- Risk: "stable",
- }
-
- cleanedCh := ch.Clean()
- c.Check(cleanedCh, Not(DeepEquals), c)
- c.Check(cleanedCh, DeepEquals, snap.Channel{
- Architecture: "arm64",
- Track: "",
- Name: "stable",
- Risk: "stable",
- })
-}
-
-func (s storeChannelSuite) TestParseChannelErrors(c *C) {
- for _, tc := range []struct {
- channel string
- err string
- }{
- {"", "channel name cannot be empty"},
- {"1.0////", "channel name has too many components: 1.0////"},
- {"1.0/cand", "invalid risk in channel name: 1.0/cand"},
- {"fix//hotfix", "invalid risk in channel name: fix//hotfix"},
- {"/stable/", "invalid track in channel name: /stable/"},
- {"//stable", "invalid risk in channel name: //stable"},
- {"stable/", "invalid branch in channel name: stable/"},
- {"/stable", "invalid track in channel name: /stable"},
- } {
- _, err := snap.ParseChannel(tc.channel, "")
- c.Check(err, ErrorMatches, tc.err)
- _, err = snap.ParseChannelVerbatim(tc.channel, "")
- c.Check(err, ErrorMatches, tc.err)
- }
-}
-
-func (s *storeChannelSuite) TestString(c *C) {
- tests := []struct {
- channel string
- str string
- }{
- {"stable", "stable"},
- {"latest/stable", "stable"},
- {"1.0/edge", "1.0/edge"},
- {"1.0/beta/foo", "1.0/beta/foo"},
- {"1.0", "1.0/stable"},
- {"candidate/foo", "candidate/foo"},
- }
-
- for _, t := range tests {
- ch, err := snap.ParseChannel(t.channel, "")
- c.Assert(err, IsNil)
-
- c.Check(ch.String(), Equals, t.str)
- }
-}
-
-func (s *storeChannelSuite) TestFull(c *C) {
- tests := []struct {
- channel string
- str string
- }{
- {"stable", "latest/stable"},
- {"latest/stable", "latest/stable"},
- {"1.0/edge", "1.0/edge"},
- {"1.0/beta/foo", "1.0/beta/foo"},
- {"1.0", "1.0/stable"},
- {"candidate/foo", "latest/candidate/foo"},
- }
-
- for _, t := range tests {
- ch, err := snap.ParseChannel(t.channel, "")
- c.Assert(err, IsNil)
-
- c.Check(ch.Full(), Equals, t.str)
- }
-}
-
-func (s *storeChannelSuite) TestMatch(c *C) {
- tests := []struct {
- req string
- c1 string
- sameArch bool
- res string
- }{
- {"stable", "stable", true, "architecture:track:risk"},
- {"stable", "beta", true, "architecture:track"},
- {"beta", "stable", true, "architecture:track:risk"},
- {"stable", "edge", false, "track"},
- {"edge", "stable", false, "track:risk"},
- {"1.0/stable", "1.0/edge", true, "architecture:track"},
- {"1.0/edge", "stable", true, "architecture:risk"},
- {"1.0/edge", "stable", false, "risk"},
- {"1.0/stable", "stable", false, "risk"},
- {"1.0/stable", "beta", false, ""},
- {"1.0/stable", "2.0/beta", false, ""},
- {"2.0/stable", "2.0/beta", false, "track"},
- {"1.0/stable", "2.0/beta", true, "architecture"},
- }
-
- for _, t := range tests {
- reqArch := "amd64"
- c1Arch := "amd64"
- if !t.sameArch {
- c1Arch = "arm64"
- }
- req, err := snap.ParseChannel(t.req, reqArch)
- c.Assert(err, IsNil)
- c1, err := snap.ParseChannel(t.c1, c1Arch)
- c.Assert(err, IsNil)
-
- c.Check(req.Match(&c1).String(), Equals, t.res)
- }
-}
diff -Nru snapd-2.40/snap/gadget.go snapd-2.42.1/snap/gadget.go
--- snapd-2.40/snap/gadget.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/gadget.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,43 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2016 Canonical Ltd
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package snap
-
-import (
- "fmt"
-
- "github.com/snapcore/snapd/gadget"
-)
-
-// 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) (*gadget.Info, error) {
- const errorFormat = "cannot read gadget snap details: %s"
-
- if info.GetType() != TypeGadget {
- return nil, fmt.Errorf(errorFormat, "not a gadget snap")
- }
-
- gi, err := gadget.ReadInfo(info.MountDir(), classic)
- if err != nil {
- return nil, fmt.Errorf(errorFormat, err)
- }
- return gi, nil
-}
diff -Nru snapd-2.40/snap/gadget_test.go snapd-2.42.1/snap/gadget_test.go
--- snapd-2.40/snap/gadget_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/gadget_test.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,81 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2014-2016 Canonical Ltd
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package snap_test
-
-import (
- "io/ioutil"
- "path/filepath"
-
- . "gopkg.in/check.v1"
-
- "github.com/snapcore/snapd/dirs"
- "github.com/snapcore/snapd/gadget"
- "github.com/snapcore/snapd/snap"
- "github.com/snapcore/snapd/snap/snaptest"
-)
-
-type gadgetYamlTestSuite struct{}
-
-var _ = Suite(&gadgetYamlTestSuite{})
-
-var mockGadgetSnapYaml = `
-name: canonical-pc
-type: gadget
-`
-
-func (s *gadgetYamlTestSuite) SetUpTest(c *C) {
- dirs.SetRootDir(c.MkDir())
-}
-
-func (s *gadgetYamlTestSuite) TearDownTest(c *C) {
- dirs.SetRootDir("/")
-}
-
-func (s *gadgetYamlTestSuite) TestReadGadgetNotAGadget(c *C) {
- info := snaptest.MockInfo(c, `
-name: other
-version: 0
-`, &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, &snap.SideInfo{Revision: snap.R(42)})
- _, 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, &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, &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, &gadget.Info{})
-}
diff -Nru snapd-2.40/snap/helpers.go snapd-2.42.1/snap/helpers.go
--- snapd-2.40/snap/helpers.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/snap/helpers.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,44 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package snap
+
+import (
+ "github.com/snapcore/snapd/strutil"
+)
+
+var snapIDsSnapd = []string{
+ // production
+ "PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4",
+ // TODO: when snapd snap is uploaded to staging, replace this with
+ // the real snap-id.
+ "todo-staging-snapd-id",
+}
+
+func IsSnapd(snapID string) bool {
+ return strutil.ListContains(snapIDsSnapd, snapID)
+}
+
+func MockSnapdSnapID(snapID string) (restore func()) {
+ old := snapIDsSnapd
+ snapIDsSnapd = append(snapIDsSnapd, snapID)
+ return func() {
+ snapIDsSnapd = old
+ }
+}
diff -Nru snapd-2.40/snap/hooktypes.go snapd-2.42.1/snap/hooktypes.go
--- snapd-2.40/snap/hooktypes.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/hooktypes.go 2019-10-30 12:17:43.000000000 +0000
@@ -34,6 +34,7 @@
NewHookType(regexp.MustCompile("^unprepare-(?:plug|slot)-[-a-z0-9]+$")),
NewHookType(regexp.MustCompile("^connect-(?:plug|slot)-[-a-z0-9]+$")),
NewHookType(regexp.MustCompile("^disconnect-(?:plug|slot)-[-a-z0-9]+$")),
+ NewHookType(regexp.MustCompile("^check-health$")),
}
// HookType represents a pattern of supported hook names.
diff -Nru snapd-2.40/snap/info.go snapd-2.42.1/snap/info.go
--- snapd-2.40/snap/info.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/info.go 2019-10-30 12:17:43.000000000 +0000
@@ -254,6 +254,10 @@
// The list of common-ids from all apps of the snap
CommonIDs []string
+
+ // List of system users (usernames) this snap may use. The group
+ // of the same name must also exist.
+ SystemUsernames map[string]*SystemUsernameInfo
}
// StoreAccount holds information about a store account, for example
@@ -358,6 +362,9 @@
}
func (s *Info) GetType() Type {
+ if s.SnapType == TypeApp && IsSnapd(s.SnapID) {
+ return TypeSnapd
+ }
return s.SnapType
}
@@ -798,6 +805,21 @@
Explicit bool
}
+// SystemUsernameInfo provides information about a system username (ie, a
+// UNIX user and group with the same name). The scope defines visibility of the
+// username wrt the snap and the system. Defined scopes:
+// - shared static, snapd-managed user/group shared between host and all
+// snaps
+// - private static, snapd-managed user/group private to a particular snap
+// (currently not implemented)
+// - external dynamic user/group shared between host and all snaps (currently
+// not implented)
+type SystemUsernameInfo struct {
+ Name string
+ Scope string
+ Attrs map[string]interface{}
+}
+
// File returns the path to the *.socket file
func (socket *SocketInfo) File() string {
return filepath.Join(dirs.SnapServicesDir, socket.App.SecurityTag()+"."+socket.Name+".socket")
diff -Nru snapd-2.40/snap/info_snap_yaml.go snapd-2.42.1/snap/info_snap_yaml.go
--- snapd-2.40/snap/info_snap_yaml.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/info_snap_yaml.go 2019-10-30 12:17:43.000000000 +0000
@@ -34,24 +34,25 @@
)
type snapYaml struct {
- Name string `yaml:"name"`
- Version string `yaml:"version"`
- Type Type `yaml:"type"`
- Architectures []string `yaml:"architectures,omitempty"`
- Assumes []string `yaml:"assumes"`
- Title string `yaml:"title"`
- Description string `yaml:"description"`
- Summary string `yaml:"summary"`
- License string `yaml:"license,omitempty"`
- Epoch Epoch `yaml:"epoch,omitempty"`
- Base string `yaml:"base,omitempty"`
- Confinement ConfinementType `yaml:"confinement,omitempty"`
- Environment strutil.OrderedMap `yaml:"environment,omitempty"`
- Plugs map[string]interface{} `yaml:"plugs,omitempty"`
- Slots map[string]interface{} `yaml:"slots,omitempty"`
- Apps map[string]appYaml `yaml:"apps,omitempty"`
- Hooks map[string]hookYaml `yaml:"hooks,omitempty"`
- Layout map[string]layoutYaml `yaml:"layout,omitempty"`
+ Name string `yaml:"name"`
+ Version string `yaml:"version"`
+ Type Type `yaml:"type"`
+ Architectures []string `yaml:"architectures,omitempty"`
+ Assumes []string `yaml:"assumes"`
+ Title string `yaml:"title"`
+ Description string `yaml:"description"`
+ Summary string `yaml:"summary"`
+ License string `yaml:"license,omitempty"`
+ Epoch Epoch `yaml:"epoch,omitempty"`
+ Base string `yaml:"base,omitempty"`
+ Confinement ConfinementType `yaml:"confinement,omitempty"`
+ Environment strutil.OrderedMap `yaml:"environment,omitempty"`
+ Plugs map[string]interface{} `yaml:"plugs,omitempty"`
+ Slots map[string]interface{} `yaml:"slots,omitempty"`
+ Apps map[string]appYaml `yaml:"apps,omitempty"`
+ Hooks map[string]hookYaml `yaml:"hooks,omitempty"`
+ Layout map[string]layoutYaml `yaml:"layout,omitempty"`
+ SystemUsernames map[string]interface{} `yaml:"system-usernames,omitempty"`
// TypoLayouts is used to detect the use of the incorrect plural form of "layout"
TypoLayouts typoDetector `yaml:"layouts,omitempty"`
@@ -223,6 +224,11 @@
snap.BadInterfaces = make(map[string]string)
SanitizePlugsSlots(snap)
+ // Collect system usernames
+ if err := setSystemUsernamesFromSnapYaml(y, snap); err != nil {
+ return nil, err
+ }
+
// FIXME: validation of the fields
return snap, nil
}
@@ -235,6 +241,7 @@
if len(y.Architectures) != 0 {
architectures = y.Architectures
}
+
typ := TypeApp
if y.Type != "" {
typ = y.Type
@@ -275,6 +282,7 @@
Plugs: make(map[string]*PlugInfo),
Slots: make(map[string]*SlotInfo),
Environment: y.Environment,
+ SystemUsernames: make(map[string]*SystemUsernameInfo),
}
sort.Strings(snap.Assumes)
@@ -497,6 +505,28 @@
}
}
+func setSystemUsernamesFromSnapYaml(y snapYaml, snap *Info) error {
+ for user, data := range y.SystemUsernames {
+ if user == "" {
+ return fmt.Errorf("system username cannot be empty")
+ }
+ scope, attrs, err := convertToUsernamesData(user, data)
+ if err != nil {
+ return err
+ }
+ if scope == "" {
+ return fmt.Errorf("system username %q does not specify a scope", user)
+ }
+ snap.SystemUsernames[user] = &SystemUsernameInfo{
+ Name: user,
+ Scope: scope,
+ Attrs: attrs,
+ }
+ }
+
+ return nil
+}
+
func bindUnscopedPlugs(snap *Info, strk *scopedTracker) {
for plugName, plug := range snap.Plugs {
if strk.plug(plug) {
@@ -576,7 +606,7 @@
for keyData, valueData := range data.(map[interface{}]interface{}) {
key, ok := keyData.(string)
if !ok {
- err := fmt.Errorf("%s %q has attribute that is not a string (found %T)",
+ err := fmt.Errorf("%s %q has attribute key that is not a string (found %T)",
plugOrSlot, name, keyData)
return "", "", nil, err
}
@@ -585,6 +615,8 @@
return "", "", nil, err
}
switch key {
+ case "":
+ return "", "", nil, fmt.Errorf("%s %q has an empty attribute key", plugOrSlot, name)
case "interface":
value, ok := valueData.(string)
if !ok {
@@ -618,3 +650,55 @@
return "", "", nil, err
}
}
+
+// Short form:
+// system-usernames:
+// snap_daemon: shared # 'scope' is 'shared'
+// lxd: external # currently unsupported
+// foo: private # currently unsupported
+// Attributes form:
+// system-usernames:
+// snap_daemon:
+// scope: shared
+// attrib1: ...
+// attrib2: ...
+func convertToUsernamesData(user string, data interface{}) (scope string, attrs map[string]interface{}, err error) {
+ switch data.(type) {
+ case string:
+ return data.(string), nil, nil
+ case nil:
+ return "", nil, nil
+ case map[interface{}]interface{}:
+ for keyData, valueData := range data.(map[interface{}]interface{}) {
+ key, ok := keyData.(string)
+ if !ok {
+ err := fmt.Errorf("system username %q has attribute key that is not a string (found %T)", user, keyData)
+ return "", nil, err
+ }
+ switch key {
+ case "scope":
+ value, ok := valueData.(string)
+ if !ok {
+ err := fmt.Errorf("scope on system username %q is not a string (found %T)", user, valueData)
+ return "", nil, err
+ }
+ scope = value
+ case "":
+ return "", nil, fmt.Errorf("system username %q has an empty attribute key", user)
+ default:
+ if attrs == nil {
+ attrs = make(map[string]interface{})
+ }
+ value, err := metautil.NormalizeValue(valueData)
+ if err != nil {
+ return "", nil, fmt.Errorf("attribute %q of system username %q: %v", key, user, err)
+ }
+ attrs[key] = value
+ }
+ }
+ return scope, attrs, nil
+ default:
+ err := fmt.Errorf("system username %q has malformed definition (found %T)", user, data)
+ return "", nil, err
+ }
+}
diff -Nru snapd-2.40/snap/info_snap_yaml_test.go snapd-2.42.1/snap/info_snap_yaml_test.go
--- snapd-2.40/snap/info_snap_yaml_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/info_snap_yaml_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -441,7 +441,18 @@
net:
1: ok
`))
- c.Assert(err, ErrorMatches, `plug "net" has attribute that is not a string \(found int\)`)
+ c.Assert(err, ErrorMatches, `plug "net" has attribute key that is not a string \(found int\)`)
+}
+
+func (s *YamlSuite) TestUnmarshalCorruptedPlugWithEmptyAttributeKey(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+plugs:
+ net:
+ "": ok
+`))
+ c.Assert(err, ErrorMatches, `plug "net" has an empty attribute key`)
}
func (s *YamlSuite) TestUnmarshalCorruptedPlugWithUnexpectedType(c *C) {
@@ -854,7 +865,18 @@
net:
1: ok
`))
- c.Assert(err, ErrorMatches, `slot "net" has attribute that is not a string \(found int\)`)
+ c.Assert(err, ErrorMatches, `slot "net" has attribute key that is not a string \(found int\)`)
+}
+
+func (s *YamlSuite) TestUnmarshalCorruptedSlotWithEmptyAttributeKey(c *C) {
+ // NOTE: yaml content cannot use tabs, indent the section with spaces.
+ _, err := snap.InfoFromSnapYaml([]byte(`
+name: snap
+slots:
+ net:
+ "": ok
+`))
+ c.Assert(err, ErrorMatches, `slot "net" has an empty attribute key`)
}
func (s *YamlSuite) TestUnmarshalCorruptedSlotWithUnexpectedType(c *C) {
@@ -1819,3 +1841,147 @@
c.Assert(app, NotNil)
c.Check(app.RestartDelay, Equals, timeout.Timeout(12*time.Second))
}
+
+func (s *YamlSuite) TestSnapYamlSystemUsernamesParsing(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+system-usernames:
+ foo: shared
+ bar:
+ scope: external
+ baz:
+ scope: private
+ attr1: norf
+ attr2: corge
+ attr3: ""
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, IsNil)
+ c.Check(info.SystemUsernames, HasLen, 3)
+ c.Assert(info.SystemUsernames["foo"], DeepEquals, &snap.SystemUsernameInfo{
+ Name: "foo",
+ Scope: "shared",
+ })
+ c.Assert(info.SystemUsernames["bar"], DeepEquals, &snap.SystemUsernameInfo{
+ Name: "bar",
+ Scope: "external",
+ })
+ c.Assert(info.SystemUsernames["baz"], DeepEquals, &snap.SystemUsernameInfo{
+ Name: "baz",
+ Scope: "private",
+ Attrs: map[string]interface{}{
+ "attr1": "norf",
+ "attr2": "corge",
+ "attr3": "",
+ },
+ })
+}
+
+func (s *YamlSuite) TestSnapYamlSystemUsernamesParsingBadType(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+system-usernames:
+ a: true
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, ErrorMatches, `system username "a" has malformed definition \(found bool\)`)
+ c.Assert(info, IsNil)
+}
+
+func (s *YamlSuite) TestSnapYamlSystemUsernamesParsingBadValue(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+system-usernames:
+ a: [b, c]
+`)
+ info, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, ErrorMatches, `system username "a" has malformed definition \(found \[\]interface {}\)`)
+ c.Assert(info, IsNil)
+}
+
+func (s *YamlSuite) TestSnapYamlSystemUsernamesParsingBadKeyEmpty(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+system-usernames:
+ "": shared
+`)
+ _, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, ErrorMatches, `system username cannot be empty`)
+}
+
+func (s *YamlSuite) TestSnapYamlSystemUsernamesParsingBadKeyList(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+system-usernames:
+- foo: shared
+`)
+ _, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, ErrorMatches, `(?m)cannot parse snap.yaml:.*`)
+}
+
+func (s *YamlSuite) TestSnapYamlSystemUsernamesParsingBadValueEmpty(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+system-usernames:
+ a: ""
+`)
+ _, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, ErrorMatches, `system username "a" does not specify a scope`)
+}
+
+func (s *YamlSuite) TestSnapYamlSystemUsernamesParsingBadValueNull(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+system-usernames:
+ a: null
+`)
+ _, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, ErrorMatches, `system username "a" does not specify a scope`)
+}
+
+func (s *YamlSuite) TestSnapYamlSystemUsernamesParsingBadAttrKeyEmpty(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+system-usernames:
+ foo:
+ scope: shared
+ "": bar
+`)
+ _, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, ErrorMatches, `system username "foo" has an empty attribute key`)
+}
+
+func (s *YamlSuite) TestSnapYamlSystemUsernamesParsingBadAttrKeyNonString(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+system-usernames:
+ foo:
+ scope: shared
+ 1: bar
+`)
+ _, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, ErrorMatches, `system username "foo" has attribute key that is not a string \(found int\)`)
+}
+
+func (s *YamlSuite) TestSnapYamlSystemUsernamesParsingBadAttrValue(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+system-usernames:
+ foo:
+ scope: shared
+ bar: null
+`)
+ _, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, ErrorMatches, `attribute "bar" of system username "foo": invalid scalar:.*`)
+}
+
+func (s *YamlSuite) TestSnapYamlSystemUsernamesParsingBadScopeNonString(c *C) {
+ y := []byte(`name: binary
+version: 1.0
+system-usernames:
+ foo:
+ scope: 10
+`)
+ _, err := snap.InfoFromSnapYaml(y)
+ c.Assert(err, ErrorMatches, `scope on system username "foo" is not a string \(found int\)`)
+}
diff -Nru snapd-2.40/snap/info_test.go snapd-2.42.1/snap/info_test.go
--- snapd-2.40/snap/info_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/info_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -1503,6 +1503,19 @@
c.Check(info2.IsActive(), Equals, false)
}
+func (s *infoSuite) TestGetTypeSnapdBackwardCompatibility(c *C) {
+ restore := snap.MockSnapdSnapID("snapd-id")
+ defer restore()
+
+ const snapdYaml = `
+name: snapd
+type: app
+version: 1
+`
+ snapInfo := snaptest.MockSnap(c, sampleYaml, &snap.SideInfo{Revision: snap.R(1), SnapID: "snapd-id"})
+ c.Check(snapInfo.GetType(), Equals, snap.TypeSnapd)
+}
+
func (s *infoSuite) TestDirAndFileHelpers(c *C) {
dirs.SetRootDir("")
diff -Nru snapd-2.40/snap/naming/snapref.go snapd-2.42.1/snap/naming/snapref.go
--- snapd-2.40/snap/naming/snapref.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/snap/naming/snapref.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,122 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package naming
+
+// A SnapRef references a snap by name and/or id.
+type SnapRef interface {
+ SnapName() string
+ ID() string
+}
+
+// Snap references a snap by name only.
+type Snap string
+
+func (s Snap) SnapName() string {
+ return string(s)
+}
+
+func (s Snap) ID() string {
+ return ""
+}
+
+type snapRef struct {
+ name string
+ id string
+}
+
+// NewSnapRef returns a reference to the snap with given name and id.
+func NewSnapRef(name, id string) SnapRef {
+ return &snapRef{name: name, id: id}
+}
+
+func (r *snapRef) SnapName() string {
+ return r.name
+}
+
+func (r *snapRef) ID() string {
+ return r.id
+}
+
+// SameSnap returns whether the two arguments refer to the same snap.
+// If ids are not available for both it will fallback to names.
+func SameSnap(snapRef1, snapRef2 SnapRef) bool {
+ id1 := snapRef1.ID()
+ id2 := snapRef2.ID()
+ if id1 != "" && id2 != "" {
+ return id1 == id2
+ }
+ return snapRef1.SnapName() == snapRef2.SnapName()
+}
+
+// SnapSet can hold a set of references to snaps.
+type SnapSet struct {
+ byID map[string]SnapRef
+ byName map[string]SnapRef
+}
+
+// NewSnapSet builds a snap set with the given references.
+func NewSnapSet(refs []SnapRef) *SnapSet {
+ sz := len(refs) + 2
+ s := &SnapSet{
+ byID: make(map[string]SnapRef, sz),
+ byName: make(map[string]SnapRef, sz),
+ }
+ for _, r := range refs {
+ s.Add(r)
+ }
+ return s
+}
+
+// Lookup finds the reference in the set matching the given one if any.
+func (s *SnapSet) Lookup(which SnapRef) SnapRef {
+ whichID := which.ID()
+ name := which.SnapName()
+ if whichID != "" {
+ if ref := s.byID[whichID]; ref != nil {
+ return ref
+ }
+ }
+ ref := s.byName[name]
+ if ref == nil || (ref.ID() != "" && whichID != "") {
+ return nil
+ }
+ return ref
+}
+
+// Contains returns whether the set has a matching reference already.
+func (s *SnapSet) Contains(ref SnapRef) bool {
+ return s.Lookup(ref) != nil
+}
+
+// Add adds one reference to the set.
+// Already added ids or names will be ignored. The assumption is that
+// a SnapSet is populated with distinct snaps.
+func (s *SnapSet) Add(ref SnapRef) {
+ if s.Contains(ref) {
+ // nothing to do
+ return
+ }
+ if id := ref.ID(); id != "" {
+ s.byID[id] = ref
+ }
+ if name := ref.SnapName(); name != "" {
+ s.byName[name] = ref
+ }
+}
diff -Nru snapd-2.40/snap/naming/snapref_test.go snapd-2.42.1/snap/naming/snapref_test.go
--- snapd-2.40/snap/naming/snapref_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/snap/naming/snapref_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,89 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package naming_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/snap/naming"
+)
+
+type snapRefSuite struct{}
+
+var _ = Suite(&snapRefSuite{})
+
+func (s *snapRefSuite) TestNewSnapRef(c *C) {
+ fooRef := naming.NewSnapRef("foo", "foo-id")
+ c.Check(fooRef.SnapName(), Equals, "foo")
+ c.Check(fooRef.ID(), Equals, "foo-id")
+
+ fooNameOnlyRef := naming.NewSnapRef("foo", "")
+ c.Check(fooNameOnlyRef.SnapName(), Equals, "foo")
+ c.Check(fooNameOnlyRef.ID(), Equals, "")
+}
+
+func (s *snapRefSuite) TestSnap(c *C) {
+ fooNameOnlyRef := naming.Snap("foo")
+ c.Check(fooNameOnlyRef.SnapName(), Equals, "foo")
+ c.Check(fooNameOnlyRef.ID(), Equals, "")
+}
+
+func (s *snapRefSuite) TestSameSnap(c *C) {
+ fooRef := naming.NewSnapRef("foo", "foo-id")
+ fooNameOnlyRef := naming.NewSnapRef("foo", "")
+ altFooRef := naming.NewSnapRef("foo-proj", "foo-id")
+ barNameOnylRef := naming.NewSnapRef("bar", "")
+ unrelFooRef := naming.NewSnapRef("foo", "unrel-id")
+
+ c.Check(naming.SameSnap(fooRef, altFooRef), Equals, true)
+ c.Check(naming.SameSnap(fooRef, fooNameOnlyRef), Equals, true)
+ c.Check(naming.SameSnap(altFooRef, fooNameOnlyRef), Equals, false)
+ c.Check(naming.SameSnap(unrelFooRef, fooRef), Equals, false)
+ c.Check(naming.SameSnap(fooRef, barNameOnylRef), Equals, false)
+ // weakness but expected
+ c.Check(naming.SameSnap(unrelFooRef, fooNameOnlyRef), Equals, true)
+}
+
+func (s *snapRefSuite) TestSnapSet(c *C) {
+ ss := naming.NewSnapSet(nil)
+ fooRef := naming.NewSnapRef("foo", "foo-id")
+ fooNameOnlyRef := naming.Snap("foo")
+
+ ss.Add(fooRef)
+ ss.Add(fooNameOnlyRef)
+
+ altFooRef := naming.NewSnapRef("foo-proj", "foo-id")
+ c.Check(ss.Lookup(fooRef), Equals, fooRef)
+ c.Check(ss.Lookup(fooNameOnlyRef), Equals, fooRef)
+ c.Check(ss.Lookup(altFooRef), Equals, fooRef)
+
+ barNameOnylRef := naming.NewSnapRef("bar", "")
+ unrelFooRef := naming.NewSnapRef("foo", "unrel-id")
+ c.Check(ss.Lookup(barNameOnylRef), Equals, nil)
+ c.Check(ss.Lookup(unrelFooRef), Equals, nil)
+
+ // weaker behavior but expected
+ ss1 := naming.NewSnapSet([]naming.SnapRef{fooNameOnlyRef})
+ ss1.Add(fooRef)
+ c.Check(ss1.Lookup(fooRef), Equals, fooNameOnlyRef)
+ c.Check(ss1.Lookup(altFooRef), Equals, nil)
+ c.Check(ss1.Lookup(barNameOnylRef), Equals, nil)
+ c.Check(ss1.Lookup(unrelFooRef), Equals, fooNameOnlyRef)
+}
diff -Nru snapd-2.40/snap/naming/validate.go snapd-2.42.1/snap/naming/validate.go
--- snapd-2.40/snap/naming/validate.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/naming/validate.go 2019-10-30 12:17:43.000000000 +0000
@@ -17,7 +17,7 @@
*
*/
-// Package naming implements naming constraints for snaps and their elements.
+// Package naming implements naming constraints and concepts for snaps and their elements.
package naming
import (
diff -Nru snapd-2.40/snap/naming/validate_test.go snapd-2.42.1/snap/naming/validate_test.go
--- snapd-2.40/snap/naming/validate_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/naming/validate_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -159,6 +159,8 @@
err := naming.ValidateHook(hook)
c.Assert(err, ErrorMatches, `invalid hook name: ".*"`)
}
+ // Regression test for https://bugs.launchpad.net/snapd/+bug/1638988
+ c.Assert(naming.ValidateHook("connect-plug-i2c"), IsNil)
}
func (s *ValidateSuite) TestValidateAppName(c *C) {
diff -Nru snapd-2.40/snap/seed_yaml.go snapd-2.42.1/snap/seed_yaml.go
--- snapd-2.40/snap/seed_yaml.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/seed_yaml.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,106 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2014-2016 Canonical Ltd
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package snap
-
-import (
- "fmt"
- "io/ioutil"
- "strings"
-
- "gopkg.in/yaml.v2"
-
- "github.com/snapcore/snapd/osutil"
-)
-
-// SeedSnap points to a snap in the seed to install, together with
-// assertions (or alone if unasserted is true) it will be used to
-// drive the installation and ultimately set SideInfo/SnapState for it.
-type SeedSnap struct {
- Name string `yaml:"name"`
-
- // cross-reference/audit
- SnapID string `yaml:"snap-id,omitempty"`
-
- // bits that are orthongonal/not in assertions
- Channel string `yaml:"channel,omitempty"`
- DevMode bool `yaml:"devmode,omitempty"`
- Classic bool `yaml:"classic,omitempty"`
-
- Private bool `yaml:"private,omitempty"`
-
- Contact string `yaml:"contact,omitempty"`
-
- // no assertions are available in the seed for this snap
- Unasserted bool `yaml:"unasserted,omitempty"`
-
- File string `yaml:"file"`
-}
-
-type Seed struct {
- Snaps []*SeedSnap `yaml:"snaps"`
-}
-
-func ReadSeedYaml(fn string) (*Seed, error) {
- errPrefix := "cannot read seed yaml"
-
- yamlData, err := ioutil.ReadFile(fn)
- if err != nil {
- return nil, fmt.Errorf("%s: %v", errPrefix, err)
- }
-
- var seed Seed
- if err := yaml.Unmarshal(yamlData, &seed); err != nil {
- return nil, fmt.Errorf("%s: cannot unmarshal %q: %s", errPrefix, yamlData, err)
- }
-
- // validate
- for _, sn := range seed.Snaps {
- if sn == nil {
- return nil, fmt.Errorf("%s: empty element in seed", errPrefix)
- }
- if err := ValidateInstanceName(sn.Name); err != nil {
- return nil, fmt.Errorf("%s: %v", errPrefix, err)
- }
- if sn.Channel != "" {
- if _, err := ParseChannel(sn.Channel, ""); err != nil {
- return nil, fmt.Errorf("%s: %v", errPrefix, err)
- }
- }
- if sn.File == "" {
- return nil, fmt.Errorf(`%s: "file" attribute for %q cannot be empty`, errPrefix, sn.Name)
- }
- if strings.Contains(sn.File, "/") {
- return nil, fmt.Errorf("%s: %q must be a filename, not a path", errPrefix, sn.File)
- }
- }
-
- return &seed, nil
-}
-
-func (seed *Seed) Write(seedFn string) error {
- data, err := yaml.Marshal(&seed)
- if err != nil {
- return err
- }
- if err := osutil.AtomicWriteFile(seedFn, data, 0644, 0); err != nil {
- return err
- }
- return nil
-}
diff -Nru snapd-2.40/snap/seed_yaml_test.go snapd-2.42.1/snap/seed_yaml_test.go
--- snapd-2.40/snap/seed_yaml_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/seed_yaml_test.go 1970-01-01 00:00:00.000000000 +0000
@@ -1,133 +0,0 @@
-// -*- Mode: Go; indent-tabs-mode: t -*-
-
-/*
- * Copyright (C) 2014-2016 Canonical Ltd
- *
- * This program is free software: you can redistribute it and/or modify
- * it under the terms of the GNU General Public License version 3 as
- * published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU General Public License for more details.
- *
- * You should have received a copy of the GNU General Public License
- * along with this program. If not, see .
- *
- */
-
-package snap_test
-
-import (
- "io/ioutil"
- "path/filepath"
-
- . "gopkg.in/check.v1"
-
- "github.com/snapcore/snapd/snap"
-)
-
-type seedYamlTestSuite struct{}
-
-var _ = Suite(&seedYamlTestSuite{})
-
-var mockSeedYaml = []byte(`
-snaps:
- - name: foo
- snap-id: snapidsnapidsnapid
- channel: stable
- devmode: true
- file: foo_1.0_all.snap
- - name: local
- unasserted: true
- file: local.snap
-`)
-
-func (s *seedYamlTestSuite) TestSimple(c *C) {
- fn := filepath.Join(c.MkDir(), "seed.yaml")
- err := ioutil.WriteFile(fn, mockSeedYaml, 0644)
- c.Assert(err, IsNil)
-
- seed, err := snap.ReadSeedYaml(fn)
- c.Assert(err, IsNil)
- c.Assert(seed.Snaps, HasLen, 2)
- c.Assert(seed.Snaps[0], DeepEquals, &snap.SeedSnap{
- File: "foo_1.0_all.snap",
- Name: "foo",
- SnapID: "snapidsnapidsnapid",
-
- Channel: "stable",
- DevMode: true,
- })
- c.Assert(seed.Snaps[1], DeepEquals, &snap.SeedSnap{
- File: "local.snap",
- Name: "local",
- Unasserted: true,
- })
-}
-
-var badMockSeedYaml = []byte(`
-snaps:
- - name: foo
- file: foo/bar.snap
-`)
-
-func (s *seedYamlTestSuite) TestNoPathAllowed(c *C) {
- fn := filepath.Join(c.MkDir(), "seed.yaml")
- err := ioutil.WriteFile(fn, badMockSeedYaml, 0644)
- c.Assert(err, IsNil)
-
- _, err = snap.ReadSeedYaml(fn)
- c.Assert(err, ErrorMatches, `cannot read seed yaml: "foo/bar.snap" must be a filename, not a path`)
-}
-
-func (s *seedYamlTestSuite) TestValidateChannelUnhappy(c *C) {
- fn := filepath.Join(c.MkDir(), "seed.yaml")
- err := ioutil.WriteFile(fn, []byte(`
-snaps:
- - name: foo
- channel: invalid/channel/
-`), 0644)
- c.Assert(err, IsNil)
-
- _, err = snap.ReadSeedYaml(fn)
- c.Assert(err, ErrorMatches, `cannot read seed yaml: invalid risk in channel name: invalid/channel/`)
-}
-
-func (s *seedYamlTestSuite) TestValidateNameUnhappy(c *C) {
- fn := filepath.Join(c.MkDir(), "seed.yaml")
- err := ioutil.WriteFile(fn, []byte(`
-snaps:
- - name: invalid--name
- file: ./foo.snap
-`), 0644)
- c.Assert(err, IsNil)
-
- _, err = snap.ReadSeedYaml(fn)
- c.Assert(err, ErrorMatches, `cannot read seed yaml: invalid snap name: "invalid--name"`)
-}
-
-func (s *seedYamlTestSuite) TestValidateNameMissing(c *C) {
- fn := filepath.Join(c.MkDir(), "seed.yaml")
- err := ioutil.WriteFile(fn, []byte(`
-snaps:
- - file: ./foo.snap
-`), 0644)
- c.Assert(err, IsNil)
-
- _, err = snap.ReadSeedYaml(fn)
- c.Assert(err, ErrorMatches, `cannot read seed yaml: invalid snap name: ""`)
-}
-
-func (s *seedYamlTestSuite) TestValidateFileMissing(c *C) {
- fn := filepath.Join(c.MkDir(), "seed.yaml")
- err := ioutil.WriteFile(fn, []byte(`
-snaps:
- - name: foo
-`), 0644)
- c.Assert(err, IsNil)
-
- _, err = snap.ReadSeedYaml(fn)
- c.Assert(err, ErrorMatches, `cannot read seed yaml: "file" attribute for "foo" cannot be empty`)
-}
diff -Nru snapd-2.40/snap/snapenv/snapenv.go snapd-2.42.1/snap/snapenv/snapenv.go
--- snapd-2.40/snap/snapenv/snapenv.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/snapenv/snapenv.go 2019-10-30 12:17:43.000000000 +0000
@@ -112,7 +112,7 @@
"SNAP_INSTANCE_KEY": info.InstanceKey,
"SNAP_VERSION": info.Version,
"SNAP_REVISION": info.Revision.String(),
- "SNAP_ARCH": arch.UbuntuArchitecture(),
+ "SNAP_ARCH": arch.DpkgArchitecture(),
// see https://github.com/snapcore/snapd/pull/2732#pullrequestreview-18827193
"SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void",
"SNAP_REEXEC": os.Getenv("SNAP_REEXEC"),
diff -Nru snapd-2.40/snap/snapenv/snapenv_test.go snapd-2.42.1/snap/snapenv/snapenv_test.go
--- snapd-2.40/snap/snapenv/snapenv_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/snapenv/snapenv_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -82,7 +82,7 @@
c.Assert(env, DeepEquals, map[string]string{
"SNAP": fmt.Sprintf("%s/foo/17", dirs.CoreSnapMountDir),
- "SNAP_ARCH": arch.UbuntuArchitecture(),
+ "SNAP_ARCH": arch.DpkgArchitecture(),
"SNAP_COMMON": "/var/snap/foo/common",
"SNAP_DATA": "/var/snap/foo/17",
"SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void",
@@ -136,7 +136,7 @@
env := snapEnv(info)
c.Check(env, DeepEquals, map[string]string{
- "SNAP_ARCH": arch.UbuntuArchitecture(),
+ "SNAP_ARCH": arch.DpkgArchitecture(),
"SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void",
"SNAP_NAME": "snapname",
"SNAP_INSTANCE_NAME": "snapname",
@@ -178,7 +178,7 @@
env := snapEnv(info)
c.Check(env, DeepEquals, map[string]string{
- "SNAP_ARCH": arch.UbuntuArchitecture(),
+ "SNAP_ARCH": arch.DpkgArchitecture(),
"SNAP_LIBRARY_PATH": "/var/lib/snapd/lib/gl:/var/lib/snapd/lib/gl32:/var/lib/snapd/void",
"SNAP_NAME": "snapname",
"SNAP_INSTANCE_NAME": "snapname_foo",
diff -Nru snapd-2.40/snap/snaptest/snaptest.go snapd-2.42.1/snap/snaptest/snaptest.go
--- snapd-2.40/snap/snaptest/snaptest.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/snaptest/snaptest.go 2019-10-30 12:17:43.000000000 +0000
@@ -30,6 +30,7 @@
"github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/channel"
"github.com/snapcore/snapd/snap/pack"
)
@@ -219,8 +220,8 @@
// MustParseChannel parses a string representing a store channel and
// includes the given architecture, if architecture is "" the system
// architecture is included. It panics on error.
-func MustParseChannel(s string, architecture string) snap.Channel {
- c, err := snap.ParseChannel(s, architecture)
+func MustParseChannel(s string, architecture string) channel.Channel {
+ c, err := channel.Parse(s, architecture)
if err != nil {
panic(err)
}
diff -Nru snapd-2.40/snap/types.go snapd-2.42.1/snap/types.go
--- snapd-2.40/snap/types.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/types.go 2019-10-30 12:17:43.000000000 +0000
@@ -24,7 +24,7 @@
"fmt"
)
-// Type represents the kind of snap (app, core, gadget, os, kernel)
+// Type represents the kind of snap (app, core, gadget, os, kernel, snapd)
type Type string
// The various types of snap parts we support
diff -Nru snapd-2.40/snap/validate.go snapd-2.42.1/snap/validate.go
--- snapd-2.40/snap/validate.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/validate.go 2019-10-30 12:17:43.000000000 +0000
@@ -30,6 +30,7 @@
"strings"
"unicode/utf8"
+ "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/snap/naming"
"github.com/snapcore/snapd/spdx"
"github.com/snapcore/snapd/strutil"
@@ -346,6 +347,11 @@
return err
}
+ // Ensure system usernames are valid
+ if err := ValidateSystemUsernames(info); err != nil {
+ return err
+ }
+
// ensure that common-id(s) are unique
if err := ValidateCommonIDs(info); err != nil {
return err
@@ -413,6 +419,16 @@
}
}
+ // Validate that layout are not attempting to define elements that normally
+ // come from other snaps. This is separate from the ValidateLayout below to
+ // simplify argument passing.
+ thisSnapMntDir := filepath.Join("/snap/", info.SnapName())
+ for _, path := range paths {
+ if strings.HasPrefix(path, "/snap/") && !strings.HasPrefix(path, thisSnapMntDir) {
+ return fmt.Errorf("layout %q defines a layout in space belonging to another snap", path)
+ }
+ }
+
// Validate each layout item and collect resulting constraints.
constraints := make([]LayoutConstraint, 0, len(info.Layout))
for _, path := range paths {
@@ -727,6 +743,72 @@
return mountedTree(path)
}
+// layoutRejectionList contains directories that cannot be used as layout
+// targets. Nothing there, or underneath can be replaced with $SNAP or
+// $SNAP_DATA, or $SNAP_COMMON content, even from the point of view of a single
+// snap.
+var layoutRejectionList = []string{
+ // Special locations that need to retain their properties:
+
+ // The /dev directory contains essential device nodes and there's no valid
+ // reason to allow snaps to replace it.
+ "/dev",
+ // The /proc directory contains essential process meta-data and
+ // miscellaneous kernel configuration parameters and there is no valid
+ // reason to allow snaps to replace it.
+ "/proc",
+ // The /sys directory exposes many kernel internals, similar to /proc and
+ // there is no known reason to allow snaps to replace it.
+ "/sys",
+ // The media directory is mounted with bi-directional mount event sharing.
+ // Any mount operations there are reflected in the host's view of /media,
+ // which may be either itself or /run/media.
+ "/media",
+ // The /run directory contains various ephemeral information files or
+ // sockets used by various programs. Providing view of the true /run allows
+ // snap applications to be integrated with the rest of the system and
+ // therefore snaps should not be allowed to replace it.
+ "/run",
+ // The /tmp directory contains a private, per-snap, view of /tmp and
+ // there's no valid reason to allow snaps to replace it.
+ "/tmp",
+ // The /var/lib/snapd directory contains essential snapd state and is
+ // sometimes consulted from inside the mount namespace.
+ "/var/lib/snapd",
+
+ // Locations that may be used to attack the host:
+
+ // The firmware is sometimes loaded on demand by the kernel, in response to
+ // a process performing generic I/O to a specific device. In that case the
+ // mount namespace of the process is searched, by the kernel, for the
+ // firmware. Therefore firmware must not be replaceable to prevent
+ // malicious firmware from attacking the host.
+ "/lib/firmware",
+ // Similarly the kernel will load modules and the modules should not be
+ // something that snaps can tamper with.
+ "/lib/modules",
+
+ // Locations that store essential data:
+
+ // The /var/snap directory contains system-wide state of particular snaps
+ // and should not be replaced as it would break content interface
+ // connections that use $SNAP_DATA or $SNAP_COMMON.
+ "/var/snap",
+ // The /home directory contains user data, including $SNAP_USER_DATA,
+ // $SNAP_USER_COMMON and should be disallowed for the same reasons as
+ // /var/snap.
+ "/home",
+
+ // Locations that should be pristine to avoid confusion.
+
+ // There's no known reason to allow snaps to replace things there.
+ "/boot",
+ // The lost+found directory is used by fsck tools to link lost blocks back
+ // into the filesystem tree. Using layouts for this element is just
+ // confusing and there is no valid reason to allow it.
+ "/lost+found",
+}
+
// ValidateLayout ensures that the given layout contains only valid subset of constructs.
func ValidateLayout(layout *Layout, constraints []LayoutConstraint) error {
si := layout.Snap
@@ -750,7 +832,7 @@
return fmt.Errorf("layout %q uses invalid mount point: must be absolute and clean", layout.Path)
}
- for _, path := range []string{"/proc", "/sys", "/dev", "/run", "/boot", "/lost+found", "/media", "/var/lib/snapd", "/var/snap", "/lib/firmware", "/lib/modules"} {
+ for _, path := range layoutRejectionList {
// We use the mountedTree constraint as this has the right semantics.
if mountedTree(path).IsOffLimits(mountPoint) {
return fmt.Errorf("layout %q in an off-limits area", layout.Path)
@@ -860,3 +942,56 @@
}
return nil
}
+
+func ValidateSystemUsernames(info *Info) error {
+ for username := range info.SystemUsernames {
+ if !osutil.IsValidUsername(username) {
+ return fmt.Errorf("invalid system username %q", username)
+ }
+ }
+ return nil
+}
+
+// neededDefaultProviders returns the names of all default-providers for
+// the content plugs that the given snap.Info needs.
+func NeededDefaultProviders(info *Info) (cps []string) {
+ // XXX: unify with the other places that parse default-providers
+ for _, plug := range info.Plugs {
+ if plug.Interface == "content" {
+ var dprovider string
+ if err := plug.Attr("default-provider", &dprovider); err == nil && dprovider != "" {
+ // usage can be "snap:slot" but we only check
+ // the snap here
+ name := strings.Split(dprovider, ":")[0]
+ cps = append(cps, name)
+ }
+ }
+ }
+ return cps
+}
+
+// ValidateBasesAndProviders checks that all bases/default-providers are part of the seed
+func ValidateBasesAndProviders(snapInfos map[string]*Info) []error {
+ var errs []error
+ for _, info := range snapInfos {
+ // ensure base is available
+ if info.Base != "" && info.Base != "none" {
+ if _, ok := snapInfos[info.Base]; !ok {
+ errs = append(errs, fmt.Errorf("cannot use snap %q: base %q is missing", info.InstanceName(), info.Base))
+ }
+ }
+ // ensure core is available
+ if info.Base == "" && info.SnapType == TypeApp && info.InstanceName() != "snapd" {
+ if _, ok := snapInfos["core"]; !ok {
+ errs = append(errs, fmt.Errorf(`cannot use snap %q: required snap "core" missing`, info.InstanceName()))
+ }
+ }
+ // ensure default-providers are available
+ for _, dp := range NeededDefaultProviders(info) {
+ if _, ok := snapInfos[dp]; !ok {
+ errs = append(errs, fmt.Errorf("cannot use snap %q: default provider %q is missing", info.InstanceName(), dp))
+ }
+ }
+ }
+ return errs
+}
diff -Nru snapd-2.40/snap/validate_test.go snapd-2.42.1/snap/validate_test.go
--- snapd-2.40/snap/validate_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/snap/validate_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -712,6 +712,8 @@
ErrorMatches, `layout "/dev" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/dev/foo", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/dev/foo" in an off-limits area`)
+ c.Check(ValidateLayout(&Layout{Snap: si, Path: "/home", Type: "tmpfs"}, nil),
+ ErrorMatches, `layout "/home" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/proc", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/proc" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/sys", Type: "tmpfs"}, nil),
@@ -734,10 +736,11 @@
ErrorMatches, `layout "/lib/firmware" in an off-limits area`)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/lib/modules", Type: "tmpfs"}, nil),
ErrorMatches, `layout "/lib/modules" in an off-limits area`)
+ c.Check(ValidateLayout(&Layout{Snap: si, Path: "/tmp", Type: "tmpfs"}, nil),
+ ErrorMatches, `layout "/tmp" in an off-limits area`)
// Several valid layouts.
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/foo", Type: "tmpfs", Mode: 01755}, nil), IsNil)
- c.Check(ValidateLayout(&Layout{Snap: si, Path: "/tmp", Type: "tmpfs"}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/usr", Bind: "$SNAP/usr"}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var", Bind: "$SNAP_DATA/var"}, nil), IsNil)
c.Check(ValidateLayout(&Layout{Snap: si, Path: "/var", Bind: "$SNAP_COMMON/var"}, nil), IsNil)
@@ -982,6 +985,21 @@
c.Assert(info.Layout, HasLen, 2)
err = ValidateLayoutAll(info)
c.Assert(err, IsNil)
+
+ // Layout replacing files in another snap's mount p oit
+ const yaml12 = `
+name: this-snap
+layout:
+ /snap/that-snap/current/stuff:
+ symlink: $SNAP/stuff
+`
+
+ strk = NewScopedTracker()
+ info, err = InfoFromSnapYamlWithSideInfo([]byte(yaml12), &SideInfo{Revision: R(42)}, strk)
+ c.Assert(err, IsNil)
+ c.Assert(info.Layout, HasLen, 1)
+ err = ValidateLayoutAll(info)
+ c.Assert(err, ErrorMatches, `layout "/snap/that-snap/current/stuff" defines a layout in space belonging to another snap`)
}
func (s *YamlSuite) TestValidateAppStartupOrder(c *C) {
@@ -1541,3 +1559,127 @@
}
}
}
+
+func (s *ValidateSuite) TestValidateSystemUsernames(c *C) {
+ const yaml1 = `name: binary
+version: 1.0
+system-usernames:
+ "b@d": shared
+`
+
+ strk := NewScopedTracker()
+ info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml1), nil, strk)
+ c.Assert(err, IsNil)
+ c.Assert(info.SystemUsernames, HasLen, 1)
+ err = Validate(info)
+ c.Assert(err, ErrorMatches, `invalid system username "b@d"`)
+}
+
+const yamlNeedDf = `name: need-df
+version: 1.0
+plugs:
+ gtk-3-themes:
+ interface: content
+ default-provider: gtk-common-themes
+`
+
+func (s *ValidateSuite) TestNeededDefaultProviders(c *C) {
+ strk := NewScopedTracker()
+ info, err := InfoFromSnapYamlWithSideInfo([]byte(yamlNeedDf), nil, strk)
+ c.Assert(err, IsNil)
+
+ dps := NeededDefaultProviders(info)
+ c.Check(dps, DeepEquals, []string{"gtk-common-themes"})
+}
+
+const yamlNeedDfWithSlot = `name: need-df
+version: 1.0
+plugs:
+ gtk-3-themes:
+ interface: content
+ default-provider: gtk-common-themes2:with-slot
+`
+
+func (s *ValidateSuite) TestNeededDefaultProvidersLegacyColonSyntax(c *C) {
+ strk := NewScopedTracker()
+ info, err := InfoFromSnapYamlWithSideInfo([]byte(yamlNeedDfWithSlot), nil, strk)
+ c.Assert(err, IsNil)
+
+ dps := NeededDefaultProviders(info)
+ c.Check(dps, DeepEquals, []string{"gtk-common-themes2"})
+}
+
+func (s *validateSuite) TestValidateSnapMissingCore(c *C) {
+ const yaml = `name: some-snap
+version: 1.0`
+
+ strk := NewScopedTracker()
+ info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), nil, strk)
+ c.Assert(err, IsNil)
+
+ infos := map[string]*Info{"some-snap": info}
+ errors := ValidateBasesAndProviders(infos)
+ c.Assert(errors, HasLen, 1)
+ c.Assert(errors[0], ErrorMatches, `cannot use snap "some-snap": required snap "core" missing`)
+}
+
+func (s *validateSuite) TestValidateSnapMissingBase(c *C) {
+ const yaml = `name: some-snap
+base: some-base
+version: 1.0`
+
+ strk := NewScopedTracker()
+ info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), nil, strk)
+ c.Assert(err, IsNil)
+
+ infos := map[string]*Info{"some-snap": info}
+ errors := ValidateBasesAndProviders(infos)
+ c.Assert(errors, HasLen, 1)
+ c.Assert(errors[0], ErrorMatches, `cannot use snap "some-snap": base "some-base" is missing`)
+}
+
+func (s *validateSuite) TestValidateSnapMissingDefaultProvider(c *C) {
+ strk := NewScopedTracker()
+ snapInfo, err := InfoFromSnapYamlWithSideInfo([]byte(yamlNeedDf), nil, strk)
+ c.Assert(err, IsNil)
+
+ var coreYaml = `name: core
+version: 1.0
+type: os`
+
+ coreInfo, err := InfoFromSnapYamlWithSideInfo([]byte(coreYaml), nil, strk)
+ c.Assert(err, IsNil)
+
+ infos := map[string]*Info{"some-snap": snapInfo, "core": coreInfo}
+ errors := ValidateBasesAndProviders(infos)
+ c.Assert(errors, HasLen, 1)
+ c.Assert(errors[0], ErrorMatches, `cannot use snap "need-df": default provider "gtk-common-themes" is missing`)
+}
+
+func (s *validateSuite) TestValidateSnapBaseNoneOK(c *C) {
+ const yaml = `name: some-snap
+base: none
+version: 1.0`
+
+ strk := NewScopedTracker()
+ info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), nil, strk)
+ c.Assert(err, IsNil)
+
+ infos := map[string]*Info{"some-snap": info}
+ errors := ValidateBasesAndProviders(infos)
+ c.Assert(errors, IsNil)
+}
+
+func (s *validateSuite) TestValidateSnapSnapd(c *C) {
+ const yaml = `name: snapd
+type: snapd
+version: 1.0`
+
+ strk := NewScopedTracker()
+ info, err := InfoFromSnapYamlWithSideInfo([]byte(yaml), nil, strk)
+ c.Assert(err, IsNil)
+
+ infos := map[string]*Info{"snapd": info}
+ errors := ValidateBasesAndProviders(infos)
+ c.Assert(errors, IsNil)
+}
diff -Nru snapd-2.40/spread.yaml snapd-2.42.1/spread.yaml
--- snapd-2.40/spread.yaml 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/spread.yaml 2019-10-30 12:17:43.000000000 +0000
@@ -64,10 +64,7 @@
- ubuntu-16.04-64:
workers: 8
- ubuntu-18.04-64:
- workers: 6
- - ubuntu-18.10-64:
- image: ubuntu-1810
- workers: 6
+ workers: 8
- ubuntu-19.04-64:
workers: 6
- ubuntu-core-16-64:
@@ -104,6 +101,14 @@
workers: 6
storage: preserve-size
+ google-unstable:
+ type: google
+ key: '$(HOST: echo "$SPREAD_GOOGLE_KEY")'
+ location: computeengine/us-east1-b
+ halt-timeout: 2h
+ systems:
+ - ubuntu-19.10-64:
+ workers: 6
- centos-7-64:
workers: 6
image: centos-7-64
@@ -115,14 +120,11 @@
halt-timeout: 2h
systems:
- ubuntu-16.04-64:
- workers: 4
+ workers: 6
- ubuntu-18.04-64:
- workers: 4
- - ubuntu-18.10-64:
- image: ubuntu-1810
- workers: 4
+ workers: 6
- ubuntu-19.04-64:
- workers: 4
+ workers: 6
google-nested:
type: google
@@ -604,20 +606,6 @@
"$TESTSLIB"/prepare-restore.sh --restore-project
restore-each: |
"$TESTSLIB"/prepare-restore.sh --restore-project-each
- if journalctl -u snapd.service | grep -F "signal: terminated"; then exit 1; fi
- case "$SPREAD_SYSTEM" in
- fedora-*|centos-*)
- # Make sure that we are not leaving behind incorrectly labeled snap
- # files on systems supporting SELinux
- (
- find /root/snap -printf '%Z\t%H/%P\n' || true
- find /home -regex '/home/[^/]*/snap\(/.*\)?' -printf '%Z\t%H/%P\n' || true
- ) | grep -c -v snappy_home_t | MATCH "0"
-
- find /var/snap -printf '%Z\t%H/%P\n' | grep -c -v snappy_var_t | MATCH "0"
- ;;
- esac
-
suites:
# The essential tests designed to run inside the autopkgtest
# environment on each platform. On autopkgtest we cannot run all tests
@@ -640,15 +628,17 @@
summary: Full-system tests for snapd
prepare: |
"$TESTSLIB"/prepare-restore.sh --prepare-suite
- debug: |
- systemctl status snapd.socket || true
- journalctl -xe
prepare-each: |
"$TESTSLIB"/prepare-restore.sh --prepare-suite-each
restore-each: |
"$TESTSLIB"/prepare-restore.sh --restore-suite-each
restore: |
"$TESTSLIB"/prepare-restore.sh --restore-suite
+ debug: |
+ if [ "$SPREAD_DEBUG_EACH" = 1 ]; then
+ systemctl status snapd.socket || true
+ journalctl -xe
+ fi
tests/core18/:
summary: Subset of core18 specific tests
systems: [ubuntu-core-18-*]
@@ -782,7 +772,6 @@
# Test cases are not yet ported to Fedora/openSUSE/Arch/AMZN2 that is why
# we keep them disabled. A later PR will enable most tests and
# drop this blacklist.
- systems: [-fedora-*, -opensuse-*, -arch-*, -amazon-*, -centos-*]
prepare: |
"$TESTSLIB"/prepare-restore.sh --prepare-suite
prepare-each: |
diff -Nru snapd-2.40/store/details_v2_test.go snapd-2.42.1/store/details_v2_test.go
--- snapd-2.40/store/details_v2_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/store/details_v2_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -298,6 +298,7 @@
"Layout",
"SideInfo.Channel",
"DownloadInfo.AnonDownloadURL", // TODO: going away at some point
+ "SystemUsernames",
}
var checker func(string, reflect.Value)
checker = func(pfx string, x reflect.Value) {
diff -Nru snapd-2.40/store/errors.go snapd-2.42.1/store/errors.go
--- snapd-2.40/store/errors.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/store/errors.go 2019-10-30 12:17:43.000000000 +0000
@@ -26,7 +26,7 @@
"sort"
"strings"
- "github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/channel"
"github.com/snapcore/snapd/strutil"
)
@@ -71,7 +71,7 @@
type RevisionNotAvailableError struct {
Action string
Channel string
- Releases []snap.Channel
+ Releases []channel.Channel
}
func (e *RevisionNotAvailableError) Error() string {
@@ -234,18 +234,18 @@
errDeviceAuthorizationNeedsRefresh = errors.New("soft-expired device authorization needs refresh")
)
-func translateSnapActionError(action, channel, code, message string, releases []snapRelease) error {
+func translateSnapActionError(action, snapChannel, code, message string, releases []snapRelease) error {
switch code {
case "revision-not-found":
e := &RevisionNotAvailableError{
Action: action,
- Channel: channel,
+ Channel: snapChannel,
}
if len(releases) != 0 {
- parsedReleases := make([]snap.Channel, len(releases))
+ parsedReleases := make([]channel.Channel, len(releases))
for i := 0; i < len(releases); i++ {
var err error
- parsedReleases[i], err = snap.ParseChannel(releases[i].Channel, releases[i].Architecture)
+ parsedReleases[i], err = channel.Parse(releases[i].Channel, releases[i].Architecture)
if err != nil {
// shouldn't happen, return error without Releases
return e
diff -Nru snapd-2.40/store/export_test.go snapd-2.42.1/store/export_test.go
--- snapd-2.40/store/export_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/store/export_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -141,8 +141,8 @@
sto.deltaFormat = dfmt
}
-func (sto *Store) DownloadDelta(deltaName string, downloadInfo *snap.DownloadInfo, w io.ReadWriteSeeker, pbar progress.Meter, user *auth.UserState) error {
- return sto.downloadDelta(deltaName, downloadInfo, w, pbar, user)
+func (sto *Store) DownloadDelta(deltaName string, downloadInfo *snap.DownloadInfo, w io.ReadWriteSeeker, pbar progress.Meter, user *auth.UserState, dlOpts *DownloadOptions) error {
+ return sto.downloadDelta(deltaName, downloadInfo, w, pbar, user, dlOpts)
}
func (sto *Store) DoRequest(ctx context.Context, client *http.Client, reqOptions *requestOptions, user *auth.UserState) (*http.Response, error) {
diff -Nru snapd-2.40/store/store.go snapd-2.42.1/store/store.go
--- snapd-2.40/store/store.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/store/store.go 2019-10-30 12:17:43.000000000 +0000
@@ -175,7 +175,13 @@
proxy func(*http.Request) (*url.URL, error)
}
+var ErrTooManyRequests = errors.New("too many requests")
+
func respToError(resp *http.Response, msg string) error {
+ if resp.StatusCode == 429 {
+ return ErrTooManyRequests
+ }
+
tpl := "cannot %s: got unexpected HTTP status code %d via %s to %q"
if oops := resp.Header.Get("X-Oops-Id"); oops != "" {
tpl += " [%s]"
@@ -342,7 +348,7 @@
architecture := cfg.Architecture
if cfg.Architecture == "" {
- architecture = arch.UbuntuArchitecture()
+ architecture = arch.DpkgArchitecture()
}
series := cfg.Series
@@ -1328,8 +1334,9 @@
}
type DownloadOptions struct {
- RateLimit int64
- IsAutoRefresh bool
+ RateLimit int64
+ IsAutoRefresh bool
+ LeavePartialOnError bool
}
// Download downloads the snap addressed by download info and returns its
@@ -1350,7 +1357,7 @@
logger.Debugf("Available deltas returned by store: %v", downloadInfo.Deltas)
if len(downloadInfo.Deltas) == 1 {
- err := s.downloadAndApplyDelta(name, targetPath, downloadInfo, pbar, user)
+ err := s.downloadAndApplyDelta(name, targetPath, downloadInfo, pbar, user, dlOpts)
if err == nil {
return nil
}
@@ -1369,10 +1376,14 @@
return err
}
defer func() {
+ fi, _ := w.Stat()
if cerr := w.Close(); cerr != nil && err == nil {
err = cerr
}
- if err != nil {
+ if err == nil {
+ return
+ }
+ if dlOpts == nil || !dlOpts.LeavePartialOnError || fi == nil || fi.Size() == 0 {
os.Remove(w.Name())
}
}()
@@ -1449,6 +1460,8 @@
Method: "GET",
URL: storeURL,
ExtraHeaders: map[string]string{},
+ // FIXME: use the new headers? with
+ // APILevel: apiV2Endps,
}
if cdnHeader != "" {
reqOptions.ExtraHeaders["Snap-CDN"] = cdnHeader
@@ -1637,7 +1650,7 @@
}
// downloadDelta downloads the delta for the preferred format, returning the path.
-func (s *Store) downloadDelta(deltaName string, downloadInfo *snap.DownloadInfo, w io.ReadWriteSeeker, pbar progress.Meter, user *auth.UserState) error {
+func (s *Store) downloadDelta(deltaName string, downloadInfo *snap.DownloadInfo, w io.ReadWriteSeeker, pbar progress.Meter, user *auth.UserState, dlOpts *DownloadOptions) error {
if len(downloadInfo.Deltas) != 1 {
return errors.New("store returned more than one download delta")
@@ -1659,7 +1672,7 @@
url = deltaInfo.DownloadURL
}
- return download(context.TODO(), deltaName, deltaInfo.Sha3_384, url, user, s, w, 0, pbar, nil)
+ return download(context.TODO(), deltaName, deltaInfo.Sha3_384, url, user, s, w, 0, pbar, dlOpts)
}
func getXdelta3Cmd(args ...string) (*exec.Cmd, error) {
@@ -1724,7 +1737,7 @@
}
// downloadAndApplyDelta downloads and then applies the delta to the current snap.
-func (s *Store) downloadAndApplyDelta(name, targetPath string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState) error {
+func (s *Store) downloadAndApplyDelta(name, targetPath string, downloadInfo *snap.DownloadInfo, pbar progress.Meter, user *auth.UserState, dlOpts *DownloadOptions) error {
deltaInfo := &downloadInfo.Deltas[0]
deltaPath := fmt.Sprintf("%s.%s-%d-to-%d.partial", targetPath, deltaInfo.Format, deltaInfo.FromRevision, deltaInfo.ToRevision)
@@ -1741,7 +1754,7 @@
os.Remove(deltaPath)
}()
- err = s.downloadDelta(deltaName, downloadInfo, w, pbar, user)
+ err = s.downloadDelta(deltaName, downloadInfo, w, pbar, user, dlOpts)
if err != nil {
return err
}
diff -Nru snapd-2.40/store/store_test.go snapd-2.42.1/store/store_test.go
--- snapd-2.40/store/store_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/store/store_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -56,6 +56,7 @@
"github.com/snapcore/snapd/progress"
"github.com/snapcore/snapd/release"
"github.com/snapcore/snapd/snap"
+ "github.com/snapcore/snapd/snap/channel"
"github.com/snapcore/snapd/snap/snaptest"
"github.com/snapcore/snapd/store"
"github.com/snapcore/snapd/testutil"
@@ -861,6 +862,56 @@
c.Assert(err, ErrorMatches, "uh, it failed")
// ... and ensure that the tempfile is removed
c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false)
+ // ... and not because it succeeded either
+ c.Assert(osutil.FileExists(path), Equals, false)
+}
+
+func (s *storeTestSuite) TestDownloadFailsLeavePartial(c *C) {
+ var tmpfile *os.File
+ restore := store.MockDownload(func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *store.Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter, dlOpts *store.DownloadOptions) error {
+ tmpfile = w.(*os.File)
+ w.Write([]byte{'X'}) // so it's not empty
+ return fmt.Errorf("uh, it failed")
+ })
+ defer restore()
+
+ snap := &snap.Info{}
+ snap.RealName = "foo"
+ snap.AnonDownloadURL = "anon-url"
+ snap.DownloadURL = "AUTH-URL"
+ snap.Size = 1
+ // simulate a failed download
+ path := filepath.Join(c.MkDir(), "downloaded-file")
+ err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, &store.DownloadOptions{LeavePartialOnError: true})
+ c.Assert(err, ErrorMatches, "uh, it failed")
+ // ... and ensure that the tempfile is *NOT* removed
+ c.Assert(osutil.FileExists(tmpfile.Name()), Equals, true)
+ // ... but the target path isn't there
+ c.Assert(osutil.FileExists(path), Equals, false)
+}
+
+func (s *storeTestSuite) TestDownloadFailsDoesNotLeavePartialIfEmpty(c *C) {
+ var tmpfile *os.File
+ restore := store.MockDownload(func(ctx context.Context, name, sha3, url string, user *auth.UserState, s *store.Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter, dlOpts *store.DownloadOptions) error {
+ tmpfile = w.(*os.File)
+ // no write, so the partial is empty
+ return fmt.Errorf("uh, it failed")
+ })
+ defer restore()
+
+ snap := &snap.Info{}
+ snap.RealName = "foo"
+ snap.AnonDownloadURL = "anon-url"
+ snap.DownloadURL = "AUTH-URL"
+ snap.Size = 1
+ // simulate a failed download
+ path := filepath.Join(c.MkDir(), "downloaded-file")
+ err := s.store.Download(s.ctx, "foo", path, &snap.DownloadInfo, nil, nil, &store.DownloadOptions{LeavePartialOnError: true})
+ c.Assert(err, ErrorMatches, "uh, it failed")
+ // ... and ensure that the tempfile *is* removed
+ c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false)
+ // ... and the target path isn't there
+ c.Assert(osutil.FileExists(path), Equals, false)
}
func (s *storeTestSuite) TestDownloadSyncFails(c *C) {
@@ -886,6 +937,8 @@
c.Assert(err, ErrorMatches, `(sync|fsync:) .*`)
// ... and ensure that the tempfile is removed
c.Assert(osutil.FileExists(tmpfile.Name()), Equals, false)
+ // ... because it's been renamed to the target path already
+ c.Assert(osutil.FileExists(path), Equals, true)
}
var downloadDeltaTests = []struct {
@@ -993,6 +1046,7 @@
for _, testCase := range downloadDeltaTests {
sto.SetDeltaFormat(testCase.format)
restore := store.MockDownload(func(ctx context.Context, name, sha3, url string, user *auth.UserState, _ *store.Store, w io.ReadWriteSeeker, resume int64, pbar progress.Meter, dlOpts *store.DownloadOptions) error {
+ c.Check(dlOpts, DeepEquals, &store.DownloadOptions{IsAutoRefresh: true})
expectedUser := s.user
if testCase.useLocalUser {
expectedUser = s.localUser
@@ -1024,7 +1078,7 @@
authedUser = nil
}
- err = sto.DownloadDelta("snapname", &testCase.info, w, nil, authedUser)
+ err = sto.DownloadDelta("snapname", &testCase.info, w, nil, authedUser, &store.DownloadOptions{IsAutoRefresh: true})
if testCase.expectError {
c.Assert(err, NotNil)
@@ -1944,7 +1998,7 @@
query := r.URL.Query()
c.Check(query.Get("fields"), Equals, "abc,def")
- c.Check(query.Get("architecture"), Equals, arch.UbuntuArchitecture())
+ c.Check(query.Get("architecture"), Equals, arch.DpkgArchitecture())
w.Header().Set("X-Suggested-Currency", "GBP")
w.WriteHeader(200)
@@ -2682,6 +2736,39 @@
sections, err := sto.Sections(s.ctx, s.user)
c.Check(err, IsNil)
c.Check(sections, DeepEquals, []string{"featured", "database"})
+ c.Check(n, Equals, 1)
+}
+
+func (s *storeTestSuite) TestSectionsQueryTooMany(c *C) {
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ assertRequest(c, r, "GET", sectionsPath)
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, "")
+
+ switch n {
+ case 0:
+ // All good.
+ default:
+ c.Fatalf("what? %d", n)
+ }
+
+ w.WriteHeader(429)
+ n++
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ serverURL, _ := url.Parse(mockServer.URL)
+ cfg := store.Config{
+ StoreBaseURL: serverURL,
+ }
+ dauthCtx := &testDauthContext{c: c, device: s.device}
+ sto := store.New(&cfg, dauthCtx)
+
+ sections, err := sto.Sections(s.ctx, s.user)
+ c.Check(err, Equals, store.ErrTooManyRequests)
+ c.Check(sections, IsNil)
+ c.Check(n, Equals, 1)
}
func (s *storeTestSuite) TestSectionsQueryCustomStore(c *C) {
@@ -2809,6 +2896,47 @@
"potato": `[{"snap":"bar","version":"2.0"}]`,
"meh": `[{"snap":"bar","version":"2.0"},{"snap":"foo","version":"1.0"}]`,
})
+ c.Check(n, Equals, 1)
+}
+
+func (s *storeTestSuite) TestSnapCommandsTooMany(c *C) {
+ c.Assert(os.MkdirAll(dirs.SnapCacheDir, 0755), IsNil)
+
+ n := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ c.Check(r.Header.Get("X-Device-Authorization"), Equals, "")
+
+ switch n {
+ case 0:
+ c.Check(r.URL.Path, Equals, "/api/v1/snaps/names")
+ default:
+ c.Fatalf("what? %d", n)
+ }
+
+ w.WriteHeader(429)
+ n++
+ }))
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ serverURL, _ := url.Parse(mockServer.URL)
+ dauthCtx := &testDauthContext{c: c, device: s.device}
+ sto := store.New(&store.Config{StoreBaseURL: serverURL}, dauthCtx)
+
+ db, err := advisor.Create()
+ c.Assert(err, IsNil)
+ defer db.Rollback()
+
+ var bufNames bytes.Buffer
+ err = sto.WriteCatalogs(s.ctx, &bufNames, db)
+ c.Assert(err, Equals, store.ErrTooManyRequests)
+ db.Commit()
+ c.Check(bufNames.String(), Equals, "")
+
+ dump, err := advisor.DumpCommands()
+ c.Assert(err, IsNil)
+ c.Check(dump, HasLen, 0)
+ c.Check(n, Equals, 1)
}
func (s *storeTestSuite) TestFind(c *C) {
@@ -2834,7 +2962,7 @@
c.Check(r.URL.Query().Get("fields"), Equals, "abc,def")
c.Check(r.Header.Get("X-Ubuntu-Series"), Equals, release.Series)
- c.Check(r.Header.Get("X-Ubuntu-Architecture"), Equals, arch.UbuntuArchitecture())
+ c.Check(r.Header.Get("X-Ubuntu-Architecture"), Equals, arch.DpkgArchitecture())
c.Check(r.Header.Get("X-Ubuntu-Classic"), Equals, "false")
c.Check(r.Header.Get("X-Ubuntu-Confinement"), Equals, "")
@@ -4325,7 +4453,7 @@
c.Check(storeID, Equals, "")
c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series)
- c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.UbuntuArchitecture())
+ c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture())
c.Check(r.Header.Get("Snap-Classic"), Equals, "false")
jsonReq, err := ioutil.ReadAll(r.Body)
@@ -4435,7 +4563,7 @@
c.Check(storeID, Equals, "")
c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series)
- c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.UbuntuArchitecture())
+ c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture())
c.Check(r.Header.Get("Snap-Classic"), Equals, "false")
jsonReq, err := ioutil.ReadAll(r.Body)
@@ -5440,7 +5568,7 @@
c.Check(storeID, Equals, "")
c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series)
- c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.UbuntuArchitecture())
+ c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture())
c.Check(r.Header.Get("Snap-Classic"), Equals, "false")
jsonReq, err := ioutil.ReadAll(r.Body)
@@ -5537,7 +5665,7 @@
c.Check(storeID, Equals, "")
c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series)
- c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.UbuntuArchitecture())
+ c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture())
c.Check(r.Header.Get("Snap-Classic"), Equals, "false")
jsonReq, err := ioutil.ReadAll(r.Body)
@@ -5613,6 +5741,45 @@
c.Assert(results[0].Channel, Equals, "candidate")
}
+func (s *storeTestSuite) TestSnapActionWithClientUserAgent(c *C) {
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ serverCalls := 0
+ mockServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
+ serverCalls++
+ assertRequest(c, r, "POST", snapActionPath)
+
+ c.Check(r.Header.Get("Snap-Client-User-Agent"), Equals, "some-snap-agent/1.0")
+
+ io.WriteString(w, `{
+ "results": []
+}`)
+ }))
+
+ c.Assert(mockServer, NotNil)
+ defer mockServer.Close()
+
+ mockServerURL, _ := url.Parse(mockServer.URL)
+ cfg := store.Config{
+ StoreBaseURL: mockServerURL,
+ }
+ dauthCtx := &testDauthContext{c: c, device: s.device}
+ sto := store.New(&cfg, dauthCtx)
+
+ // to construct the client-user-agent context we need to
+ // create a req that simulates what the req that the daemon got
+ r, err := http.NewRequest("POST", "/snapd/api", nil)
+ r.Header.Set("User-Agent", "some-snap-agent/1.0")
+ c.Assert(err, IsNil)
+ ctx := store.WithClientUserAgent(s.ctx, r)
+
+ results, err := sto.SnapAction(ctx, nil, []*store.SnapAction{{Action: "install", InstanceName: "some-snap"}}, nil, nil)
+ c.Check(serverCalls, Equals, 1)
+ c.Check(results, HasLen, 0)
+ c.Check(err, DeepEquals, &store.SnapActionError{NoResults: true})
+}
+
func (s *storeTestSuite) TestSnapActionDownloadParallelInstanceKey(c *C) {
// action here is one of install or download
restore := release.MockOnClassic(false)
@@ -5668,7 +5835,7 @@
c.Check(storeID, Equals, "")
c.Check(r.Header.Get("Snap-Device-Series"), Equals, release.Series)
- c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.UbuntuArchitecture())
+ c.Check(r.Header.Get("Snap-Device-Architecture"), Equals, arch.DpkgArchitecture())
c.Check(r.Header.Get("Snap-Classic"), Equals, "false")
jsonReq, err := ioutil.ReadAll(r.Body)
@@ -5902,7 +6069,7 @@
"snap2": &store.RevisionNotAvailableError{
Action: "refresh",
Channel: "candidate",
- Releases: []snap.Channel{
+ Releases: []channel.Channel{
snaptest.MustParseChannel("beta", "amd64"),
snaptest.MustParseChannel("beta", "arm64"),
},
@@ -6398,7 +6565,7 @@
switch r.URL.Path {
case "/v2/snaps/info/core":
c.Check(r.Method, Equals, "GET")
- c.Check(r.URL.Query(), DeepEquals, url.Values{"fields": {"download"}, "architecture": {arch.UbuntuArchitecture()}})
+ c.Check(r.URL.Query(), DeepEquals, url.Values{"fields": {"download"}, "architecture": {arch.DpkgArchitecture()}})
u, err := url.Parse("/download/core")
c.Assert(err, IsNil)
io.WriteString(w,
diff -Nru snapd-2.40/strutil/pathiter.go snapd-2.42.1/strutil/pathiter.go
--- snapd-2.40/strutil/pathiter.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/strutil/pathiter.go 2019-10-30 12:17:43.000000000 +0000
@@ -80,8 +80,13 @@
return iter.path[:iter.right]
}
-// CurrentBase returns the prefix of the path that was traversed, excluding the current name.
+// CurrentBase returns the prefix of the path that was traversed,
+// excluding the current name. The result never ends in '/' except if
+// current base is root.
func (iter *PathIterator) CurrentBase() string {
+ if iter.left > 0 && iter.path[iter.left-1] == '/' && iter.path[:iter.left] != "/" {
+ return iter.path[:iter.left-1]
+ }
return iter.path[:iter.left]
}
diff -Nru snapd-2.40/strutil/pathiter_test.go snapd-2.42.1/strutil/pathiter_test.go
--- snapd-2.40/strutil/pathiter_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/strutil/pathiter_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -66,7 +66,7 @@
c.Assert(iter.Depth(), Equals, 1)
c.Assert(iter.Next(), Equals, true)
- c.Assert(iter.CurrentBase(), Equals, "foo/")
+ c.Assert(iter.CurrentBase(), Equals, "foo")
c.Assert(iter.CurrentPath(), Equals, "foo/bar")
c.Assert(iter.CurrentName(), Equals, "bar")
c.Assert(iter.CurrentCleanName(), Equals, "bar")
@@ -97,7 +97,7 @@
c.Assert(iter.Depth(), Equals, 2)
c.Assert(iter.Next(), Equals, true)
- c.Assert(iter.CurrentBase(), Equals, "/foo/")
+ c.Assert(iter.CurrentBase(), Equals, "/foo")
c.Assert(iter.CurrentPath(), Equals, "/foo/bar/")
c.Assert(iter.CurrentName(), Equals, "bar/")
c.Assert(iter.CurrentCleanName(), Equals, "bar")
@@ -128,7 +128,7 @@
c.Assert(iter.Depth(), Equals, 2)
c.Assert(iter.Next(), Equals, true)
- c.Assert(iter.CurrentBase(), Equals, "/foo/")
+ c.Assert(iter.CurrentBase(), Equals, "/foo")
c.Assert(iter.CurrentPath(), Equals, "/foo/bar")
c.Assert(iter.CurrentName(), Equals, "bar")
c.Assert(iter.CurrentCleanName(), Equals, "bar")
@@ -138,6 +138,44 @@
c.Assert(iter.Depth(), Equals, 3)
}
+func (s *pathIterSuite) TestPathIteratorAbsoluteCleanDepth4(c *C) {
+ iter, err := strutil.NewPathIterator("/foo/bar/baz")
+ c.Assert(err, IsNil)
+ c.Assert(iter.Path(), Equals, "/foo/bar/baz")
+ c.Assert(iter.Depth(), Equals, 0)
+
+ c.Assert(iter.Next(), Equals, true)
+ c.Assert(iter.CurrentBase(), Equals, "")
+ c.Assert(iter.CurrentPath(), Equals, "/")
+ c.Assert(iter.CurrentName(), Equals, "/")
+ c.Assert(iter.CurrentCleanName(), Equals, "")
+ c.Assert(iter.Depth(), Equals, 1)
+
+ c.Assert(iter.Next(), Equals, true)
+ c.Assert(iter.CurrentBase(), Equals, "/")
+ c.Assert(iter.CurrentPath(), Equals, "/foo/")
+ c.Assert(iter.CurrentName(), Equals, "foo/")
+ c.Assert(iter.CurrentCleanName(), Equals, "foo")
+ c.Assert(iter.Depth(), Equals, 2)
+
+ c.Assert(iter.Next(), Equals, true)
+ c.Assert(iter.CurrentBase(), Equals, "/foo")
+ c.Assert(iter.CurrentPath(), Equals, "/foo/bar/")
+ c.Assert(iter.CurrentName(), Equals, "bar/")
+ c.Assert(iter.CurrentCleanName(), Equals, "bar")
+ c.Assert(iter.Depth(), Equals, 3)
+
+ c.Assert(iter.Next(), Equals, true)
+ c.Assert(iter.CurrentBase(), Equals, "/foo/bar")
+ c.Assert(iter.CurrentPath(), Equals, "/foo/bar/baz")
+ c.Assert(iter.CurrentName(), Equals, "baz")
+ c.Assert(iter.CurrentCleanName(), Equals, "baz")
+ c.Assert(iter.Depth(), Equals, 4)
+
+ c.Assert(iter.Next(), Equals, false)
+ c.Assert(iter.Depth(), Equals, 4)
+}
+
func (s *pathIterSuite) TestPathIteratorRootDir(c *C) {
iter, err := strutil.NewPathIterator("/")
c.Assert(err, IsNil)
@@ -182,14 +220,14 @@
c.Assert(iter.Depth(), Equals, 2)
c.Assert(iter.Next(), Equals, true)
- c.Assert(iter.CurrentBase(), Equals, "/zażółć/")
+ c.Assert(iter.CurrentBase(), Equals, "/zażółć")
c.Assert(iter.CurrentPath(), Equals, "/zażółć/gęślą/")
c.Assert(iter.CurrentName(), Equals, "gęślą/")
c.Assert(iter.CurrentCleanName(), Equals, "gęślą")
c.Assert(iter.Depth(), Equals, 3)
c.Assert(iter.Next(), Equals, true)
- c.Assert(iter.CurrentBase(), Equals, "/zażółć/gęślą/")
+ c.Assert(iter.CurrentBase(), Equals, "/zażółć/gęślą")
c.Assert(iter.CurrentPath(), Equals, "/zażółć/gęślą/jaźń")
c.Assert(iter.CurrentName(), Equals, "jaźń")
c.Assert(iter.CurrentCleanName(), Equals, "jaźń")
diff -Nru snapd-2.40/strutil/set.go snapd-2.42.1/strutil/set.go
--- snapd-2.40/strutil/set.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/strutil/set.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,81 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package strutil
+
+// OrderedSet is a set of strings that maintains the order of insertion.
+//
+// The order of putting items into the set is retained. Putting items "a", "b"
+// and then "a", results in order "a", "b".
+//
+// External synchronization is required for safe concurrent access.
+type OrderedSet struct {
+ positionOf map[string]int
+}
+
+// Items returns a slice of strings representing insertion order.
+//
+// Items is O(N) in the size of the set.
+func (o *OrderedSet) Items() []string {
+ if len(o.positionOf) == 0 {
+ return nil
+ }
+ items := make([]string, len(o.positionOf))
+ for item, idx := range o.positionOf {
+ items[idx] = item
+ }
+ return items
+}
+
+// Contains returns true if the set contains a given item.
+//
+// Contains is O(1) in the size of the set.
+func (o *OrderedSet) Contains(item string) bool {
+ _, ok := o.positionOf[item]
+ return ok
+}
+
+// IndexOf returns the position of an item in the set.
+func (o *OrderedSet) IndexOf(item string) (idx int, ok bool) {
+ idx, ok = o.positionOf[item]
+ return idx, ok
+}
+
+// Put adds an item into the set.
+//
+// If the item was not present then it is stored and ordered after all existing
+// elements. If the item was already present its position is not changed.
+//
+// Put is O(1) in the size of the set.
+func (o *OrderedSet) Put(item string) {
+ if o.positionOf == nil {
+ o.positionOf = make(map[string]int)
+ }
+
+ if _, ok := o.positionOf[item]; ok {
+ return
+ }
+
+ o.positionOf[item] = len(o.positionOf)
+}
+
+// Size returns the number of elements in the set.
+func (o *OrderedSet) Size() int {
+ return len(o.positionOf)
+}
diff -Nru snapd-2.40/strutil/set_test.go snapd-2.42.1/strutil/set_test.go
--- snapd-2.40/strutil/set_test.go 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/strutil/set_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,79 @@
+// -*- Mode: Go; indent-tabs-mode: t -*-
+
+/*
+ * Copyright (C) 2019 Canonical Ltd
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU General Public License version 3 as
+ * published by the Free Software Foundation.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
+ * GNU General Public License for more details.
+ *
+ * You should have received a copy of the GNU General Public License
+ * along with this program. If not, see .
+ *
+ */
+
+package strutil_test
+
+import (
+ . "gopkg.in/check.v1"
+
+ "github.com/snapcore/snapd/strutil"
+)
+
+type orderedSetSuite struct {
+ set strutil.OrderedSet
+}
+
+var _ = Suite(&orderedSetSuite{})
+
+func (s *orderedSetSuite) SetUpTest(c *C) {
+ s.set = strutil.OrderedSet{}
+}
+
+func (s *orderedSetSuite) TestZeroValueItems(c *C) {
+ c.Assert(s.set.Items(), HasLen, 0)
+}
+
+func (s *orderedSetSuite) TestZeroValueContains(c *C) {
+ c.Check(s.set.Contains("foo"), Equals, false)
+}
+
+func (s *orderedSetSuite) TestZeroValueIndexOf(c *C) {
+ _, ok := s.set.IndexOf("foo")
+ c.Check(ok, Equals, false)
+}
+
+func (s *orderedSetSuite) TestZeroValuePut(c *C) {
+ items := []string{"foo", "bar", "froz"}
+ for idx, item := range items {
+ s.set.Put(item)
+ c.Check(s.set.Contains(item), Equals, true)
+ realIdx, ok := s.set.IndexOf(item)
+ c.Check(ok, Equals, true)
+ c.Check(idx, Equals, realIdx)
+ c.Check(s.set.Size(), Equals, idx+1)
+ c.Check(s.set.Items(), DeepEquals, items[:idx+1])
+ }
+}
+
+func (s *orderedSetSuite) TestZeroValueSize(c *C) {
+ c.Assert(s.set.Size(), Equals, 0)
+}
+
+func (s *orderedSetSuite) TestDeduplication(c *C) {
+ s.set.Put("a")
+ s.set.Put("b")
+ s.set.Put("a")
+ s.set.Put("c")
+
+ c.Assert(s.set.Items(), DeepEquals, []string{"a", "b", "c"})
+ c.Check(s.set.Size(), Equals, 3)
+ c.Check(s.set.Contains("a"), Equals, true)
+ c.Check(s.set.Contains("b"), Equals, true)
+ c.Check(s.set.Contains("c"), Equals, true)
+}
diff -Nru snapd-2.40/strutil/strutil.go snapd-2.42.1/strutil/strutil.go
--- snapd-2.40/strutil/strutil.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/strutil/strutil.go 2019-10-30 12:17:43.000000000 +0000
@@ -1,7 +1,7 @@
// -*- Mode: Go; indent-tabs-mode: t -*-
/*
- * Copyright (C) 2014-2015 Canonical Ltd
+ * Copyright (C) 2014-2019 Canonical Ltd
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License version 3 as
diff -Nru snapd-2.40/systemd/systemd.go snapd-2.42.1/systemd/systemd.go
--- snapd-2.40/systemd/systemd.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/systemd/systemd.go 2019-10-30 12:17:43.000000000 +0000
@@ -674,6 +674,7 @@
Where=%s
Type=%s
Options=%s
+LazyUnmount=yes
[Install]
WantedBy=multi-user.target
diff -Nru snapd-2.40/systemd/systemd_test.go snapd-2.42.1/systemd/systemd_test.go
--- snapd-2.40/systemd/systemd_test.go 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/systemd/systemd_test.go 2019-10-30 12:17:43.000000000 +0000
@@ -505,7 +505,7 @@
func (s *SystemdTestSuite) TestAddMountUnit(c *C) {
rootDir := dirs.GlobalRootDir
- restore := squashfs.MockUseFuse(false)
+ restore := squashfs.MockNeedsFuse(false)
defer restore()
mockSnapPath := filepath.Join(c.MkDir(), "/var/lib/snappy/snaps/foo_1.0.snap")
@@ -525,6 +525,7 @@
Where=/snap/snapname/123
Type=squashfs
Options=nodev,ro,x-gdu.hide
+LazyUnmount=yes
[Install]
WantedBy=multi-user.target
@@ -538,7 +539,7 @@
}
func (s *SystemdTestSuite) TestAddMountUnitForDirs(c *C) {
- restore := squashfs.MockUseFuse(false)
+ restore := squashfs.MockNeedsFuse(false)
defer restore()
// a directory instead of a file produces a different output
@@ -557,6 +558,7 @@
Where=/snap/snapname/x1
Type=none
Options=nodev,ro,x-gdu.hide,bind
+LazyUnmount=yes
[Install]
WantedBy=multi-user.target
@@ -572,7 +574,7 @@
func (s *SystemdTestSuite) TestWriteSELinuxMountUnit(c *C) {
restore := release.MockSELinuxIsEnabled(func() (bool, error) { return true, nil })
defer restore()
- restore = squashfs.MockUseFuse(false)
+ restore = squashfs.MockNeedsFuse(false)
defer restore()
mockSnapPath := filepath.Join(c.MkDir(), "/var/lib/snappy/snaps/foo_1.0.snap")
@@ -595,6 +597,7 @@
Where=/snap/snapname/123
Type=squashfs
Options=nodev,ro,x-gdu.hide,context=system_u:object_r:snappy_snap_t:s0
+LazyUnmount=yes
[Install]
WantedBy=multi-user.target
@@ -637,6 +640,7 @@
Where=/snap/snapname/123
Type=fuse.squashfuse
Options=nodev,ro,x-gdu.hide,allow_other
+LazyUnmount=yes
[Install]
WantedBy=multi-user.target
@@ -675,6 +679,7 @@
Where=/snap/snapname/123
Type=squashfs
Options=nodev,ro,x-gdu.hide
+LazyUnmount=yes
[Install]
WantedBy=multi-user.target
diff -Nru snapd-2.40/tests/cross/go-build/task.yaml snapd-2.42.1/tests/cross/go-build/task.yaml
--- snapd-2.40/tests/cross/go-build/task.yaml 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/cross/go-build/task.yaml 2019-10-30 12:17:43.000000000 +0000
@@ -34,12 +34,17 @@
cp -ar "$PROJECT_PATH" /tmp/cross-build/src/github.com/snapcore
chown -R test:12345 /tmp/cross-build
+ UBUNTU_ARCHIVE="http://archive.ubuntu.com/ubuntu/"
+ if [ "$SPREAD_BACKEND" = "google" ]; then
+ UBUNTU_ARCHIVE="http://$(cloud-id -l | cut -f2).gce.archive.ubuntu.com/ubuntu/"
+ fi
+
mv /etc/apt/sources.list /etc/apt/sources.list.orig
cat > /etc/apt/sources.list <<-EOF
- deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu/ $UBUNTU_CODENAME main universe
- deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu/ $UBUNTU_CODENAME-updates main universe
- deb [arch=amd64,i386] http://archive.ubuntu.com/ubuntu/ $UBUNTU_CODENAME-backports main universe
- deb [arch=amd64,i386] http://security.ubuntu.com/ubuntu/ $UBUNTU_CODENAME-security main universe
+ deb [arch=amd64,i386] $UBUNTU_ARCHIVE $UBUNTU_CODENAME main universe
+ deb [arch=amd64,i386] $UBUNTU_ARCHIVE $UBUNTU_CODENAME-updates main universe
+ deb [arch=amd64,i386] $UBUNTU_ARCHIVE $UBUNTU_CODENAME-backports main universe
+ deb [arch=amd64,i386] $UBUNTU_ARCHIVE $UBUNTU_CODENAME-security main universe
deb [arch=armhf,arm64,powerpc,ppc64el,s390x] http://ports.ubuntu.com/ $UBUNTU_CODENAME main universe
deb [arch=armhf,arm64,powerpc,ppc64el,s390x] http://ports.ubuntu.com/ $UBUNTU_CODENAME-updates main universe
@@ -52,6 +57,7 @@
execute: |
cd /tmp/cross-build/src/github.com/snapcore/snapd
- for cmd in $( find ./cmd -mindepth 2 -maxdepth 2 -name \*.go -printf '%h\n' | sort -u ); do
+ # grab only packages whose name is 'main'
+ for cmd in $( GOPATH=/tmp/cross-build go list -f '{{if eq .Name "main"}}{{.ImportPath}}{{end}}' ./cmd/...); do
su -c "GOPATH=/tmp/cross-build CGO_ENABLED=1 GOARCH=$X_GOARCH CC=$X_CC go build -v -o /dev/null $cmd" test
done
diff -Nru snapd-2.40/tests/lib/assertions/developer1-my-classic-w-gadget-18.model snapd-2.42.1/tests/lib/assertions/developer1-my-classic-w-gadget-18.model
--- snapd-2.40/tests/lib/assertions/developer1-my-classic-w-gadget-18.model 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/assertions/developer1-my-classic-w-gadget-18.model 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,22 @@
+type: model
+authority-id: developer1
+series: 16
+brand-id: developer1
+model: my-classic-w-gadget-18
+classic: true
+gadget: classic-gadget-18
+required-snaps:
+ - core18
+timestamp: 2019-07-20T14:50:00+00:00
+sign-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
+
+AcLBUgQAAQoABgUCXVu5EQAALCwQAHq/HmEIMha+rrOILXhfv7qYZ90sbwC34L5r13MIEDHATweI
+AMHzxpmmDp2SuwoWHHoYQa4ond0CtSMaXD1Tj9tqxDZBUb9Zxd3DeBwYKwaWSHIoqnBT7G3g8fx1
+J2q1I5jII9ReHaNZbGTQPXfmv3od/5LTUJvovdfepCTZrK4neUNyx4d8YaYWVSyQEpVK90hlB2Ft
+vgAviiMQ8qgIHzFi5IDeDuKbRotPp6okcIQXQ9OSH8J7sYcrlQ26q40V25ipyO/WsMXDxHyexMTt
+r+tD+rwpSr4obY21qt9XWPGxs7rR3vXFDKxW7HKdOKS1q41K36Np4K4gtEAVYZ7S45OGUNSk9qvc
+1yXwtBw3AqKVuRbK/UaD/aHXa/Ues5YDmOW/18/qPkdaE01AvGgoE2OtgEVEZ39nK2Za+2dWa7Jg
+Loa5gAAI9mv7IsUtfvMbhaQp5Tpi07j2JgeibX9hEI8Fr10XF4PsX8Iso8QDYvxeh6he6BbWcyk7
+ouZH85Cf3rOgQ+F8JvOVhruYJVa9kwkpLZasudRr+VdMIerr6Tubw9L0IVGFNbMyARJ27eEHINOc
+IV7bBwp83tv8m5t67Lo6Ysk0EDiHsoBb/GGCEAQXfBPN7wvGAKVcd0bHeZu2SwkArf9+OWe90icE
+iXWpOCGMvYoMCvBkP5U0BXjPgiuX
diff -Nru snapd-2.40/tests/lib/assertions/developer1-pc-18.model snapd-2.42.1/tests/lib/assertions/developer1-pc-18.model
--- snapd-2.40/tests/lib/assertions/developer1-pc-18.model 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/assertions/developer1-pc-18.model 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,22 @@
+type: model
+authority-id: developer1
+series: 16
+brand-id: developer1
+model: my-model
+architecture: amd64
+base: core18
+gadget: pc=18
+kernel: pc-kernel=18
+timestamp: 2017-05-28T19:40:00+00:00
+sign-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
+
+AcLBUgQAAQoABgUCXS94YgAAsQkQADKGvhH3LDiCZxjfVROO/WdNgFCPnPRi6ZYKYDdrXn1q4BlU
+7Q7ee/pbCIlrc2p5L6mOXFH6U6ddyf2tni3NzJ9w0XY+6ZGVPK5Et9akPUHyTiU7h4yYYXzDwGGi
+ClIESKEqEequBc+KpvEYCuixusMnOwDL7cDKEzpYeIaUGc0ZBV2xLQL+QqR9ATIeD+OnSdMTRt/6
+0DOYSVvkT76MjBtkyXeII+hbTOjjiMp0JbCI/9efyMR5vhUZrSTejiuNUEokTW3S742Z0GGK0OlY
+DQrcxN9flVa71MHEDvyr60TQBsiYGK30WH3FyxMuXDB4PGP8y6Ym3yTyyNugQOYbDJx8dLGNbjhy
+F4LuKNrzhfbU8M3C7UrK8clnilkuWSn7lQuuI6E2dGNx09UMXUrQF17Z7Zu9aWd1fLjCa6HXN4vm
+CwIySVKYpV5hS9HYhE27d+Q8UICuu7ciGhyBvgOIexYLnNRFtYguLJZ9ymzRhwQ8s8j6XEWkZvs/
+aOvUHycmylS11b7l656KEQ8r2w+4OdXOB9LWQ5QSOxpot6oMvUNZj4JMiJWkJTQ3hXGj7oTnyFwo
+iUYTIpMCOYFsJFwCJfMSn8JeKMmCxSLsYdGoeUZqIO+U7PE35C4XKNOEhbInzMFpMdN9Xip/NDF2
+TZXwB3GIGGDSkjZCgmFWb3pjdAin
diff -Nru snapd-2.40/tests/lib/assertions/developer1-pc-18-revno2.model snapd-2.42.1/tests/lib/assertions/developer1-pc-18-revno2.model
--- snapd-2.40/tests/lib/assertions/developer1-pc-18-revno2.model 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/assertions/developer1-pc-18-revno2.model 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,25 @@
+type: model
+authority-id: developer1
+revision: 2
+series: 16
+brand-id: developer1
+model: my-model
+architecture: amd64
+base: core18
+gadget: pc=18
+kernel: pc-kernel=18
+required-snaps:
+ - test-snapd-tools-core18
+timestamp: 2019-01-18T08:00:00+00:00
+sign-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
+
+AcLBUgQAAQoABgUCXS+D/gAA+/YQAIvdKF52yhf7K6bSPVKGaDQ5c95WI+w0NnPb37u6RXv6wrr+
+zbs8kVsJWw2lDxjoiEAvbr47miF5egBcynUkjjLYLgwjhgz8eqogKpF0nvGgjfn8IzEoJj+sV5Wb
+zKzZ6XtOuA7rzMqBBwpGQpYv41bajvaCNv3f6libzAS+hQZCZLkKCcLAuPk/JzfvTggivgrBGX1k
+vtsPWSpPfD4L5/tTwXitrLHO5nyjrfljzSA6EDloBbOl+TZSg9oo3MEvQTNQrxRN15C7wuiHTFjQ
+qzbBlbypIDdIh+VQgtzI9ksi5HX7uf/FJi0J7APQmlUuLDZzJSW0FuZcP4S9bPdPRWFC4Blcw9J+
+eyJEx57uiDtX6iULM9hEXdJ4iS7jJ7UH7S4q9GniEMGCz1SHc0962iySdrDdj3UaqmvRwfJv34sR
+aeFBcBRVHoVo1Y4ERJ0jY0B9GektCyFM1DP+sfqGdz2Fb8EYeUOc3/BmBAzllptseWWfHuVeO4Ux
+bfw5FcGiGoeG0plyiWGI3GKIkSPCeaEpHdyuJP19O0wairprF79ivcE0/map5ZIUgRmewvqt89+a
+/9Tt9W3UnkjgKlTMIfxzZut5jsLm2u80GuxooOZLVVriDfybobkegVLBmSIEI3hCiiDtMuHfDQ0+
+WMcijT7tu1txEe/7hyBi84oiK3lR
diff -Nru snapd-2.40/tests/lib/assertions/developer1-pc-18-revno3.model snapd-2.42.1/tests/lib/assertions/developer1-pc-18-revno3.model
--- snapd-2.40/tests/lib/assertions/developer1-pc-18-revno3.model 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/assertions/developer1-pc-18-revno3.model 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,23 @@
+type: model
+authority-id: developer1
+revision: 3
+series: 16
+brand-id: developer1
+model: my-model
+architecture: amd64
+base: core18
+gadget: pc=18
+kernel: pc-kernel=18
+timestamp: 2019-03-20T09:11:00+00:00
+sign-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
+
+AcLBUgQAAQoABgUCXS+EegAAHt0QAGE4g8j/cqT0zoBrbm2Aq04CFJ23kOotp4Mti9na8twKwsHz
+5xM+fnTHLGh5ONyCl9/NpV0SSM9FG1BFGBlGTKaxGeu69uBKNIR61G9RXfzQl3Dep0UvG7nCnZdO
+Cd+pJH6KAiL9kKQDxIeG6myw6ais85n71sF+TgBSJb31gE4YjjH1oyxMw25+UIcPcBzpC8W8JCL2
+yA+7Ym+bcV3Avu+jATKj5SsnuOM+zzNxS1cs0Qy5qPv2UqN0ePEWaG1yawWJzxcV3hP6RiuwQYGc
+gbZpA1QQnHOGYrZG9JqRSFXYLVKvVQfQfzW3FRFLMTdGp8U5ihAj0Fk+EDKc5kBDRwbhO/r1CFmr
++kLjmWEiMjlyEiyyzBUTyfq5TiqS7GacuCOLZoOILPWXyu/VbbegcrM2AjHxDohNhBkiWo59PtCy
+/BSVNHLFd5OglDHg8LIX4CKGM9ezjnvxe9btT3EUch8OpD5sKD1Ryrg05cebBqGmCQvEINFkU2pv
+RmhYzHb1NnPsZwBhaxUxEbbj9Brn1QDS5XdNYeMgvMZLL9HyFFHfmTluaPY04xcGQPT0rOkLo+Jc
+/VN2+0bSyUgKqqdjXq5JWbndD1Wwy8Pj/biEBHFAQX02u+YKrmnnSf7g3XQ6uS+lyniN15tEVRMB
+0uEF761PHhG4cvFnTbSlpeacHYL5
diff -Nru snapd-2.40/tests/lib/assertions/developer1-pc-new-kernel.model snapd-2.42.1/tests/lib/assertions/developer1-pc-new-kernel.model
--- snapd-2.40/tests/lib/assertions/developer1-pc-new-kernel.model 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/assertions/developer1-pc-new-kernel.model 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,22 @@
+type: model
+authority-id: developer1
+revision: 2
+series: 16
+brand-id: developer1
+model: my-model
+architecture: amd64
+gadget: pc
+kernel: test-snapd-pc-kernel
+timestamp: 2019-05-07T10:00:00+00:00
+sign-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
+
+AcLBUgQAAQoABgUCXNFHYgAAXv0QAE6MkNwPrIJ8Zro+OW8vwm7ziBbojCuHuSYAUpscDYUaKEtE
+eJCybYpC1gOXPfo6cpqriqvTgTAaSRqmloQXcI2Ep6DTlXC0VJGvbll8ZFRh8CwkcvBsx6gU1aKv
+I6pUpAAr+2u0uaX0oCqt8GEr2AmmRy5384KCEabSy1/3cMNxZu2WOfwMiLaD6/eXfINM/P+jpSQX
+fdiFmO7nZASEEqQlEEBf1icJTfxZd8KWowRMlFAKBq1keBm7pTKOTZpToLjT5mgNNiObRsn/URYz
+S4Wkwme1G3jTo2ueigcVcwrA7UeKl/wm4bWZ/9bgz2vDbe5Xv0IlUxS62DqLSWA+u/y9NF9bvkiE
+pF0jb+X0KB3v8XegwXjBMe+upA17+i5v1z2cYBT2IZD780oV62Wsgxa4pumMf6cDyUgZUPPeBsgQ
+RWexshNWgdaMYgiZrkDJI6Iq8MWLwiq/je34pUf4MbdXLj3ZBka3OxQlAe1GsO73txd6TH0/Ic2U
+pw9dAh12Epjo4ojmnnejp+XW7/ByhXoeOp/+ekhIkQJ6MlPN734NPj5kyy09V4pV/r37CZfr0yXh
+W27kWr/L9XKf8MXJ8czUkyLSK/lxrqAYMRgh4AiClx1j3qPLxDhr18pK3IyUcdXfqLA4MkRByamS
+ML7BN87LXt0Z7ox8NNMle3vQHX2K
diff -Nru snapd-2.40/tests/lib/bin/apt-tool snapd-2.42.1/tests/lib/bin/apt-tool
--- snapd-2.40/tests/lib/bin/apt-tool 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/bin/apt-tool 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,36 @@
+#!/usr/bin/python3
+
+import sys
+
+import apt
+
+
+def checkpoint():
+ pkgs = set()
+ cache = apt.Cache()
+ for pkg in cache:
+ if pkg.is_installed:
+ pkgs.add(pkg.name)
+ for name in sorted(pkgs):
+ print(name)
+
+
+def restore(fname):
+ desired = set([line.strip() for line in open(fname)])
+
+ cache = apt.Cache()
+ for pkg in cache:
+ if pkg.is_installed and not pkg.name in desired:
+ print("removing", pkg)
+ pkg.mark_delete(auto_fix=False)
+ if not pkg.is_installed and pkg.name in desired:
+ print("installing", pkg)
+ pkg.mark_install(auto_fix=False, auto_install=False)
+ cache.commit()
+
+
+if __name__ == "__main__":
+ if sys.argv[1] == "checkpoint":
+ checkpoint()
+ elif sys.argv[1] == "restore":
+ restore(sys.argv[2])
diff -Nru snapd-2.40/tests/lib/bin/lxd-tool snapd-2.42.1/tests/lib/bin/lxd-tool
--- snapd-2.40/tests/lib/bin/lxd-tool 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/bin/lxd-tool 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,35 @@
+#!/bin/sh -e
+case "${1:-}" in
+ undo-lxd-mount-changes)
+ # Vanilla systems have /sys/fs/cgroup/cpuset without clone_children option.
+ # Using LXD to create a container enables this option, as can be seen here:
+ #
+ # -37 32 0:32 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,cpuset
+ # +37 32 0:32 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:15 - cgroup cgroup rw,cpuset,clone_children
+ #
+ # To restore vanilla state, disable the option now.
+ if [ "$(mountinfo-tool /sys/fs/cgroup/cpuset .fs_type)" = cgroup ]; then
+ echo 0 > /sys/fs/cgroup/cpuset/cgroup.clone_children
+ fi
+
+ # Vanilla system have /sys/fs/cgroup/unified mounted with the nsdelegate
+ # option which is available since kernel 4.13 Using LXD to create a
+ # container disables this options, as can be seen here:
+ #
+ # -32 31 0:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime shared:10 - cgroup2 cgroup rw,nsdelegate
+ # +32 31 0:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime shared:10 - cgroup2 cgroup rw
+ #
+ # To restore vanilla state, enable the option now, but only if the kernel supports that.
+ # https://lore.kernel.org/patchwork/patch/803265/
+ # https://github.com/systemd/systemd/commit/4095205ecccdfddb822ee8fdc44d11f2ded9be24
+ # The kernel version must be made compatible with the strict version
+ # comparison. I chose to cut at the "-" and take the stuff before it.
+ if [ "$(mountinfo-tool /sys/fs/cgroup/unified .fs_type)" = cgroup2 ] && version-tool --strict "$(uname -r | cut -d- -f 1)" -ge 4.13; then
+ mount -o remount,nsdelegate /sys/fs/cgroup/unified
+ fi
+ ;;
+ *)
+ echo "lxd-tool: unknown command $*" >&2
+ exit 1
+ ;;
+esac
diff -Nru snapd-2.40/tests/lib/bin/mountinfo-tool snapd-2.42.1/tests/lib/bin/mountinfo-tool
--- snapd-2.40/tests/lib/bin/mountinfo-tool 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/lib/bin/mountinfo-tool 2019-10-30 12:17:43.000000000 +0000
@@ -1,9 +1,10 @@
#!/usr/bin/env any-python
from __future__ import print_function, absolute_import, unicode_literals
-from argparse import ArgumentParser, FileType, RawTextHelpFormatter
-import sys
+from argparse import Action, ArgumentParser, FileType, RawTextHelpFormatter, SUPPRESS
import re
+import sys
+import unittest
# PY2 is true when we're running under Python 2.x It is used for appropriate
# return value selection of __str__ and __repr_ methods, which must both
@@ -21,7 +22,8 @@
# https://mypy.readthedocs.io/en/latest/common_issues.html#import-cycles
MYPY = False
if MYPY:
- from typing import Any, Dict, List, Text, Tuple, Match
+ from typing import Any, Dict, List, Text, Tuple, Match, Optional, Union, Sequence
+ from argparse import Namespace
class Device(int):
@@ -64,6 +66,47 @@
return self & ((1 << 16) - 1)
+def _format_opt_fields(fields):
+ # type: (List[Text]) -> str
+ """_format_opt_fields returns the special formatting of optional fields."""
+ if len(fields):
+ result = " ".join(fields) + " -"
+ else:
+ result = "-"
+ if PY2:
+ return result.encode()
+ return result
+
+
+# OptionalFields is a specialization of List[Text] but because of some Python 2
+# distributions lacking the typing module, we must define a variant for mypy
+# and one for without mypy. In both cases the customized formatting logic is
+# implemented by _format_opt_fields().
+if MYPY:
+
+ class OptionalFields(List[Text]):
+ def __str__(self):
+ # type: () -> str
+ return _format_opt_fields(self)
+
+
+else:
+
+ class OptionalFields(list):
+ def __str__(self):
+ # type: () -> str
+ return _format_opt_fields(self)
+
+
+class OptionalFieldsTests(unittest.TestCase):
+ def test_str(self):
+ # type: () -> None
+ fields = OptionalFields()
+ self.assertEqual("{}".format(fields), "-")
+ fields.append("master:123")
+ self.assertEqual("{}".format(fields), "master:123 -")
+
+
class MountInfoEntry(object):
"""Single entry in /proc/pid/mointinfo, see proc(5)"""
@@ -88,10 +131,30 @@
self.root_dir = ""
self.mount_point = ""
self.mount_opts = ""
- self.opt_fields = [] # type: List[Text]
+ self.opt_fields = OptionalFields() # type: OptionalFields
self.fs_type = ""
self.mount_source = ""
self.sb_opts = ""
+ # This field does not represent kernel state.
+ # It is a marker for an entry being matched by a filter.
+ self.matched = False
+
+ def __eq__(self, other):
+ # type: (object) -> Union[NotImplemented, bool]
+ if not isinstance(other, MountInfoEntry):
+ return NotImplemented
+ return (
+ self.mount_id == other.mount_id
+ and self.parent_id == other.parent_id
+ and self.dev == other.dev
+ and self.root_dir == other.root_dir
+ and self.mount_point == other.mount_point
+ and self.mount_opts == other.mount_opts
+ and self.opt_fields == other.opt_fields
+ and self.fs_type == other.fs_type
+ and self.mount_source == other.mount_source
+ and self.sb_opts == other.sb_opts
+ )
@classmethod
def parse(cls, line):
@@ -105,7 +168,7 @@
self.root_dir = next(it)
self.mount_point = next(it)
self.mount_opts = next(it)
- self.opt_fields = []
+ self.opt_fields = OptionalFields()
for opt_field in it:
if opt_field == "-":
break
@@ -125,9 +188,16 @@
# type: () -> str
result = (
"{0.mount_id} {0.parent_id} {0.dev} {0.root_dir}"
- " {0.mount_point} {0.mount_opts} {opt_fields} {0.fs_type}"
+ " {0.mount_point} {0.mount_opts} {0.opt_fields} {0.fs_type}"
" {0.mount_source} {0.sb_opts}"
- ).format(self, opt_fields=" ".join(self.opt_fields + ["-"]))
+ ).format(self)
+ if PY2:
+ return result.encode()
+ return result
+
+ def __repr__(self):
+ # type: () -> str
+ result = "MountInfoEntry.parse({!r})".format(str(self))
if PY2:
return result.encode()
return result
@@ -250,6 +320,22 @@
# type: (List[Text], int) -> List[Text]
return parts[:6] + ["{}".format(n)] + parts[7:]
+ def compose_hostfs_preferred(parts, n):
+ # type: (List[Text], int) -> List[Text]
+ return parts[:7] + ["{}".format(n)] + parts[8:]
+
+ def compose_hostfs_alternate(parts, n):
+ # type: (List[Text], int) -> List[Text]
+ return parts[:10] + ["{}".format(n)] + parts[11:]
+
+ def compose_writable(parts, n):
+ # type: (List[Text], int) -> List[Text]
+ return parts[:5] + ["{}".format(n)] + parts[6:]
+
+ def compose_hostfs_writable(parts, n):
+ # type: (List[Text], int) -> List[Text]
+ return parts[:9] + ["{}".format(n)] + parts[10:]
+
def alloc_n(snap_name, snap_rev):
# type: (Text, Text) -> int
key = (snap_name, snap_rev)
@@ -269,14 +355,49 @@
snap_name = parts[5]
snap_rev = parts[6]
compose = compose_alternate
+ elif len(parts) >= 6 and parts[:4] == ["", "writable", "system-data", "snap"]:
+ snap_name = parts[4]
+ snap_rev = parts[5]
+ compose = compose_writable
+ elif len(parts) >= 8 and parts[:6] == ["", "var", "lib", "snapd", "hostfs", "snap"]:
+ snap_name = parts[6]
+ snap_rev = parts[7]
+ compose = compose_hostfs_preferred
+ elif len(parts) >= 11 and parts[:9] == [
+ "",
+ "var",
+ "lib",
+ "snapd",
+ "hostfs",
+ "var",
+ "lib",
+ "snapd",
+ "snap",
+ ]:
+ snap_name = parts[9]
+ snap_rev = parts[10]
+ compose = compose_hostfs_alternate
+ elif len(parts) >= 10 and parts[:8] == [
+ "", # 0
+ "var", # 1
+ "lib", # 2
+ "snapd", # 3
+ "hostfs", # 4
+ "writable", # 5
+ "system-data", # 6
+ "snap", # 7
+ ]:
+ snap_name = parts[8]
+ snap_rev = parts[9]
+ compose = compose_hostfs_writable
else:
return
n = alloc_n(snap_name, snap_rev)
entry.mount_point = "/".join(compose(parts, n))
-def renumber_opt_fields(entry, seen):
- # type: (MountInfoEntry, Dict[int, int]) -> None
+def renumber_opt_fields(entry, seen, base_n):
+ # type: (MountInfoEntry, Dict[int, int], int) -> None
"""renumber_opt_fields re-numbers peer group in optional fields."""
def alloc_n(peer_group):
@@ -285,7 +406,11 @@
try:
return seen[key]
except KeyError:
- n = len(seen) + 1
+ n = (
+ len({orig for orig, renumbered in seen.items() if renumbered >= base_n})
+ + base_n
+ + 1
+ )
seen[key] = n
return n
@@ -293,7 +418,9 @@
# type: (Match[Text]) -> Text
return "{}".format(alloc_n(int(m.group(1))))
- entry.opt_fields = [re.sub("(\\d+)", fn, opt) for opt in entry.opt_fields]
+ entry.opt_fields = OptionalFields(
+ [re.sub("(\\d+)", fn, opt) for opt in entry.opt_fields]
+ )
def renumber_loop_devices(entry, seen):
@@ -317,8 +444,8 @@
entry.mount_source = re.sub("loop(\\d+)", fn, entry.mount_source)
-def renumber_mount_ids(entry, seen):
- # type: (MountInfoEntry, Dict[int, int]) -> None
+def renumber_mount_ids(entry, seen, base_n):
+ # type: (MountInfoEntry, Dict[int, int], int) -> None
"""renumber_mount_ids re-numbers mount and parent mount IDs."""
def alloc_n(mount_id):
@@ -327,7 +454,10 @@
try:
return seen[key]
except KeyError:
- n = len(seen)
+ n = (
+ len({orig for orig, renumbered in seen.items() if renumbered >= base_n})
+ + base_n
+ )
seen[key] = n
return n
@@ -368,32 +498,143 @@
entry.dev = alloc_n(entry.dev)
-def rewrite_renumber(entries):
- # type: (List[MountInfoEntry]) -> None
- """rewrite_renumber applies all re-numbering helpers."""
- seen_opt_fields = {} # type: Dict[int, int]
- seen_loops = {} # type: Dict[int, int]
- seen_snap_revs = {} # type: Dict[Tuple[Text, Text], int]
- seen_mount_ids = {} # type: Dict[int, int]
- seen_devices = {} # type: Dict[Device, Device]
- for entry in entries:
- renumber_mount_ids(entry, seen_mount_ids)
- renumber_devices(entry, seen_devices)
- renumber_snap_revision(entry, seen_snap_revs)
- renumber_opt_fields(entry, seen_opt_fields)
- renumber_loop_devices(entry, seen_loops)
-
-
-def rewrite_rename(entries):
- # type: (List[MountInfoEntry]) -> None
- """rewrite_renameapplies all re-naming helpers."""
+def renumber_ns(entry, seen):
+ # type: (MountInfoEntry, Dict[Tuple[Text, int], int]) -> None
+ """renumber_mount_ns re-numbers mount namespace ID from .root_dir property."""
+
+ def alloc_n(ns_type, ns_id):
+ # type: (Text, int) -> int
+ key = (ns_type, ns_id)
+ try:
+ return seen[key]
+ except KeyError:
+ n = len(seen)
+ seen[key] = n
+ return n
+
+ if entry.fs_type != "nsfs":
+ return
+ match = re.match(r"^([a-z_]+):\[(\d+)\]$", entry.root_dir)
+ if match:
+ ns_type = match.group(1)
+ ns_id = int(match.group(2))
+ entry.root_dir = "{}:[{}]".format(ns_type, alloc_n(ns_type, ns_id))
+
+
+def renumber_mount_option(opt, seen):
+ # type: (Text, Dict[Tuple[Text, Text], int]) -> Text
+ """renumber_mount_option re-numbers various numbers in mount options."""
+
+ def alloc_n(mount_opt_key, mount_opt_value):
+ # type: (Text, Text) -> int
+ key = (mount_opt_key, mount_opt_value)
+ try:
+ return seen[key]
+ except KeyError:
+ n = len(
+ {
+ opt_value
+ for opt_key, opt_value in seen.keys()
+ if opt_key == mount_opt_key
+ }
+ )
+ seen[key] = n
+ return n
+
+ if "=" in opt:
+ mount_opt_key, mount_opt_value = opt.split("=", 1)
+ # size, nr_inode: used by tmpfs
+ # fd, pipe_ino: used by binfmtmisc
+ if mount_opt_key == "size":
+ return "size=VARIABLE"
+ if mount_opt_key in {"nr_inodes", "fd", "pipe_ino"}:
+ return "{}={}".format(
+ mount_opt_key, alloc_n(mount_opt_key, mount_opt_value)
+ )
+ return opt
+
+
+def renumber_mount_opts(entry, seen):
+ # type: (MountInfoEntry, Dict[Tuple[Text, Text], int]) -> None
+ """renumber_mount_opts alters numbers in mount options."""
+ entry.mount_opts = ",".join(
+ renumber_mount_option(opt, seen) for opt in entry.mount_opts.split(",")
+ )
+ entry.sb_opts = ",".join(
+ renumber_mount_option(opt, seen) for opt in entry.sb_opts.split(",")
+ )
+
+
+class RewriteState(object):
+ """RewriteState holds state used in rewriting mount entries."""
+
+ def __init__(self):
+ # type: () -> None
+ self.seen_opt_fields = {} # type: Dict[int, int]
+ self.seen_loops = {} # type: Dict[int, int]
+ self.seen_snap_revs = {} # type: Dict[Tuple[Text, Text], int]
+ self.seen_mount_ids = {} # type: Dict[int, int]
+ self.seen_devices = {} # type: Dict[Device, Device]
+ self.seen_ns = {} # type: Dict[Tuple[Text, int], int]
+ # NOTE: The type of the dictionary key is Tuple[Text, Text] because
+ # while generally "numeric" the values may include suffixes like
+ # "1024k" and it is just easier to handle this way.
+ self.seen_mount_opts = {} # type: Dict[Tuple[Text, Text], int]
+
+
+def rewrite_renumber(entries, order, rs, base_n=0):
+ # type: (List[MountInfoEntry], List[int], RewriteState, int) -> None
+ """rewrite_renumber applies all re-numbering helpers to a single entry."""
+ for i in range(len(entries)):
+ entry = entries[order[i]]
+ renumber_mount_ids(entry, rs.seen_mount_ids, base_n)
+ renumber_devices(entry, rs.seen_devices)
+ renumber_snap_revision(entry, rs.seen_snap_revs)
+ renumber_opt_fields(entry, rs.seen_opt_fields, base_n)
+ renumber_loop_devices(entry, rs.seen_loops)
+ renumber_ns(entry, rs.seen_ns)
+ renumber_mount_opts(entry, rs.seen_mount_opts)
+
+
+def rewrite_rename(entries, order, rs):
+ # type: (List[MountInfoEntry], List[int], RewriteState) -> None
+ """rewrite_rename applies all re-naming helpers to a single entry."""
# TODO: allocate devices like everything else above.
- for entry in entries:
+ for i in range(len(entries)):
+ entry = entries[order[i]]
entry.mount_source = re.sub(
"/dev/[sv]d([a-z])", "/dev/sd\\1", entry.mount_source
)
+class _UnitTestAction(Action):
+ def __init__(
+ self,
+ option_strings,
+ dest=SUPPRESS,
+ default=SUPPRESS,
+ help="run program's unit test suite and exit",
+ ):
+ # type: (Text, Text, Text, Text) -> None
+ super(_UnitTestAction, self).__init__(
+ option_strings=option_strings,
+ dest=dest,
+ default=default,
+ nargs="...",
+ help=help,
+ )
+
+ def __call__(self, parser, ns, values, option_string=None):
+ # type: (ArgumentParser, Namespace, Union[str, Sequence[Any], None], Optional[Text]) -> None
+ # We allow the caller to provide the test to invoke by giving
+ # --run-unit-tests a set of arguments.
+ argv = [sys.argv[0]]
+ if isinstance(values, list):
+ argv += values
+ unittest.main(argv=argv)
+ parser.exit()
+
+
def main():
# type: () -> None
parser = ArgumentParser(
@@ -418,10 +659,14 @@
parent_id: identifier of parent mount point
dev: major:minor numbers of the mounted device
root_dir: subtree of the mounted filesystem exposed at mount_point
+
+The exit code indicates if any mount point matched the query.
""",
formatter_class=RawTextHelpFormatter,
)
+ parser.register("action", "unit-test", _UnitTestAction)
parser.add_argument("-v", "--version", action="version", version="1.0")
+ parser.add_argument("--run-unit-tests", action="unit-test")
parser.add_argument(
"-f",
metavar="MOUNTINFO",
@@ -431,9 +676,41 @@
help="parse specified mountinfo file",
)
parser.add_argument(
+ "--ref",
+ dest="refs",
+ metavar="MOUNTINFO",
+ type=FileType(),
+ action="append",
+ default=[],
+ help="refer to another table while rewriting, makes output comparable across namespaces",
+ )
+ parser.add_argument(
+ "--ref-x1000",
+ dest="ref_x1000",
+ action="store_true",
+ default=False,
+ help="start mount point IDs and peer groups at multiple of 1000, once for each ref",
+ )
+ parser.add_argument(
"--one", default=False, action="store_true", help="expect exactly one match"
)
parser.add_argument(
+ "--rewrite-order",
+ metavar="FIELD",
+ action="append",
+ default=[],
+ choices=MountInfoEntry.known_attrs.keys(),
+ help="rewrite entries in the order determined by given fields",
+ )
+ parser.add_argument(
+ "--display-order",
+ metavar="FIELD",
+ action="append",
+ default=[],
+ choices=MountInfoEntry.known_attrs.keys(),
+ help="display entries in the order determined by given fields",
+ )
+ parser.add_argument(
"exprs",
metavar="EXPRESSION",
nargs="*",
@@ -455,26 +732,462 @@
except ValueError as exc:
raise SystemExit(exc)
entries = [MountInfoEntry.parse(line) for line in opts.file]
- entries = [e for e in entries if matches(e, filters)]
+
+ # Apply entry filtering ahead of any renumbering.
+ num_matched = 0
+ for e in entries:
+ if matches(e, filters):
+ e.matched = True
+ num_matched += 1
+
+ # Build rewrite state based on reference tables. This way the entries
+ # we will display can be correlated to other tables.
+ rs = RewriteState()
+ for i, ref in enumerate(opts.refs):
+ ref_entries = [MountInfoEntry.parse(line) for line in ref]
+ ref_rewrite_order = list(range(len(ref_entries)))
+ if opts.rewrite_order:
+
+ def ref_rewrite_key_fn(i):
+ # type: (int) -> Tuple[Any, ...]
+ return tuple(
+ getattr(ref_entries[i], field) for field in opts.rewrite_order
+ )
+
+ ref_rewrite_order.sort(key=ref_rewrite_key_fn)
+ if opts.renumber:
+ rewrite_renumber(
+ ref_entries, ref_rewrite_order, rs, 1000 * i if opts.ref_x1000 else 0
+ )
+ if opts.rename:
+ rewrite_rename(ref_entries, ref_rewrite_order, rs)
+
+ # Apply entry renumbering and renaming, perhaps using reordering as well.
+ rewrite_order = list(range(len(entries)))
+ if opts.rewrite_order:
+
+ def rewrite_key_fn(i):
+ # type: (int) -> Tuple[Any, ...]
+ return tuple(getattr(entries[i], field) for field in opts.rewrite_order)
+
+ rewrite_order.sort(key=rewrite_key_fn)
if opts.renumber:
- rewrite_renumber(entries)
+ rewrite_renumber(
+ entries, rewrite_order, rs, 1000 * len(opts.refs) if opts.ref_x1000 else 0
+ )
if opts.rename:
- rewrite_rename(entries)
+ rewrite_rename(entries, rewrite_order, rs)
+
+ # Apply entry reordering for display.
+ if opts.display_order:
+
+ def display_key_fn(entry):
+ # type: (MountInfoEntry) -> Tuple[Any, ...]
+ return tuple(getattr(entry, field) for field in opts.display_order)
+
+ entries.sort(key=display_key_fn)
for e in entries:
+ if not e.matched:
+ continue
if attrs:
- values = [] # type: List[Any]
- for attr in attrs:
- value = getattr(e, attr)
- if isinstance(value, list):
- value = " ".join(value)
- values.append(value)
- print(*values)
+ print(*[getattr(e, attr) for attr in attrs])
else:
print(e)
- if opts.one and len(entries) != 1:
+ if opts.one and num_matched != 1:
raise SystemExit(
- "--one requires exactly one match, found {}".format(len(entries))
+ "--one requires exactly one match, found {}".format(num_matched)
+ )
+ # Return with an exit code indicating if anything matched.
+ # This allows mountinfo-tool to be used in scripts.
+ if num_matched == 0:
+ raise SystemExit(1)
+
+
+class MountInfoEntryTests(unittest.TestCase):
+
+ non_zero_values = {
+ "mount_id": 1,
+ "parent_id": 2,
+ "dev": Device.pack(3, 4),
+ "root_dir": "/root-dir",
+ "mount_point": "/mount-point",
+ "mount_opts": "mount-opts",
+ "opt_fields": OptionalFields(["opt:1", "fields:2"]),
+ "fs_type": "fs-type",
+ "mount_source": "mount-source",
+ "sb_opts": "sb-opts",
+ } # Dict[Text, Any]
+
+ def test_init(self):
+ # type: () -> None
+ e = MountInfoEntry()
+ self.assertEqual(e.mount_id, 0)
+ self.assertEqual(e.parent_id, 0)
+ self.assertEqual(e.dev, Device.pack(0, 0))
+ self.assertEqual(e.root_dir, "")
+ self.assertEqual(e.mount_point, "")
+ self.assertEqual(e.mount_opts, "")
+ self.assertEqual(e.opt_fields, [])
+ self.assertEqual(e.fs_type, "")
+ self.assertEqual(e.mount_source, "")
+ self.assertEqual(e.sb_opts, "")
+
+ def test_parse(self):
+ # type: () -> None
+ e = MountInfoEntry.parse(
+ "2079 2266 0:3 mnt:[4026532791] /run/snapd/ns/test-snapd-mountinfo.mnt rw - nsfs nsfs rw"
)
+ self.assertEqual(e.mount_id, 2079)
+ self.assertEqual(e.parent_id, 2266)
+ self.assertEqual(e.dev, Device.pack(0, 3))
+ self.assertEqual(e.root_dir, "mnt:[4026532791]")
+ self.assertEqual(e.mount_point, "/run/snapd/ns/test-snapd-mountinfo.mnt")
+ self.assertEqual(e.mount_opts, "rw")
+ self.assertEqual(e.opt_fields, [])
+ self.assertEqual(e.fs_type, "nsfs")
+ self.assertEqual(e.mount_source, "nsfs")
+ self.assertEqual(e.sb_opts, "rw")
+
+ def test_eq(self):
+ # type: () -> None
+ e0 = MountInfoEntry()
+ e1 = MountInfoEntry()
+ self.assertEqual(e0, e1)
+ for field, value in self.non_zero_values.items():
+ self.assertEqual(e0, e1)
+ old_value = getattr(e1, field)
+ setattr(e1, field, value)
+ self.assertNotEqual(e0, e1)
+ setattr(e1, field, old_value)
+ self.assertEqual(e0, e1)
+
+ def test_str(self):
+ # type: () -> None
+ e = MountInfoEntry()
+ for field, value in self.non_zero_values.items():
+ setattr(e, field, value)
+ self.assertEqual(
+ str(e),
+ "1 2 3:4 /root-dir /mount-point mount-opts opt:1 fields:2 - fs-type mount-source sb-opts",
+ )
+
+ def test_repr(self):
+ # type: () -> None
+ e = MountInfoEntry()
+ for field, value in self.non_zero_values.items():
+ setattr(e, field, value)
+ self.assertEqual(
+ repr(e),
+ "MountInfoEntry.parse('1 2 3:4 /root-dir /mount-point mount-opts opt:1 fields:2 - fs-type mount-source sb-opts')",
+ )
+
+ def test_dev_maj_min(self):
+ # type: () -> None
+ e = MountInfoEntry()
+ e.dev = Device.pack(1, 2)
+ self.assertEqual(e.dev_min, 2)
+ self.assertEqual(e.dev_maj, 1)
+
+
+class RenumberSnapRevisionTests(unittest.TestCase):
+ def setUp(self):
+ # type: () -> None
+ self.entry = MountInfoEntry()
+ self.seen = {} # type: Dict[Tuple[Text, Text], int]
+
+ def test_renumbering_allocation(self):
+ # type: () -> None
+ self.entry.mount_point = "/snap/core/7079"
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(self.entry.mount_point, "/snap/core/1")
+
+ self.entry.mount_point = "/snap/core/7080"
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(self.entry.mount_point, "/snap/core/2")
+
+ self.entry.mount_point = "/snap/snapd/x1"
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(self.entry.mount_point, "/snap/snapd/1")
+
+ self.assertEqual(
+ self.seen, {("core", "7079"): 1, ("core", "7080"): 2, ("snapd", "x1"): 1}
+ )
+
+ def test_preferred(self):
+ # type: () -> None
+ self.entry.mount_point = "/snap/core/7079"
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(self.entry.mount_point, "/snap/core/1")
+
+ self.entry.mount_point = "/snap/core/7079/subdir"
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(self.entry.mount_point, "/snap/core/1/subdir")
+
+ self.assertEqual(self.seen, {("core", "7079"): 1})
+
+ def test_alternate(self):
+ # type: () -> None
+ self.entry.mount_point = "/var/lib/snapd/snap/core/7079"
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(self.entry.mount_point, "/var/lib/snapd/snap/core/1")
+
+ self.entry.mount_point = "/var/lib/snapd/snap/core/7079/subdir"
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(self.entry.mount_point, "/var/lib/snapd/snap/core/1/subdir")
+
+ self.assertEqual(self.seen, {("core", "7079"): 1})
+
+ def test_preferred_via_hostfs(self):
+ # type: () -> None
+ self.entry.mount_point = "/var/lib/snapd/hostfs/snap/core/7079"
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(self.entry.mount_point, "/var/lib/snapd/hostfs/snap/core/1")
+
+ self.entry.mount_point = "/var/lib/snapd/hostfs/snap/core/7079/subdir"
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(
+ self.entry.mount_point, "/var/lib/snapd/hostfs/snap/core/1/subdir"
+ )
+
+ self.assertEqual(self.seen, {("core", "7079"): 1})
+
+ def test_alternate_via_hostfs(self):
+ # type: () -> None
+ self.entry.mount_point = "/var/lib/snapd/hostfs/var/lib/snapd/snap/core/7079"
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(
+ self.entry.mount_point, "/var/lib/snapd/hostfs/var/lib/snapd/snap/core/1"
+ )
+
+ self.entry.mount_point = (
+ "/var/lib/snapd/hostfs/var/lib/snapd/snap/core/7079/subdir"
+ )
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(
+ self.entry.mount_point,
+ "/var/lib/snapd/hostfs/var/lib/snapd/snap/core/1/subdir",
+ )
+
+ self.assertEqual(self.seen, {("core", "7079"): 1})
+
+ def test_writable(self):
+ # type: () -> None
+ self.entry.mount_point = "/writable/system-data/snap/core18/1055"
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(self.entry.mount_point, "/writable/system-data/snap/core18/1")
+
+ self.entry.mount_point = "/writable/system-data/snap/core18/1055/subdir"
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(
+ self.entry.mount_point, "/writable/system-data/snap/core18/1/subdir"
+ )
+
+ self.assertEqual(self.seen, {("core18", "1055"): 1})
+
+ def test_writable_via_hostfs(self):
+ # type: () -> None
+ self.entry.mount_point = (
+ "/var/lib/snapd/hostfs/writable/system-data/snap/core18/1055"
+ )
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(
+ self.entry.mount_point,
+ "/var/lib/snapd/hostfs/writable/system-data/snap/core18/1",
+ )
+
+ self.entry.mount_point = (
+ "/var/lib/snapd/hostfs/writable/system-data/snap/core18/1055/subdir"
+ )
+ renumber_snap_revision(self.entry, self.seen)
+ self.assertEqual(
+ self.entry.mount_point,
+ "/var/lib/snapd/hostfs/writable/system-data/snap/core18/1/subdir",
+ )
+
+ self.assertEqual(self.seen, {("core18", "1055"): 1})
+
+
+class RenumberMountNsTests(unittest.TestCase):
+ def setUp(self):
+ # type: () -> None
+ self.entry = MountInfoEntry()
+ self.entry.fs_type = "nsfs"
+ self.seen = {} # type: Dict[Tuple[Text, int], int]
+
+ def test_renumbering_allocation(self):
+ # type: () -> None
+ self.entry.root_dir = "mnt:[4026532909]"
+ renumber_ns(self.entry, self.seen)
+ self.assertEqual(self.entry.root_dir, "mnt:[0]")
+
+ self.entry.root_dir = "mnt:[4026532791]"
+ renumber_ns(self.entry, self.seen)
+ self.assertEqual(self.entry.root_dir, "mnt:[1]")
+
+ self.entry.root_dir = "pid:[4026531836]"
+ renumber_ns(self.entry, self.seen)
+ self.assertEqual(self.entry.root_dir, "pid:[2]")
+
+ self.assertEqual(
+ self.seen,
+ {("mnt", 4026532909): 0, ("mnt", 4026532791): 1, ("pid", 4026531836): 2},
+ )
+
+
+class RenumberMountOptionsTests(unittest.TestCase):
+ def setUp(self):
+ # type: () -> None
+ self.seen = {} # type: Dict[Tuple[Text, Text], int]
+
+ def test_renumber_allocation(self):
+ # type: () -> None
+ """
+ numbers are allocated from subsets matching the key, this reduces delta.
+ """
+ self.assertEqual(renumber_mount_option("pipe_ino=100", self.seen), "pipe_ino=0")
+ self.assertEqual(renumber_mount_option("pipe_ino=100", self.seen), "pipe_ino=0")
+ self.assertEqual(renumber_mount_option("pipe_ino=200", self.seen), "pipe_ino=1")
+ self.assertEqual(renumber_mount_option("pipe_ino=100", self.seen), "pipe_ino=0")
+ self.assertEqual(renumber_mount_option("fd=21", self.seen), "fd=0")
+ self.assertEqual(renumber_mount_option("fd=21", self.seen), "fd=0")
+ self.assertEqual(renumber_mount_option("fd=45", self.seen), "fd=1")
+ self.assertEqual(renumber_mount_option("fd=21", self.seen), "fd=0")
+
+ def test_renumber_size_variable(self):
+ # type: () -> None
+ """
+ size is special-cased and always rewritten to the same value because it is prone to fluctuations
+ """
+ self.assertEqual(renumber_mount_option("size=100", self.seen), "size=VARIABLE")
+ self.assertEqual(renumber_mount_option("size=200", self.seen), "size=VARIABLE")
+
+ def test_renumber_devtmpfs_opts(self):
+ # type: () -> None
+ """
+ certain devtmpfs options are renumbered.
+
+ 23 98 0:6 / /dev rw,nosuid shared:21 - devtmpfs devtmpfs rw,size=4057388k,nr_inodes=1014347,mode=755
+
+ Here the size and nr_inodes options are not deterministic and need to
+ be rewritten. The size quantity is very susceptible to free memory
+ fluctuations and is treated specially.
+ """
+ # Options size= and nr_inodes= are renumbered.
+ self.assertEqual(
+ renumber_mount_option("size=4057388k", self.seen), "size=VARIABLE"
+ )
+ self.assertEqual(
+ renumber_mount_option("nr_inodes=1014347", self.seen), "nr_inodes=0"
+ )
+ # Option mode= is not renumbered.
+ self.assertEqual(renumber_mount_option("mode=755", self.seen), "mode=755")
+ self.assertEqual(self.seen, {("nr_inodes", "1014347"): 0})
+
+ def test_renumber_binfmt_misc_opts(self):
+ # type: () -> None
+ """
+ certain binfmt_misc options are renumbered.
+
+ 47 22 0:42 / /proc/sys/fs/binfmt_misc rw,relatime shared:28 - autofs systemd-1 rw,fd=40,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=16610
+
+ Here the fd and pipe_ino options are not deterministic and need to be rewritten.
+ """
+ self.assertEqual(renumber_mount_option("fd=40", self.seen), "fd=0")
+ self.assertEqual(
+ renumber_mount_option("pipe_ino=16610", self.seen), "pipe_ino=0"
+ )
+ self.assertEqual(self.seen, {("fd", "40"): 0, ("pipe_ino", "16610"): 0})
+
+
+class RenumberMountIdsTests(unittest.TestCase):
+ def setUp(self):
+ # type: () -> None
+ self.seen = {} # type: Dict[int, int]
+
+ def test_smoke(self):
+ # type: () -> None
+ # Renumber the first mount entry, from the "initial" mount namespace,
+ # with the initial, zero multiple.
+ entry = MountInfoEntry()
+ entry.mount_id = 12
+ entry.parent_id = 5
+ renumber_mount_ids(entry, self.seen, 0)
+ self.assertEqual(entry.parent_id, 0)
+ self.assertEqual(entry.mount_id, 1)
+
+ # Renumber another entry, now in a separate mount namespace, with a
+ # different multiplier. The parent ID stays in the < 1000 range. NOTE:
+ # in reality mount namespaces never share mount IDs but that's fine.
+ # The test just checks the renumber logic.
+ entry = MountInfoEntry()
+ entry.mount_id = 13
+ entry.parent_id = 5
+ renumber_mount_ids(entry, self.seen, 1000)
+ self.assertEqual(entry.parent_id, 0)
+ self.assertEqual(entry.mount_id, 1000)
+
+
+class RenumberOptFieldsTests(unittest.TestCase):
+ def setUp(self):
+ # type: () -> None
+ self.seen = {} # type: Dict[int, int]
+
+ def test_smoke(self):
+ # type: () -> None
+ # Renumber the first mount entry, from the "initial" mount namespace,
+ # with the initial, zero multiple.
+ entry = MountInfoEntry()
+ entry.opt_fields = OptionalFields(["shared:12"])
+ renumber_opt_fields(entry, self.seen, 0)
+ self.assertEqual(entry.opt_fields, ["shared:1"])
+
+ # Renumber the second entry, now in a separate mount namespace, with a
+ # different multiplier. Given that the peer group number was not seen
+ # before it gets allocated into the 1000-1999 range.
+ entry = MountInfoEntry()
+ entry.opt_fields = OptionalFields(["shared:13"])
+ renumber_opt_fields(entry, self.seen, 1000)
+ self.assertEqual(entry.opt_fields, ["shared:1001"])
+
+ # Renumber the third entry, in the same mount namespace as the second
+ # one. Given that the peer group number was seen in the initial mount
+ # namespace it is renumbered the same was as the original was, even
+ # though the actual sharing mode is different.
+ entry = MountInfoEntry()
+ entry.opt_fields = OptionalFields(["master:12"])
+ renumber_opt_fields(entry, self.seen, 1000)
+ self.assertEqual(entry.opt_fields, ["master:1"])
+
+
+class RewriteTests(unittest.TestCase):
+ def setUp(self):
+ # type: () -> None
+ self.entries = [
+ MountInfoEntry.parse(line)
+ for line in (
+ "2079 2266 0:3 mnt:[4026532791] /run/snapd/ns/test-snapd-mountinfo.mnt rw - nsfs nsfs rw",
+ "23 98 0:6 / /dev rw,nosuid shared:21 - devtmpfs devtmpfs rw,size=4057388k,nr_inodes=1014347,mode=755",
+ "47 22 0:42 / /proc/sys/fs/binfmt_misc rw,relatime shared:28 - autofs systemd-1 rw,fd=40,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=16610",
+ )
+ ]
+ self.order = list(range(len(self.entries)))
+ self.rs = RewriteState()
+
+ def test_rewrite_renumber(self):
+ # type: () -> None
+ rewrite_renumber(self.entries, self.order, self.rs)
+ self.assertEqual(
+ self.entries,
+ [
+ MountInfoEntry.parse(line)
+ for line in (
+ "1 0 0:0 mnt:[0] /run/snapd/ns/test-snapd-mountinfo.mnt rw - nsfs nsfs rw",
+ "3 2 0:1 / /dev rw,nosuid shared:1 - devtmpfs devtmpfs rw,size=VARIABLE,nr_inodes=0,mode=755",
+ "5 4 0:2 / /proc/sys/fs/binfmt_misc rw,relatime shared:2 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0",
+ )
+ ],
+ )
+
if __name__ == "__main__":
main()
diff -Nru snapd-2.40/tests/lib/bin/retry-tool snapd-2.42.1/tests/lib/bin/retry-tool
--- snapd-2.40/tests/lib/bin/retry-tool 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/bin/retry-tool 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,111 @@
+#!/usr/bin/env any-python
+from __future__ import print_function, absolute_import, unicode_literals
+
+import argparse
+import subprocess
+import sys
+import time
+
+
+# Define MYPY as False and use it as a conditional for typing import. Despite
+# this declaration mypy will really treat MYPY as True when type-checking.
+# This is required so that we can import typing on Python 2.x without the
+# typing module installed. For more details see:
+# https://mypy.readthedocs.io/en/latest/common_issues.html#import-cycles
+MYPY = False
+if MYPY:
+ from typing import List, Text
+
+
+def _make_parser():
+ # type: () -> argparse.ArgumentParser
+ parser = argparse.ArgumentParser(
+ description="""
+Retry executes COMMAND at most N times, waiting for SECONDS between each
+attempt. On failure the exit code from the final attempt is returned.
+"""
+ )
+ parser.add_argument(
+ "-n",
+ "--attempts",
+ metavar="N",
+ type=int,
+ default=3,
+ help="number of attempts (default %(default)s)",
+ )
+ parser.add_argument(
+ "--wait",
+ metavar="SECONDS",
+ type=float,
+ default=1,
+ help="grace period between attempts (default %(default)ss)",
+ )
+ parser.add_argument(
+ "--quiet",
+ dest="verbose",
+ action="store_false",
+ default=True,
+ help="refrain from printing any output",
+ )
+ parser.add_argument(
+ "cmd", metavar="COMMAND", nargs="...", help="command to execute"
+ )
+ return parser
+
+
+def run_cmd(cmd, n, wait, verbose):
+ # type: (List[Text], int, float, bool) -> int
+ retcode = 0
+ for i in range(1, n + 1):
+ retcode = subprocess.call(cmd)
+ if retcode == 0:
+ break
+ if verbose:
+ print(
+ "retry: command {} failed with code {}".format(" ".join(cmd), retcode),
+ file=sys.stderr,
+ )
+ if i < n:
+ if verbose:
+ print(
+ "retry: next attempt in {} second(s) (attempt {} of {})".format(
+ wait, i, n
+ ),
+ file=sys.stderr,
+ )
+ time.sleep(wait)
+ else:
+ if verbose and n > 1:
+ print(
+ "retry: command {} keeps failing after {} attempts".format(
+ " ".join(cmd), n
+ ),
+ file=sys.stderr,
+ )
+ return retcode
+
+
+def main():
+ # type: () -> None
+ parser = _make_parser()
+ ns = parser.parse_args()
+ # The command cannot be empty but it is difficult to express in argparse itself.
+ if len(ns.cmd) == 0:
+ parser.print_usage()
+ parser.exit(0)
+ # Return the last exit code as the exit code of this process.
+ try:
+ retcode = run_cmd(ns.cmd, ns.attempts, ns.wait, ns.verbose)
+ except OSError as exc:
+ if ns.verbose:
+ print(
+ "retry: cannot execute command {}: {}".format(" ".join(ns.cmd), exc),
+ file=sys.stderr,
+ )
+ raise SystemExit(1)
+ else:
+ raise SystemExit(retcode)
+
+
+if __name__ == "__main__":
+ main()
diff -Nru snapd-2.40/tests/lib/bin/user-tool snapd-2.42.1/tests/lib/bin/user-tool
--- snapd-2.40/tests/lib/bin/user-tool 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/bin/user-tool 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,55 @@
+#!/usr/bin/env any-python
+from __future__ import print_function, absolute_import, unicode_literals
+
+import argparse
+import os
+import subprocess
+
+# Define MYPY as False and use it as a conditional for typing import. Despite
+# this declaration mypy will really treat MYPY as True when type-checking.
+# This is required so that we can import typing on Python 2.x without the
+# typing module installed. For more details see:
+# https://mypy.readthedocs.io/en/latest/common_issues.html#import-cycles
+MYPY = False
+if MYPY:
+ from typing import Text
+
+
+def remove_user_with_group(user_name):
+ # type: (Text) -> None
+ """remove the user and group with the same name, if present."""
+ if os.path.exists("/var/lib/extrausers/passwd"):
+ subprocess.call(["userdel", "--extrausers", "--force", "--remove", user_name])
+ else:
+ subprocess.call(["userdel", "--force", "--remove", user_name])
+ # Some systems do not set "USERGROUPS_ENAB yes" so we need to cleanup
+ # the group manually. Use "-f" (force) when available, older versions
+ # do not have it.
+ proc = subprocess.Popen(["groupdel", "-h"], stdout=subprocess.PIPE)
+ out, _ = proc.communicate()
+ if b"force" in out:
+ subprocess.call(["groupdel", "-f", user_name])
+ else:
+ subprocess.call(["groupdel", user_name])
+ # Ensure the user user really got deleted
+ if subprocess.call(["getent", "passwd", user_name]) == 0:
+ raise SystemExit("user exists after removal?")
+ if subprocess.call(["getent", "group", user_name]) == 0:
+ raise SystemExit("group exists after removal?")
+
+
+def main():
+ # type: () -> None
+ parser = argparse.ArgumentParser()
+ sub = parser.add_subparsers()
+ cmd = sub.add_parser(
+ "remove-with-group", description="Remove system user and group, if present"
+ )
+ cmd.set_defaults(func=lambda ns: remove_user_with_group(ns.user))
+ cmd.add_argument("user", help="name of the user and group to remove")
+ ns = parser.parse_args()
+ ns.func(ns)
+
+
+if __name__ == "__main__":
+ main()
diff -Nru snapd-2.40/tests/lib/bin/version-tool snapd-2.42.1/tests/lib/bin/version-tool
--- snapd-2.40/tests/lib/bin/version-tool 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/bin/version-tool 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,308 @@
+#!/usr/bin/env any-python
+from __future__ import print_function, absolute_import, unicode_literals
+
+from argparse import Action, ArgumentParser, RawTextHelpFormatter, SUPPRESS
+import itertools
+import re
+import sys
+import sys
+import unittest
+
+# PY2 is true when we're running under Python 2.x It is used for appropriate
+# return value selection of __str__ and __repr_ methods, which must both
+# return str, not unicode (in Python 2) and str (in Python 3). In both cases
+# the return type annotation is exactly the same, but due to unicode_literals
+# being in effect, and the fact we often use a format string (which is an
+# unicode string in Python 2), we must encode the it to byte string when
+# running under Python 2.
+PY2 = sys.version_info[0] == 2
+
+# Define MYPY as False and use it as a conditional for typing import. Despite
+# this declaration mypy will really treat MYPY as True when type-checking.
+# This is required so that we can import typing on Python 2.x without the
+# typing module installed. For more details see:
+# https://mypy.readthedocs.io/en/latest/common_issues.html#import-cycles
+MYPY = False
+if MYPY:
+ from typing import Any, Text, Tuple, Optional, Union, Sequence
+ from argparse import Namespace
+
+
+class _UnitTestAction(Action):
+ def __init__(
+ self,
+ option_strings,
+ dest=SUPPRESS,
+ default=SUPPRESS,
+ help="run program's unit test suite and exit",
+ ):
+ # type: (Text, Text, Text, Text) -> None
+ super(_UnitTestAction, self).__init__(
+ option_strings=option_strings,
+ dest=dest,
+ default=default,
+ nargs="...",
+ help=help,
+ )
+
+ def __call__(self, parser, ns, values, option_string=None):
+ # type: (ArgumentParser, Namespace, Union[str, Sequence[Any], None], Optional[Text]) -> None
+ # We allow the caller to provide the test to invoke by giving
+ # --run-unit-tests a set of arguments.
+ argv = [sys.argv[0]]
+ if isinstance(values, list):
+ argv += values
+ unittest.main(argv=argv)
+ parser.exit()
+
+
+def consistent_relation(rel_op, delta):
+ # type: (Text, int) -> bool
+ """
+ consistent_relation returns true if the relation is consistent with delta.
+
+ The relation operator is one of ==, !=, <, <=, > or >=.
+ Delta is either 0, a positive or a negative number.
+ """
+ if rel_op == "==":
+ return delta == 0
+ elif rel_op == "!=":
+ return delta != 0
+ elif rel_op == ">":
+ return delta > 0
+ elif rel_op == ">=":
+ return delta >= 0
+ elif rel_op == "<":
+ return delta < 0
+ elif rel_op == "<=":
+ return delta <= 0
+ raise ValueError("unexpected relational operator " + rel_op)
+
+
+class ConsistentRelationTests(unittest.TestCase):
+ def test_eq(self):
+ # type: () -> None
+ self.assertFalse(consistent_relation("==", -1))
+ self.assertTrue(consistent_relation("==", 0))
+ self.assertFalse(consistent_relation("==", +1))
+
+ def test_ne(self):
+ # type: () -> None
+ self.assertTrue(consistent_relation("!=", -1))
+ self.assertFalse(consistent_relation("!=", 0))
+ self.assertTrue(consistent_relation("!=", +1))
+
+ def test_gt(self):
+ # type: () -> None
+ self.assertFalse(consistent_relation(">", -1))
+ self.assertFalse(consistent_relation(">", 0))
+ self.assertTrue(consistent_relation(">", +1))
+
+ def test_ge(self):
+ # type: () -> None
+ self.assertFalse(consistent_relation(">=", -1))
+ self.assertTrue(consistent_relation(">=", 0))
+ self.assertTrue(consistent_relation(">=", +1))
+
+ def test_lt(self):
+ # type: () -> None
+ self.assertTrue(consistent_relation("<", -1))
+ self.assertFalse(consistent_relation("<", 0))
+ self.assertFalse(consistent_relation("<", +1))
+
+ def test_le(self):
+ # type: () -> None
+ self.assertTrue(consistent_relation("<=", -1))
+ self.assertTrue(consistent_relation("<=", 0))
+ self.assertFalse(consistent_relation("<=", +1))
+
+ def test_unknown(self):
+ # type: () -> None
+ with self.assertRaises(ValueError):
+ consistent_relation("???", 0)
+
+
+def strict_version_cmp(a, b):
+ # type: (Text, Text) -> int
+ """
+ strictly_version_cmp compares two version numbers without leeway.
+
+ The algorithm considers each version to be a tuple of integers. Non,
+ integer elements or element fragments are regarded as an error and raised
+ as ValueError.
+
+ Comparison is performed on by considering the leftmost element in each
+ tuple. First pair of numbers that are not equal determine the result of the
+ comparison. Tuples have unequal length then missing elements are
+ substituted with zero.
+
+ The return value is 0 if the version strings are equal, -1 if version a is
+ smaller or +1 if version b is smaller.
+ """
+ try:
+ a_items = [int(item, 10) for item in a.split(".")]
+ except ValueError:
+ raise ValueError("version {} is not purely numeric".format(a))
+ try:
+ b_items = [int(item, 10) for item in b.split(".")]
+ except ValueError:
+ raise ValueError("version {} is not purely numeric".format(b))
+ if PY2:
+ zip_longest_fn = itertools.izip_longest
+ else:
+ zip_longest_fn = itertools.zip_longest
+ for a_val, b_val in zip_longest_fn(a_items, b_items, fillvalue=0):
+ delta = a_val - b_val
+ if delta != 0:
+ return 1 if delta > 0 else -1
+ return 0
+
+
+class StrictVersionCmpTests(unittest.TestCase):
+ def test_simple(self):
+ # type: () -> None
+ self.assertEqual(strict_version_cmp("10", "10"), 0)
+ self.assertEqual(strict_version_cmp("10", "20"), -1)
+ self.assertEqual(strict_version_cmp("20", "10"), +1)
+
+ def test_many_segments(self):
+ # type: () -> None
+ self.assertEqual(strict_version_cmp("1.2.3", "1.2.3"), 0)
+ self.assertEqual(strict_version_cmp("1.2.3", "1.3.4"), -1)
+ self.assertEqual(strict_version_cmp("1.4.3", "1.2.3"), +1)
+ self.assertEqual(strict_version_cmp("1.0.0", "1.1.0"), -1)
+ self.assertEqual(strict_version_cmp("0.1.2", "1.1.2"), -1)
+
+ def test_unequal_length(self):
+ # type: () -> None
+ self.assertEqual(strict_version_cmp("1", "1.0"), 0)
+ self.assertEqual(strict_version_cmp("1", "1.2"), -1)
+ self.assertEqual(strict_version_cmp("1.2", "1"), +1)
+ self.assertEqual(strict_version_cmp("1", "1.0.1"), -1)
+ self.assertEqual(strict_version_cmp("1.1", "1.0.1"), +1)
+
+ def test_version_with_text(self):
+ # type: () -> None
+ with self.assertRaises(ValueError) as cm:
+ strict_version_cmp("1-foo", "1")
+ self.assertEqual(cm.exception.args, ("version 1-foo is not purely numeric",))
+ with self.assertRaises(ValueError) as cm:
+ strict_version_cmp("1.2-foo", "1.2")
+ self.assertEqual(cm.exception.args, ("version 1.2-foo is not purely numeric",))
+
+
+def _make_parser():
+ # type: () -> ArgumentParser
+ parser = ArgumentParser(
+ epilog="""
+Relational operator is one of:
+
+ -eq -ne -gt -ge -lt -le
+
+Version comparison is performed using the selected algorithm.
+
+strict:
+ The algorithm considers each version to be a tuple of integers.
+ Non-integer elements are considered to be invalid version.
+
+ Comparison is performed on by considering the leftmost element in each
+ tuple. First pair of numbers that are not equal determine the result of the
+ comparison. Tuples have unequal length then missing elements are
+ substituted with zero.
+ """,
+ formatter_class=RawTextHelpFormatter,
+ )
+ parser.register("action", "unit-test", _UnitTestAction)
+ parser.add_argument("-v", "--version", action="version", version="1.0")
+ parser.add_argument(
+ "--verbose", action="store_true", help="describe comparison process"
+ )
+
+ parser.add_argument("version_a", metavar="VERSION-A")
+ parser.add_argument("version_b", metavar="VERSION-B")
+
+ # algorithm selection
+ alg_grp = parser.add_mutually_exclusive_group(required=True)
+ alg_grp.add_argument(
+ "--strict",
+ dest="algorithm",
+ action="store_const",
+ const=strict_version_cmp,
+ help="select the strict version comparison",
+ )
+ # relation selection
+ rel_op_grp = parser.add_mutually_exclusive_group(required=True)
+ rel_op_grp.add_argument(
+ "-eq",
+ action="store_const",
+ const="==",
+ dest="rel_op",
+ help="test that versions are equal",
+ )
+ rel_op_grp.add_argument(
+ "-ne",
+ action="store_const",
+ const="!=",
+ dest="rel_op",
+ help="test that versions are not equal",
+ )
+ rel_op_grp.add_argument(
+ "-gt",
+ action="store_const",
+ const=">",
+ dest="rel_op",
+ help="test that version-a is greater than version-b",
+ )
+ rel_op_grp.add_argument(
+ "-ge",
+ action="store_const",
+ const=">=",
+ dest="rel_op",
+ help="test that version-a is greater than or equal to version-b",
+ )
+ rel_op_grp.add_argument(
+ "-lt",
+ action="store_const",
+ const="<",
+ dest="rel_op",
+ help="test that version-a is less than version-b",
+ )
+ rel_op_grp.add_argument(
+ "-le",
+ action="store_const",
+ const="<=",
+ dest="rel_op",
+ help="test that version-a is less than or equal to version-b",
+ )
+
+ # maintenance commands
+ maint_grp = parser.add_argument_group("maintenance commands")
+ maint_grp.add_argument("--run-unit-tests", action="unit-test", help=SUPPRESS)
+ return parser
+
+
+def main():
+ # type: () -> None
+ opts = _make_parser().parse_args()
+ try:
+ delta = opts.algorithm(opts.version_a, opts.version_b)
+ except ValueError as exc:
+ print("error: {}".format(exc), file=sys.stderr)
+ raise SystemExit(2)
+ else:
+ is_consistent = consistent_relation(opts.rel_op, delta)
+ if opts.verbose:
+ print(
+ "delta between {} and {} is: {}".format(
+ opts.version_a, opts.version_b, delta
+ )
+ )
+ if is_consistent:
+ print("delta {} is consistent with {}".format(delta, opts.rel_op))
+ else:
+ print("delta {} is inconsistent with {}".format(delta, opts.rel_op))
+ raise SystemExit(0 if is_consistent else 1)
+
+
+if __name__ == "__main__":
+ main()
diff -Nru snapd-2.40/tests/lib/boot.sh snapd-2.42.1/tests/lib/boot.sh
--- snapd-2.40/tests/lib/boot.sh 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/lib/boot.sh 2019-10-30 12:17:43.000000000 +0000
@@ -1,6 +1,7 @@
#!/bin/bash
GRUB_EDITENV=grub-editenv
+GRUBENV_FILE=/boot/grub/grubenv
case "$SPREAD_SYSTEM" in
fedora-*|opensuse-*|amazon-*|centos-*)
GRUB_EDITENV=grub2-editenv
@@ -11,12 +12,16 @@
if [ $# -eq 0 ]; then
if command -v "$GRUB_EDITENV" >/dev/null; then
"$GRUB_EDITENV" list
+ elif [ -s "$GRUBENV_FILE" ]; then
+ cat "$GRUBENV_FILE"
else
fw_printenv
fi
else
if command -v "$GRUB_EDITENV" >/dev/null; then
"$GRUB_EDITENV" list | grep "^$1"
+ elif [ -s "$GRUBENV_FILE" ]; then
+ grep "^$1" "$GRUBENV_FILE"
else
fw_printenv "$1"
fi | sed "s/^${1}=//"
@@ -29,6 +34,8 @@
if command -v "$GRUB_EDITENV" >/dev/null; then
"$GRUB_EDITENV" /boot/grub/grubenv unset "$var"
+ elif [ -s "$GRUBENV_FILE" ]; then
+ sed -i "/^$var=/d" "$GRUBENV_FILE"
else
fw_setenv "$var"
fi
@@ -45,3 +52,10 @@
exit 1
fi
}
+
+wait_core_post_boot() {
+ # booted
+ while [ "$(bootenv snap_mode)" != "" ]; do
+ sleep 1
+ done
+}
diff -Nru snapd-2.40/tests/lib/desktop-portal.sh snapd-2.42.1/tests/lib/desktop-portal.sh
--- snapd-2.40/tests/lib/desktop-portal.sh 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/lib/desktop-portal.sh 2019-10-30 12:17:43.000000000 +0000
@@ -51,7 +51,7 @@
distro_purge_package xdg-desktop-portal
distro_auto_remove_packages
- if [ -d "${USER_RUNTIME_DIR}" ]; then
+ if [ -d "${USER_RUNTIME_DIR}" ]; then
umount --lazy "${USER_RUNTIME_DIR}/doc" || :
rm -rf "${USER_RUNTIME_DIR:?}"/* "${USER_RUNTIME_DIR:?}"/.[!.]*
fi
diff -Nru snapd-2.40/tests/lib/journalctl.sh snapd-2.42.1/tests/lib/journalctl.sh
--- snapd-2.40/tests/lib/journalctl.sh 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/lib/journalctl.sh 2019-10-30 12:17:43.000000000 +0000
@@ -11,6 +11,7 @@
}
start_new_journalctl_log(){
+ sync_journalctl_log
echo "New test starts here - $SPREAD_JOB" | systemd-cat -t snapd-test
cursor=$(get_last_journalctl_cursor)
if [ -z "$cursor" ]; then
@@ -51,8 +52,7 @@
if [ -f "$JOURNALCTL_CURSOR_FILE" ]; then
cursor=$(tail -n1 "$JOURNALCTL_CURSOR_FILE")
fi
- journalctl --flush || true
- journalctl --sync || true
+ sync_journalctl_log
get_journalctl_log_from_cursor "$cursor" "$@"
}
@@ -65,3 +65,8 @@
journalctl "$@" --cursor "$cursor"
fi
}
+
+sync_journalctl_log(){
+ journalctl --flush || true
+ journalctl --sync || true
+}
diff -Nru snapd-2.40/tests/lib/nested.sh snapd-2.42.1/tests/lib/nested.sh
--- snapd-2.40/tests/lib/nested.sh 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/lib/nested.sh 2019-10-30 12:17:43.000000000 +0000
@@ -39,6 +39,7 @@
}
create_assertions_disk(){
+ mkdir -p "$WORK_DIR"
dd if=/dev/null of="$WORK_DIR/assertions.disk" bs=1M seek=1
mkfs.ext4 -F "$WORK_DIR/assertions.disk"
debugfs -w -R "write $TESTSLIB/assertions/auto-import.assert auto-import.assert" "$WORK_DIR/assertions.disk"
diff -Nru snapd-2.40/tests/lib/pkgdb.sh snapd-2.42.1/tests/lib/pkgdb.sh
--- snapd-2.40/tests/lib/pkgdb.sh 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/lib/pkgdb.sh 2019-10-30 12:17:43.000000000 +0000
@@ -54,6 +54,9 @@
xdelta3)
echo "xdelta"
;;
+ openvswitch-switch)
+ echo "openvswitch"
+ ;;
*)
echo "$i"
;;
@@ -238,7 +241,7 @@
;;
esac
- # fix dependency issue where libp11-kit0 needs to be downgraded to
+ # fix dependency issue where libp11-kit0 needs to be downgraded to
# install gnome-keyring
case "$SPREAD_SYSTEM" in
debian-9-*)
@@ -601,6 +604,7 @@
gccgo-6
evolution-data-server
gnome-online-accounts
+ packagekit
"
pkg_linux_image_extra
;;
@@ -611,24 +615,24 @@
gnome-online-accounts
kpartx
libvirt-bin
+ packagekit
qemu
x11-utils
xvfb
"
pkg_linux_image_extra
;;
- ubuntu-17.10-64)
- pkg_linux_image_extra
- ;;
ubuntu-18.04-64)
echo "
gccgo-8
evolution-data-server
+ packagekit
"
;;
- ubuntu-18.10-64|ubuntu-19.04-64)
+ ubuntu-19.04-64|ubuntu-19.10-64)
echo "
evolution-data-server
+ packagekit
"
;;
ubuntu-*)
@@ -641,6 +645,7 @@
eatmydata
evolution-data-server
net-tools
+ packagekit
sbuild
"
;;
@@ -683,6 +688,7 @@
mock
net-tools
nfs-utils
+ PackageKit
python3-yaml
python3-dbus
python3-gobject
@@ -711,6 +717,7 @@
nc
net-tools
nfs-utils
+ PackageKit
system-lsb-core
rpm-build
xdg-user-dirs
@@ -733,6 +740,7 @@
lsb-release
man
nfs-kernel-server
+ PackageKit
python3-yaml
netcat-openbsd
osc
@@ -762,6 +770,7 @@
net-tools
nfs-utils
openbsd-netcat
+ packagekit
python
python-docutils
python-dbus
diff -Nru snapd-2.40/tests/lib/prepare-restore.sh snapd-2.42.1/tests/lib/prepare-restore.sh
--- snapd-2.40/tests/lib/prepare-restore.sh 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/lib/prepare-restore.sh 2019-10-30 12:17:43.000000000 +0000
@@ -90,19 +90,14 @@
base_version="$(head -1 debian/changelog | awk -F '[()]' '{print $2}')"
version="1337.$base_version"
packaging_path=packaging/$distro-$release
- archive_name=snapd-$version.tar.gz
- archive_compression=z
- extra_tar_args=
rpm_dir=$(rpm --eval "%_topdir")
-
+ pack_args=
case "$SPREAD_SYSTEM" in
fedora-*|amazon-*|centos-*)
- extra_tar_args="$extra_tar_args --exclude=vendor/*"
- archive_name=snapd_$version.no-vendor.tar.xz
;;
opensuse-*)
- archive_name=snapd_$version.vendor.tar.xz
- archive_compression=J
+ # use bundled snapd*.vendor.tar.xz archive
+ pack_args=-s
;;
*)
echo "ERROR: RPM build for system $SPREAD_SYSTEM is not yet supported"
@@ -112,18 +107,10 @@
sed -i -e "s/^Version:.*$/Version: $version/g" "$packaging_path/snapd.spec"
# Create a source tarball for the current snapd sources
- mkdir -p "/tmp/pkg/snapd-$version"
- cp -ra -- * "/tmp/pkg/snapd-$version/"
mkdir -p "$rpm_dir/SOURCES"
- # shellcheck disable=SC2086
- (cd /tmp/pkg && tar "-c${archive_compression}f" "$rpm_dir/SOURCES/$archive_name" $extra_tar_args "snapd-$version")
- case "$SPREAD_SYSTEM" in
- fedora-*|amazon-*|centos-*)
- # need to build the vendor tree
- (cd /tmp/pkg && tar "-cJf" "$rpm_dir/SOURCES/snapd_${version}.only-vendor.tar.xz" "snapd-$version/vendor")
- ;;
- esac
cp "$packaging_path"/* "$rpm_dir/SOURCES/"
+ # shellcheck disable=SC2086
+ ./packaging/pack-source -v "$version" -o "$rpm_dir/SOURCES" $pack_args
# Cleanup all artifacts from previous builds
rm -rf "$rpm_dir"/BUILD/*
@@ -222,6 +209,12 @@
###
prepare_project() {
+ if [[ "$SPREAD_SYSTEM" == ubuntu-* ]] && [[ "$SPREAD_SYSTEM" != ubuntu-core-* ]]; then
+ apt-get remove --purge -y lxd lxcfs || true
+ apt-get autoremove --purge -y
+ lxd-tool undo-lxd-mount-changes
+ fi
+
# Check if running inside a container.
# The testsuite will not work in such an environment
if systemd-detect-virt -c; then
@@ -363,7 +356,7 @@
quiet eatmydata apt-get install -y --force-yes apparmor libapparmor1 seccomp libseccomp2 systemd cgroup-lite util-linux
fi
- # WORKAROUND for older postrm scripts that did not do
+ # WORKAROUND for older postrm scripts that did not do
# "rm -rf /var/cache/snapd"
rm -rf /var/cache/snapd/aux
case "$SPREAD_SYSTEM" in
@@ -488,11 +481,7 @@
echo "install snaps profiler"
if [ "$PROFILE_SNAPS" = 1 ]; then
- profiler_snap=test-snapd-profiler
- if is_core18_system; then
- profiler_snap=test-snapd-profiler-core18
- fi
-
+ profiler_snap="$(get_snap_for_system test-snapd-profiler)"
rm -f "/var/snap/${profiler_snap}/common/profiler.log"
snap install "${profiler_snap}"
snap connect "${profiler_snap}":system-observe
@@ -550,10 +539,7 @@
logs_id=$(find "$logs_dir" -maxdepth 1 -name '*.journal.log' | wc -l)
logs_file=$(echo "${logs_id}_${SPREAD_JOB}" | tr '/' '_' | tr ':' '__')
- profiler_snap=test-snapd-profiler
- if is_core18_system; then
- profiler_snap=test-snapd-profiler-core18
- fi
+ profiler_snap="$(get_snap_for_system test-snapd-profiler)"
mkdir -p "$logs_dir"
if [ -e "/var/snap/${profiler_snap}/common/profiler.log" ]; then
@@ -561,6 +547,18 @@
fi
get_journalctl_log > "${logs_dir}/${logs_file}.journal.log"
fi
+
+ # On Arch it seems that using sudo / su for working with the test user
+ # spawns the /run/user/12345 tmpfs for XDG_RUNTIME_DIR which asynchronously
+ # cleans up itself sometime after the test but not instantly, leading to
+ # random failures in the mount leak detector. Give it a moment but don't
+ # clean it up ourselves, this should report actual test errors, if any.
+ for i in $(seq 10); do
+ if not mountinfo-tool /run/user/12345 .fs_type=tmpfs; then
+ break
+ fi
+ sleep 1
+ done
}
restore_suite() {
@@ -618,12 +616,38 @@
exit 1
fi
+ if getent passwd snap_daemon; then
+ echo "Test left the snap_daemon user behind, this should not happen"
+ exit 1
+ fi
+ if getent group snap_daemon; then
+ echo "Test left the snap_daemon group behind, this should not happen"
+ exit 1
+ fi
+
# Something is hosing the filesystem so look for signs of that
not grep -F "//deleted /etc" /proc/self/mountinfo
+
+ if journalctl -u snapd.service | grep -F "signal: terminated"; then
+ exit 1;
+ fi
+
+ case "$SPREAD_SYSTEM" in
+ fedora-*|centos-*)
+ # Make sure that we are not leaving behind incorrectly labeled snap
+ # files on systems supporting SELinux
+ (
+ find /root/snap -printf '%Z\t%H/%P\n' || true
+ find /home -regex '/home/[^/]*/snap\(/.*\)?' -printf '%Z\t%H/%P\n' || true
+ ) | grep -c -v snappy_home_t | MATCH "0"
+
+ find /var/snap -printf '%Z\t%H/%P\n' | grep -c -v snappy_var_t | MATCH "0"
+ ;;
+ esac
}
restore_project() {
- # Delete the snapd state used to accelerate prepare/restore code in certain suites.
+ # Delete the snapd state used to accelerate prepare/restore code in certain suites.
delete_snapd_state
# Remove all of the code we pushed and any build results. This removes
diff -Nru snapd-2.40/tests/lib/prepare.sh snapd-2.42.1/tests/lib/prepare.sh
--- snapd-2.40/tests/lib/prepare.sh 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/lib/prepare.sh 2019-10-30 12:17:43.000000000 +0000
@@ -410,7 +410,7 @@
# ubuntu-image channel to that of the gadget, so that we don't
# need to download it
snap download --channel="$KERNEL_CHANNEL" pc-kernel
-
+
EXTRA_FUNDAMENTAL="--extra-snaps $PWD/pc-kernel_*.snap"
IMAGE_CHANNEL="$GADGET_CHANNEL"
fi
@@ -438,7 +438,7 @@
"$EXTRA_FUNDAMENTAL" \
--extra-snaps "${extra_snap[0]}" \
--output "$IMAGE_HOME/$IMAGE"
- rm -f ./pc-kernel_*.{snap,assert} ./pc_*.{snap,assert}
+ rm -f ./pc-kernel_*.{snap,assert} ./pc_*.{snap,assert} ./snapd_*.{snap,assert}
# mount fresh image and add all our SPREAD_PROJECT data
kpartx -avs "$IMAGE_HOME/$IMAGE"
@@ -461,7 +461,7 @@
--exclude /gopath/bin/govendor \
--exclude /gopath/pkg/ \
/home/gopath /mnt/user-data/
-
+
# now modify the image
if is_core18_system; then
UNPACK_DIR="/tmp/core18-snap"
@@ -509,12 +509,12 @@
grep -v "^root:" "$UNPACK_DIR/etc/$f" > /mnt/system-data/root/test-etc/"$f"
# append this systems root user so that linode can connect
grep "^root:" /etc/"$f" >> /mnt/system-data/root/test-etc/"$f"
-
+
# make sure the group is as expected
chgrp --reference "$UNPACK_DIR/etc/$f" /mnt/system-data/root/test-etc/"$f"
# now bind mount read-only those passwd files on boot
cat >/mnt/system-data/etc/systemd/system/etc-"$f".mount <> /mnt/system-data/var/lib/extrausers/"$f"
@@ -547,7 +547,7 @@
# inside and outside which is a pain (see 12345 above), but
# using the ids directly is the wrong kind of fragile
chown --verbose test:test /mnt/user-data/test
-
+
# we do what sync-dirs is normally doing on boot, but because
# we have subdirs/files in /etc/systemd/system (created below)
# the writeable-path sync-boot won't work
diff -Nru snapd-2.40/tests/lib/reset.sh snapd-2.42.1/tests/lib/reset.sh
--- snapd-2.40/tests/lib/reset.sh 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/lib/reset.sh 2019-10-30 12:17:43.000000000 +0000
@@ -17,19 +17,6 @@
# Reload all service units as in some situations the unit might
# have changed on the disk.
systemctl daemon-reload
-
- echo "Ensure the service is active before stopping it"
- retries=20
- systemctl status snapd.service snapd.socket || true
- while systemctl status snapd.service snapd.socket | grep "Active: activating"; do
- if [ $retries -eq 0 ]; then
- echo "snapd service or socket not active"
- exit 1
- fi
- retries=$(( retries - 1 ))
- sleep 1
- done
-
systemd_stop_units snapd.service snapd.socket
case "$SPREAD_SYSTEM" in
@@ -119,11 +106,19 @@
;;
*)
# make sure snapd is running before we attempt to remove snaps, in case a test stopped it
- if ! systemctl status snapd.service snapd.socket; then
+ if ! systemctl status snapd.service snapd.socket >/dev/null; then
systemctl start snapd.service snapd.socket
fi
- if ! echo "$SKIP_REMOVE_SNAPS" | grep -w "$snap"; then
- if snap info "$snap" | grep -E '^type: +(base|core)'; then
+ # Check if a snap should be kept, there's a list of those in spread.yaml.
+ keep=0
+ for precious_snap in $SKIP_REMOVE_SNAPS; do
+ if [ "$snap" = "$precious_snap" ]; then
+ keep=1
+ break
+ fi
+ done
+ if [ "$keep" -eq 0 ]; then
+ if snap info --verbose "$snap" | grep -E '^type: +(base|core)'; then
if [ -z "$remove_bases" ]; then
remove_bases="$snap"
else
Binary files /tmp/tmpqD7xhb/6u5KrkVo8z/snapd-2.40/tests/lib/snaps/basic18/meta/icon.png and /tmp/tmpqD7xhb/2JDV8QJ2c_/snapd-2.42.1/tests/lib/snaps/basic18/meta/icon.png differ
diff -Nru snapd-2.40/tests/lib/snaps/basic18/meta/snap.yaml snapd-2.42.1/tests/lib/snaps/basic18/meta/snap.yaml
--- snapd-2.40/tests/lib/snaps/basic18/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/basic18/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,5 @@
+name: basic18
+version: 1.0
+summary: Basic snap with base core18
+description: A basic buildable snap using core18
+base: core18
diff -Nru snapd-2.40/tests/lib/snaps/classic-gadget/meta/gadget.yaml snapd-2.42.1/tests/lib/snaps/classic-gadget/meta/gadget.yaml
--- snapd-2.40/tests/lib/snaps/classic-gadget/meta/gadget.yaml 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/classic-gadget/meta/gadget.yaml 2019-10-30 12:17:43.000000000 +0000
@@ -1 +1 @@
-# on classic this can be empty (or absent)
\ No newline at end of file
+# on classic this can be empty (or absent)
diff -Nru snapd-2.40/tests/lib/snaps/classic-gadget-18/meta/gadget.yaml snapd-2.42.1/tests/lib/snaps/classic-gadget-18/meta/gadget.yaml
--- snapd-2.40/tests/lib/snaps/classic-gadget-18/meta/gadget.yaml 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/classic-gadget-18/meta/gadget.yaml 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1 @@
+# on classic this can be empty (or absent)
diff -Nru snapd-2.40/tests/lib/snaps/classic-gadget-18/meta/hooks/prepare-device snapd-2.42.1/tests/lib/snaps/classic-gadget-18/meta/hooks/prepare-device
--- snapd-2.40/tests/lib/snaps/classic-gadget-18/meta/hooks/prepare-device 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/classic-gadget-18/meta/hooks/prepare-device 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,2 @@
+#!/bin/sh
+snapctl set device-service.url=http://localhost:11029
Binary files /tmp/tmpqD7xhb/6u5KrkVo8z/snapd-2.40/tests/lib/snaps/classic-gadget-18/meta/icon.png and /tmp/tmpqD7xhb/2JDV8QJ2c_/snapd-2.42.1/tests/lib/snaps/classic-gadget-18/meta/icon.png differ
diff -Nru snapd-2.40/tests/lib/snaps/classic-gadget-18/meta/snap.yaml snapd-2.42.1/tests/lib/snaps/classic-gadget-18/meta/snap.yaml
--- snapd-2.40/tests/lib/snaps/classic-gadget-18/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/classic-gadget-18/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,5 @@
+name: classic-gadget-18
+type: gadget
+version: 1.0
+summary: Classic gadget using core18
+base: core18
diff -Nru snapd-2.40/tests/lib/snaps/config-versions/meta/hooks/configure snapd-2.42.1/tests/lib/snaps/config-versions/meta/hooks/configure
--- snapd-2.40/tests/lib/snaps/config-versions/meta/hooks/configure 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/config-versions/meta/hooks/configure 2019-10-30 12:17:43.000000000 +0000
@@ -1,2 +1,3 @@
#!/bin/sh
+snapctl set configure-marker="executed-for-v1"
diff -Nru snapd-2.40/tests/lib/snaps/config-versions/meta/hooks/post-refresh snapd-2.42.1/tests/lib/snaps/config-versions/meta/hooks/post-refresh
--- snapd-2.40/tests/lib/snaps/config-versions/meta/hooks/post-refresh 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/config-versions/meta/hooks/post-refresh 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+snapctl set post-refresh-hook-marker="executed-for-v1"
diff -Nru snapd-2.40/tests/lib/snaps/config-versions-v2/meta/hooks/configure snapd-2.42.1/tests/lib/snaps/config-versions-v2/meta/hooks/configure
--- snapd-2.40/tests/lib/snaps/config-versions-v2/meta/hooks/configure 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/config-versions-v2/meta/hooks/configure 2019-10-30 12:17:43.000000000 +0000
@@ -1,2 +1,9 @@
#!/bin/sh
+snapctl set configure-marker="executed-for-v2"
+
+command=$(snapctl get fail-configure)
+if [ "x$command" = "xyes" ]; then
+ echo "failing configure hook as requested"
+ exit 1
+fi
diff -Nru snapd-2.40/tests/lib/snaps/config-versions-v2/meta/hooks/post-refresh snapd-2.42.1/tests/lib/snaps/config-versions-v2/meta/hooks/post-refresh
--- snapd-2.40/tests/lib/snaps/config-versions-v2/meta/hooks/post-refresh 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/config-versions-v2/meta/hooks/post-refresh 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,3 @@
+#!/bin/sh
+
+snapctl set post-refresh-hook-marker="executed-for-v2"
diff -Nru snapd-2.40/tests/lib/snaps/snapctl-hooks/meta/hooks/configure snapd-2.42.1/tests/lib/snaps/snapctl-hooks/meta/hooks/configure
--- snapd-2.40/tests/lib/snaps/snapctl-hooks/meta/hooks/configure 2019-07-12 08:40:08.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/snapctl-hooks/meta/hooks/configure 2019-10-30 12:17:43.000000000 +0000
@@ -75,10 +75,11 @@
fi
}
-test_snapctl_get_foo_null() {
+test_snapctl_foo_null() {
echo "Getting foo"
+ # note, snapctl doesn't fail on non-existing keys, this check is for other unexpected errors
if ! output="$(snapctl get foo)"; then
- echo "Expected snapctl get to be able to retrieve value just set"
+ echo "Did not expect snapctl get to fail"
exit 1
fi
@@ -89,6 +90,22 @@
fi
}
+test_snapctl_unset() {
+ echo "Unsetting an option"
+ if ! snapctl set root.key2! ; then
+ echo "snapctl set unexpectedly failed when un-setting root.key2"
+ exit 1
+ fi
+}
+
+test_snapctl_unset_with_unset() {
+ echo "Unsetting an option"
+ if ! snapctl unset root.key2 ; then
+ echo "snapctl set unexpectedly failed when un-setting root.key2"
+ exit 1
+ fi
+}
+
test_exit_one() {
echo "Failing as requested."
exit 1
@@ -110,8 +127,8 @@
"test-snapctl-get-foo")
test_snapctl_get_foo
;;
- "test-snapctl-get-foo-null")
- test_snapctl_get_foo_null
+ "test-snapctl-foo-null")
+ test_snapctl_foo_null
;;
"test-exit-one")
test_exit_one
@@ -122,6 +139,14 @@
"test-get-nested")
test_get_nested
;;
+ "test-unset-with-unset")
+ test_snapctl_unset_with_unset
+ ;;
+ "noop")
+ ;;
+ "test-unset")
+ test_snapctl_unset
+ ;;
*)
echo "Invalid command: '$command'"
exit 1
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-core-migration.base-core/bin/sh snapd-2.42.1/tests/lib/snaps/test-snapd-core-migration.base-core/bin/sh
--- snapd-2.40/tests/lib/snaps/test-snapd-core-migration.base-core/bin/sh 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-core-migration.base-core/bin/sh 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec /bin/sh "$@"
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-core-migration.base-core/meta/snap.yaml snapd-2.42.1/tests/lib/snaps/test-snapd-core-migration.base-core/meta/snap.yaml
--- snapd-2.40/tests/lib/snaps/test-snapd-core-migration.base-core/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-core-migration.base-core/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,7 @@
+name: test-snapd-core-migration
+version: 1
+# TODO: uncomment when "base: core" works.
+# base: core
+apps:
+ sh:
+ command: bin/sh
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-core-migration.base-core18/bin/sh snapd-2.42.1/tests/lib/snaps/test-snapd-core-migration.base-core18/bin/sh
--- snapd-2.40/tests/lib/snaps/test-snapd-core-migration.base-core18/bin/sh 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-core-migration.base-core18/bin/sh 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,2 @@
+#!/bin/sh
+exec /bin/sh "$@"
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-core-migration.base-core18/meta/snap.yaml snapd-2.42.1/tests/lib/snaps/test-snapd-core-migration.base-core18/meta/snap.yaml
--- snapd-2.40/tests/lib/snaps/test-snapd-core-migration.base-core18/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-core-migration.base-core18/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,6 @@
+name: test-snapd-core-migration
+version: 2
+base: core18
+apps:
+ sh:
+ command: bin/sh
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/Makefile snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/Makefile
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/Makefile 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/Makefile 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,8 @@
+all:
+ cd ./src && make all
+
+install:
+ cd ./src && make install
+
+clean:
+ cd ./src && make clean
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/snapcraft.yaml snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/snapcraft.yaml
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/snapcraft.yaml 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,98 @@
+name: test-snapd-daemon-user
+version: "3"
+summary: test-snapd-daemon-user summary
+description: |
+ Test the snap_daemon user functionality
+confinement: strict
+grade: devel
+base: core18
+
+environment:
+ LD_LIBRARY_PATH: /usr/lib32
+
+apps:
+ drop:
+ command: bin/drop
+ drop32:
+ command: bin/drop32
+ drop-exec:
+ command: bin/drop-exec
+ drop-exec32:
+ command: bin/drop-exec32
+ drop-syscall:
+ command: bin/drop-syscall
+ drop-syscall32:
+ command: bin/drop-syscall32
+ setgid:
+ command: bin/setgid
+ setgid32:
+ command: bin/setgid32
+ setregid:
+ command: bin/setregid
+ setregid32:
+ command: bin/setregid32
+ setresgid:
+ command: bin/setresgid
+ setresgid32:
+ command: bin/setresgid32
+ setuid:
+ command: bin/setuid
+ setuid32:
+ command: bin/setuid32
+ setreuid:
+ command: bin/setreuid
+ setreuid32:
+ command: bin/setreuid32
+ setresuid:
+ command: bin/setresuid
+ setresuid32:
+ command: bin/setresuid32
+ chown:
+ command: bin/chown
+ chown32:
+ command: bin/chown32
+ lchown:
+ command: bin/lchown
+ lchown32:
+ command: bin/lchown32
+ fchown:
+ command: bin/fchown
+ fchown32:
+ command: bin/fchown32
+ fchownat:
+ command: bin/fchownat
+ fchownat32:
+ command: bin/fchownat32
+
+passthrough:
+ system-usernames:
+ snap_daemon: shared
+
+parts:
+ test-snapd-daemon-user:
+ plugin: make
+ source: .
+ build-packages:
+ - libc6-dev
+ - libseccomp-dev
+ - on amd64:
+ - gcc-multilib
+ - on arm64:
+ - gcc # ideally: "gcc-multilib:armhf" (LP: #1827067)
+ - on armhf:
+ - gcc
+ - on i386:
+ - gcc
+ - on ppc64el:
+ - gcc
+ - on s390x:
+ - gcc
+ stage-packages:
+ - libseccomp2
+ #- on amd64: ["libseccomp2:i386"] # possible if sources.list is modified
+ dumper:
+ plugin: dump
+ source: files
+ after:
+ - test-snapd-daemon-user
+
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/chown32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/chown32.c
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/chown32.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/chown32.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,58 @@
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "display.h"
+
+int main(int argc, char *argv[])
+{
+ if (argc < 4) {
+ fprintf(stderr, "Usage: %s \n", argv[0]);
+ exit(EXIT_FAILURE);
+ }
+
+ uid_t uid = -1;
+ struct passwd *pwd;
+
+ gid_t gid = -1;
+ struct group *grp;
+
+ if (strcmp(argv[2], "-1") != 0) {
+ pwd = getpwnam(argv[2]);
+ if (pwd == NULL) {
+ printf("'%s' not found\n", argv[2]);
+ exit(EXIT_FAILURE);
+ }
+ uid = pwd->pw_uid;
+ }
+
+ if (strcmp(argv[3], "-1") != 0) {
+ grp = getgrnam(argv[3]);
+ if (grp == NULL) {
+ printf("'%s' not found\n", argv[3]);
+ exit(EXIT_FAILURE);
+ }
+ gid = grp->gr_gid;
+ }
+
+ printf("Before: ");
+ display_perms(argv[1]);
+
+ if (chown(argv[1], uid, gid) < 0) {
+ perror("chown");
+ goto fail;
+ }
+
+ printf("After: ");
+ display_perms(argv[1]);
+
+ exit(0);
+
+ fail:
+ exit(EXIT_FAILURE);
+}
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/chown.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/chown.c
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/chown.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/chown.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,58 @@
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "display.h"
+
+int main(int argc, char *argv[])
+{
+ if (argc < 4) {
+ fprintf(stderr, "Usage: %s \n", argv[0]);
+ exit(EXIT_FAILURE);
+ }
+
+ uid_t uid = -1;
+ struct passwd *pwd;
+
+ gid_t gid = -1;
+ struct group *grp;
+
+ if (strcmp(argv[2], "-1") != 0) {
+ pwd = getpwnam(argv[2]);
+ if (pwd == NULL) {
+ printf("'%s' not found\n", argv[2]);
+ exit(EXIT_FAILURE);
+ }
+ uid = pwd->pw_uid;
+ }
+
+ if (strcmp(argv[3], "-1") != 0) {
+ grp = getgrnam(argv[3]);
+ if (grp == NULL) {
+ printf("'%s' not found\n", argv[3]);
+ exit(EXIT_FAILURE);
+ }
+ gid = grp->gr_gid;
+ }
+
+ printf("Before: ");
+ display_perms(argv[1]);
+
+ if (chown(argv[1], uid, gid) < 0) {
+ perror("chown");
+ goto fail;
+ }
+
+ printf("After: ");
+ display_perms(argv[1]);
+
+ exit(0);
+
+ fail:
+ exit(EXIT_FAILURE);
+}
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/display32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/display32.c
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/display32.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/display32.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,66 @@
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+int display(void) {
+ int i;
+ uid_t ruid, euid, suid;
+ gid_t rgid, egid, sgid;
+ gid_t *groups = NULL;
+ int ngroups = 0;
+ long ngroups_max = sysconf(_SC_NGROUPS_MAX) + 1;
+
+ if (getresuid(&ruid, &euid, &suid) < 0) {
+ perror("Could not getresuid");
+ exit(1);
+ }
+ if (getresgid(&rgid, &egid, &sgid) < 0) {
+ perror("Could not getresgid");
+ exit(1);
+ }
+
+ /* Get our supplementary groups */
+ groups = (gid_t *) malloc(ngroups_max * sizeof(gid_t));
+ if (groups == NULL) {
+ printf("Could not allocate memory\n");
+ exit(EXIT_FAILURE);
+ }
+ ngroups = getgroups(ngroups_max, groups);
+ if (ngroups < 0) {
+ perror("getgroups");
+ free(groups);
+ exit(1);
+ }
+
+ /* Display dropped privileges */
+ printf("ruid=%d, euid=%d, suid=%d, ", ruid, euid, suid);
+ printf("rgid=%d, egid=%d, sgid=%d, ", rgid, egid, sgid);
+ printf("groups=");
+ for (i = 0; i < ngroups; i++) {
+ printf("%d", groups[i]);
+ if (i < ngroups - 1)
+ printf(",");
+ }
+ printf("\n");
+
+ free(groups);
+
+ return 0;
+}
+
+int display_perms(char *fn) {
+ struct stat sb;
+ if (lstat(fn, &sb) < 0) {
+ perror("Could not lstat");
+ exit(1);
+ }
+
+ printf("%s: uid=%d, gid=%d\n", fn, sb.st_uid, sb.st_gid);
+
+ return 0;
+}
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/display.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/display.c
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/display.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/display.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,66 @@
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+int display(void) {
+ int i;
+ uid_t ruid, euid, suid;
+ gid_t rgid, egid, sgid;
+ gid_t *groups = NULL;
+ int ngroups = 0;
+ long ngroups_max = sysconf(_SC_NGROUPS_MAX) + 1;
+
+ if (getresuid(&ruid, &euid, &suid) < 0) {
+ perror("Could not getresuid");
+ exit(1);
+ }
+ if (getresgid(&rgid, &egid, &sgid) < 0) {
+ perror("Could not getresgid");
+ exit(1);
+ }
+
+ /* Get our supplementary groups */
+ groups = (gid_t *) malloc(ngroups_max * sizeof(gid_t));
+ if (groups == NULL) {
+ printf("Could not allocate memory\n");
+ exit(EXIT_FAILURE);
+ }
+ ngroups = getgroups(ngroups_max, groups);
+ if (ngroups < 0) {
+ perror("getgroups");
+ free(groups);
+ exit(1);
+ }
+
+ /* Display dropped privileges */
+ printf("ruid=%d, euid=%d, suid=%d, ", ruid, euid, suid);
+ printf("rgid=%d, egid=%d, sgid=%d, ", rgid, egid, sgid);
+ printf("groups=");
+ for (i = 0; i < ngroups; i++) {
+ printf("%d", groups[i]);
+ if (i < ngroups - 1)
+ printf(",");
+ }
+ printf("\n");
+
+ free(groups);
+
+ return 0;
+}
+
+int display_perms(char *fn) {
+ struct stat sb;
+ if (lstat(fn, &sb) < 0) {
+ perror("Could not lstat");
+ exit(1);
+ }
+
+ printf("%s: uid=%d, gid=%d\n", fn, sb.st_uid, sb.st_gid);
+
+ return 0;
+}
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/display.h snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/display.h
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/display.h 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/display.h 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,2 @@
+int display(void);
+int display_perms(char*);
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/drop32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/drop32.c
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/drop32.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/drop32.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,68 @@
+/*
+ * gcc ./drop.c -o drop
+ */
+
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "display.h"
+
+int main(int argc, char *argv[])
+{
+ if (argc < 2) {
+ fprintf(stderr, "Usage: %s [setgroups]\n", argv[0]);
+ exit(EXIT_FAILURE);
+ }
+
+ /* Convert our username to a passwd entry */
+ struct passwd *pwd = getpwnam(argv[1]);
+ if (pwd == NULL) {
+ printf("'%s' not found\n", argv[1]);
+ exit(EXIT_FAILURE);
+ }
+
+ printf("Before: ");
+ display();
+
+ /* Drop supplementary groups first if can (TODO: CAP_SETGID) */
+ if (argc == 3 && strcmp(argv[2], "setgroups") == 0) {
+ gid_t gid_list[1];
+ gid_list[0] = pwd->pw_gid;
+ if (geteuid() == 0 && setgroups(1, gid_list) < 0) {
+ perror("setgroups");
+ goto fail;
+ }
+ } else {
+ // not portable outside of Linux, but snap-friendly
+ if (setgroups(0, NULL) < 0) {
+ perror("setgroups");
+ goto fail;
+ }
+ }
+
+ /* Drop gid after supplementary groups */
+ if (setgid(pwd->pw_gid) < 0) {
+ perror("setgid");
+ goto fail;
+ }
+
+ /* Drop uid after gid */
+ if (setuid(pwd->pw_uid) < 0) {
+ perror("setuid");
+ goto fail;
+ }
+
+ printf("After: ");
+ display();
+
+ exit(0);
+
+ fail:
+ exit(EXIT_FAILURE);
+}
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/drop.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/drop.c
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/drop.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/drop.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,68 @@
+/*
+ * gcc ./drop.c -o drop
+ */
+
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "display.h"
+
+int main(int argc, char *argv[])
+{
+ if (argc < 2) {
+ fprintf(stderr, "Usage: %s [setgroups]\n", argv[0]);
+ exit(EXIT_FAILURE);
+ }
+
+ /* Convert our username to a passwd entry */
+ struct passwd *pwd = getpwnam(argv[1]);
+ if (pwd == NULL) {
+ printf("'%s' not found\n", argv[1]);
+ exit(EXIT_FAILURE);
+ }
+
+ printf("Before: ");
+ display();
+
+ /* Drop supplementary groups first if can (TODO: CAP_SETGID) */
+ if (argc == 3 && strcmp(argv[2], "setgroups") == 0) {
+ gid_t gid_list[1];
+ gid_list[0] = pwd->pw_gid;
+ if (geteuid() == 0 && setgroups(1, gid_list) < 0) {
+ perror("setgroups");
+ goto fail;
+ }
+ } else {
+ // not portable outside of Linux, but snap-friendly
+ if (setgroups(0, NULL) < 0) {
+ perror("setgroups");
+ goto fail;
+ }
+ }
+
+ /* Drop gid after supplementary groups */
+ if (setgid(pwd->pw_gid) < 0) {
+ perror("setgid");
+ goto fail;
+ }
+
+ /* Drop uid after gid */
+ if (setuid(pwd->pw_uid) < 0) {
+ perror("setuid");
+ goto fail;
+ }
+
+ printf("After: ");
+ display();
+
+ exit(0);
+
+ fail:
+ exit(EXIT_FAILURE);
+}
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/drop-exec32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/drop-exec32.c
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/drop-exec32.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/drop-exec32.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,60 @@
+/*
+ * gcc ./drop.c -o drop
+ */
+
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "display.h"
+
+int main(int argc, char *argv[])
+{
+ if (argc < 3) {
+ fprintf(stderr, "Usage: %s \n", argv[0]);
+ exit(EXIT_FAILURE);
+ }
+
+ /* Convert our username to a passwd entry */
+ struct passwd *pwd = getpwnam(argv[1]);
+ if (pwd == NULL) {
+ printf("'%s' not found\n", argv[1]);
+ exit(EXIT_FAILURE);
+ }
+
+ printf("Before: ");
+ display();
+
+ // not portable outside of Linux, but snap-friendly
+ if (setgroups(0, NULL) < 0) {
+ perror("setgroups");
+ goto fail;
+ }
+
+ /* Drop gid after supplementary groups */
+ if (setgid(pwd->pw_gid) < 0) {
+ perror("setgid");
+ goto fail;
+ }
+
+ /* Drop uid after gid */
+ if (setuid(pwd->pw_uid) < 0) {
+ perror("setuid");
+ goto fail;
+ }
+
+ printf("After: ");
+ display();
+
+ printf("Executing: %s...\n", argv[2]);
+ execv(argv[2], (char *const *)&argv[2]);
+ perror("execv failed");
+
+ fail:
+ exit(EXIT_FAILURE);
+}
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/drop-exec.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/drop-exec.c
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/drop-exec.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/drop-exec.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,60 @@
+/*
+ * gcc ./drop.c -o drop
+ */
+
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "display.h"
+
+int main(int argc, char *argv[])
+{
+ if (argc < 3) {
+ fprintf(stderr, "Usage: %s \n", argv[0]);
+ exit(EXIT_FAILURE);
+ }
+
+ /* Convert our username to a passwd entry */
+ struct passwd *pwd = getpwnam(argv[1]);
+ if (pwd == NULL) {
+ printf("'%s' not found\n", argv[1]);
+ exit(EXIT_FAILURE);
+ }
+
+ printf("Before: ");
+ display();
+
+ // not portable outside of Linux, but snap-friendly
+ if (setgroups(0, NULL) < 0) {
+ perror("setgroups");
+ goto fail;
+ }
+
+ /* Drop gid after supplementary groups */
+ if (setgid(pwd->pw_gid) < 0) {
+ perror("setgid");
+ goto fail;
+ }
+
+ /* Drop uid after gid */
+ if (setuid(pwd->pw_uid) < 0) {
+ perror("setuid");
+ goto fail;
+ }
+
+ printf("After: ");
+ display();
+
+ printf("Executing: %s...\n", argv[2]);
+ execv(argv[2], (char *const *)&argv[2]);
+ perror("execv failed");
+
+ fail:
+ exit(EXIT_FAILURE);
+}
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/drop-syscall32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/drop-syscall32.c
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/drop-syscall32.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/drop-syscall32.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,68 @@
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "display.h"
+
+int main(int argc, char *argv[])
+{
+ if (argc < 2) {
+ fprintf(stderr, "Usage: %s [setgroups]\n", argv[0]);
+ exit(EXIT_FAILURE);
+ }
+
+ /* Convert our username to a passwd entry */
+ struct passwd *pwd = getpwnam(argv[1]);
+ if (pwd == NULL) {
+ printf("'%s' not found\n", argv[1]);
+ exit(EXIT_FAILURE);
+ }
+
+ printf("Before: ");
+ display();
+
+ /* Drop supplementary groups first if can (TODO: CAP_SETGID) */
+ errno = 0;
+ if (argc == 3 && strcmp(argv[2], "setgroups") == 0) {
+ gid_t gid_list[1];
+ gid_list[0] = pwd->pw_gid;
+ if (geteuid() == 0 && syscall(__NR_setgroups32, 1, gid_list) < 0) {
+ perror("setgroups");
+ goto fail;
+ }
+ } else {
+ // not portable outside of Linux, but snap-friendly
+ if (syscall(__NR_setgroups32, 0, NULL) < 0) {
+ perror("setgroups");
+ goto fail;
+ }
+ }
+
+ /* Drop gid after supplementary groups */
+ errno = 0;
+ if (syscall(__NR_setgid32, pwd->pw_gid) < 0) {
+ perror("setgid");
+ goto fail;
+ }
+
+ /* Drop uid after gid */
+ errno = 0;
+ if (syscall(__NR_setuid32, pwd->pw_uid) < 0) {
+ perror("setuid");
+ goto fail;
+ }
+
+ printf("After: ");
+ display();
+
+ exit(0);
+
+ fail:
+ exit(EXIT_FAILURE);
+}
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/drop-syscall.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/drop-syscall.c
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/drop-syscall.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/drop-syscall.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,68 @@
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "display.h"
+
+int main(int argc, char *argv[])
+{
+ if (argc < 2) {
+ fprintf(stderr, "Usage: %s [setgroups]\n", argv[0]);
+ exit(EXIT_FAILURE);
+ }
+
+ /* Convert our username to a passwd entry */
+ struct passwd *pwd = getpwnam(argv[1]);
+ if (pwd == NULL) {
+ printf("'%s' not found\n", argv[1]);
+ exit(EXIT_FAILURE);
+ }
+
+ printf("Before: ");
+ display();
+
+ /* Drop supplementary groups first if can (TODO: CAP_SETGID) */
+ errno = 0;
+ if (argc == 3 && strcmp(argv[2], "setgroups") == 0) {
+ gid_t gid_list[1];
+ gid_list[0] = pwd->pw_gid;
+ if (geteuid() == 0 && syscall(__NR_setgroups, 1, gid_list) < 0) {
+ perror("setgroups");
+ goto fail;
+ }
+ } else {
+ // not portable outside of Linux, but snap-friendly
+ if (syscall(__NR_setgroups, 0, NULL) < 0) {
+ perror("setgroups");
+ goto fail;
+ }
+ }
+
+ /* Drop gid after supplementary groups */
+ errno = 0;
+ if (syscall(__NR_setgid, pwd->pw_gid) < 0) {
+ perror("setgid");
+ goto fail;
+ }
+
+ /* Drop uid after gid */
+ errno = 0;
+ if (syscall(__NR_setuid, pwd->pw_uid) < 0) {
+ perror("setuid");
+ goto fail;
+ }
+
+ printf("After: ");
+ display();
+
+ exit(0);
+
+ fail:
+ exit(EXIT_FAILURE);
+}
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/fchown32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/fchown32.c
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/fchown32.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/fchown32.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,67 @@
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+#include
+
+#include "display.h"
+
+int main(int argc, char *argv[])
+{
+ if (argc < 4) {
+ fprintf(stderr, "Usage: %s \n", argv[0]);
+ exit(EXIT_FAILURE);
+ }
+
+ uid_t uid = -1;
+ struct passwd *pwd;
+
+ gid_t gid = -1;
+ struct group *grp;
+
+ if (strcmp(argv[2], "-1") != 0) {
+ pwd = getpwnam(argv[2]);
+ if (pwd == NULL) {
+ printf("'%s' not found\n", argv[2]);
+ exit(EXIT_FAILURE);
+ }
+ uid = pwd->pw_uid;
+ }
+
+ if (strcmp(argv[3], "-1") != 0) {
+ grp = getgrnam(argv[3]);
+ if (grp == NULL) {
+ printf("'%s' not found\n", argv[3]);
+ exit(EXIT_FAILURE);
+ }
+ gid = grp->gr_gid;
+ }
+
+ printf("Before: ");
+ display_perms(argv[1]);
+
+ int fd = open(argv[1], O_RDWR);
+ if (fd < 0) {
+ perror("open");
+ goto fail;
+ }
+
+ if (fchown(fd, uid, gid) < 0) {
+ perror("fchown");
+ goto fail;
+ }
+
+ printf("After: ");
+ display_perms(argv[1]);
+
+ exit(0);
+
+ fail:
+ exit(EXIT_FAILURE);
+}
diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/fchownat32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/fchownat32.c
--- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/fchownat32.c 1970-01-01 00:00:00.000000000 +0000
+++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/fchownat32.c 2019-10-30 12:17:43.000000000 +0000
@@ -0,0 +1,85 @@
+#define _GNU_SOURCE
+#include
+#include
+#include
+#include
+#include