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 +#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; + } + + char *fn = strdup(argv[1]); + printf("fn=%s\n", fn); + if (fn == NULL) { + perror("strdup"); + goto fail; + } + // this makes sure the calls to dirname and basename are always ok + if (fn[0] != '/') { + fprintf(stderr, "'%s' must be absolute path\n", fn); + goto fail; + } + char *dir = dirname(fn); + + printf("dir=%s\n", dir); + if (chdir(dir) < 0) { + perror("chdir"); + goto fail; + } + + printf("Before: "); + display_perms(argv[1]); + + free(fn); + fn = strdup(argv[1]); + char *base = basename(fn); + + if (fchownat(AT_FDCWD, base, uid, gid, 0) < 0) { + perror("fchownat"); + 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/fchownat.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/fchownat.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/fchownat.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/fchownat.c 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,85 @@ +#define _GNU_SOURCE +#include +#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; + } + + char *fn = strdup(argv[1]); + printf("fn=%s\n", fn); + if (fn == NULL) { + perror("strdup"); + goto fail; + } + // this makes sure the calls to dirname and basename are always ok + if (fn[0] != '/') { + fprintf(stderr, "'%s' must be absolute path\n", fn); + goto fail; + } + char *dir = dirname(fn); + + printf("dir=%s\n", dir); + if (chdir(dir) < 0) { + perror("chdir"); + goto fail; + } + + printf("Before: "); + display_perms(argv[1]); + + free(fn); + fn = strdup(argv[1]); + char *base = basename(fn); + + if (fchownat(AT_FDCWD, base, uid, gid, 0) < 0) { + perror("fchownat"); + 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/fchown.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/fchown.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/fchown.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/fchown.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/lchown32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/lchown32.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/lchown32.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/lchown32.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 (lchown(argv[1], uid, gid) < 0) { + perror("lchown"); + 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/lchown.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/lchown.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/lchown.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/lchown.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 (lchown(argv[1], uid, gid) < 0) { + perror("lchown"); + 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/Makefile snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/Makefile --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/Makefile 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/Makefile 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,263 @@ +# could use 'uname_p := $(shell uname -p)' but this doesn't work right on the +# builders where i386 is built in a 32 bit chroot with amd64 kernel (eg, LP) +# amd64 x86_64-linux-gnu +# arm64 aarch64-linux-gnu +# armhf arm-linux-gnueabihf +# i386 i386-linux-gnu +# ppc64el powerpc64le-linux-gnu +# s390x s390x-linux-gnu +arch_triplet := $(SNAPCRAFT_ARCH_TRIPLET) + +CFLAGS += -g -O0 -Wall -Wstrict-prototypes + +# snapcraft will copy anything from here +INSTALL_DIR := ../../install/bin + +buildNative := drop drop-exec drop-syscall setgid setregid setresgid setuid setreuid setresuid chown lchown fchown fchownat +buildSecond := drop32 drop-exec32 drop-syscall32 setgid32 setregid32 setresgid32 setuid32 setreuid32 setresuid32 chown32 lchown32 fchown32 fchownat32 + +#ifneq (,$(filter $(arch_triplet), x86_64-linux-gnu aarch64-linux-gnu)) +ifneq (,$(filter $(arch_triplet), x86_64-linux-gnu)) +all: $(buildNative) $(buildSecond) +else +all: $(buildNative) +endif + +display.o: display.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +drop.o: drop.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +drop: display.o drop.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +drop-exec.o: drop-exec.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +drop-exec: display.o drop-exec.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +# Since drop-syscall bypasses glibc, 32 bit archs need to use drop-syscall32.c +# with the *32 syscalls for set*id/setgroups to uids/gids > 2^16. For all the +# other binaries, glibc is used and does this automatically. +ifneq (,$(filter $(arch_triplet), i386-linux-gnu arm-linux-gnueabihf)) +drop-syscall.o: drop-syscall32.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +drop-syscall: display.o drop-syscall32.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} +else +drop-syscall.o: drop-syscall.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +drop-syscall: display.o drop-syscall.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} +endif + +setgid.o: setgid.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +setgid: display.o setgid.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +setregid.o: setregid.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +setregid: display.o setregid.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +setresgid.o: setresgid.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +setresgid: display.o setresgid.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +setuid.o: setuid.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +setuid: display.o setuid.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +setreuid.o: setreuid.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +setreuid: display.o setreuid.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +setresuid.o: setresuid.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +setresuid: display.o setresuid.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +chown.o: chown.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +chown: display.o chown.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +lchown.o: lchown.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +lchown: display.o lchown.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +fchown.o: fchown.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +fchown: display.o fchown.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +fchownat.o: fchownat.c + ${CC} ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +fchownat: display.o fchownat.o + ${CC} ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +#ifneq (,$(filter $(arch_triplet), x86_64-linux-gnu aarch64-linux-gnu)) +ifneq (,$(filter $(arch_triplet), x86_64-linux-gnu)) +display32.o: display32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +drop32.o: drop32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +drop32: display32.o drop32.o + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +drop-exec32.o: drop-exec32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +drop-exec32: display32.o drop-exec32.o + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +drop-syscall32.o: drop-syscall32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +drop-syscall32: display32.o drop-syscall32.o + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +setgid32.o: setgid32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +setgid32: display32.o setgid32.o + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +setregid32.o: setregid32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +setregid32: display32.o setregid32.o + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +setresgid32.o: setresgid32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +setresgid32: display32.o setresgid32.o + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +setuid32.o: setuid32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +setuid32: display32.o setuid32.o + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +setreuid32.o: setreuid32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +setreuid32: display32.o setreuid32.o + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +setresuid32.o: setresuid32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +setresuid32: display32.o setresuid32.o + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +chown32.o: chown32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +chown32: display32.o chown32.o + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +lchown32.o: lchown32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +lchown32: display32.o lchown32.o + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +fchown32.o: fchown32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +fchown32: display32.o fchown32.o + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} + +fchownat32.o: fchownat32.c + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $< -c ${LDLIBS} + +fchownat32: display32.o fchownat32.o + ${CC} -m32 ${CFLAGS} ${LDFLAGS} $^ -o $@ ${LDLIBS} +endif + + +# Only depend on buildSecond on architectures where we are building it +# with -m32 +#ifneq (,$(filter $(arch_triplet), x86_64-linux-gnu aarch64-linux-gnu)) +ifneq (,$(filter $(arch_triplet), x86_64-linux-gnu)) +install: $(buildNative) $(buildSecond) +else +install: $(buildNative) +endif + mkdir -p ${INSTALL_DIR} + cp -f drop ${INSTALL_DIR}/drop + cp -f drop-exec ${INSTALL_DIR}/drop-exec + cp -f drop-syscall ${INSTALL_DIR}/drop-syscall + cp -f setgid ${INSTALL_DIR}/setgid + cp -f setregid ${INSTALL_DIR}/setregid + cp -f setresgid ${INSTALL_DIR}/setresgid + cp -f setuid ${INSTALL_DIR}/setuid + cp -f setreuid ${INSTALL_DIR}/setreuid + cp -f setresuid ${INSTALL_DIR}/setresuid + cp -f chown ${INSTALL_DIR}/chown + cp -f lchown ${INSTALL_DIR}/lchown + cp -f fchown ${INSTALL_DIR}/fchown + cp -f fchownat ${INSTALL_DIR}/fchownat +#ifneq (,$(filter $(arch_triplet), x86_64-linux-gnu aarch64-linux-gnu)) +ifneq (,$(filter $(arch_triplet), x86_64-linux-gnu)) + cp -f drop32 ${INSTALL_DIR}/drop32 + cp -f drop-exec32 ${INSTALL_DIR}/drop-exec32 + cp -f drop-syscall32 ${INSTALL_DIR}/drop-syscall32 + cp -f setgid32 ${INSTALL_DIR}/setgid32 + cp -f setregid32 ${INSTALL_DIR}/setregid32 + cp -f setresgid32 ${INSTALL_DIR}/setresgid32 + cp -f setuid32 ${INSTALL_DIR}/setuid32 + cp -f setreuid32 ${INSTALL_DIR}/setreuid32 + cp -f setresuid32 ${INSTALL_DIR}/setresuid32 + cp -f chown32 ${INSTALL_DIR}/chown32 + cp -f lchown32 ${INSTALL_DIR}/lchown32 + cp -f fchown32 ${INSTALL_DIR}/fchown32 + cp -f fchownat32 ${INSTALL_DIR}/fchownat32 +else + # *32 binaries are same on 32 bit systems + cp -f drop ${INSTALL_DIR}/drop32 + cp -f drop-exec ${INSTALL_DIR}/drop-exec32 + cp -f drop-syscall ${INSTALL_DIR}/drop-syscall32 + cp -f setgid ${INSTALL_DIR}/setgid32 + cp -f setregid ${INSTALL_DIR}/setregid32 + cp -f setresgid ${INSTALL_DIR}/setresgid32 + cp -f setuid ${INSTALL_DIR}/setuid32 + cp -f setreuid ${INSTALL_DIR}/setreuid32 + cp -f setresuid ${INSTALL_DIR}/setresuid32 + cp -f chown ${INSTALL_DIR}/chown32 + cp -f lchown ${INSTALL_DIR}/lchown32 + cp -f fchown ${INSTALL_DIR}/fchown32 + cp -f fchownat ${INSTALL_DIR}/fchownat32 +endif + + +clean: + rm -f ./*.o + rm -f ./drop ./drop32 ./drop-exec ./drop-exec32 ./drop-syscall ./drop-syscall32 + rm -f ./setgid ./setgid32 ./setregid ./setregid32 ./setresgid ./setresgid32 + rm -f ./setuid ./setuid32 ./setreuid ./setreuid32 ./setresuid ./setresuid32 + rm -f ./chown ./chown32 lchown lchown32 fchown fchown32 fchownat fchownat32 diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setgid32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setgid32.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setgid32.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setgid32.c 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,40 @@ +#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 \n", argv[0]); + exit(EXIT_FAILURE); + } + + struct group *grp = getgrnam(argv[1]); + if (grp == NULL) { + printf("'%s' not found\n", argv[1]); + exit(EXIT_FAILURE); + } + + printf("Before: "); + display(); + + if (setgid(grp->gr_gid) < 0) { + perror("setgid"); + goto fail; + } + + printf("After: "); + display(); + + exit(0); + + fail: + exit(EXIT_FAILURE); +} diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setgid.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setgid.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setgid.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setgid.c 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,40 @@ +#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 \n", argv[0]); + exit(EXIT_FAILURE); + } + + struct group *grp = getgrnam(argv[1]); + if (grp == NULL) { + printf("'%s' not found\n", argv[1]); + exit(EXIT_FAILURE); + } + + printf("Before: "); + display(); + + if (setgid(grp->gr_gid) < 0) { + perror("setgid"); + goto fail; + } + + printf("After: "); + display(); + + exit(0); + + fail: + exit(EXIT_FAILURE); +} diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setregid32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setregid32.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setregid32.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setregid32.c 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,50 @@ +#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); + } + + gid_t uids[2]; + struct group *grps[2]; + + for (int i=1; igr_gid; + } + } + + printf("Before: "); + display(); + + if (setregid(uids[0], uids[1]) < 0) { + perror("setregid"); + goto fail; + } + + printf("After: "); + display(); + + exit(0); + + fail: + exit(EXIT_FAILURE); +} diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setregid.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setregid.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setregid.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setregid.c 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,50 @@ +#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); + } + + gid_t uids[2]; + struct group *grps[2]; + + for (int i=1; igr_gid; + } + } + + printf("Before: "); + display(); + + if (setregid(uids[0], uids[1]) < 0) { + perror("setregid"); + goto fail; + } + + printf("After: "); + display(); + + exit(0); + + fail: + exit(EXIT_FAILURE); +} diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setresgid32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setresgid32.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setresgid32.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setresgid32.c 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,50 @@ +#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); + } + + gid_t gids[3]; + struct group *grps[3]; + + for (int i=1; igr_gid; + } + } + + printf("Before: "); + display(); + + if (setresgid(gids[0], gids[1], gids[2]) < 0) { + perror("setresgid"); + goto fail; + } + + printf("After: "); + display(); + + exit(0); + + fail: + exit(EXIT_FAILURE); +} diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setresgid.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setresgid.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setresgid.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setresgid.c 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,50 @@ +#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); + } + + gid_t gids[3]; + struct group *grps[3]; + + for (int i=1; igr_gid; + } + } + + printf("Before: "); + display(); + + if (setresgid(gids[0], gids[1], gids[2]) < 0) { + perror("setresgid"); + goto fail; + } + + printf("After: "); + display(); + + exit(0); + + fail: + exit(EXIT_FAILURE); +} diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setresuid32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setresuid32.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setresuid32.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setresuid32.c 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,50 @@ +#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 uids[3]; + struct passwd *pwds[3]; + + for (int i=1; ipw_uid; + } + } + + printf("Before: "); + display(); + + if (setresuid(uids[0], uids[1], uids[2]) < 0) { + perror("setreuid"); + goto fail; + } + + printf("After: "); + display(); + + exit(0); + + fail: + exit(EXIT_FAILURE); +} diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setresuid.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setresuid.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setresuid.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setresuid.c 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,50 @@ +#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 uids[3]; + struct passwd *pwds[3]; + + for (int i=1; ipw_uid; + } + } + + printf("Before: "); + display(); + + if (setresuid(uids[0], uids[1], uids[2]) < 0) { + perror("setreuid"); + goto fail; + } + + printf("After: "); + display(); + + exit(0); + + fail: + exit(EXIT_FAILURE); +} diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setreuid32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setreuid32.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setreuid32.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setreuid32.c 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,50 @@ +#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); + } + + uid_t uids[2]; + struct passwd *pwds[2]; + + for (int i=1; ipw_uid; + } + } + + printf("Before: "); + display(); + + if (setreuid(uids[0], uids[1]) < 0) { + perror("setreuid"); + goto fail; + } + + printf("After: "); + display(); + + exit(0); + + fail: + exit(EXIT_FAILURE); +} diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setreuid.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setreuid.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setreuid.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setreuid.c 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,50 @@ +#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); + } + + uid_t uids[2]; + struct passwd *pwds[2]; + + for (int i=1; ipw_uid; + } + } + + printf("Before: "); + display(); + + if (setreuid(uids[0], uids[1]) < 0) { + perror("setreuid"); + goto fail; + } + + printf("After: "); + display(); + + exit(0); + + fail: + exit(EXIT_FAILURE); +} diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setuid32.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setuid32.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setuid32.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setuid32.c 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,40 @@ +#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 \n", argv[0]); + exit(EXIT_FAILURE); + } + + struct passwd *pwd = getpwnam(argv[1]); + if (pwd == NULL) { + printf("'%s' not found\n", argv[1]); + exit(EXIT_FAILURE); + } + + printf("Before: "); + display(); + + 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/setuid.c snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setuid.c --- snapd-2.40/tests/lib/snaps/test-snapd-daemon-user/src/setuid.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-daemon-user/src/setuid.c 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,40 @@ +#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 \n", argv[0]); + exit(EXIT_FAILURE); + } + + struct passwd *pwd = getpwnam(argv[1]); + if (pwd == NULL) { + printf("'%s' not found\n", argv[1]); + exit(EXIT_FAILURE); + } + + printf("Before: "); + display(); + + 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-health/health snapd-2.42.1/tests/lib/snaps/test-snapd-health/health --- snapd-2.40/tests/lib/snaps/test-snapd-health/health 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-health/health 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,3 @@ +#!/bin/sh + +snapctl set-health "$@" diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-health/meta/hooks/check-health snapd-2.42.1/tests/lib/snaps/test-snapd-health/meta/hooks/check-health --- snapd-2.40/tests/lib/snaps/test-snapd-health/meta/hooks/check-health 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-health/meta/hooks/check-health 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,3 @@ +#!/bin/sh + +snapctl set-health okay diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-health/meta/hooks/configure snapd-2.42.1/tests/lib/snaps/test-snapd-health/meta/hooks/configure --- snapd-2.40/tests/lib/snaps/test-snapd-health/meta/hooks/configure 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-health/meta/hooks/configure 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,9 @@ +#!/bin/sh + +h=$(snapctl get force-health) +if [ "$h" ]; then + # shellcheck disable=SC2086 + snapctl set-health $h +fi +snapctl set force-health="" + diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-health/meta/snap.yaml snapd-2.42.1/tests/lib/snaps/test-snapd-health/meta/snap.yaml --- snapd-2.40/tests/lib/snaps/test-snapd-health/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-health/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,5 @@ +name: test-snapd-health +version: 1 +apps: + test-snapd-health: + command: health diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-icon-theme/bin/echo snapd-2.42.1/tests/lib/snaps/test-snapd-icon-theme/bin/echo --- snapd-2.40/tests/lib/snaps/test-snapd-icon-theme/bin/echo 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-icon-theme/bin/echo 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,2 @@ +#!/bin/sh +echo "From test-snapd-icon-theme snap" diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-icon-theme/meta/gui/echo.desktop snapd-2.42.1/tests/lib/snaps/test-snapd-icon-theme/meta/gui/echo.desktop --- snapd-2.40/tests/lib/snaps/test-snapd-icon-theme/meta/gui/echo.desktop 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-icon-theme/meta/gui/echo.desktop 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,5 @@ +[Desktop Entry] +Type=Application +Name=Echo +Exec=test-snapd-icon-theme.echo +Icon=snap.test-snapd-icon-theme.foo diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-icon-theme/meta/gui/icons/hicolor/scalable/apps/snap.test-snapd-icon-theme.foo.svg snapd-2.42.1/tests/lib/snaps/test-snapd-icon-theme/meta/gui/icons/hicolor/scalable/apps/snap.test-snapd-icon-theme.foo.svg --- snapd-2.40/tests/lib/snaps/test-snapd-icon-theme/meta/gui/icons/hicolor/scalable/apps/snap.test-snapd-icon-theme.foo.svg 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-icon-theme/meta/gui/icons/hicolor/scalable/apps/snap.test-snapd-icon-theme.foo.svg 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,5 @@ + + + + + diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-icon-theme/meta/snap.yaml snapd-2.42.1/tests/lib/snaps/test-snapd-icon-theme/meta/snap.yaml --- snapd-2.40/tests/lib/snaps/test-snapd-icon-theme/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-icon-theme/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,6 @@ +name: test-snapd-icon-theme +version: 1.0 + +apps: + echo: + command: bin/echo diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-illegal-system-username/bin/sh snapd-2.42.1/tests/lib/snaps/test-snapd-illegal-system-username/bin/sh --- snapd-2.40/tests/lib/snaps/test-snapd-illegal-system-username/bin/sh 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-illegal-system-username/bin/sh 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,3 @@ +#!/bin/sh + +exec /bin/sh "$@" diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-illegal-system-username/meta/snap.yaml snapd-2.42.1/tests/lib/snaps/test-snapd-illegal-system-username/meta/snap.yaml --- snapd-2.40/tests/lib/snaps/test-snapd-illegal-system-username/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-illegal-system-username/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,10 @@ +name: test-snapd-illegal-system-username +summary: Snap specifying an illegal username to system-usernames +version: 1.0 +system-usernames: + daemon: shared + snap_daemon: shared + +apps: + test-snapd-illegal-system-username: + command: bin/sh diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-packagekit/snapcraft.yaml snapd-2.42.1/tests/lib/snaps/test-snapd-packagekit/snapcraft.yaml --- snapd-2.40/tests/lib/snaps/test-snapd-packagekit/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-packagekit/snapcraft.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,31 @@ +name: test-snapd-packagekit +base: core18 +version: '1.1.9' +license: GPL-2.0+ +summary: utilities to test packagekit-control interface of snapd +description: | + This package contains the command line utilities associated with the + PackageKit daemon. + +grade: stable +confinement: strict + +apps: + pkcon: + command: usr/bin/pkcon + plugs: + - packagekit-control + pkmon: + command: usr/bin/pkmon + plugs: + - packagekit-control + +parts: + packagekit-tools: + plugin: nil + stage-packages: + - packagekit-tools + stage: + - usr/bin/pkcon + - usr/bin/pkmon + - usr/lib/*/libpackagekit-glib2.so* diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml snapd-2.42.1/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml --- snapd-2.40/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-policy-app-consumer/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -14,6 +14,15 @@ alsa: command: bin/run plugs: [ alsa ] + audio-playback: + command: bin/run + plugs: [ audio-playback ] + audio-record: + command: bin/run + plugs: [ audio-record ] + appstream-metadata: + command: bin/run + plugs: [ appstream-metadata ] autopilot-introspection: command: bin/run plugs: [ autopilot-introspection ] @@ -128,6 +137,9 @@ gpio-memory-control: command: bin/run plugs: [ gpio-memory-control ] + gpio-control: + command: bin/run + plugs: [ gpio-control ] greengrass-support: command: bin/run plugs: [ greengrass-support ] @@ -272,6 +284,9 @@ optical-drive: command: bin/run plugs: [ optical-drive ] + packagekit-control: + command: bin/run + plugs: [ packagekit-control ] password-manager-service: command: bin/run plugs: [ password-manager-service ] diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-policy-app-provider-core/meta/snap.yaml snapd-2.42.1/tests/lib/snaps/test-snapd-policy-app-provider-core/meta/snap.yaml --- snapd-2.40/tests/lib/snaps/test-snapd-policy-app-provider-core/meta/snap.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-policy-app-provider-core/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -5,6 +5,8 @@ confinement: strict slots: + audio-playback: null + audio-record: null avahi-control: null avahi-observe: null bluez: null @@ -48,6 +50,12 @@ x11: null apps: + audio-playback: + command: bin/run + slots: [ audio-playback ] + audio-record: + command: bin/run + slots: [ audio-record ] avahi-control: command: bin/run slots: [ avahi-control ] diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-sh/meta/snap.yaml snapd-2.42.1/tests/lib/snaps/test-snapd-sh/meta/snap.yaml --- snapd-2.40/tests/lib/snaps/test-snapd-sh/meta/snap.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-sh/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -13,6 +13,9 @@ apps: test-snapd-sh: command: bin/sh + with-appstream-metadata-plug: + command: bin/sh + plugs: [appstream-metadata] with-broadcom-asic-control-plug: command: bin/sh plugs: [broadcom-asic-control] diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-svcs-disable-install-hook/meta/hooks/install snapd-2.42.1/tests/lib/snaps/test-snapd-svcs-disable-install-hook/meta/hooks/install --- snapd-2.40/tests/lib/snaps/test-snapd-svcs-disable-install-hook/meta/hooks/install 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-svcs-disable-install-hook/meta/hooks/install 2019-10-30 12:17:43.000000000 +0000 @@ -2,5 +2,5 @@ for service in simple forking; do echo "Disabling $service service from install hook" - snapctl stop --disable "test-snapd-svcs-disable-install-hook.$service" + snapctl stop --disable "$SNAP_NAME.$service" done diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/bin/forking.sh snapd-2.42.1/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/bin/forking.sh --- snapd-2.40/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/bin/forking.sh 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/bin/forking.sh 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +sleep 60 & diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/bin/simple.sh snapd-2.42.1/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/bin/simple.sh --- snapd-2.40/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/bin/simple.sh 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/bin/simple.sh 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,3 @@ +#!/bin/bash -e + +sleep 60 diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/meta/hooks/post-refresh snapd-2.42.1/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/meta/hooks/post-refresh --- snapd-2.40/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/meta/hooks/post-refresh 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/meta/hooks/post-refresh 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,6 @@ +#!/bin/bash -e + +for service in simple forking; do + echo "Disabling $service service from install hook" + snapctl stop --disable "$SNAP_NAME.$service" +done diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/meta/snap.yaml snapd-2.42.1/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/meta/snap.yaml --- snapd-2.40/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-svcs-disable-refresh-hook/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,9 @@ +name: test-snapd-svcs-disable-refresh-hook +version: 1.0 +apps: + simple: + daemon: simple + command: bin/simple.sh + forking: + daemon: forking + command: bin/forking.sh diff -Nru snapd-2.40/tests/lib/snaps/test-snapd-with-configure-core18/snapcraft.yaml snapd-2.42.1/tests/lib/snaps/test-snapd-with-configure-core18/snapcraft.yaml --- snapd-2.40/tests/lib/snaps/test-snapd-with-configure-core18/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps/test-snapd-with-configure-core18/snapcraft.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,10 @@ +name: test-snapd-with-configure-core18 +version: '1.0' +summary: Basic snap with a configure hook for core18 +description: A basic snap with a passthrough configure hook for core18 +base: core18 + +parts: + copy: + plugin: dump + source: . diff -Nru snapd-2.40/tests/lib/snaps.sh snapd-2.42.1/tests/lib/snaps.sh --- snapd-2.40/tests/lib/snaps.sh 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/lib/snaps.sh 2019-10-30 12:17:43.000000000 +0000 @@ -72,21 +72,8 @@ } is_classic_confinement_supported() { - case "$SPREAD_SYSTEM" in - ubuntu-core-*) - return 1 - ;; - ubuntu-*|debian-*) - return 0 - ;; - fedora-*|centos-*) - return 1 - ;; - opensuse-*) - return 0 - ;; - *) - return 0 - ;; - esac + if snap debug sandbox-features --required=confinement-options:classic; then + return 0 + fi + return 1 } diff -Nru snapd-2.40/tests/lib/state.sh snapd-2.42.1/tests/lib/state.sh --- snapd-2.40/tests/lib/state.sh 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/lib/state.sh 2019-10-30 12:17:43.000000000 +0000 @@ -44,6 +44,7 @@ # Copy the state preserving the timestamps cp -a /var/lib/snapd "$SNAPD_STATE_PATH"/snapd-lib + cp -rf /var/cache/snapd "$SNAPD_STATE_PATH"/snapd-cache cp -rf "$boot_path" "$SNAPD_STATE_PATH"/boot cp -f /etc/systemd/system/snap-*core*.mount "$SNAPD_STATE_PATH"/system-units else @@ -59,6 +60,7 @@ # shellcheck disable=SC2086 tar cf "$SNAPD_STATE_FILE" \ /var/lib/snapd \ + /var/cache/snapd \ "$SNAP_MOUNT_DIR" \ /etc/systemd/system/"$escaped_snap_mount_dir"-*core*.mount \ /etc/systemd/system/multi-user.target.wants/"$escaped_snap_mount_dir"-*core*.mount \ @@ -89,6 +91,7 @@ test -n "$boot_path" || return 1 restore_snapd_lib + cp -rf "$SNAPD_STATE_PATH"/snapd-cache/* /var/cache/snapd cp -rf "$SNAPD_STATE_PATH"/boot/* "$boot_path" cp -f "$SNAPD_STATE_PATH"/system-units/* /etc/systemd/system else @@ -108,14 +111,14 @@ } restore_snapd_lib() { - # Clean all the state but the snaps and seed dirs. Then make a selective clean for + # Clean all the state but the snaps and seed dirs. Then make a selective clean for # snaps and seed dirs leaving the .snap files which then are going to be synchronized. find /var/lib/snapd/* -maxdepth 0 ! \( -name 'snaps' -o -name 'seed' -o -name 'cache' \) -exec rm -rf {} \; # Copy the whole state but the snaps, seed and cache dirs find "$SNAPD_STATE_PATH"/snapd-lib/* -maxdepth 0 ! \( -name 'snaps' -o -name 'seed' -o -name 'cache' \) -exec cp -rf {} /var/lib/snapd \; - # Synchronize snaps, seed and cache directories. The this is done separately in order to avoid copying + # Synchronize snaps, seed and cache directories. The this is done separately in order to avoid copying # the snap files due to it is a heavy task and take most of the time of the restore phase. rsync -av --delete "$SNAPD_STATE_PATH"/snapd-lib/snaps /var/lib/snapd rsync -av --delete "$SNAPD_STATE_PATH"/snapd-lib/seed /var/lib/snapd diff -Nru snapd-2.40/tests/lib/store.sh snapd-2.42.1/tests/lib/store.sh --- snapd-2.40/tests/lib/store.sh 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/lib/store.sh 2019-10-30 12:17:43.000000000 +0000 @@ -36,20 +36,37 @@ local dir="$1" shift - fakestore make-refreshable --dir "$dir" "$@" + fakestore make-refreshable --dir "$dir" "$@" } make_snap_installable(){ local dir="$1" local snap_path="$2" + new_snap_declaration "$dir" "$snap_path" + new_snap_revision "$dir" "$snap_path" +} + +new_snap_declaration(){ + local dir="$1" + local snap_path="$2" + shift 2 + cp -a "$snap_path" "$dir" - p=$(fakestore new-snap-declaration --dir "$dir" "${snap_path}") + p=$(fakestore new-snap-declaration --dir "$dir" "$@" "${snap_path}" ) snap ack "$p" - p=$(fakestore new-snap-revision --dir "$dir" "${snap_path}") +} + +new_snap_revision(){ + local dir="$1" + local snap_path="$2" + shift 2 + + p=$(fakestore new-snap-revision --dir "$dir" "$@" "${snap_path}") snap ack "$p" } + setup_fake_store(){ # before switching make sure we have a session macaroon snap find test-snapd-tools diff -Nru snapd-2.40/tests/lib/systemd.sh snapd-2.42.1/tests/lib/systemd.sh --- snapd-2.40/tests/lib/systemd.sh 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/lib/systemd.sh 2019-10-30 12:17:43.000000000 +0000 @@ -52,7 +52,7 @@ return fi # show debug output every 1min - if [ $(( i % 60 )) = 0 ]; then + if [ "$i" -gt 0 ] && [ $(( i % 60 )) = 0 ]; then systemctl status "$service_name" || true; fi sleep 1; @@ -67,10 +67,10 @@ if systemctl is-active "$unit"; then echo "Ensure the service is active before stopping it" retries=20 - systemctl status "$unit" || true - while systemctl status "$unit" | grep "Active: activating"; do + while systemctl status "$unit" | grep -q "Active: activating"; do if [ $retries -eq 0 ]; then echo "$unit unit not active" + systemctl status "$unit" || true exit 1 fi retries=$(( retries - 1 )) diff -Nru snapd-2.40/tests/lib/systems.sh snapd-2.42.1/tests/lib/systems.sh --- snapd-2.40/tests/lib/systems.sh 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/lib/systems.sh 2019-10-30 12:17:43.000000000 +0000 @@ -14,6 +14,13 @@ return 1 } +is_core20_system(){ + if [[ "$SPREAD_SYSTEM" == ubuntu-core-20-* ]]; then + return 0 + fi + return 1 +} + is_classic_system(){ if [[ "$SPREAD_SYSTEM" != ubuntu-core-* ]]; then return 0 @@ -21,9 +28,40 @@ return 1 } + is_ubuntu_14_system(){ if [[ "$SPREAD_SYSTEM" == ubuntu-14.04-* ]]; then return 0 fi return 1 } + +get_snap_for_system(){ + local snap=$1 + + case "$SPREAD_SYSTEM" in + ubuntu-core-18-*) + echo "${snap}-core18" + ;; + ubuntu-core-20-*) + echo "${snap}-core20" + ;; + *) + echo "$snap" + ;; + esac +} + +get_core_for_system(){ + case "$SPREAD_SYSTEM" in + ubuntu-core-18-*) + echo "core18" + ;; + ubuntu-core-20-*) + echo "core20" + ;; + *) + echo "core" + ;; + esac +} diff -Nru snapd-2.40/tests/main/apt-hooks/task.yaml snapd-2.42.1/tests/main/apt-hooks/task.yaml --- snapd-2.40/tests/main/apt-hooks/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/apt-hooks/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,7 +1,7 @@ summary: Ensure apt hooks work # apt hook only available on 18.04+ and aws-cli only for amd64 -systems: [ubuntu-18.04-64, ubuntu-18.10-64] +systems: [ubuntu-18.04-64, ubuntu-19.04-64, ubuntu-19.10-64] debug: | ls -lh /var/cache/snapd diff -Nru snapd-2.40/tests/main/auto-refresh/task.yaml snapd-2.42.1/tests/main/auto-refresh/task.yaml --- snapd-2.40/tests/main/auto-refresh/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/auto-refresh/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,12 +1,5 @@ summary: Check that auto-refresh works -# FIXME: core18 right now does not have a canonical model. This means -# that it will not get a serial. We code in devicestate.go that will only -# try to auto-refresh after the systems has tried at least 3 times to get -# a serial. But this means this test will timeout because the first retry -# for the serial happens after 5min, then 10min, then 20min. -systems: [-ubuntu-core-18-*] - environment: SNAP_NAME/regular: test-snapd-tools SNAP_NAME/parallel: test-snapd-tools_instance diff -Nru snapd-2.40/tests/main/auto-refresh-retry/task.yaml snapd-2.42.1/tests/main/auto-refresh-retry/task.yaml --- snapd-2.40/tests/main/auto-refresh-retry/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/auto-refresh-retry/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -8,6 +8,7 @@ restore: | rm -f /etc/systemd/system/snapd.service.d/override.conf ip netns delete testns || true + umount /run/netns || true debug: | systemctl cat snapd.service diff -Nru snapd-2.40/tests/main/base-migration/task.yaml snapd-2.42.1/tests/main/base-migration/task.yaml --- snapd-2.40/tests/main/base-migration/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/base-migration/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,15 +1,22 @@ -summary: a snap migrates from base "core" to "core18" +summary: A snap migrates from base "core" to "core18" + prepare: | snap install core18 - snap pack test-snapd-core-migration.base-core - snap pack test-snapd-core-migration.base-core18 + snap pack "$TESTSLIB/snaps/test-snapd-core-migration.base-core" + snap pack "$TESTSLIB/snaps/test-snapd-core-migration.base-core18" + execute: | # When we install a snap that is using (implicitly) base: core, then that # snap runs on top of the core16 runtime environment. This can be seen by # looking at the os-release file which will match that of ubuntu core 16. snap install --dangerous test-snapd-core-migration_1_all.snap - test-snapd-core-migration.sh -c "cat /usr/lib/os-release" | MATCH 'VERSION_ID="16"' + su test -c 'snap run test-snapd-core-migration.sh -c "cat /usr/lib/os-release"' | MATCH 'VERSION_ID="16"' + + # The mount namespace information file is owned by root.root and has mode 0644 + # even though the user invoking the snap command was non-root. MATCH 'base-snap-name=core' < /run/snapd/ns/snap.test-snapd-core-migration.info + test "$(stat -c '%u.%g' /run/snapd/ns/snap.test-snapd-core-migration.info)" = 0.0 + test "$(stat -c '%a' /run/snapd/ns/snap.test-snapd-core-migration.info)" = 644 # When said snap is refreshed to use "base: core18" then, because there are # no active processes in that snap, the base will change correctly to core18. @@ -44,6 +51,10 @@ # Nothing changes after the background app terminates. test-snapd-core-migration.sh -c "cat /usr/lib/os-release" | MATCH 'VERSION_ID="18"' MATCH 'base-snap-name=core18' < /run/snapd/ns/snap.test-snapd-core-migration.info + restore: | - snap remove test-snapd-core-migration + + if snap list test-snapd-core-migration; then + snap remove test-snapd-core-migration + fi rm -f test-snapd-core-migration_{1,2}_all.snap diff -Nru snapd-2.40/tests/main/base-migration/test-snapd-core-migration.base-core/bin/sh snapd-2.42.1/tests/main/base-migration/test-snapd-core-migration.base-core/bin/sh --- snapd-2.40/tests/main/base-migration/test-snapd-core-migration.base-core/bin/sh 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/base-migration/test-snapd-core-migration.base-core/bin/sh 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -#!/bin/sh -exec /bin/sh "$@" diff -Nru snapd-2.40/tests/main/base-migration/test-snapd-core-migration.base-core/meta/snap.yaml snapd-2.42.1/tests/main/base-migration/test-snapd-core-migration.base-core/meta/snap.yaml --- snapd-2.40/tests/main/base-migration/test-snapd-core-migration.base-core/meta/snap.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/base-migration/test-snapd-core-migration.base-core/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -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/main/base-migration/test-snapd-core-migration.base-core18/bin/sh snapd-2.42.1/tests/main/base-migration/test-snapd-core-migration.base-core18/bin/sh --- snapd-2.40/tests/main/base-migration/test-snapd-core-migration.base-core18/bin/sh 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/base-migration/test-snapd-core-migration.base-core18/bin/sh 1970-01-01 00:00:00.000000000 +0000 @@ -1,2 +0,0 @@ -#!/bin/sh -exec /bin/sh "$@" diff -Nru snapd-2.40/tests/main/base-migration/test-snapd-core-migration.base-core18/meta/snap.yaml snapd-2.42.1/tests/main/base-migration/test-snapd-core-migration.base-core18/meta/snap.yaml --- snapd-2.40/tests/main/base-migration/test-snapd-core-migration.base-core18/meta/snap.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/base-migration/test-snapd-core-migration.base-core18/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,6 +0,0 @@ -name: test-snapd-core-migration -version: 2 -base: core18 -apps: - sh: - command: bin/sh diff -Nru snapd-2.40/tests/main/cgroup-devices/task.sh snapd-2.42.1/tests/main/cgroup-devices/task.sh --- snapd-2.40/tests/main/cgroup-devices/task.sh 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/cgroup-devices/task.sh 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,93 @@ +#!/bin/sh -ex +echo "Install a test service" +snap pack test-snapd-service + +# Because this service is of type "simple" it is considered "ready" instantly. +# In reality the process needs to go through snap "run" chain to be really +# ready. As a workaround, touch a "remove-me" file that is removed by the +# service on startup, restart the service and the wait for the file to +# disappear. +mkdir -p /var/snap/test-snapd-service/common +touch /var/snap/test-snapd-service/common/remove-me +snap install --dangerous ./test-snapd-service_1.0_all.snap +# Wait for the service to really be alive and running. Otherwise the "main pid" +# will be still tracking snap-run-confine-exec chain and be unreliable. +for _ in $(seq 5); do + if [ ! -e /var/snap/test-snapd-service/common/remove-me ]; then + break + fi + sleep 1 +done + +echo "Extract the PID of the main process tracked by systemd" +# It would be nicer to use "systemctl show --property=... --value" but it doesn't work on older systemd. +pid=$(systemctl show snap.test-snapd-service.test-snapd-service.service --property=ExecMainPID | cut -d = -f 2) + +echo "Extract the device cgroup of the main process" +initial_device_cgroup=$(grep devices < "/proc/$pid/cgroup" | cut -d : -f 3) + +# Initially, because there are no udev tags corresponding to this application, +# snap-confine is not moving the process to a new cgroup. As such the service +# runs in the cgroup created by systemd, which varies across version of systemd. +case "$initial_device_cgroup" in + /) + ;; + /system.slice) + ;; + /system.slice/snap.test-snapd-service.test-snapd-service.service) + ;; + *) + echo "Unexpected initial device cgroup: $initial_device_cgroup" + exit 1 +esac + +echo "Ensure that the claim of the process is consistent with the claim of the cgroup" +# This is just a sanity check. +MATCH "$pid" < "/sys/fs/cgroup/devices/$initial_device_cgroup/cgroup.procs" + +echo "Verify the constraints imposed by the device cgroup made by systemd" +# This may change over time as it is governed by systemd. +test 'a *:* rwm' = "$(cat "/sys/fs/cgroup/devices/$initial_device_cgroup/devices.list")" + +echo "Connect the joystick interface" +snap connect test-snapd-service:joystick + +echo "Refresh the value of the main pid and the effective device cgroup after snap connect" +# NOTE: As of snapd 2.40 the PID and cgroup are expected to be the same as before. +pid_check=$(systemctl show snap.test-snapd-service.test-snapd-service.service --property=ExecMainPID | cut -d = -f 2) +test "$pid" -eq "$pid_check" +updated_device_cgroup=$(grep devices < "/proc/$pid/cgroup" | cut -d : -f 3) + +echo "Verify that the main process is still in the systemd-made cgroup" +test "$updated_device_cgroup" = "$initial_device_cgroup" + +echo "Verify the constraints imposed by the device cgroup made by systemd" +test 'a *:* rwm' = "$(cat "/sys/fs/cgroup/devices/$updated_device_cgroup/devices.list")" + +echo "Run /bin/true via snap-confine, so that we create the device cgroup made by snapd" +snap run --shell test-snapd-service -c /bin/true + +echo "Verify the constraints imposed by the device cgroup made by snapd" +# NOTE: the actual permissions may drift over time. We just care about the fact +# that there *are* some constraints here now and there were none before. +test 'c 1:3 rwm' = "$(head -n 1 "/sys/fs/cgroup/devices/snap.test-snapd-service.test-snapd-service/devices.list")" +# The device cgroup made by snapd is, currently, still empty. +test -z "$(cat "/sys/fs/cgroup/devices/snap.test-snapd-service.test-snapd-service/cgroup.procs")" + +echo "Restart the test service" +# See the comment for the similar code above. +touch /var/snap/test-snapd-service/common/remove-me +systemctl restart snap.test-snapd-service.test-snapd-service.service +for _ in $(seq 5); do + if [ ! -e /var/snap/test-snapd-service/common/remove-me ]; then + break + fi + sleep 1 +done + +echo "Refresh the value of the main pid and the effective device cgroup after service restart" +pid=$(systemctl show snap.test-snapd-service.test-snapd-service.service --property=ExecMainPID | cut -d = -f 2) +final_device_cgroup=$(grep devices < "/proc/$pid/cgroup" | cut -d : -f 3) + +echo "Verify that the main process is now in the snapd-made cgroup" +test "$final_device_cgroup" = /snap.test-snapd-service.test-snapd-service diff -Nru snapd-2.40/tests/main/cgroup-devices/task.yaml snapd-2.42.1/tests/main/cgroup-devices/task.yaml --- snapd-2.40/tests/main/cgroup-devices/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/cgroup-devices/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,5 @@ +summary: measuring basic properties of device cgroup +execute: ./task.sh +restore: | + rm -f test-snapd-service_1.0_all.snap + snap remove test-snapd-service diff -Nru snapd-2.40/tests/main/cgroup-devices/test-snapd-service/bin/service snapd-2.42.1/tests/main/cgroup-devices/test-snapd-service/bin/service --- snapd-2.40/tests/main/cgroup-devices/test-snapd-service/bin/service 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/cgroup-devices/test-snapd-service/bin/service 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,6 @@ +#!/bin/sh +rm -f "$SNAP_COMMON/remove-me" +while true; do + echo "running" + sleep 10 +done diff -Nru snapd-2.40/tests/main/cgroup-devices/test-snapd-service/meta/snap.yaml snapd-2.42.1/tests/main/cgroup-devices/test-snapd-service/meta/snap.yaml --- snapd-2.40/tests/main/cgroup-devices/test-snapd-service/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/cgroup-devices/test-snapd-service/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,7 @@ +name: test-snapd-service +version: 1.0 +apps: + test-snapd-service: + command: bin/service + daemon: simple + plugs: [joystick] diff -Nru snapd-2.40/tests/main/classic-confinement-not-supported/task.yaml snapd-2.42.1/tests/main/classic-confinement-not-supported/task.yaml --- snapd-2.40/tests/main/classic-confinement-not-supported/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/classic-confinement-not-supported/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -3,7 +3,7 @@ environment: CLASSIC_SNAP: test-snapd-classic-confinement -systems: [ubuntu-core-16-*, fedora-*] +systems: [ubuntu-core-1*, fedora-*] prepare: | #shellcheck source=tests/lib/snaps.sh diff -Nru snapd-2.40/tests/main/classic-custom-device-reg/task.yaml snapd-2.42.1/tests/main/classic-custom-device-reg/task.yaml --- snapd-2.40/tests/main/classic-custom-device-reg/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/classic-custom-device-reg/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,12 +1,12 @@ summary: | - Test gadget customized device initialisation and registration also on classic + Test gadget customized device initialisation and registration also on classic systems: [-ubuntu-core-*] environment: SEED_DIR: /var/lib/snapd/seed -kill-timeout: 3m +kill-timeout: 5m prepare: | if [ "$TRUST_TEST_KEYS" = "false" ]; then diff -Nru snapd-2.40/tests/main/classic-firstboot/task.yaml snapd-2.42.1/tests/main/classic-firstboot/task.yaml --- snapd-2.40/tests/main/classic-firstboot/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/classic-firstboot/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -12,6 +12,7 @@ fi snap pack "$TESTSLIB/snaps/basic" + snap pack "$TESTSLIB/snaps/test-snapd-service" snap download "--$CORE_CHANNEL" core "$TESTSLIB/reset.sh" --keep-stopped @@ -25,6 +26,9 @@ - name: basic unasserted: true file: basic.snap + - name: test-snapd-service + unasserted: true + file: test-snapd-service.snap EOF echo Copy the needed assertions to /var/lib/snapd/ @@ -36,6 +40,7 @@ echo "Copy the needed snaps to $SEED_DIR/snaps" cp ./core_*.snap "$SEED_DIR/snaps/core.snap" cp ./basic_1.0_all.snap "$SEED_DIR/snaps/basic.snap" + cp ./test-snapd-service_1.0_all.snap "$SEED_DIR/snaps/test-snapd-service.snap" restore: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -77,4 +82,24 @@ fi snap list | MATCH "^basic" + snap list | MATCH "^test-snapd-service" test -f "$SEED_DIR/snaps/basic.snap" + test -f "$SEED_DIR/snaps/test-snapd-service.snap" + + echo "Checking that service restart happened before mark-seeded" + # sanity check + snap change 1 | MATCH "Mark system seeded" + snap change 1 | MATCH "restart of .*test-snapd-service.test-snapd-other-service" + snap install jq + # extract task ids from state + # XXX: snap change output cannot be used for that; ideally we would have some sort of 'snap debug ..' command. + MARKSEEDED_WAIT=$(jq -r '.tasks[] | select (.kind == "mark-seeded") | ."wait-tasks"' < /var/lib/snapd/state.json) + SERVICECMD_TASK=$(jq -r '.tasks[] | select (.kind == "exec-command") | .id' < /var/lib/snapd/state.json) + if test -z "$SERVICECMD_TASK" ; then + echo "Could not find exec-command task" + exit 1 + fi + if ! echo "$MARKSEEDED_WAIT" | MATCH "$SERVICECMD_TASK" ; then + echo "Expected mark-seeded task to wait for exec-command" + exit 1 + fi diff -Nru snapd-2.40/tests/main/classic-prepare-image/task.yaml snapd-2.42.1/tests/main/classic-prepare-image/task.yaml --- snapd-2.40/tests/main/classic-prepare-image/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/classic-prepare-image/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -10,7 +10,7 @@ STORE_ADDR: localhost:11028 SEED_DIR: /var/lib/snapd/seed -kill-timeout: 3m +kill-timeout: 5m prepare: | if [ "$TRUST_TEST_KEYS" = "false" ]; then diff -Nru snapd-2.40/tests/main/classic-prepare-image-no-core/task.yaml snapd-2.42.1/tests/main/classic-prepare-image-no-core/task.yaml --- snapd-2.40/tests/main/classic-prepare-image-no-core/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/classic-prepare-image-no-core/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,98 @@ +summary: Check that prepare-image --classic works. + +systems: [-ubuntu-core-*, -fedora-*, -opensuse-*, -arch-*, -amazon-*, -centos-*] + +backends: [-autopkgtest] + +environment: + ROOT: /tmp/root + STORE_DIR: $(pwd)/fake-store-blobdir + STORE_ADDR: localhost:11028 + SEED_DIR: /var/lib/snapd/seed + +kill-timeout: 5m + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + setup_fake_store "$STORE_DIR" + + snap pack "$TESTSLIB/snaps/basic18" + snap pack "$TESTSLIB/snaps/classic-gadget-18" + + echo Expose the needed assertions through the fakestore + cp "$TESTSLIB"/assertions/developer1.account "$STORE_DIR/asserts" + cp "$TESTSLIB"/assertions/developer1.account-key "$STORE_DIR/asserts" + # have snap use the fakestore for assertions (but nothing else) + export SNAPPY_FORCE_SAS_URL=http://$STORE_ADDR + + echo Running prepare-image + #shellcheck disable=SC2086 + ARCH="$(dpkg-architecture -qDEB_HOST_ARCH)" + su -c "SNAPPY_USE_STAGING_STORE=$SNAPPY_USE_STAGING_STORE snap prepare-image --classic --arch $ARCH --channel $CORE_CHANNEL --snap basic18_*.snap --snap classic-gadget-18_*.snap $TESTSLIB/assertions/developer1-my-classic-w-gadget-18.model $ROOT" + + "$TESTSLIB/reset.sh" --keep-stopped + cp -ar "$ROOT/$SEED_DIR" "$SEED_DIR" + + # start fake device svc + systemd_create_and_start_unit fakedevicesvc "$(command -v fakedevicesvc) localhost:11029" + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + #shellcheck source=tests/lib/systemd.sh + . "$TESTSLIB/systemd.sh" + systemctl stop snapd.service snapd.socket + systemd_stop_and_destroy_unit fakedevicesvc + + rm -rf "$SEED_DIR" + systemctl start snapd.socket snapd.service + + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$STORE_DIR" + rm -f -- *.snap + rm -rf "$ROOT" + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + # kick seeding + systemctl start snapd.service snapd.socket + + echo "Wait for seeding to be done" + snap wait system seed.loaded + + echo "We have a model assertion" + snap known model|MATCH "model: my-classic-w-gadget-18" + + echo "Wait for device initialisation to be done" + while ! snap changes | grep -q "Done.*Initialize device"; do sleep 1; done + + echo "Check we have a serial" + snap known serial|MATCH "authority-id: developer1" + snap known serial|MATCH "brand-id: developer1" + snap known serial|MATCH "model: my-classic-w-gadget-18" + snap known serial|MATCH "serial: 7777" + + snap list | MATCH "^basic18" + test -f "$SEED_DIR/snaps/basic18_"*.snap + snap list | MATCH "^classic-gadget-18" + test -f "$SEED_DIR/snaps/classic-gadget-18_"*.snap + snap list | MATCH "^core18" + test -f "$SEED_DIR/snaps/core18_"*.snap + if snap list |MATCH "^core " ; then + echo "Should not have needed or installed core" + exit 1 + fi diff -Nru snapd-2.40/tests/main/classic-snapd-firstboot/task.yaml snapd-2.42.1/tests/main/classic-snapd-firstboot/task.yaml --- snapd-2.40/tests/main/classic-snapd-firstboot/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/classic-snapd-firstboot/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,86 @@ +summary: Check that firstboot assertions are imported and snaps installed also on classic + +systems: [-ubuntu-core-*] + +environment: + SEED_DIR: /var/lib/snapd/seed + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + snap pack "$TESTSLIB/snaps/basic18" + snap download "--$SNAPD_CHANNEL" snapd + snap download core18 + + "$TESTSLIB/reset.sh" --keep-stopped + mkdir -p "$SEED_DIR/snaps" + mkdir -p "$SEED_DIR/assertions" + cat > "$SEED_DIR/seed.yaml" </dev/null |grep -q -E "^basic18" ; then + break + fi + sleep 1 + done + + echo "Ensure snapd.seeded.service is active" + systemctl status snapd.seeded.service + + echo "Verifying the imported assertions" + if ! snap known model | MATCH "model: my-classic" ; then + echo "Model assertion was not imported on firstboot" + exit 1 + fi + + snap list | MATCH "^basic18" + test -f "$SEED_DIR/snaps/basic18.snap" diff -Nru snapd-2.40/tests/main/classic-ubuntu-core-transition/task.yaml snapd-2.42.1/tests/main/classic-ubuntu-core-transition/task.yaml --- snapd-2.40/tests/main/classic-ubuntu-core-transition/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/classic-ubuntu-core-transition/task.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,128 +0,0 @@ -summary: Ensure that the ubuntu-core -> core transition works - -# we never test on core because the transition can only happen on "classic" we -# disable on ppc64el because the downloads are very slow there Fedora, openSUSE, -# Arch, CentOS are disabled at the moment as there is something fishy going on -# and the snapd service gets terminated during the process. -systems: [-ubuntu-core-*, -ubuntu-*-ppc64el, -fedora-*, -opensuse-*, -ubuntu-*-i386, -arch-*, -amazon-*, -centos-*, -debian-sid-*] - -# autopkgtest run only a subset of tests that deals with the integration -# with the distro -backends: [-autopkgtest] - -warn-timeout: 1m -kill-timeout: 5m - -debug: | - snap list || true - snap info core || true - snap info ubuntu-core || true - snap changes - #shellcheck source=tests/lib/changes.sh - . "$TESTSLIB/changes.sh" - snap change "$(change_id 'Transition ubuntu-core to core')" || true - -execute: | - #shellcheck source=tests/lib/pkgdb.sh - . "$TESTSLIB/pkgdb.sh" - #shellcheck source=tests/lib/systemd.sh - . "$TESTSLIB"/systemd.sh - curl() { - local url="$1" - # sadly systemd active means not that its really ready so we wait - # here for the socket to be available - while ! ss -t -l -n|grep :80; do - ss -l -l -n - sleep 1 - done - python3 -c "import urllib.request; print(urllib.request.urlopen(\"$url\").read().decode(\"utf-8\"))" - } - - #shellcheck source=tests/lib/pkgdb.sh - . "$TESTSLIB/pkgdb.sh" - echo "Ensure core is gone and we have ubuntu-core instead" - distro_purge_package snapd - distro_install_build_snapd - - # need to be seeded to allow snap install - snap wait system seed.loaded - - # modify daemon state to set ubuntu-core-transition-last-retry-time to the - # current time to prevent the ubuntu-core transition before the test snap is - # installed - systemctl stop snapd.{service,socket} - now="$(date --utc -Ins)" - jq -c '. + {data: (.data + {"ubuntu-core-transition-last-retry-time": "'"$now"'"})}' < /var/lib/snapd/state.json > state.json.new - mv state.json.new /var/lib/snapd/state.json - systemctl start snapd.{service,socket} - - snap download "--${CORE_CHANNEL}" ubuntu-core - snap ack ./ubuntu-core_*.assert - snap install ./ubuntu-core_*.snap - - snap install test-snapd-python-webserver - snap interfaces -i network | MATCH ":network +test-snapd-python-webserver" - snap interfaces -i network-bind | MATCH ":network-bind +.*test-snapd-python-webserver" - - echo "Ensure the webserver is working" - wait_for_service snap.test-snapd-python-webserver.test-snapd-python-webserver - curl http://localhost | MATCH "XKCD rocks" - - # restore ubuntu-core-transition-last-retry-time to its previous value and restart the daemon - systemctl stop snapd.{service,socket} - jq -c 'del(.["data"]["ubuntu-core-transition-last-retry-time"])' < /var/lib/snapd/state.json > state.json.new - mv state.json.new /var/lib/snapd/state.json - systemctl start snapd.{service,socket} - - echo "Ensure transition is triggered" - # wait for steady state or ensure-state-soon will be pointless - ok=0 - for i in $(seq 40); do - if ! snap changes|grep -q ".*.Doing.*" ; then - ok=1 - break - fi - sleep .5 - done - if [ $ok -ne 1 ] ; then - echo "Did not reach steady state" - exit 1 - fi - snap debug ensure-state-soon - - # wait for transition - ok=0 - for i in $(seq 240); do - if snap changes|grep -q ".*Done.*Transition ubuntu-core to core" ; then - ok=1 - break - fi - sleep 1 - done - if [ $ok -ne 1 ] ; then - echo "Transition did not start or finish" - exit 1 - fi - - if snap list|grep ubuntu-core; then - echo "ubuntu-core still installed, transition failed" - exit 1 - fi - snap interfaces -i network | MATCH ":network +test-snapd-python-webserver" - snap interfaces -i network-bind | MATCH ":network-bind +.*test-snapd-python-webserver" - echo "Ensure the webserver is still working" - wait_for_service snap.test-snapd-python-webserver.test-snapd-python-webserver - curl http://localhost | MATCH "XKCD rocks" - - systemctl restart snap.test-snapd-python-webserver.test-snapd-python-webserver - wait_for_service snap.test-snapd-python-webserver.test-snapd-python-webserver - echo "Ensure the webserver is working after a snap restart" - curl http://localhost | MATCH "XKCD rocks" - - echo "Ensure snap set core works" - snap set core system.power-key-action=ignore - if [ "$(snap get core system.power-key-action)" != "ignore" ]; then - echo "snap get did not return the expected result: " - snap get core system.power-key-action - exit 1 - fi diff -Nru snapd-2.40/tests/main/classic-ubuntu-core-transition-auth/task.yaml snapd-2.42.1/tests/main/classic-ubuntu-core-transition-auth/task.yaml --- snapd-2.40/tests/main/classic-ubuntu-core-transition-auth/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/classic-ubuntu-core-transition-auth/task.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,76 +0,0 @@ -summary: Ensure that the ubuntu-core -> core transition works with auth.json - -# we never test on core because the transition can only happen on "classic" -# we disable on ppc64el because the downloads are very slow there -# Fedora, openSUSE and Arch are disabled at the moment as there is something -# fishy going on and the snapd service gets terminated during the process. -systems: [-ubuntu-core-*, -ubuntu-*-ppc64el, -fedora-*, -opensuse-*, -arch-*] - -# autopkgtest run only a subset of tests that deals with the integration -# with the distro -backends: [-autopkgtest] - -warn-timeout: 1m - -kill-timeout: 5m - -debug: | - snap changes - #shellcheck source=tests/lib/changes.sh - . "$TESTSLIB/changes.sh" - snap change "$(change_id 'Transition ubuntu-core to core')" || true - -execute: | - #shellcheck source=tests/lib/pkgdb.sh - . "$TESTSLIB/pkgdb.sh" - echo "Ensure core is gone and we have ubuntu-core instead" - distro_purge_package snapd - distro_install_build_snapd - - # need to be seeded to allow snap install - snap wait system seed.loaded - - snap download "--${CORE_CHANNEL}" ubuntu-core - snap ack ./ubuntu-core_*.assert - snap install ./ubuntu-core_*.snap - - mkdir -p /root/.snap/ - echo '{}' > /root/.snap/auth.json - mkdir -p /home/test/.snap/ - echo '{}' > /home/test/.snap/auth.json - - echo "Ensure transition is triggered" - # wait for steady state or ensure-state-soon will be pointless - ok=0 - for i in $(seq 40); do - if ! snap changes|grep -q ".*.Doing.*" ; then - ok=1 - break - fi - sleep .5 - done - if [ $ok -ne 1 ] ; then - echo "Did not reach steady state" - exit 1 - fi - snap debug ensure-state-soon - - - # wait for transition - ok=0 - for i in $(seq 240); do - if snap changes|grep -q ".*Done.*Transition ubuntu-core to core" ; then - ok=1 - break - fi - sleep 1 - done - if [ $ok -ne 1 ] ; then - echo "Transition did not start or finish" - exit 1 - fi - - if snap list|grep ubuntu-core; then - echo "ubuntu-core still installed, transition failed" - exit 1 - fi diff -Nru snapd-2.40/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml snapd-2.42.1/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml --- snapd-2.40/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/classic-ubuntu-core-transition-two-cores/task.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,78 +0,0 @@ -summary: Ensure that the ubuntu-core -> core transition works with two cores - -# we never test on core because the transition can only happen on "classic" -# we disable on ppc64el because the downloads are very slow there -systems: [-ubuntu-core-*, -ubuntu-*-ppc64el] - -# autopkgtest run only a subset of tests that deals with the integration -# with the distro -backends: [-autopkgtest] - -warn-timeout: 1m - -kill-timeout: 5m - -debug: | - snap changes - #shellcheck source=tests/lib/changes.sh - . "$TESTSLIB"/changes.sh - snap change "$(change_id 'Transition ubuntu-core to core')" || true - -execute: | - echo "install a snap" - snap install test-snapd-python-webserver - snap interfaces -i network | MATCH ":network.*test-snapd-python-webserver" - - #shellcheck source=tests/lib/names.sh - . "$TESTSLIB/names.sh" - cp /var/lib/snapd/state.json /var/lib/snapd/state.json.old - jq -r '.data.snaps["core"].type="xxx"' < /var/lib/snapd/state.json.old > /var/lib/snapd/state.json - - systemctl stop snapd.service snapd.socket - systemctl start snapd.service snapd.socket - - snap download "--${CORE_CHANNEL}" ubuntu-core - snap ack ./ubuntu-core_*.assert - snap install ./ubuntu-core_*.snap - - cp /var/lib/snapd/state.json /var/lib/snapd/state.json.old - jq -r '.data.snaps["core"].type="os"' < /var/lib/snapd/state.json.old > /var/lib/snapd/state.json - - snap list | MATCH "ubuntu-core " - snap list | MATCH "core " - - echo "Ensure transition is triggered" - # wait for steady state or ensure-state-soon will be pointless - ok=0 - for _ in $(seq 40); do - if ! snap changes|grep -q ".*.Doing.*" ; then - ok=1 - break - fi - sleep .5 - done - if [ $ok -ne 1 ] ; then - echo "Did not reach steady state" - exit 1 - fi - snap debug ensure-state-soon - - # wait for transition - ok=0 - for _ in $(seq 240); do - if snap changes|grep -q ".*Done.*Transition ubuntu-core to core" ; then - ok=1 - break - fi - sleep 1 - done - if [ $ok -ne 1 ] ; then - echo "Transition did not start or finish" - exit 1 - fi - - if ! snap list|MATCH -v ubuntu-core; then - echo "ubuntu-core still installed, transition failed" - exit 1 - fi - snap interfaces -i network | MATCH ":network.*test-snapd-python-webserver" diff -Nru snapd-2.40/tests/main/config-versions/task.yaml snapd-2.42.1/tests/main/config-versions/task.yaml --- snapd-2.40/tests/main/config-versions/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/config-versions/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -22,12 +22,19 @@ echo "Install test snap" install_local config-versions + # sanity + snap get config-versions configure-marker | MATCH "executed-for-v1" + echo "Setting config value affecting rev 1" snap set config-versions value=100 echo "Install a new version of the test snap" install_local config-versions-v2 + # sanity + snap get config-versions configure-marker | MATCH "executed-for-v2" + snap get config-versions post-refresh-hook-marker | MATCH "executed-for-v2" + echo "Expecting config value to be carried over to the new version 2" verify_config_value 100 @@ -57,3 +64,13 @@ echo "Revert back to the rev 2" snap revert --revision=x2 config-versions verify_config_value 400 + + echo "Failing refresh of rev 2" + snap refresh --revision=x1 config-versions + snap get config-versions post-refresh-hook-marker | MATCH "executed-for-v1" + snap get config-versions configure-marker | MATCH "executed-for-v1" + # force failure of v2 configure hook + snap set config-versions fail-configure=yes + snap refresh --revision=x2 config-versions || true + snap changes | MATCH "Error .* Refresh \"config-versions\" snap" + snap get config-versions post-refresh-hook-marker | MATCH "executed-for-v1" diff -Nru snapd-2.40/tests/main/core18-configure-hook/task.yaml snapd-2.42.1/tests/main/core18-configure-hook/task.yaml --- snapd-2.40/tests/main/core18-configure-hook/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/core18-configure-hook/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -30,3 +30,8 @@ fi test -e /var/snap/test-snapd-with-configure-core18/common/configure-ran + +restore: | + if snap list test-snapd-with-configure-core18; then + snap remove test-snapd-with-configure-core18 + fi diff -Nru snapd-2.40/tests/main/core18-with-hooks/task.yaml snapd-2.42.1/tests/main/core18-with-hooks/task.yaml --- snapd-2.40/tests/main/core18-with-hooks/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/core18-with-hooks/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -10,3 +10,5 @@ journalctl -u test-snapd-snapctl-core18.service +restore: | + snap remove test-snapd-snapctl-core18 diff -Nru snapd-2.40/tests/main/create-user/task.yaml snapd-2.42.1/tests/main/create-user/task.yaml --- snapd-2.40/tests/main/create-user/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/create-user/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -3,7 +3,7 @@ # FIXME: enable on ubuntu-core-18-* as well once the shadow SRU # https://launchpad.net/ubuntu/+source/shadow/1:4.5-1ubuntu2 # is available on -updates -systems: [ubuntu-core-16-*] +systems: [ubuntu-core-1*] environment: USER_EMAIL: mvo@ubuntu.com diff -Nru snapd-2.40/tests/main/debs/task.yaml snapd-2.42.1/tests/main/debs/task.yaml --- snapd-2.40/tests/main/debs/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/debs/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,19 @@ +summary: Ensure our debs are correctly built + +systems: [-ubuntu-core-*, -fedora-*, -opensuse-*, -arch-*, -amazon-*, -centos-*] + +execute: | + echo "Ensure that our debs have the 'built-using' header" + out=$(dpkg -I "$GOHOME"/snapd_*.deb) + if [[ "$SPREAD_SYSTEM" = ubuntu-* ]]; then + # Apparmor & seccomp is only compiled in on Ubuntu for now. + echo "$out" | MATCH 'Built-Using:.*apparmor \(=' + echo "$out" | MATCH 'Built-Using:.*libseccomp \(=' + fi + echo "$out" | MATCH 'Built-Using:.*libcap2 \(=' + + # not running on 14.04 because we don't have user sessions there + if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then + echo "Ensure that the snapd.session-agent.socket symlinks is part of the deb and that it has the right (relative) target" + dpkg -c "$GOHOME"/snapd_*.deb |MATCH -- '-> \.\./snapd.session-agent.socket' + fi diff -Nru snapd-2.40/tests/main/debs-have-built-using/task.yaml snapd-2.42.1/tests/main/debs-have-built-using/task.yaml --- snapd-2.40/tests/main/debs-have-built-using/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/debs-have-built-using/task.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,12 +0,0 @@ -summary: Ensure that our debs have the "built-using" header - -systems: [-ubuntu-core-*, -fedora-*, -opensuse-*, -arch-*, -amazon-*, -centos-*] - -execute: | - out=$(dpkg -I "$GOHOME"/snapd_*.deb) - if [[ "$SPREAD_SYSTEM" = ubuntu-* ]]; then - # Apparmor & seccomp is only compiled in on Ubuntu for now. - echo "$out" | MATCH 'Built-Using:.*apparmor \(=' - echo "$out" | MATCH 'Built-Using:.*libseccomp \(=' - fi - echo "$out" | MATCH 'Built-Using:.*libcap2 \(=' diff -Nru snapd-2.40/tests/main/desktop-portal-filechooser/task.yaml snapd-2.42.1/tests/main/desktop-portal-filechooser/task.yaml --- snapd-2.40/tests/main/desktop-portal-filechooser/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/desktop-portal-filechooser/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -19,12 +19,11 @@ # Ships xdg-desktop-portal 0.11 - ubuntu-18.04-* - # Ships xdg-desktop-portal 1.0.3 - - ubuntu-18.10-* - # Ships xdg-desktop-portal 1.2.0 - ubuntu-19.04-* + - ubuntu-19.10-* + prepare: | #shellcheck source=tests/lib/desktop-portal.sh . "$TESTSLIB"/desktop-portal.sh @@ -53,16 +52,28 @@ echo "The confined application can write files via the portal" [ ! -f /tmp/file-to-write.txt ] + + # TODO: workaround for testing on 18.04 # The python code does open(path, 'w'), which attempts to truncate the # file if it exists. Then in fuse handlers inside document-portal, the # code path for when the inode exists and the caller requested O_TRUNC, # returns -ENOSYS, resulting in OSError: # [Errno 38] Function not implemented on the Python side - # To avoid the issue described we are creating manually the file. + # Creating the file beforehand prevents that. touch /tmp/file-to-write.txt chown test:test /tmp/file-to-write.txt + as_user test-snapd-portal-client save-file "from-sandbox" - [ -f /tmp/file-to-write.txt ] + + # TODO: another workaround for testing on 18.04 + # Retry checking until the file contains the written text. In particular on + # 18.04, xdg-desktop-portal 0.11 (perhaps still the same for later + # versions), the writes were implemented such that the client writes to a + # fuse filesystem entry, while the portal forwards the writes to the actual + # backing file. It was observed that, even after client does close and the + # corresponding *fuse_release() code completes on the portal side, the + # contents are not visible in the destination file. + retry-tool -n 5 test -s /tmp/file-to-write.txt MATCH "from-sandbox" < /tmp/file-to-write.txt debug: | diff -Nru snapd-2.40/tests/main/desktop-portal-open-file/task.yaml snapd-2.42.1/tests/main/desktop-portal-open-file/task.yaml --- snapd-2.40/tests/main/desktop-portal-open-file/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/desktop-portal-open-file/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -11,8 +11,8 @@ # Expand as needed. systems: - ubuntu-18.04-* - - ubuntu-18.10-* - ubuntu-19.04-* + - ubuntu-19.10-* environment: EDITOR_HISTORY: /tmp/editor-history.txt diff -Nru snapd-2.40/tests/main/desktop-portal-open-uri/task.yaml snapd-2.42.1/tests/main/desktop-portal-open-uri/task.yaml --- snapd-2.40/tests/main/desktop-portal-open-uri/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/desktop-portal-open-uri/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -8,7 +8,6 @@ # Expand as needed. systems: - ubuntu-18.04-* - - ubuntu-18.10-* - ubuntu-19.04-* environment: diff -Nru snapd-2.40/tests/main/desktop-portal-screenshot/task.yaml snapd-2.42.1/tests/main/desktop-portal-screenshot/task.yaml --- snapd-2.40/tests/main/desktop-portal-screenshot/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/desktop-portal-screenshot/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -22,8 +22,8 @@ # Expand as needed. systems: - ubuntu-18.04-* - - ubuntu-18.10-* - ubuntu-19.04-* + - ubuntu-19.10-* prepare: | #shellcheck source=tests/lib/desktop-portal.sh diff -Nru snapd-2.40/tests/main/enable-disable-units-gpio/task.yaml snapd-2.42.1/tests/main/enable-disable-units-gpio/task.yaml --- snapd-2.40/tests/main/enable-disable-units-gpio/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/enable-disable-units-gpio/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -10,7 +10,7 @@ a reboot the systemd service tries to regenerate the gpio device node if it does not find it. -systems: [ubuntu-core-16-64] +systems: [ubuntu-core-1*-64] prepare: | # Core image that were created using spread will have a fake "gpio-pin". diff -Nru snapd-2.40/tests/main/experimental-features/task.yaml snapd-2.42.1/tests/main/experimental-features/task.yaml --- snapd-2.40/tests/main/experimental-features/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/experimental-features/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -15,3 +15,11 @@ # When a feature that is not exported is enabled, a file is not created. snap set core experimental.layouts=true test ! -f /var/lib/snapd/features/layouts + + # Features are exported when snapd starts up + snap set core experimental.parallel-instances=true + test -f /var/lib/snapd/features/parallel-instances + systemctl stop snapd + rm /var/lib/snapd/features/parallel-instances + systemctl start snapd + test -f /var/lib/snapd/features/parallel-instances diff -Nru snapd-2.40/tests/main/failover/task.yaml snapd-2.42.1/tests/main/failover/task.yaml --- snapd-2.40/tests/main/failover/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/failover/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -69,6 +69,8 @@ . "$TESTSLIB"/boot.sh #shellcheck source=tests/lib/journalctl.sh . "$TESTSLIB"/journalctl.sh + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh if [ "$TARGET_SNAP" = kernel ]; then TARGET_SNAP_NAME=$kernel_name @@ -87,7 +89,7 @@ eval "${INJECT_FAILURE}" # repack new target snap - snap pack "$BUILD_DIR/unpack" && mv ${TARGET_SNAP_NAME}_*.snap failing.snap + mksnap_fast "$BUILD_DIR/unpack" failing.snap # use journalctl wrapper to grep only the logs collected while the test is running if get_journalctl_log | MATCH "Waiting for system reboot"; then @@ -96,13 +98,22 @@ fi # install new target snap - snap install --dangerous failing.snap + snap install --no-wait --dangerous failing.snap # use journalctl wrapper to grep only the logs collected while the test is running - while ! check_journalctl_log "Waiting for system reboot"; do - sleep 1 + # waiting up to 100s to reach waiting for reboot + for _ in $(seq 50); do + if check_journalctl_log "Waiting for system reboot"; then + break + fi + sleep 2 done + if ! check_journalctl_log "Waiting for system reboot"; then + echo "Taking too long to reach waiting for reboot" + exit 1 + fi + # check boot env vars readlink /snap/core/current > failBoot test "$(bootenv snap_"${TARGET_SNAP}")" = "${TARGET_SNAP_NAME}_$(cat prevBoot).snap" diff -Nru snapd-2.40/tests/main/gadget-update-pc/pc-gadget-2.yaml snapd-2.42.1/tests/main/gadget-update-pc/pc-gadget-2.yaml --- snapd-2.40/tests/main/gadget-update-pc/pc-gadget-2.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/gadget-update-pc/pc-gadget-2.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,37 @@ +volumes: + pc: + bootloader: grub + structure: + - name: mbr + type: mbr + size: 440 + content: + - image: pc-boot.img + - name: BIOS Boot + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + offset: 1M + offset-write: mbr+92 + content: + - image: pc-core.img + # write new content right after the previous one + - image: foo.img + update: + edition: 1 + - name: EFI System + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + filesystem: vfat + filesystem-label: system-boot + size: 50M + content: + - source: grubx64.efi + target: EFI/boot/grubx64.efi + - source: shim.efi.signed + target: EFI/boot/bootx64.efi + - source: grub.cfg + target: EFI/ubuntu/grub.cfg + # drop a new file + - source: foo.cfg + target: foo.cfg + update: + edition: 1 diff -Nru snapd-2.40/tests/main/gadget-update-pc/pc-gadget-3.yaml snapd-2.42.1/tests/main/gadget-update-pc/pc-gadget-3.yaml --- snapd-2.40/tests/main/gadget-update-pc/pc-gadget-3.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/gadget-update-pc/pc-gadget-3.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,44 @@ +volumes: + pc: + bootloader: grub + structure: + - name: mbr + type: mbr + size: 440 + content: + - image: pc-boot.img + - name: BIOS Boot + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + offset: 1M + offset-write: mbr+92 + content: + - image: pc-core.img + # content is modified again + - image: foo.img + update: + edition: 2 + - name: EFI System + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + filesystem: vfat + filesystem-label: system-boot + size: 50M + content: + - source: grubx64.efi + target: EFI/boot/grubx64.efi + - source: shim.efi.signed + target: EFI/boot/bootx64.efi + - source: grub.cfg + target: EFI/ubuntu/grub.cfg + # drop new files + - source: foo.cfg + target: foo.cfg + - source: bar.cfg + target: bar.cfg + update: + edition: 2 + preserve: + # foo is modified in the test and shall be preserved + - foo.cfg + # bar does not exist and will be installed + - bar.cfg diff -Nru snapd-2.40/tests/main/gadget-update-pc/pc-gadget.yaml snapd-2.42.1/tests/main/gadget-update-pc/pc-gadget.yaml --- snapd-2.40/tests/main/gadget-update-pc/pc-gadget.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/gadget-update-pc/pc-gadget.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,28 @@ +volumes: + pc: + bootloader: grub + structure: + - name: mbr + type: mbr + size: 440 + content: + - image: pc-boot.img + - name: BIOS Boot + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + offset: 1M + offset-write: mbr+92 + content: + - image: pc-core.img + - name: EFI System + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + filesystem: vfat + filesystem-label: system-boot + size: 50M + content: + - source: grubx64.efi + target: EFI/boot/grubx64.efi + - source: shim.efi.signed + target: EFI/boot/bootx64.efi + - source: grub.cfg + target: EFI/ubuntu/grub.cfg diff -Nru snapd-2.40/tests/main/gadget-update-pc/task.yaml snapd-2.42.1/tests/main/gadget-update-pc/task.yaml --- snapd-2.40/tests/main/gadget-update-pc/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/gadget-update-pc/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,167 @@ +summary: Exercise a gadget update on a PC + +environment: + BLOB_DIR: $(pwd)/fake-store-blobdir + # snap-id of 'pc' gadget snap + PC_SNAP_ID: UqFziVZDHLSyO3TqSWgNBoAdHbLI4dAH + START_REVISION: 1000 + +systems: [ubuntu-core-*] + +prepare: | + # external backends do not enable test keys + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + if ! test -d /snap/pc; then + echo "This test needs a host using 'pc' gadget snap" + exit 1 + fi + + snap ack "$TESTSLIB/assertions/testrootorg-store.account-key" + + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + setup_fake_store "$BLOB_DIR" + + cp /var/lib/snapd/snaps/pc_*.snap gadget.snap + unsquashfs -d pc-snap gadget.snap + + # gadget YAMLs should be identical, otherwise the test needs to be updated + diff -up pc-gadget.yaml pc-snap/meta/gadget.yaml + + # prepare a vanilla version + sed -i -e 's/^version: \(.*\)/version: \1-1/' pc-snap/meta/snap.yaml + # pack it + snap pack pc-snap --filename=pc_x1.snap + + cat < decl-headers.json + {"snap-id": "$PC_SNAP_ID"} + EOF + cat < rev-headers.json + {"snap-id": "$PC_SNAP_ID", "snap-revision": "$START_REVISION"} + EOF + + new_snap_declaration "$BLOB_DIR" pc_x1.snap --snap-decl-json decl-headers.json + new_snap_revision "$BLOB_DIR" pc_x1.snap --snap-rev-json rev-headers.json + + # prepare first update + cp pc-gadget-2.yaml pc-snap/meta/gadget.yaml + echo 'this is foo-x2' > foo-x2.img + cp foo-x2.img pc-snap/foo.img + echo 'this is foo.cfg' > pc-snap/foo.cfg + sed -i -e 's/^version: \(.*\)-1/version: \1-2/' pc-snap/meta/snap.yaml + snap pack pc-snap --filename=pc_x2.snap + cat < rev-headers-2.json + {"snap-id": "$PC_SNAP_ID", "snap-revision": "$((START_REVISION+1))"} + EOF + + # prepare second update + cp pc-gadget-3.yaml pc-snap/meta/gadget.yaml + echo 'this is updated foo-x3' > foo-x3.img + cp foo-x3.img pc-snap/foo.img + echo 'this is updated foo.cfg' > pc-snap/foo.cfg + echo 'this is bar.cfg' > pc-snap/bar.cfg + sed -i -e 's/^version: \(.*\)-2/version: \1-3/' pc-snap/meta/snap.yaml + snap pack pc-snap --filename=pc_x3.snap + cat < rev-headers-3.json + {"snap-id": "$PC_SNAP_ID", "snap-revision": "$((START_REVISION+2))"} + EOF + + snap install pc_x1.snap + +restore: | + # external backends do not enable test keys + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + if ! test -d /snap/pc; then + echo "This test needs a host using 'pc' gadget snap" + exit 1 + fi + + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$BLOB_DIR" + + # restore the original gadget snap + snap install gadget.snap + +execute: | + # external backends do not enable test keys + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + if ! test -d /snap/pc; then + echo "This test needs a host using 'pc' gadget snap" + exit 1 + fi + + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + + # XXX: the test hardcodes a bunch of locations + # - 'BIOS Boot' and 'EFI System' are modified during the update + # - 'EFI System' is mounted at /boot/efi + + if [[ "$SPREAD_REBOOT" == 0 ]]; then + + new_snap_declaration "$BLOB_DIR" pc_x2.snap --snap-decl-json decl-headers.json + new_snap_revision "$BLOB_DIR" pc_x2.snap --snap-rev-json rev-headers-2.json + + snap install pc_x2.snap + + REBOOT + fi + + if [[ "$SPREAD_REBOOT" == 1 ]]; then + # wait for change to complete + snap watch --last=install\? + + # verify the update + + # a filesystem structure entry was copied to the right place + test "$(cat /boot/efi/foo.cfg)" = 'this is foo.cfg' + + szimg=$(stat -c '%s' /snap/pc/current/pc-core.img) + # using foo.img from x2 + szfoo=$(stat -c '%s' foo-x2.img) + # a raw content was written + dd if='/dev/disk/by-partlabel/BIOS\x20Boot' skip="$szimg" bs=1 count="$szfoo" of=foo-written.img + test "$(cat foo-written.img)" = 'this is foo-x2' + + # prepare & install the next update + new_snap_declaration "$BLOB_DIR" pc_x3.snap --snap-decl-json decl-headers.json + new_snap_revision "$BLOB_DIR" pc_x3.snap --snap-rev-json rev-headers-3.json + + snap install pc_x3.snap + + REBOOT + fi + + if [[ "$SPREAD_REBOOT" == 2 ]]; then + # wait for change to complete + snap watch --last=install\? + + # verify the update + + # a new filesystem structure entry was copied to the right place + test "$(cat /boot/efi/bar.cfg)" = 'this is bar.cfg' + # this one was preserved + test "$(cat /boot/efi/foo.cfg)" = 'this is foo.cfg' + + # raw content was updated + szimg=$(stat -c '%s' /snap/pc/current/pc-core.img) + # using foo.img from x3 + szfoo=$(stat -c '%s' foo-x3.img) + # a raw content was written + dd if='/dev/disk/by-partlabel/BIOS\x20Boot' skip="$szimg" bs=1 count="$szfoo" of=foo-updated-written.img + test "$(cat foo-updated-written.img)" = 'this is updated foo-x3' + + fi diff -Nru snapd-2.40/tests/main/health/task.yaml snapd-2.42.1/tests/main/health/task.yaml --- snapd-2.40/tests/main/health/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/health/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,21 @@ +summary: Check that health works + +prepare: | + snap install jq + +execute: | + echo "Test that 'try'ing a snap with a set-health hook sets health in state:" + test "$(snap run jq '.data.health."test-snapd-health"' < /var/lib/snapd/state.json)" = "null" + snap try "$TESTSLIB/snaps/test-snapd-health" + test "$(snap run jq '.data.health."test-snapd-health".status' < /var/lib/snapd/state.json)" = "1" + # TODO: also check for health in info etc + # TODO: also check installing from store + + echo "Test that a snap app can run 'snapctl set-health':" + test-snapd-health error "Something went wrong" + test "$(snap run jq -r '.data.health."test-snapd-health".message' < /var/lib/snapd/state.json)" = "Something went wrong" + + echo "Test that a snap hook (different from check-health) can run 'snapctl set-health':" + snap set test-snapd-health force-health=okay + test "$(snap run jq '.data.health."test-snapd-health".status' < /var/lib/snapd/state.json)" = "1" + diff -Nru snapd-2.40/tests/main/install/task.yaml snapd-2.42.1/tests/main/install/task.yaml --- snapd-2.40/tests/main/install/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/install/task.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,20 +0,0 @@ -summary: Check that install works - -# limiting to ubuntu because we need a known fonts package -# to install so that actual caches get generated -systems: [ubuntu-16.04-64, ubuntu-18.04-64] - -prepare: | - apt install -y fonts-kiloji - -restore: | - apt autoremove -y fonts-kiloji - -execute: | - echo "With no fontconfig cache" - rm /var/cache/fontconfig/* - - echo "Installing a snap generates a fontconfig cache" - snap install test-snapd-tools - ls /var/cache/fontconfig/*.cache-6 - ls /var/cache/fontconfig/*.cache-7 diff -Nru snapd-2.40/tests/main/install-fontconfig-cache-gen/task.yaml snapd-2.42.1/tests/main/install-fontconfig-cache-gen/task.yaml --- snapd-2.40/tests/main/install-fontconfig-cache-gen/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/install-fontconfig-cache-gen/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,20 @@ +summary: Check that install works + +# limiting to ubuntu because we need a known fonts package +# to install so that actual caches get generated +systems: [ubuntu-16.04-64, ubuntu-18.04-64] + +prepare: | + apt install -y fonts-kiloji + +restore: | + apt autoremove -y fonts-kiloji + +execute: | + echo "With no fontconfig cache" + rm /var/cache/fontconfig/* + + echo "Installing a snap generates a fontconfig cache" + snap install test-snapd-tools + ls /var/cache/fontconfig/*.cache-6 + ls /var/cache/fontconfig/*.cache-7 diff -Nru snapd-2.40/tests/main/install-snaps/task.yaml snapd-2.42.1/tests/main/install-snaps/task.yaml --- snapd-2.40/tests/main/install-snaps/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/install-snaps/task.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,134 +0,0 @@ -summary: Check install popular snaps - -details: | - This test is intended to install some popular snaps from - different channels. The idea is to detect any problem - installing that are currently published and some of them - with many revisions. - The execution of this test is made on the nightly build. - -manual: true - -environment: - # High Profile - SNAP/azurecli: azure-cli - SNAP/awscli: aws-cli - SNAP/heroku: heroku - SNAP/hiri: hiri - SNAP/kubectl: kubectl - SNAP/rocketchatserver: rocketchat-server - # Selected from recent insights posts - SNAP/corebird: corebird - SNAP/gitterdesktop: gitter-desktop - SNAP/helm: helm - SNAP/mattermostdesktop: mattermost-desktop - SNAP/mentaplexmediaserver: menta-plexmediaserver - SNAP/openspades: openspades - SNAP/pintown: pin-town - SNAP/postgresql10: postgresql10 - SNAP/storjshare: storjshare - SNAP/slackterm: slack-term - SNAP/vectr: vectr - SNAP/wekan: wekan - SNAP/wormhole: wormhole - # Featured snaps in Ubuntu Software - SNAP/anboxinstaller: anbox-installer - SNAP/lxd: lxd - # Top non canonical snaps - SNAP/atom: atom - SNAP/discord: discord - SNAP/docker: docker - SNAP/etcd: etcd - SNAP/geocoder: geocoder - SNAP/gimp: gimp - SNAP/huggle: huggle - SNAP/hugo: hugo - SNAP/ia: ia - SNAP/kurly: kurly - SNAP/micro: micro - SNAP/nikola: nikola - SNAP/parity: parity - SNAP/paritybitcoin: parity-bitcoin - SNAP/remmina: remmina - SNAP/skype: skype - SNAP/slack: slack - SNAP/spotify: spotify - SNAP/telegramsergiusens: telegram-sergiusens - SNAP/zeronet: zeronet - SNAP/zeronetjs: zeronet-js - # Top canonical snaps - SNAP/bare: bare - SNAP/bluez: bluez - SNAP/conjureup: conjure-up - SNAP/gedit: gedit - SNAP/go: go - SNAP/juju: juju - SNAP/neutron: neutron - SNAP/nova: nova - SNAP/snapcraft: snapcraft - SNAP/solc: solc - SNAP/vault: vault - -prepare: | - cp /etc/systemd/system/snapd.service.d/local.conf /etc/systemd/system/snapd.service.d/local.conf.bak - sed 's/SNAPD_CONFIGURE_HOOK_TIMEOUT=.*s/SNAPD_CONFIGURE_HOOK_TIMEOUT=180s/g' -i /etc/systemd/system/snapd.service.d/local.conf - systemctl daemon-reload - systemctl restart snapd.socket - - if [ ! -d '/snap' ]; then - #shellcheck source=tests/lib/dirs.sh - . "$TESTSLIB/dirs.sh" - ln -s "$SNAP_MOUNT_DIR" /snap - fi - -restore: | - mv /etc/systemd/system/snapd.service.d/local.conf.bak /etc/systemd/system/snapd.service.d/local.conf - systemctl daemon-reload - systemctl restart snapd.socket - - if [ -L /snap ]; then - unlink /snap - fi - -execute: | - #shellcheck source=tests/lib/snaps.sh - . "$TESTSLIB/snaps.sh" - - CHANNELS="stable candidate beta edge" - for CHANNEL in $CHANNELS; do - # shellcheck disable=SC2153 - if ! CHANNEL_INFO="$(snap info --unicode=never "$SNAP" | grep " $CHANNEL: ")"; then - echo "Snap $SNAP not found" - exit - fi - if echo "$CHANNEL_INFO" | MATCH "$CHANNEL:.*--"; then - continue - fi - - if echo "$CHANNEL_INFO" | MATCH "$CHANNEL:.*classic"; then - if is_classic_confinement_supported; then - snap install "$SNAP" "--$CHANNEL" --classic - else - echo "The snap $SNAP requires classic confinement which is not supported yet" - exit - fi - elif echo "$CHANNEL_INFO" | MATCH "$CHANNEL:.*jailmode"; then - snap install "$SNAP" "--$CHANNEL" --jailmode - elif echo "$CHANNEL_INFO" | MATCH "$CHANNEL:.*devmode"; then - snap install "$SNAP" "--$CHANNEL" --devmode - else - snap install "$SNAP" "--$CHANNEL" - fi - break - done - - echo "Check the snap is properly installed" - snap list | MATCH "$SNAP" - - echo "Check the snap is properly removed" - snap remove "$SNAP" - - if snap list | MATCH "$SNAP"; then - echo "Snap $SNAP not removed properly" - exit 1 - fi diff -Nru snapd-2.40/tests/main/interfaces-account-control/task.yaml snapd-2.42.1/tests/main/interfaces-account-control/task.yaml --- snapd-2.40/tests/main/interfaces-account-control/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-account-control/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -24,13 +24,11 @@ for f in /var/lib/extrausers/*; do sed -i '/^alice:/d' "$f" done + snap remove "$TSNAP" execute: | - #shellcheck source=tests/lib/dirs.sh - . "$TESTSLIB"/dirs.sh - - "$SNAP_MOUNT_DIR"/bin/"$TSNAP".useradd --extrausers alice - echo alice:password | "$SNAP_MOUNT_DIR"/bin/"$TSNAP".chpasswd + snap run "$TSNAP".useradd --extrausers alice + echo alice:password | snap run "$TSNAP".chpasswd # User deletion is unsupported yet on Core: https://bugs.launchpad.net/ubuntu/+source/shadow/+bug/1659534 - # $SNAP_MOUNT_DIR/bin/"$TSNAP".userdel --extrausers alice + # snap run $TSNAP".userdel --extrausers alice diff -Nru snapd-2.40/tests/main/interfaces-appstream-metadata/task.yaml snapd-2.42.1/tests/main/interfaces-appstream-metadata/task.yaml --- snapd-2.40/tests/main/interfaces-appstream-metadata/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-appstream-metadata/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,60 @@ +summary: The appstream-metadata interface grants access to package metadata + +details: | + A number of Linux distributions use the AppStream format to + provide metadata about both installed and available packages. + + The appstream-metadata interface makes this information available + to a confined application by creating bind mounts from the host + system to equivalent points in the sandbox. Together with an + interface granting access to the host system packaging system + (e.g. via PackageKit), it is possible to confine a graphical + package manager. + +systems: [-ubuntu-core-*] + +prepare: | + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh + install_local test-snapd-sh + + # Set up some dummy Appstream metadata on the host system + mkdir -p /usr/share/metainfo + echo "Appstream metainfo 1" > /usr/share/metainfo/test1.metainfo.xml + mkdir -p /usr/share/appdata + echo "Appstream metainfo 2" > /usr/share/appdata/test2.metainfo.xml + mkdir -p /usr/share/app-info/xmls + echo "Appstream app-info 1" > /usr/share/app-info/xmls/test1.xml + mkdir -p /var/cache/app-info/xmls + echo "Appstream app-info 2" > /var/cache/app-info/xmls/test2.xml + # Apt exposes Appstream metadata via absolute symlinks to + # /var/lib/apt/lists + mkdir -p /var/lib/apt/lists + echo "Appstream app-info 3" | gzip -c > /var/lib/apt/lists/test3.yml.gz + mkdir -p /var/lib/app-info/yaml + ln -s /var/lib/apt/lists/test3.yml.gz /var/lib/app-info/yaml + +restore: | + rm -f /usr/share/metainfo/test1.metainfo.xml + rm -f /usr/share/appdata/test2.metainfo.xml + rm -f /usr/share/app-info/xmls/test1.xml + rm -f /var/cache/app-info/xmls/test2.xml + rm -f /var/lib/app-info/yaml/test3.yml.gz + rm -f /var/lib/apt/lists/test3.yml.gz + +execute: | + echo "The plug is disconnected by default" + snap connections test-snapd-sh | MATCH "appstream-metadata +test-snapd-sh:appstream-metadata +- +-" + + echo "The plug can be connected" + snap connect test-snapd-sh:appstream-metadata + snap connections test-snapd-sh | MATCH "appstream-metadata +test-snapd-sh:appstream-metadata +:appstream-metadata +manual" + + echo "Appstream metadata is now available from the sandbox" + test-snapd-sh.with-appstream-metadata-plug -c "cat /usr/share/metainfo/test1.metainfo.xml" | MATCH "Appstream metainfo 1" + test-snapd-sh.with-appstream-metadata-plug -c "cat /usr/share/appdata/test2.metainfo.xml" | MATCH "Appstream metainfo 2" + + test-snapd-sh.with-appstream-metadata-plug -c "cat /usr/share/app-info/xmls/test1.xml" | MATCH "Appstream app-info 1" + test-snapd-sh.with-appstream-metadata-plug -c "cat /var/cache/app-info/xmls/test2.xml" | MATCH "Appstream app-info 2" + + test-snapd-sh.with-appstream-metadata-plug -c "cat /var/lib/app-info/yaml/test3.yml.gz" | gunzip -c | MATCH "Appstream app-info 3" diff -Nru snapd-2.40/tests/main/interfaces-calendar-service/task.yaml snapd-2.42.1/tests/main/interfaces-calendar-service/task.yaml --- snapd-2.40/tests/main/interfaces-calendar-service/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-calendar-service/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -2,7 +2,12 @@ # Only test on classic systems. Don't test on Ubuntu 14.04, which # does not ship a new enough evolution-data-server. Don't test on AMZN2. -systems: [-ubuntu-core-*, -ubuntu-14.04-*, -amazon-*, -centos-*] +# +# FIXME: disable opensuse-tumbleweed until +# https://github.com/snapcore/snapd/pull/7230 is landed +# ubuntu-19.10: test-snapd-eds is incompatible with eds version shipped with the distro +# arch-linux: test-snapd-eds is incompatible with eds version shipped with the distro +systems: [-ubuntu-core-*, -ubuntu-14.04-*, -amazon-*, -centos-*, -opensuse-tumbleweed-*, -ubuntu-19.10-*, -arch-linux-*, -debian-sid-*] # fails in the autopkgtest env with: # [Wed Aug 15 16:34:12 2018] audit: type=1400 diff -Nru snapd-2.40/tests/main/interfaces-contacts-service/task.yaml snapd-2.42.1/tests/main/interfaces-contacts-service/task.yaml --- snapd-2.40/tests/main/interfaces-contacts-service/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-contacts-service/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -3,7 +3,10 @@ # Only test on classic systems. Don't test on Ubuntu 14.04, which # does not ship a new enough evolution-data-server. # amazon: no need to run this on amazon -systems: [-ubuntu-core-*, -ubuntu-14.04-*, -amazon-*, -centos-*] +# ubuntu-19.10: test-snapd-eds is incompatible with eds shipped with the distro +# arch-linux: test-snapd-eds is incompatible with eds version shipped with the distro +# opensuse-tumbleweed: test-snapd-eds is incompatible with eds version shipped with the distro +systems: [-ubuntu-core-*, -ubuntu-14.04-*, -amazon-*, -centos-*, -ubuntu-19.10-*, -arch-linux-*, -debian-sid-*, -opensuse-tumbleweed-*] # fails in autopkgtest environment with: # [Wed Aug 15 16:08:23 2018] audit: type=1400 @@ -15,21 +18,23 @@ backends: [-autopkgtest] environment: - XDG: $(pwd)/xdg + XDG: /tmp/xdg XDG_CONFIG_HOME: $XDG/config XDG_DATA_HOME: $XDG/share XDG_CACHE_HOME: $XDG/cache -restore: | - if [ -e dbus-launch.pid ]; then - kill "$(cat dbus-launch.pid)" - fi +debug: | + echo "Output process to see what might write to $XDG" + ps uafx + echo "Output dbus-session" + systemctl status dbus-session || true + echo "Show what is in $XDG" + ls -alR "$XDG" - # In case the process gvfsd-metadata does not finish by itself, it is manually stopped - # The reason is that gvfsd-metadata locks the xdg/share/gvfs-metadata directory content - # producing an error when the xdg directory is removed. - if pid="$(pidof gvfsd-metadata)"; then - kill -9 "$pid" || true +restore: | + echo "Stop dbus session bus and all its children" + if systemctl is-active dbus-session; then + systemctl stop dbus-session fi rm -rf "$XDG" @@ -44,9 +49,10 @@ fi mkdir -p "$XDG_CONFIG_HOME" "$XDG_DATA_HOME" "$XDG_CACHE_HOME" - echo "Setting up D-Bus session bus" - eval "$(dbus-launch --sh-syntax)" - echo "$DBUS_SESSION_BUS_PID" > dbus-launch.pid + echo "Setting up D-Bus session bus in a systemd unit" + systemd-run --unit=dbus-session --property=Type=forking -r /bin/sh -c "XDG_CONFIG_HOME=$XDG_CONFIG_HOME XDG_DATA_HOME=$XDG_DATA_HOME XDG_CACHE_HOME=$XDG_CACHE_HOME dbus-launch --sh-syntax > /tmp/dbus-sh" + retry-tool -n 20 test -e /tmp/dbus-sh + eval "$(cat /tmp/dbus-sh)" echo "The interface is initially disconnected" snap interfaces -i contacts-service | MATCH -- '- +test-snapd-eds:contacts-service' diff -Nru snapd-2.40/tests/main/interfaces-fuse_support/task.yaml snapd-2.42.1/tests/main/interfaces-fuse_support/task.yaml --- snapd-2.40/tests/main/interfaces-fuse_support/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-fuse_support/task.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,97 +0,0 @@ -summary: Ensure that the fuse-support interface works. - -# no support for fuse on 14.04 -systems: [-ubuntu-14.04-*] - -details: | - The fuse-support interface allows a snap to manage FUSE file systems. - - A snap which defines the fuse-support plug must be shown in the interfaces list. - The plug must be autoconnected on install and, as usual, must be able to be - reconnected. - - A snap declaring a plug on this interface must be able to create a fuse filesystem - in a writable zone. The fuse-consumer test snap creates a readable file with a known - name and content in the mount point given to the command. - -environment: - MOUNT_POINT/regular: /var/snap/test-snapd-fuse-consumer/current/mount_point - MOUNT_POINT_OUTSIDE/regular: /var/snap/test-snapd-fuse-consumer/current/mount_point - NAME/regular: test-snapd-fuse-consumer - # snap with instance key 'foo' - MOUNT_POINT/parallel: /var/snap/test-snapd-fuse-consumer_foo/current/mount_point - MOUNT_POINT_OUTSIDE/parallel: /var/snap/test-snapd-fuse-consumer_foo/current/mount_point - NAME/parallel: test-snapd-fuse-consumer_foo - -prepare: | - if [[ "$SPREAD_VARIANT" == "parallel" ]]; then - snap set system experimental.parallel-instances=true - fi - echo "Given a snap declaring a fuse plug is installed" - snap install "$NAME" - - echo "And a user writable mount point is created" - mkdir -p "$MOUNT_POINT_OUTSIDE" - -restore: | - # remove the mount point - mounts=$(nsenter "--mount=/run/snapd/ns/$NAME.mnt" cat /proc/mounts | \ - grep "$(basename "$MOUNT_POINT") fuse" | cut -f2 -d' ') - for m in $mounts; do - nsenter "--mount=/run/snapd/ns/$NAME.mnt" umount "$m" - done - rm -rf "$MOUNT_POINT_OUTSIDE" - - if [[ "$SPREAD_VARIANT" == "parallel" ]]; then - snap set system experimental.parallel-instances=null - fi - -execute: | - echo "The interface is disconnected by default" - snap interfaces -i fuse-support | MATCH "^- +$NAME:fuse-support" - - if [ "$(snap debug confinement)" = strict ]; then - echo "The snap is not able to create a fuse file system with the plug disconnected" - if "$NAME.create" -f "$MOUNT_POINT" 2> fuse.error; then - echo "Expected permission error creating fuse filesystem with disconnected plug" - exit 1 - fi - MATCH "Permission denied" < fuse.error - fi - - echo "When the plug is connected" - snap connect "$NAME:fuse-support" - - echo "Then the snap is able to create a fuse filesystem" - # start fuse consumer in foreground and make it a background job - "$NAME.create" -f "$MOUNT_POINT" & - createpid=$! - # cleanup the background job on exit - trap 'kill $createpid; wait $createpid' EXIT - - # it may take a while for hello file to appear - for _ in $(seq 100); do - if test -r "/proc/${createpid}/root/${MOUNT_POINT}/hello"; then - break - fi - sleep .1 - done - test -r "/proc/${createpid}/root/${MOUNT_POINT}/hello" - # prefer cat ... | MATCH so that we can see which PID is used - #shellcheck disable=SC2002 - cat "/proc/${createpid}/root/${MOUNT_POINT}/hello" | MATCH "Hello World!" - - # SIGTERM triggers a clean exit - kill $createpid - trap - EXIT - - # the create app will try to unmount the fuse filesystem while exiting, - # it will fail, as seccomp rules are blocking umount2 - echo "The snap exited with an error" - # SIGSYS - 31 on x86, wait exit status if killed by signal = 128 + - wait $createpid || test "$?" = "159" - - # verify that the mount_point was not removed from mount namespace of the snap - mountpath=$(nsenter "--mount=/run/snapd/ns/$NAME.mnt" cat /proc/mounts | \ - grep "$(basename "$MOUNT_POINT") fuse" | cut -f2 -d' ') - test -n "$mountpath" diff -Nru snapd-2.40/tests/main/interfaces-fuse-support/task.yaml snapd-2.42.1/tests/main/interfaces-fuse-support/task.yaml --- snapd-2.40/tests/main/interfaces-fuse-support/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-fuse-support/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,107 @@ +summary: Ensure that the fuse-support interface works. + +# no support for fuse on 14.04 +systems: [-ubuntu-14.04-*] + +details: | + The fuse-support interface allows a snap to manage FUSE file systems. + + A snap which defines the fuse-support plug must be shown in the interfaces list. + The plug must be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to create a fuse filesystem + in a writable zone. The fuse-consumer test snap creates a readable file with a known + name and content in the mount point given to the command. + +environment: + MOUNT_POINT/regular: /var/snap/test-snapd-fuse-consumer/current/mount_point + MOUNT_POINT_OUTSIDE/regular: /var/snap/test-snapd-fuse-consumer/current/mount_point + NAME/regular: test-snapd-fuse-consumer + # snap with instance key 'foo' + MOUNT_POINT/parallel: /var/snap/test-snapd-fuse-consumer_foo/current/mount_point + MOUNT_POINT_OUTSIDE/parallel: /var/snap/test-snapd-fuse-consumer_foo/current/mount_point + NAME/parallel: test-snapd-fuse-consumer_foo + +prepare: | + if not mountinfo-tool /sys/fs/fuse/connections .fs_type=fusectl; then + touch please-unmount-fuse-connections + fi + + if [[ "$SPREAD_VARIANT" == "parallel" ]]; then + snap set system experimental.parallel-instances=true + fi + echo "Given a snap declaring a fuse plug is installed" + snap install "$NAME" + + echo "And a user writable mount point is created" + mkdir -p "$MOUNT_POINT_OUTSIDE" + +restore: | + # remove the mount point + mounts=$(nsenter "--mount=/run/snapd/ns/$NAME.mnt" cat /proc/mounts | \ + grep "$(basename "$MOUNT_POINT") fuse" | cut -f2 -d' ') + for m in $mounts; do + nsenter "--mount=/run/snapd/ns/$NAME.mnt" umount "$m" + done + rm -rf "$MOUNT_POINT_OUTSIDE" + + if [[ "$SPREAD_VARIANT" == "parallel" ]]; then + snap set system experimental.parallel-instances=null + fi + if [ -e please-unmount-fuse-connections ]; then + if mountinfo-tool /sys/fs/fuse/connections .fs_type=fusectl; then + umount /sys/fs/fuse/connections + fi + rm -f please-unmount-fuse-connections + fi + +execute: | + echo "The interface is disconnected by default" + snap interfaces -i fuse-support | MATCH "^- +$NAME:fuse-support" + + if [ "$(snap debug confinement)" = strict ]; then + echo "The snap is not able to create a fuse file system with the plug disconnected" + if "$NAME.create" -f "$MOUNT_POINT" 2> fuse.error; then + echo "Expected permission error creating fuse filesystem with disconnected plug" + exit 1 + fi + MATCH "Permission denied" < fuse.error + fi + + echo "When the plug is connected" + snap connect "$NAME:fuse-support" + + echo "Then the snap is able to create a fuse filesystem" + # start fuse consumer in foreground and make it a background job + "$NAME.create" -f "$MOUNT_POINT" & + createpid=$! + # cleanup the background job on exit + trap 'kill $createpid; wait $createpid' EXIT + + # it may take a while for hello file to appear + for _ in $(seq 100); do + if test -r "/proc/${createpid}/root/${MOUNT_POINT}/hello"; then + break + fi + sleep .1 + done + test -r "/proc/${createpid}/root/${MOUNT_POINT}/hello" + # prefer cat ... | MATCH so that we can see which PID is used + #shellcheck disable=SC2002 + cat "/proc/${createpid}/root/${MOUNT_POINT}/hello" | MATCH "Hello World!" + + # SIGTERM triggers a clean exit + kill $createpid + trap - EXIT + + # the create app will try to unmount the fuse filesystem while exiting, + # it will fail, as seccomp rules are blocking umount2 + echo "The snap exited with an error" + # SIGSYS - 31 on x86, wait exit status if killed by signal = 128 + + wait $createpid || test "$?" = "159" + + # verify that the mount_point was not removed from mount namespace of the snap + mountpath=$(nsenter "--mount=/run/snapd/ns/$NAME.mnt" cat /proc/mounts | \ + grep "$(basename "$MOUNT_POINT") fuse" | cut -f2 -d' ') + test -n "$mountpath" diff -Nru snapd-2.40/tests/main/interfaces-many-core-provided/task.yaml snapd-2.42.1/tests/main/interfaces-many-core-provided/task.yaml --- snapd-2.40/tests/main/interfaces-many-core-provided/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-many-core-provided/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -16,7 +16,7 @@ # - Debian sid amd64 VM # - Debian 9 amd64 VM # - TODO: All Fedora systems (for classic-only; unrelated error elsewhere) -systems: [ubuntu-core-16-64, ubuntu-14.04-32, ubuntu-16.04-64, ubuntu-18.04-64, ubuntu-18.04-32, ubuntu-*-amd64, ubuntu-*-armhf, ubuntu-*-arm64, ubuntu-*-i386, ubuntu-*-ppc64el, debian-*] +systems: [ubuntu-core-1*-64, ubuntu-14.04-32, ubuntu-16.04-64, ubuntu-18.04-64, ubuntu-18.04-32, ubuntu-*-amd64, ubuntu-*-armhf, ubuntu-*-arm64, ubuntu-*-i386, ubuntu-*-ppc64el, debian-*] # memory issue inside the adt environment backends: [-autopkgtest] diff -Nru snapd-2.40/tests/main/interfaces-many-snap-provided/task.yaml snapd-2.42.1/tests/main/interfaces-many-snap-provided/task.yaml --- snapd-2.40/tests/main/interfaces-many-snap-provided/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-many-snap-provided/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -16,7 +16,7 @@ # - Debian sid amd64 VM # - Debian 9 amd64 VM # - TODO: All Fedora systems (for classic-only; unrelated error elsewhere) -systems: [ubuntu-core-16-64, ubuntu-14.04-32, ubuntu-16.04-64, ubuntu-18.04-64, ubuntu-18.04-32, ubuntu-*-amd64, ubuntu-*-armhf, ubuntu-*-arm64, ubuntu-*-i386, ubuntu-*-ppc64el, debian-*] +systems: [ubuntu-core-1*-64, ubuntu-14.04-32, ubuntu-16.04-64, ubuntu-18.04-64, ubuntu-18.04-32, ubuntu-*-amd64, ubuntu-*-armhf, ubuntu-*-arm64, ubuntu-*-i386, ubuntu-*-ppc64el, debian-*] # memory issue inside the adt environment backends: [-autopkgtest] diff -Nru snapd-2.40/tests/main/interfaces-network-control/task.yaml snapd-2.42.1/tests/main/interfaces-network-control/task.yaml --- snapd-2.40/tests/main/interfaces-network-control/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-network-control/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -44,6 +44,7 @@ ip netns delete test-ns || true ip link delete veth0 || true + umount /run/netns || true execute: | #shellcheck source=tests/lib/network.sh diff -Nru snapd-2.40/tests/main/interfaces-network-control-ip-netns/task.yaml snapd-2.42.1/tests/main/interfaces-network-control-ip-netns/task.yaml --- snapd-2.40/tests/main/interfaces-network-control-ip-netns/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-network-control-ip-netns/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -41,3 +41,4 @@ # If this doesn't work maybe it is because the test didn't execute correctly snapd-hacker-toolbelt.busybox sh -c '/bin/ip netns delete canary' 2>/dev/null || true ip netns delete canary 2>/dev/null || true + umount /run/netns || true diff -Nru snapd-2.40/tests/main/interfaces-network-manager/task.yaml snapd-2.42.1/tests/main/interfaces-network-manager/task.yaml --- snapd-2.40/tests/main/interfaces-network-manager/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-network-manager/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -12,7 +12,7 @@ # boards because the (wifi) network is already managed by netplan # there and when n-m gets installed/removed it will hang when # trying to deconfigure the wifi network which is already owned. -systems: [ubuntu-core-16-64, ubuntu-core-16-32] +systems: [ubuntu-core-1*-64, ubuntu-core-1*-32] prepare: | echo "Give a network-manager snap is installed" diff -Nru snapd-2.40/tests/main/interfaces-openvswitch/task.yaml snapd-2.42.1/tests/main/interfaces-openvswitch/task.yaml --- snapd-2.40/tests/main/interfaces-openvswitch/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-openvswitch/task.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,111 +0,0 @@ -summary: Ensure that the openvswitch interface works. - -# amazon: no openvswitch package -systems: - - -ubuntu-core-* - - -amazon-* - -details: | - The openvswitch interface allows to task to the openvswitch socket (rw mode). - - A snap which defines a openvswitch plug must be shown in the interfaces list. - The plug must not be autoconnected on install and, as usual, must be able to be - reconnected. - - A snap declaring a plug on this interface must be able to do all the operations that - are carried through the socket, in this test we exercise bridge and port creation, - list and deletion. - -# marking as manual due to errors during prepare -manual: true - -prepare: | - #shellcheck source=tests/lib/pkgdb.sh - . "$TESTSLIB/pkgdb.sh" - - echo "Given openvswitch is installed" - distro_install_package --no-install-recommends openvswitch-switch - - # Ensure the openvswitch service is started which isn't the case by - # default on all distributions - systemctl enable --now openvswitch - - echo "And a snap declaring a plug on the openvswitch interface is installed" - snap install --edge test-snapd-openvswitch-consumer - - echo "And a tap interface is defined" - ip tuntap add tap1 mode tap - -restore: | - #shellcheck source=tests/lib/pkgdb.sh - . "$TESTSLIB/pkgdb.sh" - - ovs-vsctl del-port br0 tap1 || true - ovs-vsctl del-br br0 || true - - distro_purge_package openvswitch-switch - distro_auto_remove_packages - - ip link delete tap1 || true - -execute: | - echo "The interface is disconnected by default" - snap interfaces -i openvswitch | MATCH -- '^- +test-snapd-openvswitch-consumer:openvswitch' - - echo "When the plug is connected" - snap connect test-snapd-openvswitch-consumer:openvswitch - - echo "Then the snap is able to create a bridge" - test-snapd-openvswitch-consumer.ovs-vsctl add-br br0 - ovs-vsctl list-br | MATCH br0 - - echo "And the snap is able to create a port" - test-snapd-openvswitch-consumer.ovs-vsctl add-port br0 tap1 - ovs-vsctl list-ports br0 | MATCH tap1 - - echo "And the snap is able to delete a port" - test-snapd-openvswitch-consumer.ovs-vsctl del-port br0 tap1 - ovs-vsctl list-ports br0 | MATCH -v tap1 - - echo "And the snap is able to delete a bridge" - test-snapd-openvswitch-consumer.ovs-vsctl del-br br0 - ovs-vsctl list-br | MATCH -v br0 - - if [ "$(snap debug confinement)" = partial ] ; then - exit 0 - fi - - echo "When the plug is disconnected" - snap disconnect test-snapd-openvswitch-consumer:openvswitch - - echo "Then the snap is not able to create a bridge" - if test-snapd-openvswitch-consumer.ovs-vsctl add-br br0 2> bridge-creation.error; then - echo "Expected permission error accessing openvswitch socket with disconnected plug" - exit 1 - fi - MATCH 'database connection failed \(Permission denied\)' < bridge-creation.error - - ovs-vsctl add-br br0 - - echo "And the snap is not able to create a port" - if test-snapd-openvswitch-consumer.ovs-vsctl add-port br0 tap1 2> port-creation.error; then - echo "Expected permission error accessing openvswitch socket with disconnected plug" - exit 1 - fi - MATCH 'database connection failed \(Permission denied\)' < port-creation.error - - ovs-vsctl add-port br0 tap1 - - echo "And the snap is not able to delete a port" - if test-snapd-openvswitch-consumer.ovs-vsctl del-port br0 tap1 2> port-deletion.error; then - echo "Expected permission error accessing openvswitch socket with disconnected plug" - exit 1 - fi - MATCH 'database connection failed \(Permission denied\)' < port-deletion.error - - echo "And the snap is not able to delete a bridge" - if test-snapd-openvswitch-consumer.ovs-vsctl del-br br0 2> br-creation.error; then - echo "Expected permission error accessing openvswitch socket with disconnected plug" - exit 1 - fi - MATCH 'database connection failed \(Permission denied\)' < br-creation.error diff -Nru snapd-2.40/tests/main/interfaces-packagekit-control/task.yaml snapd-2.42.1/tests/main/interfaces-packagekit-control/task.yaml --- snapd-2.40/tests/main/interfaces-packagekit-control/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-packagekit-control/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,29 @@ +summary: The packagekit-control interface grants access to PackageKit + +systems: + # Ubuntu Core does not provide a packagekitd implementation + - -ubuntu-core-* + # Ubuntu 14.04 PackageKit seems to be too old + - -ubuntu-14.04-* + +execute: | + echo "Installing test-snapd-packagekit" + snap install --edge test-snapd-packagekit + + echo "The packagekit-control plug is disconnected by default" + snap connections test-snapd-packagekit | MATCH "packagekit-control +test-snapd-packagekit:packagekit-control +- +-" + if [ "$(snap debug confinement)" = strict ]; then + echo "Access to PackageKit is blocked without" + test-snapd-packagekit.pkcon backend-details | MATCH "Failed to contact PackageKit" + fi + + echo "The plug can be connected" + snap connect test-snapd-packagekit:packagekit-control + snap connections test-snapd-packagekit | MATCH "packagekit-control +test-snapd-packagekit:packagekit-control +:packagekit-control +manual" + + echo "With the plug connected it is possible to communicate with packagekit" + test-snapd-packagekit.pkcon backend-details | MATCH "Name:" + test-snapd-packagekit.pkcon resolve snapd | MATCH "Installed[[:space:]]+snapd" + +restore: | + snap remove test-snapd-packagekit diff -Nru snapd-2.40/tests/main/interfaces-snapd-control-with-manage/task.yaml snapd-2.42.1/tests/main/interfaces-snapd-control-with-manage/task.yaml --- snapd-2.40/tests/main/interfaces-snapd-control-with-manage/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-snapd-control-with-manage/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,12 +1,5 @@ summary: Ensure that the snapd-control "refresh-schedule" attribute works. -# FIXME: core18 right now does not have a canonical model. This means that it -# will not get a serial. We have code in devicestate.go that will only try to -# auto-refresh after the system has tried at least 3 times to get a serial. -# But this means this test will timeout because the first retry for the serial -# happens after 5min, then 10min, then 20min. -systems: [-ubuntu-core-18-*] - environment: BLOB_DIR: $(pwd)/fake-store-blobdir diff -Nru snapd-2.40/tests/main/interfaces-timeserver-control/task.yaml snapd-2.42.1/tests/main/interfaces-timeserver-control/task.yaml --- snapd-2.40/tests/main/interfaces-timeserver-control/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/interfaces-timeserver-control/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -16,7 +16,20 @@ # Install a snap declaring a plug on timeserver-control install_local test-snapd-timedate-control-consumer + if [[ "$SPREAD_SYSTEM" == ubuntu-19.10-* ]]; then + # ensure chrony is not installed as it interferes with + # the systemd-timesyncd.service + apt remove -y chrony + systemctl restart systemd-timesyncd.service + fi + restore: | + if [[ "$SPREAD_SYSTEM" == ubuntu-19.10-* ]]; then + # bring it back + apt install -y chrony + systemctl restart systemd-timesyncd.service + fi + # Restore the initial timeserver if [ -s timeserver.txt ]; then timedatectl set-ntp "$(cat timeserver.txt)" diff -Nru snapd-2.40/tests/main/kernel-snap-refresh-on-core/task.yaml snapd-2.42.1/tests/main/kernel-snap-refresh-on-core/task.yaml --- snapd-2.40/tests/main/kernel-snap-refresh-on-core/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/kernel-snap-refresh-on-core/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -20,13 +20,6 @@ grub-editenv list execute: | - wait_core_post_boot() { - # booted - while [ "$(bootenv snap_mode)" != "" ]; do - sleep 1 - done - } - same=$(snap info pc-kernel | awk " /^ ${KERNEL_CHANNEL}:/ {ch1=\$2} /^ ${NEW_KERNEL_CHANNEL}:/ {ch2=\$2} diff -Nru snapd-2.40/tests/main/listing/task.yaml snapd-2.42.1/tests/main/listing/task.yaml --- snapd-2.40/tests/main/listing/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/listing/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -8,38 +8,71 @@ snap set system experimental.parallel-instances=true install_local_as test-snapd-tools test-snapd-tools_foo -restore: +restore: | snap set system experimental.parallel-instances=null -# TODO: update test for core18 but its already pretty confusing as is -systems: [-ubuntu-core-18-*] - -# autopkgtest run only a subset of tests that deals with the integration -# with the distro -backends: [-autopkgtest] - execute: | echo "List prints core snap version" # most core versions should be like "16-2", so [0-9]{2}-[0-9.]+ # but edge will have a timestamp in there, "16.2+201701010932", so add an optional \+[0-9]+ to the end # *current* edge also has .git. and a hash snippet, so add an optional .git.[0-9a-f]+ to the already optional timestamp + + # Expressions for version and revision + NUMERIC_VERSION="[0-9]+(\.[0-9]+)*" + CORE_GIT_VERSION="[0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\\+git[0-9]+\\.[0-9a-f]+)?" + SNAPD_GIT_VERSION="+[0-9.]+(~[a-z0-9]+)?(\\+git[0-9]+\\.[0-9a-z]+)?" + FINAL_VERSION="[0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\\+[0-9]+\\.[0-9a-f]+)?" + SIDELOAD_REV="x[0-9]+" + NUMBER_REV="[0-9]+" + + # Default values + NAME=core + VERSION=$CORE_GIT_VERSION + REV=$NUMBER_REV + PUBLISHER="canonical\\*" + TRACKING=- + NOTES=core + #shellcheck disable=SC2166 - if [ "$SPREAD_BACKEND" = "linode" -o "$SPREAD_BACKEND" = "google" -o "$SPREAD_BACKEND" == "qemu" ] && [ "$SPREAD_SYSTEM" = "ubuntu-core-16-64" ]; then + if [ "$SPREAD_BACKEND" = "google" -o "$SPREAD_BACKEND" == "qemu" ] && [ "$SPREAD_SYSTEM" = "ubuntu-core-16-64" ]; then echo "With customized images the core snap is sideloaded" - expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+git[0-9]+\.[0-9a-f]+)? +x[0-9]+ +- +- +core.*$' + REV=$SIDELOAD_REV + PUBLISHER=- + + elif [ "$SPREAD_BACKEND" = "google" -o "$SPREAD_BACKEND" == "qemu" ] && [ "$SPREAD_SYSTEM" = "ubuntu-core-18-64" ]; then + echo "With customized images the snapd snap is sideloaded" + NAME=snapd + VERSION=$SNAPD_GIT_VERSION + REV=$SIDELOAD_REV + PUBLISHER=- + NOTES=snapd + elif [ "$SRU_VALIDATION" = "1" ] || [ -n "$PPA_VALIDATION_NAME" ]; then echo "When either sru or ppa validation is done the core snap is installed from the store" - expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+[0-9]+\.[0-9a-f]+)? +[0-9]+ +stable +canonical\* +core.*$' - elif [ "$SPREAD_BACKEND" = "external" ] || [ "$SPREAD_BACKEND" = "autopkgtest" ]; then - expected='^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\+git[0-9]+\.[0-9a-f]+)? +[0-9]+ +(edge|beta|candidate|stable) +canonical\* +core.*$' + VERSION=$FINAL_VERSION + TRACKING=stable + + elif [ "$SPREAD_BACKEND" = "external" ] && [[ "$SPREAD_SYSTEM" == ubuntu-core-16-* ]]; then + echo "On the external device the core snap tested could be in any track" + TRACKING="(edge|beta|candidate|stable)" + + elif [ "$SPREAD_BACKEND" = "external" ] && [[ "$SPREAD_SYSTEM" == ubuntu-core-18-* ]]; then + echo "On the external device the snapd snap tested could be in any track" + NAME=snapd + VERSION=$SNAPD_GIT_VERSION + TRACKING="(edge|beta|candidate|stable)" + NOTES=snapd + else - expected="^core .* [0-9]{2}-[0-9.]+(~[a-z0-9]+)?(\\+git[0-9]+\\.[0-9a-f]+)? +[0-9]+ +$CORE_CHANNEL +canonical\\* +core.*$" + TRACKING=$CORE_CHANNEL fi + + expected="^$NAME +$VERSION +$REV +$TRACKING +$PUBLISHER +$NOTES.*$" snap list --unicode=never | MATCH "$expected" echo "List prints installed snaps and versions" - snap list | MATCH '^test-snapd-tools +[0-9]+(\.[0-9]+)* +x[0-9]+ +- +- +- *$' - snap list | MATCH '^test-snapd-tools_foo +[0-9]+(\.[0-9]+)* +x[0-9]+ +- +- +- *$' + snap list | MATCH "^test-snapd-tools +$NUMERIC_VERSION +$SIDELOAD_REV +- +- +- *$" + snap list | MATCH "^test-snapd-tools_foo +$NUMERIC_VERSION +$SIDELOAD_REV +- +- +- *$" echo "Install test-snapd-tools again" #shellcheck source=tests/lib/snaps.sh @@ -47,7 +80,7 @@ install_local test-snapd-tools echo "And run snap list --all" - output=$(snap list --all |grep 'test-snapd-tools ') + output=$(snap list --all | grep 'test-snapd-tools ') if [ "$(grep -c test-snapd-tools <<< "$output")" != "2" ]; then echo "Expected two test-snapd-tools in the output, got:" echo "$output" diff -Nru snapd-2.40/tests/main/lxd/task.yaml snapd-2.42.1/tests/main/lxd/task.yaml --- snapd-2.40/tests/main/lxd/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/lxd/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -31,6 +31,8 @@ lxd.lxc stop my-ubuntu --force lxd.lxc delete my-ubuntu + lxd-tool undo-lxd-mount-changes + debug: | # shellcheck source=tests/lib/journalctl.sh . "$TESTSLIB/journalctl.sh" @@ -71,6 +73,9 @@ echo "Cleanup container" lxd.lxc exec my-ubuntu -- apt autoremove --purge -y snapd ubuntu-core-launcher + echo "Ensure apt is up-to-date" + lxd.lxc exec my-ubuntu -- apt update + echo "Install snapd" lxd.lxc exec my-ubuntu -- mkdir -p "$GOHOME" lxd.lxc file push "$GOHOME"/snapd_*.deb "my-ubuntu/$GOHOME/" diff -Nru snapd-2.40/tests/main/lxd-no-fuse/task.yaml snapd-2.42.1/tests/main/lxd-no-fuse/task.yaml --- snapd-2.40/tests/main/lxd-no-fuse/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/lxd-no-fuse/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,44 @@ +summary: Check that we give a useful error when fuse is missing in lxd + +# we just need a single system to verify this +systems: [ubuntu-18.04-64] + +execute: | + echo "Ensure we use the snap" + apt autoremove -y lxd + + echo "Install lxd" + snap install --candidate lxd + + echo "Create a trivial container using the lxd snap" + snap set lxd waitready.timeout=240 + lxd waitready + lxd init --auto + + echo "Setting up proxy for lxc" + if [ -n "${http_proxy:-}" ]; then + lxd.lxc config set core.proxy_http "$http_proxy" + fi + if [ -n "${https_proxy:-}" ]; then + lxd.lxc config set core.proxy_https "$http_proxy" + fi + + lxd.lxc launch "images:ubuntu/18.04" my-ubuntu + + echo "Remove fuse to trigger the fuse sanity check" + lxd.lxc exec my-ubuntu -- apt autoremove -y fuse + + echo "Install snapd" + lxd.lxc exec my-ubuntu -- mkdir -p "$GOHOME" + lxd.lxc file push "$GOHOME"/snapd_*.deb "my-ubuntu/$GOHOME/" + lxd.lxc exec my-ubuntu -- apt install -y "$GOHOME"/snapd_*.deb + + echo "And validate that we get a useful error" + if lxd.lxc exec my-ubuntu snap install test-snapd-tools 2> err.txt; then + echo "snap install should fail but it did not?" + exit 1 + fi + MATCH 'The "fuse" filesystem is required' < err.txt + +restore: | + lxd-tool undo-lxd-mount-changes diff -Nru snapd-2.40/tests/main/lxd-snapfuse/task.yaml snapd-2.42.1/tests/main/lxd-snapfuse/task.yaml --- snapd-2.40/tests/main/lxd-snapfuse/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/lxd-snapfuse/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,50 @@ +summary: Check snapfuse works + +# we just need a single system to verify this +systems: [ubuntu-18.04-64] + +restore: | + lxd-tool undo-lxd-mount-changes + +execute: | + echo "Ensure we use the snap" + apt autoremove -y lxd + + echo "Ensure we have no squashfuse package installed" + apt autoremove -y squashfuse + + echo "Install lxd" + snap install --candidate lxd + + echo "Create a trivial container using the lxd snap" + snap set lxd waitready.timeout=240 + lxd waitready + lxd init --auto + + echo "Setting up proxy for lxc" + if [ -n "${http_proxy:-}" ]; then + lxd.lxc config set core.proxy_http "$http_proxy" + fi + if [ -n "${https_proxy:-}" ]; then + lxd.lxc config set core.proxy_https "$http_proxy" + fi + + lxd.lxc launch "images:ubuntu/18.04" my-ubuntu + + echo "Install snapd" + lxd.lxc exec my-ubuntu -- mkdir -p "$GOHOME" + lxd.lxc file push "$GOHOME"/snapd_*.deb "my-ubuntu/$GOHOME/" + lxd.lxc exec my-ubuntu -- apt install -y "$GOHOME"/snapd_*.deb + + echo "And validate that we can use snaps" + lxd.lxc exec my-ubuntu -- snap install test-snapd-tools + echo "And we can run snaps as regular users" + lxd.lxc exec my-ubuntu -- su -c "/snap/bin/test-snapd-tools.echo from-the-inside" ubuntu | MATCH from-the-inside + echo "And as root" + lxd.lxc exec my-ubuntu -- test-snapd-tools.echo from-the-inside | MATCH from-the-inside + + echo "And snapfuse is actually running" + ps afx | MATCH snapfuse + + echo "We can also remove snaps successfully" + lxd.lxc exec my-ubuntu -- snap remove test-snapd-tools diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/HOST.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/HOST.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/HOST.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/HOST.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,39 @@ +0:0 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +1:0 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime shared:3 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime shared:4 - mqueue mqueue rw +1:3 / /dev/pts rw,nosuid,noexec,relatime shared:5 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:4 / /dev/shm rw,nosuid,nodev shared:6 - tmpfs tmpfs rw +1:5 / /proc rw,nosuid,nodev,noexec,relatime shared:7 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime shared:8 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime shared:9 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime shared:10 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +1:9 / /run rw,nosuid,noexec,relatime shared:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/cgmanager/fs rw,relatime shared:12 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +1:11 / /run/lock rw,nosuid,nodev,noexec,relatime shared:13 - tmpfs tmpfs rw,size=VARIABLE +1:12 / /run/rpc_pipefs rw,relatime shared:14 - rpc_pipefs sunrpc rw +1:13 / /run/user/0 rw,nosuid,nodev,relatime shared:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /snap/core/1 ro,nodev,relatime shared:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime shared:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime shared:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime shared:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime shared:20 - squashfs /dev/loop4 ro +1:14 / /sys rw,nosuid,nodev,noexec,relatime shared:21 - sysfs sysfs rw +1:15 / /sys/fs/cgroup rw shared:22 - tmpfs tmpfs rw,mode=755 +1:16 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:23 - cgroup cgroup rw,blkio +1:17 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:24 - cgroup cgroup rw,cpu,cpuacct +1:18 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:25 - cgroup cgroup rw,cpuset +1:19 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:26 - cgroup cgroup rw,devices +1:20 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:27 - cgroup cgroup rw,freezer +1:21 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:28 - cgroup cgroup rw,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb +1:22 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:29 - cgroup cgroup rw,memory +1:23 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:30 - cgroup cgroup rw,net_cls,net_prio +1:24 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:31 - cgroup cgroup rw,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event +1:25 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:32 - cgroup cgroup rw,pids,release_agent=/run/cgmanager/agents/cgm-release-agent.pids +1:26 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime shared:33 - cgroup cgroup rw,rdma,release_agent=/run/cgmanager/agents/cgm-release-agent.rdma +1:27 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:34 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +1:28 / /sys/fs/fuse/connections rw,relatime shared:35 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:36 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime shared:37 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime shared:38 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:39 - securityfs securityfs rw diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-16.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-16.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-16.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-16.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,76 @@ +2:0 / / ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +1:0 / /dev rw,nosuid,relatime master:2 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime master:3 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime master:4 - mqueue mqueue rw +1:33 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:3 / /dev/pts rw,nosuid,noexec,relatime master:5 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:33 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:4 / /dev/shm rw,nosuid,nodev master:6 - tmpfs tmpfs rw +0:0 /etc /etc rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:0 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:0 /etc/ssl /etc/ssl ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +0:0 /home /home rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /lib/firmware /lib/firmware rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /lib/modules /lib/modules rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /media /media rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /mnt /mnt rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:5 / /proc rw,nosuid,nodev,noexec,relatime master:7 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime master:8 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime master:9 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime master:10 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +0:0 /root /root rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:9 / /run rw,nosuid,noexec,relatime master:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/cgmanager/fs rw,relatime master:12 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +1:11 / /run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:9 /netns /run/netns rw,nosuid,noexec,relatime shared:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:12 / /run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:13 / /run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +0:0 /snap /snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:0 / /snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +1:14 / /sys rw,nosuid,nodev,noexec,relatime master:21 - sysfs sysfs rw +1:15 / /sys/fs/cgroup rw master:22 - tmpfs tmpfs rw,mode=755 +1:16 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,blkio +1:17 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,cpu,cpuacct +1:18 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset +1:19 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,devices +1:20 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:27 - cgroup cgroup rw,freezer +1:21 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:28 - cgroup cgroup rw,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb +1:22 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:29 - cgroup cgroup rw,memory +1:23 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:30 - cgroup cgroup rw,net_cls,net_prio +1:24 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:31 - cgroup cgroup rw,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event +1:25 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:32 - cgroup cgroup rw,pids,release_agent=/run/cgmanager/agents/cgm-release-agent.pids +1:26 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime master:33 - cgroup cgroup rw,rdma,release_agent=/run/cgmanager/agents/cgm-release-agent.rdma +1:27 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:34 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +1:28 / /sys/fs/fuse/connections rw,relatime master:35 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:36 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime master:37 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime master:38 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:39 - securityfs securityfs rw +0:0 /tmp /tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /tmp/snap.test-snapd-mountinfo-core16/tmp /tmp rw,relatime - ext4 /dev/sda1 rw,data=ordered +1:34 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +2:0 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +1:35 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd /var/lib/snapd rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 / /var/lib/snapd/hostfs rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda1 rw,data=ordered +1:9 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /var/lib/snapd/hostfs/run/cgmanager/fs rw,relatime master:12 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +1:11 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:12 / /var/lib/snapd/hostfs/run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:13 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +0:0 /var/log /var/log rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/snap /var/snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/tmp /var/tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-18.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-18.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-18.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-18.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,77 @@ +2:1 / / ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +1:0 / /dev rw,nosuid,relatime master:2 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime master:3 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime master:4 - mqueue mqueue rw +1:33 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:3 / /dev/pts rw,nosuid,noexec,relatime master:5 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:33 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:4 / /dev/shm rw,nosuid,nodev master:6 - tmpfs tmpfs rw +0:0 /etc /etc rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:1 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:1 /etc/ssl /etc/ssl ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +0:0 /home /home rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /lib/firmware /lib/firmware rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /lib/modules /lib/modules rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /media /media rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /mnt /mnt rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:5 / /proc rw,nosuid,nodev,noexec,relatime master:7 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime master:8 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime master:9 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime master:10 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +0:0 /root /root rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:9 / /run rw,nosuid,noexec,relatime master:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/cgmanager/fs rw,relatime master:12 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +1:11 / /run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:9 /netns /run/netns rw,nosuid,noexec,relatime shared:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:12 / /run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:13 / /run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +0:0 /snap /snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:0 / /snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +1:14 / /sys rw,nosuid,nodev,noexec,relatime master:21 - sysfs sysfs rw +1:15 / /sys/fs/cgroup rw master:22 - tmpfs tmpfs rw,mode=755 +1:16 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,blkio +1:17 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,cpu,cpuacct +1:18 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset +1:19 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,devices +1:20 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:27 - cgroup cgroup rw,freezer +1:21 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:28 - cgroup cgroup rw,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb +1:22 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:29 - cgroup cgroup rw,memory +1:23 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:30 - cgroup cgroup rw,net_cls,net_prio +1:24 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:31 - cgroup cgroup rw,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event +1:25 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:32 - cgroup cgroup rw,pids,release_agent=/run/cgmanager/agents/cgm-release-agent.pids +1:26 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime master:33 - cgroup cgroup rw,rdma,release_agent=/run/cgmanager/agents/cgm-release-agent.rdma +1:27 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:34 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +1:28 / /sys/fs/fuse/connections rw,relatime master:35 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:36 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime master:37 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime master:38 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:39 - securityfs securityfs rw +0:0 /tmp /tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /tmp/snap.test-snapd-mountinfo-core18/tmp /tmp rw,relatime - ext4 /dev/sda1 rw,data=ordered +2:0 /usr/lib/snapd /usr/lib/snapd ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +1:34 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +2:1 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +1:35 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd /var/lib/snapd rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 / /var/lib/snapd/hostfs rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda1 rw,data=ordered +1:9 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /var/lib/snapd/hostfs/run/cgmanager/fs rw,relatime master:12 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +1:11 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:12 / /var/lib/snapd/hostfs/run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:13 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +0:0 /var/log /var/log rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/snap /var/snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/tmp /var/tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-C7.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-C7.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-C7.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/PER-SNAP-C7.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,39 @@ +0:0 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +1:0 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime shared:3 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime shared:4 - mqueue mqueue rw +1:3 / /dev/pts rw,nosuid,noexec,relatime shared:5 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:4 / /dev/shm rw,nosuid,nodev shared:6 - tmpfs tmpfs rw +1:5 / /proc rw,nosuid,nodev,noexec,relatime shared:7 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime shared:8 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime shared:9 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime shared:10 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +1:9 / /run rw,nosuid,noexec,relatime shared:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/cgmanager/fs rw,relatime shared:12 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +1:11 / /run/lock rw,nosuid,nodev,noexec,relatime shared:13 - tmpfs tmpfs rw,size=VARIABLE +1:12 / /run/rpc_pipefs rw,relatime shared:14 - rpc_pipefs sunrpc rw +1:13 / /run/user/0 rw,nosuid,nodev,relatime shared:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /snap/core/1 ro,nodev,relatime shared:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime shared:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime shared:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime shared:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime shared:20 - squashfs /dev/loop4 ro +1:14 / /sys rw,nosuid,nodev,noexec,relatime shared:21 - sysfs sysfs rw +1:15 / /sys/fs/cgroup rw shared:22 - tmpfs tmpfs rw,mode=755 +1:16 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:23 - cgroup cgroup rw,blkio +1:17 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:24 - cgroup cgroup rw,cpu,cpuacct +1:18 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:25 - cgroup cgroup rw,cpuset +1:19 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:26 - cgroup cgroup rw,devices +1:20 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:27 - cgroup cgroup rw,freezer +1:21 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:28 - cgroup cgroup rw,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb +1:22 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:29 - cgroup cgroup rw,memory +1:23 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:30 - cgroup cgroup rw,net_cls,net_prio +1:24 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:31 - cgroup cgroup rw,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event +1:25 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:32 - cgroup cgroup rw,pids,release_agent=/run/cgmanager/agents/cgm-release-agent.pids +1:26 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime shared:33 - cgroup cgroup rw,rdma,release_agent=/run/cgmanager/agents/cgm-release-agent.rdma +1:27 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:34 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +1:28 / /sys/fs/fuse/connections rw,relatime shared:35 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:36 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime shared:37 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime shared:38 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:39 - securityfs securityfs rw diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-16.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-16.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-16.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-16.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,76 @@ +2:0 / / ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +1:0 / /dev rw,nosuid,relatime master:2 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime master:3 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime master:4 - mqueue mqueue rw +1:33 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:3 / /dev/pts rw,nosuid,noexec,relatime master:5 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:33 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:4 / /dev/shm rw,nosuid,nodev master:6 - tmpfs tmpfs rw +0:0 /etc /etc rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:0 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:0 /etc/ssl /etc/ssl ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +0:0 /home /home rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /lib/firmware /lib/firmware rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /lib/modules /lib/modules rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /media /media rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /mnt /mnt rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:5 / /proc rw,nosuid,nodev,noexec,relatime master:7 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime master:8 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime master:9 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime master:10 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +0:0 /root /root rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:9 / /run rw,nosuid,noexec,relatime master:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/cgmanager/fs rw,relatime master:12 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +1:11 / /run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:9 /netns /run/netns rw,nosuid,noexec,relatime shared:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:12 / /run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:13 / /run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +0:0 /snap /snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:0 / /snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +1:14 / /sys rw,nosuid,nodev,noexec,relatime master:21 - sysfs sysfs rw +1:15 / /sys/fs/cgroup rw master:22 - tmpfs tmpfs rw,mode=755 +1:16 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,blkio +1:17 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,cpu,cpuacct +1:18 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset +1:19 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,devices +1:20 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:27 - cgroup cgroup rw,freezer +1:21 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:28 - cgroup cgroup rw,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb +1:22 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:29 - cgroup cgroup rw,memory +1:23 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:30 - cgroup cgroup rw,net_cls,net_prio +1:24 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:31 - cgroup cgroup rw,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event +1:25 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:32 - cgroup cgroup rw,pids,release_agent=/run/cgmanager/agents/cgm-release-agent.pids +1:26 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime master:33 - cgroup cgroup rw,rdma,release_agent=/run/cgmanager/agents/cgm-release-agent.rdma +1:27 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:34 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +1:28 / /sys/fs/fuse/connections rw,relatime master:35 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:36 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime master:37 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime master:38 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:39 - securityfs securityfs rw +0:0 /tmp /tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /tmp/snap.test-snapd-mountinfo-core16/tmp /tmp rw,relatime - ext4 /dev/sda1 rw,data=ordered +1:34 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +2:0 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +1:35 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd /var/lib/snapd rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda1 rw,data=ordered +0:0 / /var/lib/snapd/hostfs rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:9 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /var/lib/snapd/hostfs/run/cgmanager/fs rw,relatime master:12 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +1:11 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:12 / /var/lib/snapd/hostfs/run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:13 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +0:0 /var/log /var/log rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/snap /var/snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/tmp /var/tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-18.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-18.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-18.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-18.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,77 @@ +2:1 / / ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +1:0 / /dev rw,nosuid,relatime master:2 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime master:3 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime master:4 - mqueue mqueue rw +1:33 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:3 / /dev/pts rw,nosuid,noexec,relatime master:5 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:33 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:4 / /dev/shm rw,nosuid,nodev master:6 - tmpfs tmpfs rw +0:0 /etc /etc rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:1 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:1 /etc/ssl /etc/ssl ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +0:0 /home /home rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /lib/firmware /lib/firmware rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /lib/modules /lib/modules rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /media /media rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /mnt /mnt rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:5 / /proc rw,nosuid,nodev,noexec,relatime master:7 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime master:8 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime master:9 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime master:10 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +0:0 /root /root rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:9 / /run rw,nosuid,noexec,relatime master:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/cgmanager/fs rw,relatime master:12 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +1:11 / /run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:9 /netns /run/netns rw,nosuid,noexec,relatime shared:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:12 / /run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:13 / /run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +0:0 /snap /snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:0 / /snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +1:14 / /sys rw,nosuid,nodev,noexec,relatime master:21 - sysfs sysfs rw +1:15 / /sys/fs/cgroup rw master:22 - tmpfs tmpfs rw,mode=755 +1:16 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,blkio +1:17 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,cpu,cpuacct +1:18 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset +1:19 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,devices +1:20 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:27 - cgroup cgroup rw,freezer +1:21 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:28 - cgroup cgroup rw,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb +1:22 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:29 - cgroup cgroup rw,memory +1:23 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:30 - cgroup cgroup rw,net_cls,net_prio +1:24 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:31 - cgroup cgroup rw,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event +1:25 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:32 - cgroup cgroup rw,pids,release_agent=/run/cgmanager/agents/cgm-release-agent.pids +1:26 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime master:33 - cgroup cgroup rw,rdma,release_agent=/run/cgmanager/agents/cgm-release-agent.rdma +1:27 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:34 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +1:28 / /sys/fs/fuse/connections rw,relatime master:35 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:36 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime master:37 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime master:38 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:39 - securityfs securityfs rw +0:0 /tmp /tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /tmp/snap.test-snapd-mountinfo-core18/tmp /tmp rw,relatime - ext4 /dev/sda1 rw,data=ordered +2:0 /usr/lib/snapd /usr/lib/snapd ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +1:34 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +2:1 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +1:35 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd /var/lib/snapd rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda1 rw,data=ordered +0:0 / /var/lib/snapd/hostfs rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:9 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /var/lib/snapd/hostfs/run/cgmanager/fs rw,relatime master:12 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +1:11 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:12 / /var/lib/snapd/hostfs/run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:13 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +0:0 /var/log /var/log rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/snap /var/snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/tmp /var/tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-C7.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-C7.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-C7.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-16.04-64/PER-USER-C7.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,39 @@ +0:0 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +1:0 / /dev rw,nosuid,relatime shared:2 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime shared:3 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime shared:4 - mqueue mqueue rw +1:3 / /dev/pts rw,nosuid,noexec,relatime shared:5 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:4 / /dev/shm rw,nosuid,nodev shared:6 - tmpfs tmpfs rw +1:5 / /proc rw,nosuid,nodev,noexec,relatime shared:7 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime shared:8 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime shared:9 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime shared:10 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +1:9 / /run rw,nosuid,noexec,relatime shared:11 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/cgmanager/fs rw,relatime shared:12 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +1:11 / /run/lock rw,nosuid,nodev,noexec,relatime shared:13 - tmpfs tmpfs rw,size=VARIABLE +1:12 / /run/rpc_pipefs rw,relatime shared:14 - rpc_pipefs sunrpc rw +1:13 / /run/user/0 rw,nosuid,nodev,relatime shared:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /snap/core/1 ro,nodev,relatime shared:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime shared:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime shared:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime shared:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime shared:20 - squashfs /dev/loop4 ro +1:14 / /sys rw,nosuid,nodev,noexec,relatime shared:21 - sysfs sysfs rw +1:15 / /sys/fs/cgroup rw shared:22 - tmpfs tmpfs rw,mode=755 +1:16 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:23 - cgroup cgroup rw,blkio +1:17 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:24 - cgroup cgroup rw,cpu,cpuacct +1:18 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:25 - cgroup cgroup rw,cpuset +1:19 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:26 - cgroup cgroup rw,devices +1:20 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:27 - cgroup cgroup rw,freezer +1:21 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:28 - cgroup cgroup rw,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb +1:22 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:29 - cgroup cgroup rw,memory +1:23 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:30 - cgroup cgroup rw,net_cls,net_prio +1:24 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:31 - cgroup cgroup rw,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event +1:25 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:32 - cgroup cgroup rw,pids,release_agent=/run/cgmanager/agents/cgm-release-agent.pids +1:26 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime shared:33 - cgroup cgroup rw,rdma,release_agent=/run/cgmanager/agents/cgm-release-agent.rdma +1:27 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:34 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +1:28 / /sys/fs/fuse/connections rw,relatime shared:35 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:36 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime shared:37 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime shared:38 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:39 - securityfs securityfs rw diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/HOST.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/HOST.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/HOST.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/HOST.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,40 @@ +0:0 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +0:1 / /boot/efi rw,relatime shared:2 - vfat /dev/sda15 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 / /dev rw,nosuid,relatime shared:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime shared:4 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime shared:5 - mqueue mqueue rw +1:3 / /dev/pts rw,nosuid,noexec,relatime shared:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:4 / /dev/shm rw,nosuid,nodev shared:7 - tmpfs tmpfs rw +1:5 / /proc rw,nosuid,nodev,noexec,relatime shared:8 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime shared:9 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime shared:10 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime shared:11 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +1:9 / /run rw,nosuid,noexec,relatime shared:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/lock rw,nosuid,nodev,noexec,relatime shared:13 - tmpfs tmpfs rw,size=VARIABLE +1:11 / /run/rpc_pipefs rw,relatime shared:14 - rpc_pipefs sunrpc rw +1:12 / /run/user/0 rw,nosuid,nodev,relatime shared:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /snap/core/1 ro,nodev,relatime shared:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime shared:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime shared:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime shared:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime shared:20 - squashfs /dev/loop4 ro +1:13 / /sys rw,nosuid,nodev,noexec,relatime shared:21 - sysfs sysfs rw +1:14 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:22 - tmpfs tmpfs ro,mode=755 +1:15 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:23 - cgroup cgroup rw,blkio +1:16 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:24 - cgroup cgroup rw,cpu,cpuacct +1:17 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:25 - cgroup cgroup rw,cpuset +1:18 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:26 - cgroup cgroup rw,devices +1:19 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:27 - cgroup cgroup rw,freezer +1:20 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:28 - cgroup cgroup rw,hugetlb +1:21 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:29 - cgroup cgroup rw,memory +1:22 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:30 - cgroup cgroup rw,net_cls,net_prio +1:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:31 - cgroup cgroup rw,perf_event +1:24 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:32 - cgroup cgroup rw,pids +1:25 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime shared:33 - cgroup cgroup rw,rdma +1:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:34 - cgroup cgroup rw,xattr,name=systemd +1:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime shared:35 - cgroup2 cgroup rw,nsdelegate +1:28 / /sys/fs/fuse/connections rw,relatime shared:36 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:37 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime shared:38 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime shared:39 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:40 - securityfs securityfs rw diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-16.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-16.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-16.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-16.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,75 @@ +2:0 / / ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +1:0 / /dev rw,nosuid,relatime master:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime master:4 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime master:5 - mqueue mqueue rw +1:33 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:3 / /dev/pts rw,nosuid,noexec,relatime master:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:33 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:4 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw +0:0 /etc /etc rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:0 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:0 /etc/ssl /etc/ssl ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +0:0 /home /home rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /lib/modules /lib/modules rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /media /media rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /mnt /mnt rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:5 / /proc rw,nosuid,nodev,noexec,relatime master:8 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime master:9 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime master:10 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime master:11 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +0:0 /root /root rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:9 / /run rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:9 /netns /run/netns rw,nosuid,noexec,relatime shared:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:11 / /run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:12 / /run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +0:0 /snap /snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:0 / /snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +1:13 / /sys rw,nosuid,nodev,noexec,relatime master:21 - sysfs sysfs rw +1:14 / /sys/fs/cgroup ro,nosuid,nodev,noexec master:22 - tmpfs tmpfs ro,mode=755 +1:15 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,blkio +1:16 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,cpu,cpuacct +1:17 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset +1:18 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,devices +1:19 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:27 - cgroup cgroup rw,freezer +1:20 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:28 - cgroup cgroup rw,hugetlb +1:21 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:29 - cgroup cgroup rw,memory +1:22 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:30 - cgroup cgroup rw,net_cls,net_prio +1:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:31 - cgroup cgroup rw,perf_event +1:24 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:32 - cgroup cgroup rw,pids +1:25 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime master:33 - cgroup cgroup rw,rdma +1:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:34 - cgroup cgroup rw,xattr,name=systemd +1:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime master:35 - cgroup2 cgroup rw,nsdelegate +1:28 / /sys/fs/fuse/connections rw,relatime master:36 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:37 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime master:38 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime master:39 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:40 - securityfs securityfs rw +0:0 /tmp /tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /tmp/snap.test-snapd-mountinfo-core16/tmp /tmp rw,relatime - ext4 /dev/sda1 rw,data=ordered +1:34 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +2:0 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +1:35 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd /var/lib/snapd rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 / /var/lib/snapd/hostfs rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda1 rw,data=ordered +0:1 / /var/lib/snapd/hostfs/boot/efi rw,relatime master:2 - vfat /dev/sda15 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:9 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:11 / /var/lib/snapd/hostfs/run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:12 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +0:0 /var/log /var/log rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/snap /var/snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/tmp /var/tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-18.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-18.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-18.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-18.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,76 @@ +2:1 / / ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +1:0 / /dev rw,nosuid,relatime master:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime master:4 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime master:5 - mqueue mqueue rw +1:33 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:3 / /dev/pts rw,nosuid,noexec,relatime master:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:33 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:4 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw +0:0 /etc /etc rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:1 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:1 /etc/ssl /etc/ssl ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +0:0 /home /home rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /lib/modules /lib/modules rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /media /media rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /mnt /mnt rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:5 / /proc rw,nosuid,nodev,noexec,relatime master:8 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime master:9 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime master:10 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime master:11 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +0:0 /root /root rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:9 / /run rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:9 /netns /run/netns rw,nosuid,noexec,relatime shared:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:11 / /run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:12 / /run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +0:0 /snap /snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:0 / /snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +1:13 / /sys rw,nosuid,nodev,noexec,relatime master:21 - sysfs sysfs rw +1:14 / /sys/fs/cgroup ro,nosuid,nodev,noexec master:22 - tmpfs tmpfs ro,mode=755 +1:15 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,blkio +1:16 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,cpu,cpuacct +1:17 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset +1:18 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,devices +1:19 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:27 - cgroup cgroup rw,freezer +1:20 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:28 - cgroup cgroup rw,hugetlb +1:21 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:29 - cgroup cgroup rw,memory +1:22 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:30 - cgroup cgroup rw,net_cls,net_prio +1:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:31 - cgroup cgroup rw,perf_event +1:24 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:32 - cgroup cgroup rw,pids +1:25 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime master:33 - cgroup cgroup rw,rdma +1:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:34 - cgroup cgroup rw,xattr,name=systemd +1:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime master:35 - cgroup2 cgroup rw,nsdelegate +1:28 / /sys/fs/fuse/connections rw,relatime master:36 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:37 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime master:38 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime master:39 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:40 - securityfs securityfs rw +0:0 /tmp /tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /tmp/snap.test-snapd-mountinfo-core18/tmp /tmp rw,relatime - ext4 /dev/sda1 rw,data=ordered +2:0 /usr/lib/snapd /usr/lib/snapd ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +1:34 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +2:1 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +1:35 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd /var/lib/snapd rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 / /var/lib/snapd/hostfs rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda1 rw,data=ordered +0:1 / /var/lib/snapd/hostfs/boot/efi rw,relatime master:2 - vfat /dev/sda15 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:9 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:11 / /var/lib/snapd/hostfs/run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:12 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +0:0 /var/log /var/log rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/snap /var/snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/tmp /var/tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-C7.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-C7.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-C7.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/PER-SNAP-C7.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,40 @@ +0:0 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +0:1 / /boot/efi rw,relatime shared:2 - vfat /dev/sda15 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 / /dev rw,nosuid,relatime shared:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime shared:4 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime shared:5 - mqueue mqueue rw +1:3 / /dev/pts rw,nosuid,noexec,relatime shared:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:4 / /dev/shm rw,nosuid,nodev shared:7 - tmpfs tmpfs rw +1:5 / /proc rw,nosuid,nodev,noexec,relatime shared:8 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime shared:9 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime shared:10 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime shared:11 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +1:9 / /run rw,nosuid,noexec,relatime shared:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/lock rw,nosuid,nodev,noexec,relatime shared:13 - tmpfs tmpfs rw,size=VARIABLE +1:11 / /run/rpc_pipefs rw,relatime shared:14 - rpc_pipefs sunrpc rw +1:12 / /run/user/0 rw,nosuid,nodev,relatime shared:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /snap/core/1 ro,nodev,relatime shared:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime shared:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime shared:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime shared:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime shared:20 - squashfs /dev/loop4 ro +1:13 / /sys rw,nosuid,nodev,noexec,relatime shared:21 - sysfs sysfs rw +1:14 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:22 - tmpfs tmpfs ro,mode=755 +1:15 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:23 - cgroup cgroup rw,blkio +1:16 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:24 - cgroup cgroup rw,cpu,cpuacct +1:17 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:25 - cgroup cgroup rw,cpuset +1:18 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:26 - cgroup cgroup rw,devices +1:19 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:27 - cgroup cgroup rw,freezer +1:20 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:28 - cgroup cgroup rw,hugetlb +1:21 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:29 - cgroup cgroup rw,memory +1:22 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:30 - cgroup cgroup rw,net_cls,net_prio +1:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:31 - cgroup cgroup rw,perf_event +1:24 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:32 - cgroup cgroup rw,pids +1:25 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime shared:33 - cgroup cgroup rw,rdma +1:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:34 - cgroup cgroup rw,xattr,name=systemd +1:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime shared:35 - cgroup2 cgroup rw,nsdelegate +1:28 / /sys/fs/fuse/connections rw,relatime shared:36 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:37 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime shared:38 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime shared:39 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:40 - securityfs securityfs rw diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-16.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-16.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-16.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-16.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,75 @@ +2:0 / / ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +1:0 / /dev rw,nosuid,relatime master:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime master:4 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime master:5 - mqueue mqueue rw +1:33 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:3 / /dev/pts rw,nosuid,noexec,relatime master:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:33 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:4 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw +0:0 /etc /etc rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:0 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:0 /etc/ssl /etc/ssl ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +0:0 /home /home rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /lib/modules /lib/modules rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /media /media rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /mnt /mnt rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:5 / /proc rw,nosuid,nodev,noexec,relatime master:8 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime master:9 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime master:10 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime master:11 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +0:0 /root /root rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:9 / /run rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:9 /netns /run/netns rw,nosuid,noexec,relatime shared:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:11 / /run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:12 / /run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +0:0 /snap /snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:0 / /snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +1:13 / /sys rw,nosuid,nodev,noexec,relatime master:21 - sysfs sysfs rw +1:14 / /sys/fs/cgroup ro,nosuid,nodev,noexec master:22 - tmpfs tmpfs ro,mode=755 +1:15 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,blkio +1:16 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,cpu,cpuacct +1:17 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset +1:18 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,devices +1:19 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:27 - cgroup cgroup rw,freezer +1:20 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:28 - cgroup cgroup rw,hugetlb +1:21 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:29 - cgroup cgroup rw,memory +1:22 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:30 - cgroup cgroup rw,net_cls,net_prio +1:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:31 - cgroup cgroup rw,perf_event +1:24 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:32 - cgroup cgroup rw,pids +1:25 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime master:33 - cgroup cgroup rw,rdma +1:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:34 - cgroup cgroup rw,xattr,name=systemd +1:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime master:35 - cgroup2 cgroup rw,nsdelegate +1:28 / /sys/fs/fuse/connections rw,relatime master:36 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:37 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime master:38 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime master:39 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:40 - securityfs securityfs rw +0:0 /tmp /tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /tmp/snap.test-snapd-mountinfo-core16/tmp /tmp rw,relatime - ext4 /dev/sda1 rw,data=ordered +1:34 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +2:0 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +1:35 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd /var/lib/snapd rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda1 rw,data=ordered +0:0 / /var/lib/snapd/hostfs rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:1 / /var/lib/snapd/hostfs/boot/efi rw,relatime master:2 - vfat /dev/sda15 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:9 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:11 / /var/lib/snapd/hostfs/run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:12 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +0:0 /var/log /var/log rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/snap /var/snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/tmp /var/tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-18.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-18.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-18.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-18.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,76 @@ +2:1 / / ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +1:0 / /dev rw,nosuid,relatime master:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime master:4 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime master:5 - mqueue mqueue rw +1:33 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:3 / /dev/pts rw,nosuid,noexec,relatime master:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:33 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +1:4 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw +0:0 /etc /etc rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:1 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:1 /etc/ssl /etc/ssl ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +0:0 /home /home rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /lib/modules /lib/modules rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /media /media rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /mnt /mnt rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:5 / /proc rw,nosuid,nodev,noexec,relatime master:8 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime master:9 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime master:10 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime master:11 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +0:0 /root /root rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +1:9 / /run rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:9 /netns /run/netns rw,nosuid,noexec,relatime shared:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:11 / /run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:12 / /run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +0:0 /snap /snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +2:0 / /snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +1:13 / /sys rw,nosuid,nodev,noexec,relatime master:21 - sysfs sysfs rw +1:14 / /sys/fs/cgroup ro,nosuid,nodev,noexec master:22 - tmpfs tmpfs ro,mode=755 +1:15 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:23 - cgroup cgroup rw,blkio +1:16 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:24 - cgroup cgroup rw,cpu,cpuacct +1:17 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:25 - cgroup cgroup rw,cpuset +1:18 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:26 - cgroup cgroup rw,devices +1:19 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:27 - cgroup cgroup rw,freezer +1:20 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:28 - cgroup cgroup rw,hugetlb +1:21 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:29 - cgroup cgroup rw,memory +1:22 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:30 - cgroup cgroup rw,net_cls,net_prio +1:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:31 - cgroup cgroup rw,perf_event +1:24 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:32 - cgroup cgroup rw,pids +1:25 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime master:33 - cgroup cgroup rw,rdma +1:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:34 - cgroup cgroup rw,xattr,name=systemd +1:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime master:35 - cgroup2 cgroup rw,nsdelegate +1:28 / /sys/fs/fuse/connections rw,relatime master:36 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:37 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime master:38 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime master:39 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:40 - securityfs securityfs rw +0:0 /tmp /tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /tmp/snap.test-snapd-mountinfo-core18/tmp /tmp rw,relatime - ext4 /dev/sda1 rw,data=ordered +2:0 /usr/lib/snapd /usr/lib/snapd ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +1:34 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +2:1 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +1:35 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd /var/lib/snapd rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda1 rw,data=ordered +0:0 / /var/lib/snapd/hostfs rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:1 / /var/lib/snapd/hostfs/boot/efi rw,relatime master:2 - vfat /dev/sda15 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:9 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:13 - tmpfs tmpfs rw,size=VARIABLE +1:11 / /var/lib/snapd/hostfs/run/rpc_pipefs rw,relatime master:14 - rpc_pipefs sunrpc rw +1:9 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:12 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:16 - squashfs /dev/loop0 ro +2:1 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:17 - squashfs /dev/loop1 ro +2:2 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime master:18 - squashfs /dev/loop2 ro +2:3 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:19 - squashfs /dev/loop3 ro +2:4 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:20 - squashfs /dev/loop4 ro +0:0 /var/log /var/log rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/snap /var/snap rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered +0:0 /var/tmp /var/tmp rw,relatime master:1 - ext4 /dev/sda1 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-C7.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-C7.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-C7.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-18.04-64/PER-USER-C7.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,40 @@ +0:0 / / rw,relatime shared:1 - ext4 /dev/sda1 rw,data=ordered +0:1 / /boot/efi rw,relatime shared:2 - vfat /dev/sda15 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 / /dev rw,nosuid,relatime shared:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +1:1 / /dev/hugepages rw,relatime shared:4 - hugetlbfs hugetlbfs rw,pagesize=2M +1:2 / /dev/mqueue rw,relatime shared:5 - mqueue mqueue rw +1:3 / /dev/pts rw,nosuid,noexec,relatime shared:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +1:4 / /dev/shm rw,nosuid,nodev shared:7 - tmpfs tmpfs rw +1:5 / /proc rw,nosuid,nodev,noexec,relatime shared:8 - proc proc rw +1:6 / /proc/fs/nfsd rw,relatime shared:9 - nfsd nfsd rw +1:7 / /proc/sys/fs/binfmt_misc rw,relatime shared:10 - binfmt_misc binfmt_misc rw +1:8 / /proc/sys/fs/binfmt_misc rw,relatime shared:11 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +1:9 / /run rw,nosuid,noexec,relatime shared:12 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +1:10 / /run/lock rw,nosuid,nodev,noexec,relatime shared:13 - tmpfs tmpfs rw,size=VARIABLE +1:11 / /run/rpc_pipefs rw,relatime shared:14 - rpc_pipefs sunrpc rw +1:12 / /run/user/0 rw,nosuid,nodev,relatime shared:15 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +2:0 / /snap/core/1 ro,nodev,relatime shared:16 - squashfs /dev/loop0 ro +2:1 / /snap/core18/1 ro,nodev,relatime shared:17 - squashfs /dev/loop1 ro +2:2 / /snap/test-snapd-mountinfo-classic/1 ro,nodev,relatime shared:18 - squashfs /dev/loop2 ro +2:3 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime shared:19 - squashfs /dev/loop3 ro +2:4 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime shared:20 - squashfs /dev/loop4 ro +1:13 / /sys rw,nosuid,nodev,noexec,relatime shared:21 - sysfs sysfs rw +1:14 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:22 - tmpfs tmpfs ro,mode=755 +1:15 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:23 - cgroup cgroup rw,blkio +1:16 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:24 - cgroup cgroup rw,cpu,cpuacct +1:17 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:25 - cgroup cgroup rw,cpuset +1:18 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:26 - cgroup cgroup rw,devices +1:19 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:27 - cgroup cgroup rw,freezer +1:20 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:28 - cgroup cgroup rw,hugetlb +1:21 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:29 - cgroup cgroup rw,memory +1:22 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:30 - cgroup cgroup rw,net_cls,net_prio +1:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:31 - cgroup cgroup rw,perf_event +1:24 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:32 - cgroup cgroup rw,pids +1:25 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime shared:33 - cgroup cgroup rw,rdma +1:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:34 - cgroup cgroup rw,xattr,name=systemd +1:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime shared:35 - cgroup2 cgroup rw,nsdelegate +1:28 / /sys/fs/fuse/connections rw,relatime shared:36 - fusectl fusectl rw +1:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:37 - pstore pstore rw +1:30 / /sys/kernel/config rw,relatime shared:38 - configfs configfs rw +1:31 / /sys/kernel/debug rw,relatime shared:39 - debugfs debugfs rw +1:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:40 - securityfs securityfs rw diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-core-16-64/HOST.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-16-64/HOST.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-core-16-64/HOST.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-16-64/HOST.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,119 @@ +0:0 / / ro,relatime shared:1 - squashfs /dev/loop0 ro +1:0 / /boot/efi rw,relatime shared:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 /EFI/ubuntu /boot/grub rw,relatime shared:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +2:0 / /dev rw,nosuid,relatime shared:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +2:1 / /dev/hugepages rw,relatime shared:4 - hugetlbfs hugetlbfs rw +2:2 / /dev/mqueue rw,relatime shared:5 - mqueue mqueue rw +2:3 / /dev/pts rw,nosuid,noexec,relatime shared:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +2:4 / /dev/shm rw,nosuid,nodev shared:7 - tmpfs tmpfs rw +1:1 /system-data/etc/apparmor.d/cache /etc/apparmor.d/cache rw,relatime shared:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /etc/cloud rw,relatime shared:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /etc/dbus-1/system.d rw,relatime shared:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/keyboard /etc/default/keyboard rw,relatime shared:11 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /etc/default/swapfile rw,relatime shared:12 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/environment /etc/environment rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /etc/fstab rw,nosuid,noexec,relatime shared:14 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /etc/group ro,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /etc/gshadow ro,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /etc/hosts rw,relatime shared:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init /etc/init rw,relatime shared:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init.d /etc/init.d rw,relatime shared:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /etc/iproute2 rw,relatime shared:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /etc/machine-id rw,relatime shared:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /etc/modprobe.d rw,relatime shared:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /etc/modules-load.d rw,relatime shared:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /etc/netplan rw,relatime shared:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /etc/network/if-up.d rw,relatime shared:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/interfaces.d /etc/network/interfaces.d rw,relatime shared:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/passwd /etc/passwd ro,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ppp /etc/ppp rw,relatime shared:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc0.d /etc/rc0.d rw,relatime shared:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc1.d /etc/rc1.d rw,relatime shared:28 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc2.d /etc/rc2.d rw,relatime shared:29 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc3.d /etc/rc3.d rw,relatime shared:30 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc4.d /etc/rc4.d rw,relatime shared:31 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc5.d /etc/rc5.d rw,relatime shared:32 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc6.d /etc/rc6.d rw,relatime shared:33 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rcS.d /etc/rcS.d rw,relatime shared:34 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rsyslog.d /etc/rsyslog.d rw,relatime shared:35 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /etc/shadow ro,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /etc/ssh rw,relatime shared:36 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sudoers.d /etc/sudoers.d rw,relatime shared:37 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /etc/sysctl.d rw,relatime shared:38 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/logind.conf.d /etc/systemd/logind.conf.d rw,relatime shared:39 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/network /etc/systemd/network rw,relatime shared:40 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime shared:41 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime shared:42 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system.conf.d /etc/systemd/system.conf.d rw,relatime shared:43 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/timesyncd.conf /etc/systemd/timesyncd.conf rw,relatime shared:44 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/user.conf.d /etc/systemd/user.conf.d rw,relatime shared:45 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /etc/udev/rules.d rw,relatime shared:46 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/update-motd.d /etc/update-motd.d rw,relatime shared:47 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /etc/writable rw,relatime shared:48 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /home rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /lib/firmware ro,relatime shared:49 - squashfs /dev/loop1 ro +0:1 /modules /lib/modules ro,relatime shared:50 - squashfs /dev/loop1 ro +2:6 / /media rw,relatime shared:51 - tmpfs tmpfs rw +2:7 / /mnt rw,relatime shared:52 - tmpfs tmpfs rw +2:8 / /proc rw,nosuid,nodev,noexec,relatime shared:53 - proc proc rw +2:9 / /proc/sys/fs/binfmt_misc rw,relatime shared:54 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct +1:1 /system-data/root /root rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +2:10 / /run rw,nosuid,noexec,relatime shared:55 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:5 / /run rw,nosuid,noexec,relatime shared:56 - tmpfs tmpfs rw,mode=755 +2:11 / /run/cgmanager/fs rw,relatime shared:57 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +2:12 / /run/lock rw,nosuid,nodev,noexec,relatime shared:58 - tmpfs tmpfs rw,size=VARIABLE +2:10 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:13 / /run/user/0 rw,nosuid,nodev,relatime shared:59 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /snap rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /snap/core/1 ro,nodev,relatime shared:60 - squashfs /dev/loop2 ro +0:3 / /snap/core18/1 ro,nodev,relatime shared:61 - squashfs /dev/loop3 ro +0:4 / /snap/pc-kernel/1 ro,nodev,relatime shared:62 - squashfs /dev/loop4 ro +0:5 / /snap/pc/1 ro,nodev,relatime shared:63 - squashfs /dev/loop5 ro +0:6 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime shared:64 - squashfs /dev/loop6 ro +0:7 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime shared:65 - squashfs /dev/loop7 ro +0:8 / /snap/test-snapd-rsync/1 ro,nodev,relatime shared:66 - squashfs /dev/loop8 ro +2:14 / /sys rw,nosuid,nodev,noexec,relatime shared:67 - sysfs sysfs rw +2:15 / /sys/fs/cgroup rw shared:68 - tmpfs tmpfs rw,mode=755 +2:16 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:69 - cgroup cgroup rw,blkio +2:17 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:70 - cgroup cgroup rw,cpu,cpuacct +2:18 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:71 - cgroup cgroup rw,cpuset,clone_children +2:19 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:72 - cgroup cgroup rw,devices +2:20 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:73 - cgroup cgroup rw,freezer +2:21 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:74 - cgroup cgroup rw,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb +2:22 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:75 - cgroup cgroup rw,memory +2:23 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:76 - cgroup cgroup rw,net_cls,net_prio +2:24 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:77 - cgroup cgroup rw,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event +2:25 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:78 - cgroup cgroup rw,pids,release_agent=/run/cgmanager/agents/cgm-release-agent.pids +2:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:79 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +2:27 / /sys/fs/fuse/connections rw,relatime shared:80 - fusectl fusectl rw +2:28 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:81 - pstore pstore rw +2:29 / /sys/kernel/debug rw,relatime shared:82 - debugfs debugfs rw +2:30 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:83 - securityfs securityfs rw +2:31 / /tmp rw,relatime shared:84 - tmpfs tmpfs rw +1:1 /system-data/var/cache/apparmor /var/cache/apparmor rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/cache/snapd /var/cache/snapd rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/apparmor /var/lib/apparmor rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/cloud /var/lib/cloud rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/console-conf /var/lib/console-conf rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dbus /var/lib/dbus rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dhcp /var/lib/dhcp rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/extrausers /var/lib/extrausers rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/initramfs-tools /var/lib/initramfs-tools rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/logrotate /var/lib/logrotate rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/misc /var/lib/misc rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +2:32 / /var/lib/sudo rw,relatime shared:85 - tmpfs tmpfs rw,mode=700 +1:1 /system-data/var/lib/systemd/random-seed /var/lib/systemd/random-seed rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/systemd/rfkill /var/lib/systemd/rfkill rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/waagent /var/lib/waagent rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/log /var/log rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/snap rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/tmp rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 / /writable rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /writable/system-data/snap/core/1 ro,nodev,relatime shared:60 - squashfs /dev/loop2 ro +0:3 / /writable/system-data/snap/core18/1 ro,nodev,relatime shared:61 - squashfs /dev/loop3 ro +0:4 / /writable/system-data/snap/pc-kernel/1 ro,nodev,relatime shared:62 - squashfs /dev/loop4 ro +0:5 / /writable/system-data/snap/pc/1 ro,nodev,relatime shared:63 - squashfs /dev/loop5 ro +0:6 / /writable/system-data/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime shared:64 - squashfs /dev/loop6 ro +0:7 / /writable/system-data/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime shared:65 - squashfs /dev/loop7 ro +0:8 / /writable/system-data/snap/test-snapd-rsync/1 ro,nodev,relatime shared:66 - squashfs /dev/loop8 ro diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-core-16-64/PER-SNAP-16.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-16-64/PER-SNAP-16.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-core-16-64/PER-SNAP-16.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-16-64/PER-SNAP-16.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,223 @@ +0:0 / / ro,relatime master:1 - squashfs /dev/loop0 ro +1:0 / /boot/efi rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 /EFI/ubuntu /boot/grub rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +2:0 / /dev rw,nosuid,relatime master:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +2:1 / /dev/hugepages rw,relatime master:4 - hugetlbfs hugetlbfs rw +2:2 / /dev/mqueue rw,relatime master:5 - mqueue mqueue rw +2:33 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:3 / /dev/pts rw,nosuid,noexec,relatime master:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +2:33 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:4 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw +1:1 /system-data/etc/apparmor.d/cache /etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/keyboard /etc/default/keyboard rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /etc/default/swapfile rw,relatime master:12 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/environment /etc/environment rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /etc/fstab rw,nosuid,noexec,relatime master:14 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /etc/group ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /etc/gshadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /etc/hosts rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init /etc/init rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init.d /etc/init.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /etc/iproute2 rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /etc/machine-id rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /etc/modprobe.d rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /etc/modules-load.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /etc/netplan rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /etc/network/if-up.d rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/interfaces.d /etc/network/interfaces.d rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/passwd /etc/passwd ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ppp /etc/ppp rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc0.d /etc/rc0.d rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc1.d /etc/rc1.d rw,relatime master:28 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc2.d /etc/rc2.d rw,relatime master:29 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc3.d /etc/rc3.d rw,relatime master:30 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc4.d /etc/rc4.d rw,relatime master:31 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc5.d /etc/rc5.d rw,relatime master:32 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc6.d /etc/rc6.d rw,relatime master:33 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rcS.d /etc/rcS.d rw,relatime master:34 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rsyslog.d /etc/rsyslog.d rw,relatime master:35 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /etc/shadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /etc/ssh rw,relatime master:36 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sudoers.d /etc/sudoers.d rw,relatime master:37 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /etc/sysctl.d rw,relatime master:38 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/logind.conf.d /etc/systemd/logind.conf.d rw,relatime master:39 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/network /etc/systemd/network rw,relatime master:40 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime master:41 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime master:42 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system.conf.d /etc/systemd/system.conf.d rw,relatime master:43 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/timesyncd.conf /etc/systemd/timesyncd.conf rw,relatime master:44 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/user.conf.d /etc/systemd/user.conf.d rw,relatime master:45 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /etc/udev/rules.d rw,relatime master:46 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/update-motd.d /etc/update-motd.d rw,relatime master:47 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /etc/writable rw,relatime master:48 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /home rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /lib/firmware ro,relatime master:49 - squashfs /dev/loop1 ro +0:1 /modules /lib/modules ro,relatime master:50 - squashfs /dev/loop1 ro +2:6 / /media rw,relatime master:51 - tmpfs tmpfs rw +2:6 / /media rw,relatime shared:51 - tmpfs tmpfs rw +2:7 / /mnt rw,relatime master:52 - tmpfs tmpfs rw +2:8 / /proc rw,nosuid,nodev,noexec,relatime master:53 - proc proc rw +2:9 / /proc/sys/fs/binfmt_misc rw,relatime master:54 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct +1:1 /system-data/root /root rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +2:5 / /run rw,nosuid,noexec,relatime master:56 - tmpfs tmpfs rw,mode=755 +2:10 / /run rw,nosuid,noexec,relatime master:55 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /run/cgmanager/fs rw,relatime master:57 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +2:12 / /run/lock rw,nosuid,nodev,noexec,relatime master:58 - tmpfs tmpfs rw,size=VARIABLE +2:10 /netns /run/netns rw,nosuid,noexec,relatime shared:55 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:10 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:13 / /run/user/0 rw,nosuid,nodev,relatime master:59 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro +2:14 / /sys rw,nosuid,nodev,noexec,relatime master:67 - sysfs sysfs rw +2:15 / /sys/fs/cgroup rw master:68 - tmpfs tmpfs rw,mode=755 +2:16 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:69 - cgroup cgroup rw,blkio +2:17 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:70 - cgroup cgroup rw,cpu,cpuacct +2:18 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:71 - cgroup cgroup rw,cpuset,clone_children +2:19 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:72 - cgroup cgroup rw,devices +2:20 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:73 - cgroup cgroup rw,freezer +2:21 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:74 - cgroup cgroup rw,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb +2:22 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:75 - cgroup cgroup rw,memory +2:23 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:76 - cgroup cgroup rw,net_cls,net_prio +2:24 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:77 - cgroup cgroup rw,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event +2:25 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:78 - cgroup cgroup rw,pids,release_agent=/run/cgmanager/agents/cgm-release-agent.pids +2:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:79 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +2:27 / /sys/fs/fuse/connections rw,relatime master:80 - fusectl fusectl rw +2:28 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:81 - pstore pstore rw +2:29 / /sys/kernel/debug rw,relatime master:82 - debugfs debugfs rw +2:30 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:83 - securityfs securityfs rw +2:31 / /tmp rw,relatime master:84 - tmpfs tmpfs rw +2:31 /snap.test-snapd-mountinfo-core16/tmp /tmp rw,relatime - tmpfs tmpfs rw +2:34 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +0:0 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,relatime master:1 - squashfs /dev/loop0 ro +2:35 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +1:1 /system-data/var/cache/apparmor /var/cache/apparmor rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/cache/snapd /var/cache/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/apparmor /var/lib/apparmor rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/cloud /var/lib/cloud rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/console-conf /var/lib/console-conf rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dbus /var/lib/dbus rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dhcp /var/lib/dhcp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/extrausers /var/lib/extrausers rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/initramfs-tools /var/lib/initramfs-tools rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/logrotate /var/lib/logrotate rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/misc /var/lib/misc rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:0 / /var/lib/snapd/hostfs ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda3 rw,data=ordered +1:0 / /var/lib/snapd/hostfs/boot/efi rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 /EFI/ubuntu /var/lib/snapd/hostfs/boot/grub rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:1 /system-data/etc/apparmor.d/cache /var/lib/snapd/hostfs/etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /var/lib/snapd/hostfs/etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /var/lib/snapd/hostfs/etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/keyboard /var/lib/snapd/hostfs/etc/default/keyboard rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /var/lib/snapd/hostfs/etc/default/swapfile rw,relatime master:12 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/environment /var/lib/snapd/hostfs/etc/environment rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /var/lib/snapd/hostfs/etc/fstab rw,nosuid,noexec,relatime master:14 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /var/lib/snapd/hostfs/etc/group ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /var/lib/snapd/hostfs/etc/gshadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /var/lib/snapd/hostfs/etc/hosts rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init /var/lib/snapd/hostfs/etc/init rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init.d /var/lib/snapd/hostfs/etc/init.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /var/lib/snapd/hostfs/etc/iproute2 rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /var/lib/snapd/hostfs/etc/machine-id rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /var/lib/snapd/hostfs/etc/modprobe.d rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /var/lib/snapd/hostfs/etc/modules-load.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /var/lib/snapd/hostfs/etc/netplan rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /var/lib/snapd/hostfs/etc/network/if-up.d rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/interfaces.d /var/lib/snapd/hostfs/etc/network/interfaces.d rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/passwd /var/lib/snapd/hostfs/etc/passwd ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ppp /var/lib/snapd/hostfs/etc/ppp rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc0.d /var/lib/snapd/hostfs/etc/rc0.d rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc1.d /var/lib/snapd/hostfs/etc/rc1.d rw,relatime master:28 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc2.d /var/lib/snapd/hostfs/etc/rc2.d rw,relatime master:29 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc3.d /var/lib/snapd/hostfs/etc/rc3.d rw,relatime master:30 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc4.d /var/lib/snapd/hostfs/etc/rc4.d rw,relatime master:31 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc5.d /var/lib/snapd/hostfs/etc/rc5.d rw,relatime master:32 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc6.d /var/lib/snapd/hostfs/etc/rc6.d rw,relatime master:33 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rcS.d /var/lib/snapd/hostfs/etc/rcS.d rw,relatime master:34 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rsyslog.d /var/lib/snapd/hostfs/etc/rsyslog.d rw,relatime master:35 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /var/lib/snapd/hostfs/etc/shadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /var/lib/snapd/hostfs/etc/ssh rw,relatime master:36 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sudoers.d /var/lib/snapd/hostfs/etc/sudoers.d rw,relatime master:37 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /var/lib/snapd/hostfs/etc/sysctl.d rw,relatime master:38 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/logind.conf.d /var/lib/snapd/hostfs/etc/systemd/logind.conf.d rw,relatime master:39 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/network /var/lib/snapd/hostfs/etc/systemd/network rw,relatime master:40 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /var/lib/snapd/hostfs/etc/systemd/system rw,relatime master:41 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /var/lib/snapd/hostfs/etc/systemd/system rw,relatime master:42 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system.conf.d /var/lib/snapd/hostfs/etc/systemd/system.conf.d rw,relatime master:43 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/timesyncd.conf /var/lib/snapd/hostfs/etc/systemd/timesyncd.conf rw,relatime master:44 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/user.conf.d /var/lib/snapd/hostfs/etc/systemd/user.conf.d rw,relatime master:45 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /var/lib/snapd/hostfs/etc/udev/rules.d rw,relatime master:46 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/update-motd.d /var/lib/snapd/hostfs/etc/update-motd.d rw,relatime master:47 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /var/lib/snapd/hostfs/etc/writable rw,relatime master:48 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /var/lib/snapd/hostfs/home rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /var/lib/snapd/hostfs/lib/firmware ro,relatime master:49 - squashfs /dev/loop1 ro +0:1 /modules /var/lib/snapd/hostfs/lib/modules ro,relatime master:50 - squashfs /dev/loop1 ro +2:6 / /var/lib/snapd/hostfs/media rw,relatime master:51 - tmpfs tmpfs rw +2:7 / /var/lib/snapd/hostfs/mnt rw,relatime master:52 - tmpfs tmpfs rw +1:1 /system-data/root /var/lib/snapd/hostfs/root rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +2:5 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:56 - tmpfs tmpfs rw,mode=755 +2:10 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:55 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /var/lib/snapd/hostfs/run/cgmanager/fs rw,relatime master:57 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +2:12 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:58 - tmpfs tmpfs rw,size=VARIABLE +2:10 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:13 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:59 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /var/lib/snapd/hostfs/snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /var/lib/snapd/hostfs/snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro +2:31 / /var/lib/snapd/hostfs/tmp rw,relatime master:84 - tmpfs tmpfs rw +1:1 /system-data/var/cache/apparmor /var/lib/snapd/hostfs/var/cache/apparmor rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/cache/snapd /var/lib/snapd/hostfs/var/cache/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/apparmor /var/lib/snapd/hostfs/var/lib/apparmor rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/cloud /var/lib/snapd/hostfs/var/lib/cloud rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/console-conf /var/lib/snapd/hostfs/var/lib/console-conf rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dbus /var/lib/snapd/hostfs/var/lib/dbus rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dhcp /var/lib/snapd/hostfs/var/lib/dhcp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/extrausers /var/lib/snapd/hostfs/var/lib/extrausers rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/initramfs-tools /var/lib/snapd/hostfs/var/lib/initramfs-tools rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/logrotate /var/lib/snapd/hostfs/var/lib/logrotate rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/misc /var/lib/snapd/hostfs/var/lib/misc rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd/hostfs/var/lib/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +2:32 / /var/lib/snapd/hostfs/var/lib/sudo rw,relatime master:85 - tmpfs tmpfs rw,mode=700 +1:1 /system-data/var/lib/systemd/random-seed /var/lib/snapd/hostfs/var/lib/systemd/random-seed rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/systemd/rfkill /var/lib/snapd/hostfs/var/lib/systemd/rfkill rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/waagent /var/lib/snapd/hostfs/var/lib/waagent rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/log /var/lib/snapd/hostfs/var/log rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/lib/snapd/hostfs/var/snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/lib/snapd/hostfs/var/tmp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 / /var/lib/snapd/hostfs/writable rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/writable/system-data/snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /var/lib/snapd/hostfs/writable/system-data/snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/writable/system-data/snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro +2:32 / /var/lib/sudo rw,relatime master:85 - tmpfs tmpfs rw,mode=700 +1:1 /system-data/var/lib/systemd/random-seed /var/lib/systemd/random-seed rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/systemd/rfkill /var/lib/systemd/rfkill rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/waagent /var/lib/waagent rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/log /var/log rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/tmp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 / /writable rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /writable/system-data/snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /writable/system-data/snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /writable/system-data/snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /writable/system-data/snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /writable/system-data/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /writable/system-data/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /writable/system-data/snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-core-16-64/PER-SNAP-18.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-16-64/PER-SNAP-18.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-core-16-64/PER-SNAP-18.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-16-64/PER-SNAP-18.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,202 @@ +0:3 / / ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +2:0 / /dev rw,nosuid,relatime master:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +2:1 / /dev/hugepages rw,relatime master:4 - hugetlbfs hugetlbfs rw +2:2 / /dev/mqueue rw,relatime master:5 - mqueue mqueue rw +2:33 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:3 / /dev/pts rw,nosuid,noexec,relatime master:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +2:33 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:4 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw +0:0 /etc /etc ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/etc/apparmor.d/cache /etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/keyboard /etc/default/keyboard rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /etc/default/swapfile rw,relatime master:12 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/environment /etc/environment rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /etc/fstab rw,nosuid,noexec,relatime master:14 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /etc/group ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /etc/gshadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /etc/hosts rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init /etc/init rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init.d /etc/init.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /etc/iproute2 rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /etc/machine-id rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /etc/modprobe.d rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /etc/modules-load.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /etc/netplan rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /etc/network/if-up.d rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/interfaces.d /etc/network/interfaces.d rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +0:3 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +1:1 /system-data/root/test-etc/passwd /etc/passwd ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ppp /etc/ppp rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc0.d /etc/rc0.d rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc1.d /etc/rc1.d rw,relatime master:28 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc2.d /etc/rc2.d rw,relatime master:29 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc3.d /etc/rc3.d rw,relatime master:30 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc4.d /etc/rc4.d rw,relatime master:31 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc5.d /etc/rc5.d rw,relatime master:32 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc6.d /etc/rc6.d rw,relatime master:33 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rcS.d /etc/rcS.d rw,relatime master:34 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rsyslog.d /etc/rsyslog.d rw,relatime master:35 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /etc/shadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /etc/ssh rw,relatime master:36 - ext4 /dev/sda3 rw,data=ordered +0:3 /etc/ssl /etc/ssl ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +1:1 /system-data/etc/sudoers.d /etc/sudoers.d rw,relatime master:37 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /etc/sysctl.d rw,relatime master:38 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/logind.conf.d /etc/systemd/logind.conf.d rw,relatime master:39 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/network /etc/systemd/network rw,relatime master:40 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime master:41 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime master:42 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system.conf.d /etc/systemd/system.conf.d rw,relatime master:43 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/timesyncd.conf /etc/systemd/timesyncd.conf rw,relatime master:44 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/user.conf.d /etc/systemd/user.conf.d rw,relatime master:45 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /etc/udev/rules.d rw,relatime master:46 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/update-motd.d /etc/update-motd.d rw,relatime master:47 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /etc/writable rw,relatime master:48 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /home rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /lib/firmware ro,relatime master:49 - squashfs /dev/loop1 ro +0:1 /modules /lib/modules ro,relatime master:50 - squashfs /dev/loop1 ro +2:6 / /media rw,relatime shared:51 - tmpfs tmpfs rw +2:7 / /mnt rw,relatime master:52 - tmpfs tmpfs rw +2:8 / /proc rw,nosuid,nodev,noexec,relatime master:53 - proc proc rw +2:9 / /proc/sys/fs/binfmt_misc rw,relatime master:54 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct +1:1 /system-data/root /root rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +2:10 / /run rw,nosuid,noexec,relatime master:55 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /run/cgmanager/fs rw,relatime master:57 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +2:12 / /run/lock rw,nosuid,nodev,noexec,relatime master:58 - tmpfs tmpfs rw,size=VARIABLE +2:10 /netns /run/netns rw,nosuid,noexec,relatime shared:55 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:10 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:13 / /run/user/0 rw,nosuid,nodev,relatime master:59 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro +2:14 / /sys rw,nosuid,nodev,noexec,relatime master:67 - sysfs sysfs rw +2:15 / /sys/fs/cgroup rw master:68 - tmpfs tmpfs rw,mode=755 +2:16 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:69 - cgroup cgroup rw,blkio +2:17 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:70 - cgroup cgroup rw,cpu,cpuacct +2:18 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:71 - cgroup cgroup rw,cpuset,clone_children +2:19 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:72 - cgroup cgroup rw,devices +2:20 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:73 - cgroup cgroup rw,freezer +2:21 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:74 - cgroup cgroup rw,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb +2:22 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:75 - cgroup cgroup rw,memory +2:23 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:76 - cgroup cgroup rw,net_cls,net_prio +2:24 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:77 - cgroup cgroup rw,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event +2:25 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:78 - cgroup cgroup rw,pids,release_agent=/run/cgmanager/agents/cgm-release-agent.pids +2:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:79 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +2:27 / /sys/fs/fuse/connections rw,relatime master:80 - fusectl fusectl rw +2:28 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:81 - pstore pstore rw +2:29 / /sys/kernel/debug rw,relatime master:82 - debugfs debugfs rw +2:30 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:83 - securityfs securityfs rw +2:31 / /tmp rw,relatime master:84 - tmpfs tmpfs rw +2:31 /snap.test-snapd-mountinfo-core18/tmp /tmp rw,relatime - tmpfs tmpfs rw +0:0 /usr/lib/snapd /usr/lib/snapd ro,relatime master:1 - squashfs /dev/loop0 ro +2:34 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +0:3 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +2:35 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/extrausers /var/lib/extrausers rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:0 / /var/lib/snapd/hostfs ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda3 rw,data=ordered +1:0 / /var/lib/snapd/hostfs/boot/efi rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 /EFI/ubuntu /var/lib/snapd/hostfs/boot/grub rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:1 /system-data/etc/apparmor.d/cache /var/lib/snapd/hostfs/etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /var/lib/snapd/hostfs/etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /var/lib/snapd/hostfs/etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/keyboard /var/lib/snapd/hostfs/etc/default/keyboard rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /var/lib/snapd/hostfs/etc/default/swapfile rw,relatime master:12 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/environment /var/lib/snapd/hostfs/etc/environment rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /var/lib/snapd/hostfs/etc/fstab rw,nosuid,noexec,relatime master:14 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /var/lib/snapd/hostfs/etc/group ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /var/lib/snapd/hostfs/etc/gshadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /var/lib/snapd/hostfs/etc/hosts rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init /var/lib/snapd/hostfs/etc/init rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init.d /var/lib/snapd/hostfs/etc/init.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /var/lib/snapd/hostfs/etc/iproute2 rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /var/lib/snapd/hostfs/etc/machine-id rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /var/lib/snapd/hostfs/etc/modprobe.d rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /var/lib/snapd/hostfs/etc/modules-load.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /var/lib/snapd/hostfs/etc/netplan rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /var/lib/snapd/hostfs/etc/network/if-up.d rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/interfaces.d /var/lib/snapd/hostfs/etc/network/interfaces.d rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/passwd /var/lib/snapd/hostfs/etc/passwd ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ppp /var/lib/snapd/hostfs/etc/ppp rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc0.d /var/lib/snapd/hostfs/etc/rc0.d rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc1.d /var/lib/snapd/hostfs/etc/rc1.d rw,relatime master:28 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc2.d /var/lib/snapd/hostfs/etc/rc2.d rw,relatime master:29 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc3.d /var/lib/snapd/hostfs/etc/rc3.d rw,relatime master:30 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc4.d /var/lib/snapd/hostfs/etc/rc4.d rw,relatime master:31 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc5.d /var/lib/snapd/hostfs/etc/rc5.d rw,relatime master:32 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc6.d /var/lib/snapd/hostfs/etc/rc6.d rw,relatime master:33 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rcS.d /var/lib/snapd/hostfs/etc/rcS.d rw,relatime master:34 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rsyslog.d /var/lib/snapd/hostfs/etc/rsyslog.d rw,relatime master:35 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /var/lib/snapd/hostfs/etc/shadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /var/lib/snapd/hostfs/etc/ssh rw,relatime master:36 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sudoers.d /var/lib/snapd/hostfs/etc/sudoers.d rw,relatime master:37 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /var/lib/snapd/hostfs/etc/sysctl.d rw,relatime master:38 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/logind.conf.d /var/lib/snapd/hostfs/etc/systemd/logind.conf.d rw,relatime master:39 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/network /var/lib/snapd/hostfs/etc/systemd/network rw,relatime master:40 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /var/lib/snapd/hostfs/etc/systemd/system rw,relatime master:41 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /var/lib/snapd/hostfs/etc/systemd/system rw,relatime master:42 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system.conf.d /var/lib/snapd/hostfs/etc/systemd/system.conf.d rw,relatime master:43 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/timesyncd.conf /var/lib/snapd/hostfs/etc/systemd/timesyncd.conf rw,relatime master:44 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/user.conf.d /var/lib/snapd/hostfs/etc/systemd/user.conf.d rw,relatime master:45 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /var/lib/snapd/hostfs/etc/udev/rules.d rw,relatime master:46 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/update-motd.d /var/lib/snapd/hostfs/etc/update-motd.d rw,relatime master:47 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /var/lib/snapd/hostfs/etc/writable rw,relatime master:48 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /var/lib/snapd/hostfs/home rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /var/lib/snapd/hostfs/lib/firmware ro,relatime master:49 - squashfs /dev/loop1 ro +0:1 /modules /var/lib/snapd/hostfs/lib/modules ro,relatime master:50 - squashfs /dev/loop1 ro +2:6 / /var/lib/snapd/hostfs/media rw,relatime master:51 - tmpfs tmpfs rw +2:7 / /var/lib/snapd/hostfs/mnt rw,relatime master:52 - tmpfs tmpfs rw +1:1 /system-data/root /var/lib/snapd/hostfs/root rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +2:5 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:56 - tmpfs tmpfs rw,mode=755 +2:10 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:55 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /var/lib/snapd/hostfs/run/cgmanager/fs rw,relatime master:57 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +2:12 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:58 - tmpfs tmpfs rw,size=VARIABLE +2:10 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:13 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:59 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /var/lib/snapd/hostfs/snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /var/lib/snapd/hostfs/snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro +2:31 / /var/lib/snapd/hostfs/tmp rw,relatime master:84 - tmpfs tmpfs rw +1:1 /system-data/var/cache/apparmor /var/lib/snapd/hostfs/var/cache/apparmor rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/cache/snapd /var/lib/snapd/hostfs/var/cache/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/apparmor /var/lib/snapd/hostfs/var/lib/apparmor rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/cloud /var/lib/snapd/hostfs/var/lib/cloud rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/console-conf /var/lib/snapd/hostfs/var/lib/console-conf rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dbus /var/lib/snapd/hostfs/var/lib/dbus rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dhcp /var/lib/snapd/hostfs/var/lib/dhcp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/extrausers /var/lib/snapd/hostfs/var/lib/extrausers rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/initramfs-tools /var/lib/snapd/hostfs/var/lib/initramfs-tools rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/logrotate /var/lib/snapd/hostfs/var/lib/logrotate rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/misc /var/lib/snapd/hostfs/var/lib/misc rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd/hostfs/var/lib/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +2:32 / /var/lib/snapd/hostfs/var/lib/sudo rw,relatime master:85 - tmpfs tmpfs rw,mode=700 +1:1 /system-data/var/lib/systemd/random-seed /var/lib/snapd/hostfs/var/lib/systemd/random-seed rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/systemd/rfkill /var/lib/snapd/hostfs/var/lib/systemd/rfkill rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/waagent /var/lib/snapd/hostfs/var/lib/waagent rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/log /var/lib/snapd/hostfs/var/log rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/lib/snapd/hostfs/var/snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/lib/snapd/hostfs/var/tmp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 / /var/lib/snapd/hostfs/writable rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/writable/system-data/snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /var/lib/snapd/hostfs/writable/system-data/snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/writable/system-data/snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro +1:1 /system-data/var/log /var/log rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/tmp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-core-16-64/PER-USER-16.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-16-64/PER-USER-16.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-core-16-64/PER-USER-16.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-16-64/PER-USER-16.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,223 @@ +0:0 / / ro,relatime master:1 - squashfs /dev/loop0 ro +1:0 / /boot/efi rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 /EFI/ubuntu /boot/grub rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +2:0 / /dev rw,nosuid,relatime master:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +2:1 / /dev/hugepages rw,relatime master:4 - hugetlbfs hugetlbfs rw +2:2 / /dev/mqueue rw,relatime master:5 - mqueue mqueue rw +2:33 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:3 / /dev/pts rw,nosuid,noexec,relatime master:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +2:33 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:4 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw +1:1 /system-data/etc/apparmor.d/cache /etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/keyboard /etc/default/keyboard rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /etc/default/swapfile rw,relatime master:12 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/environment /etc/environment rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /etc/fstab rw,nosuid,noexec,relatime master:14 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /etc/group ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /etc/gshadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /etc/hosts rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init /etc/init rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init.d /etc/init.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /etc/iproute2 rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /etc/machine-id rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /etc/modprobe.d rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /etc/modules-load.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /etc/netplan rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /etc/network/if-up.d rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/interfaces.d /etc/network/interfaces.d rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/passwd /etc/passwd ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ppp /etc/ppp rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc0.d /etc/rc0.d rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc1.d /etc/rc1.d rw,relatime master:28 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc2.d /etc/rc2.d rw,relatime master:29 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc3.d /etc/rc3.d rw,relatime master:30 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc4.d /etc/rc4.d rw,relatime master:31 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc5.d /etc/rc5.d rw,relatime master:32 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc6.d /etc/rc6.d rw,relatime master:33 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rcS.d /etc/rcS.d rw,relatime master:34 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rsyslog.d /etc/rsyslog.d rw,relatime master:35 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /etc/shadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /etc/ssh rw,relatime master:36 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sudoers.d /etc/sudoers.d rw,relatime master:37 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /etc/sysctl.d rw,relatime master:38 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/logind.conf.d /etc/systemd/logind.conf.d rw,relatime master:39 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/network /etc/systemd/network rw,relatime master:40 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime master:41 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime master:42 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system.conf.d /etc/systemd/system.conf.d rw,relatime master:43 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/timesyncd.conf /etc/systemd/timesyncd.conf rw,relatime master:44 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/user.conf.d /etc/systemd/user.conf.d rw,relatime master:45 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /etc/udev/rules.d rw,relatime master:46 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/update-motd.d /etc/update-motd.d rw,relatime master:47 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /etc/writable rw,relatime master:48 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /home rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /lib/firmware ro,relatime master:49 - squashfs /dev/loop1 ro +0:1 /modules /lib/modules ro,relatime master:50 - squashfs /dev/loop1 ro +2:6 / /media rw,relatime master:51 - tmpfs tmpfs rw +2:6 / /media rw,relatime shared:51 - tmpfs tmpfs rw +2:7 / /mnt rw,relatime master:52 - tmpfs tmpfs rw +2:8 / /proc rw,nosuid,nodev,noexec,relatime master:53 - proc proc rw +2:9 / /proc/sys/fs/binfmt_misc rw,relatime master:54 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct +1:1 /system-data/root /root rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +2:5 / /run rw,nosuid,noexec,relatime master:56 - tmpfs tmpfs rw,mode=755 +2:10 / /run rw,nosuid,noexec,relatime master:55 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /run/cgmanager/fs rw,relatime master:57 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +2:12 / /run/lock rw,nosuid,nodev,noexec,relatime master:58 - tmpfs tmpfs rw,size=VARIABLE +2:10 /netns /run/netns rw,nosuid,noexec,relatime shared:55 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:10 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:13 / /run/user/0 rw,nosuid,nodev,relatime master:59 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro +2:14 / /sys rw,nosuid,nodev,noexec,relatime master:67 - sysfs sysfs rw +2:15 / /sys/fs/cgroup rw master:68 - tmpfs tmpfs rw,mode=755 +2:16 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:69 - cgroup cgroup rw,blkio +2:17 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:70 - cgroup cgroup rw,cpu,cpuacct +2:18 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:71 - cgroup cgroup rw,cpuset,clone_children +2:19 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:72 - cgroup cgroup rw,devices +2:20 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:73 - cgroup cgroup rw,freezer +2:21 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:74 - cgroup cgroup rw,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb +2:22 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:75 - cgroup cgroup rw,memory +2:23 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:76 - cgroup cgroup rw,net_cls,net_prio +2:24 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:77 - cgroup cgroup rw,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event +2:25 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:78 - cgroup cgroup rw,pids,release_agent=/run/cgmanager/agents/cgm-release-agent.pids +2:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:79 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +2:27 / /sys/fs/fuse/connections rw,relatime master:80 - fusectl fusectl rw +2:28 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:81 - pstore pstore rw +2:29 / /sys/kernel/debug rw,relatime master:82 - debugfs debugfs rw +2:30 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:83 - securityfs securityfs rw +2:31 / /tmp rw,relatime master:84 - tmpfs tmpfs rw +2:31 /snap.test-snapd-mountinfo-core16/tmp /tmp rw,relatime - tmpfs tmpfs rw +2:34 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +0:0 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,relatime master:1 - squashfs /dev/loop0 ro +2:35 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +1:1 /system-data/var/cache/apparmor /var/cache/apparmor rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/cache/snapd /var/cache/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/apparmor /var/lib/apparmor rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/cloud /var/lib/cloud rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/console-conf /var/lib/console-conf rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dbus /var/lib/dbus rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dhcp /var/lib/dhcp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/extrausers /var/lib/extrausers rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/initramfs-tools /var/lib/initramfs-tools rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/logrotate /var/lib/logrotate rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/misc /var/lib/misc rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:0 / /var/lib/snapd/hostfs ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda3 rw,data=ordered +1:0 / /var/lib/snapd/hostfs/boot/efi rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 /EFI/ubuntu /var/lib/snapd/hostfs/boot/grub rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:1 /system-data/etc/apparmor.d/cache /var/lib/snapd/hostfs/etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /var/lib/snapd/hostfs/etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /var/lib/snapd/hostfs/etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/keyboard /var/lib/snapd/hostfs/etc/default/keyboard rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /var/lib/snapd/hostfs/etc/default/swapfile rw,relatime master:12 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/environment /var/lib/snapd/hostfs/etc/environment rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /var/lib/snapd/hostfs/etc/fstab rw,nosuid,noexec,relatime master:14 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /var/lib/snapd/hostfs/etc/group ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /var/lib/snapd/hostfs/etc/gshadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /var/lib/snapd/hostfs/etc/hosts rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init /var/lib/snapd/hostfs/etc/init rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init.d /var/lib/snapd/hostfs/etc/init.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /var/lib/snapd/hostfs/etc/iproute2 rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /var/lib/snapd/hostfs/etc/machine-id rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /var/lib/snapd/hostfs/etc/modprobe.d rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /var/lib/snapd/hostfs/etc/modules-load.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /var/lib/snapd/hostfs/etc/netplan rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /var/lib/snapd/hostfs/etc/network/if-up.d rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/interfaces.d /var/lib/snapd/hostfs/etc/network/interfaces.d rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/passwd /var/lib/snapd/hostfs/etc/passwd ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ppp /var/lib/snapd/hostfs/etc/ppp rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc0.d /var/lib/snapd/hostfs/etc/rc0.d rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc1.d /var/lib/snapd/hostfs/etc/rc1.d rw,relatime master:28 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc2.d /var/lib/snapd/hostfs/etc/rc2.d rw,relatime master:29 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc3.d /var/lib/snapd/hostfs/etc/rc3.d rw,relatime master:30 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc4.d /var/lib/snapd/hostfs/etc/rc4.d rw,relatime master:31 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc5.d /var/lib/snapd/hostfs/etc/rc5.d rw,relatime master:32 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc6.d /var/lib/snapd/hostfs/etc/rc6.d rw,relatime master:33 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rcS.d /var/lib/snapd/hostfs/etc/rcS.d rw,relatime master:34 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rsyslog.d /var/lib/snapd/hostfs/etc/rsyslog.d rw,relatime master:35 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /var/lib/snapd/hostfs/etc/shadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /var/lib/snapd/hostfs/etc/ssh rw,relatime master:36 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sudoers.d /var/lib/snapd/hostfs/etc/sudoers.d rw,relatime master:37 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /var/lib/snapd/hostfs/etc/sysctl.d rw,relatime master:38 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/logind.conf.d /var/lib/snapd/hostfs/etc/systemd/logind.conf.d rw,relatime master:39 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/network /var/lib/snapd/hostfs/etc/systemd/network rw,relatime master:40 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /var/lib/snapd/hostfs/etc/systemd/system rw,relatime master:41 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /var/lib/snapd/hostfs/etc/systemd/system rw,relatime master:42 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system.conf.d /var/lib/snapd/hostfs/etc/systemd/system.conf.d rw,relatime master:43 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/timesyncd.conf /var/lib/snapd/hostfs/etc/systemd/timesyncd.conf rw,relatime master:44 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/user.conf.d /var/lib/snapd/hostfs/etc/systemd/user.conf.d rw,relatime master:45 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /var/lib/snapd/hostfs/etc/udev/rules.d rw,relatime master:46 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/update-motd.d /var/lib/snapd/hostfs/etc/update-motd.d rw,relatime master:47 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /var/lib/snapd/hostfs/etc/writable rw,relatime master:48 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /var/lib/snapd/hostfs/home rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /var/lib/snapd/hostfs/lib/firmware ro,relatime master:49 - squashfs /dev/loop1 ro +0:1 /modules /var/lib/snapd/hostfs/lib/modules ro,relatime master:50 - squashfs /dev/loop1 ro +2:6 / /var/lib/snapd/hostfs/media rw,relatime master:51 - tmpfs tmpfs rw +2:7 / /var/lib/snapd/hostfs/mnt rw,relatime master:52 - tmpfs tmpfs rw +1:1 /system-data/root /var/lib/snapd/hostfs/root rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +2:5 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:56 - tmpfs tmpfs rw,mode=755 +2:10 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:55 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /var/lib/snapd/hostfs/run/cgmanager/fs rw,relatime master:57 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +2:12 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:58 - tmpfs tmpfs rw,size=VARIABLE +2:10 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:13 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:59 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /var/lib/snapd/hostfs/snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /var/lib/snapd/hostfs/snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro +2:31 / /var/lib/snapd/hostfs/tmp rw,relatime master:84 - tmpfs tmpfs rw +1:1 /system-data/var/cache/apparmor /var/lib/snapd/hostfs/var/cache/apparmor rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/cache/snapd /var/lib/snapd/hostfs/var/cache/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/apparmor /var/lib/snapd/hostfs/var/lib/apparmor rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/cloud /var/lib/snapd/hostfs/var/lib/cloud rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/console-conf /var/lib/snapd/hostfs/var/lib/console-conf rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dbus /var/lib/snapd/hostfs/var/lib/dbus rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dhcp /var/lib/snapd/hostfs/var/lib/dhcp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/extrausers /var/lib/snapd/hostfs/var/lib/extrausers rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/initramfs-tools /var/lib/snapd/hostfs/var/lib/initramfs-tools rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/logrotate /var/lib/snapd/hostfs/var/lib/logrotate rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/misc /var/lib/snapd/hostfs/var/lib/misc rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd/hostfs/var/lib/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +2:32 / /var/lib/snapd/hostfs/var/lib/sudo rw,relatime master:85 - tmpfs tmpfs rw,mode=700 +1:1 /system-data/var/lib/systemd/random-seed /var/lib/snapd/hostfs/var/lib/systemd/random-seed rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/systemd/rfkill /var/lib/snapd/hostfs/var/lib/systemd/rfkill rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/waagent /var/lib/snapd/hostfs/var/lib/waagent rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/log /var/lib/snapd/hostfs/var/log rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/lib/snapd/hostfs/var/snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/lib/snapd/hostfs/var/tmp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 / /var/lib/snapd/hostfs/writable rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/writable/system-data/snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /var/lib/snapd/hostfs/writable/system-data/snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/writable/system-data/snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro +2:32 / /var/lib/sudo rw,relatime master:85 - tmpfs tmpfs rw,mode=700 +1:1 /system-data/var/lib/systemd/random-seed /var/lib/systemd/random-seed rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/systemd/rfkill /var/lib/systemd/rfkill rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/waagent /var/lib/waagent rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/log /var/log rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/tmp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 / /writable rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /writable/system-data/snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /writable/system-data/snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /writable/system-data/snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /writable/system-data/snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /writable/system-data/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /writable/system-data/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /writable/system-data/snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-core-16-64/PER-USER-18.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-16-64/PER-USER-18.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-core-16-64/PER-USER-18.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-16-64/PER-USER-18.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,202 @@ +0:3 / / ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +2:0 / /dev rw,nosuid,relatime master:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +2:1 / /dev/hugepages rw,relatime master:4 - hugetlbfs hugetlbfs rw +2:2 / /dev/mqueue rw,relatime master:5 - mqueue mqueue rw +2:33 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:3 / /dev/pts rw,nosuid,noexec,relatime master:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +2:33 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:4 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw +0:0 /etc /etc ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/etc/apparmor.d/cache /etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/keyboard /etc/default/keyboard rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /etc/default/swapfile rw,relatime master:12 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/environment /etc/environment rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /etc/fstab rw,nosuid,noexec,relatime master:14 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /etc/group ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /etc/gshadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /etc/hosts rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init /etc/init rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init.d /etc/init.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /etc/iproute2 rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /etc/machine-id rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /etc/modprobe.d rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /etc/modules-load.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /etc/netplan rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /etc/network/if-up.d rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/interfaces.d /etc/network/interfaces.d rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +0:3 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +1:1 /system-data/root/test-etc/passwd /etc/passwd ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ppp /etc/ppp rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc0.d /etc/rc0.d rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc1.d /etc/rc1.d rw,relatime master:28 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc2.d /etc/rc2.d rw,relatime master:29 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc3.d /etc/rc3.d rw,relatime master:30 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc4.d /etc/rc4.d rw,relatime master:31 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc5.d /etc/rc5.d rw,relatime master:32 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc6.d /etc/rc6.d rw,relatime master:33 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rcS.d /etc/rcS.d rw,relatime master:34 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rsyslog.d /etc/rsyslog.d rw,relatime master:35 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /etc/shadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /etc/ssh rw,relatime master:36 - ext4 /dev/sda3 rw,data=ordered +0:3 /etc/ssl /etc/ssl ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +1:1 /system-data/etc/sudoers.d /etc/sudoers.d rw,relatime master:37 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /etc/sysctl.d rw,relatime master:38 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/logind.conf.d /etc/systemd/logind.conf.d rw,relatime master:39 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/network /etc/systemd/network rw,relatime master:40 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime master:41 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime master:42 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system.conf.d /etc/systemd/system.conf.d rw,relatime master:43 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/timesyncd.conf /etc/systemd/timesyncd.conf rw,relatime master:44 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/user.conf.d /etc/systemd/user.conf.d rw,relatime master:45 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /etc/udev/rules.d rw,relatime master:46 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/update-motd.d /etc/update-motd.d rw,relatime master:47 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /etc/writable rw,relatime master:48 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /home rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /lib/firmware ro,relatime master:49 - squashfs /dev/loop1 ro +0:1 /modules /lib/modules ro,relatime master:50 - squashfs /dev/loop1 ro +2:6 / /media rw,relatime shared:51 - tmpfs tmpfs rw +2:7 / /mnt rw,relatime master:52 - tmpfs tmpfs rw +2:8 / /proc rw,nosuid,nodev,noexec,relatime master:53 - proc proc rw +2:9 / /proc/sys/fs/binfmt_misc rw,relatime master:54 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct +1:1 /system-data/root /root rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +2:10 / /run rw,nosuid,noexec,relatime master:55 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /run/cgmanager/fs rw,relatime master:57 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +2:12 / /run/lock rw,nosuid,nodev,noexec,relatime master:58 - tmpfs tmpfs rw,size=VARIABLE +2:10 /netns /run/netns rw,nosuid,noexec,relatime shared:55 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:10 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:13 / /run/user/0 rw,nosuid,nodev,relatime master:59 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro +2:14 / /sys rw,nosuid,nodev,noexec,relatime master:67 - sysfs sysfs rw +2:15 / /sys/fs/cgroup rw master:68 - tmpfs tmpfs rw,mode=755 +2:16 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:69 - cgroup cgroup rw,blkio +2:17 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:70 - cgroup cgroup rw,cpu,cpuacct +2:18 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:71 - cgroup cgroup rw,cpuset,clone_children +2:19 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:72 - cgroup cgroup rw,devices +2:20 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:73 - cgroup cgroup rw,freezer +2:21 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:74 - cgroup cgroup rw,hugetlb,release_agent=/run/cgmanager/agents/cgm-release-agent.hugetlb +2:22 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:75 - cgroup cgroup rw,memory +2:23 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:76 - cgroup cgroup rw,net_cls,net_prio +2:24 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:77 - cgroup cgroup rw,perf_event,release_agent=/run/cgmanager/agents/cgm-release-agent.perf_event +2:25 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:78 - cgroup cgroup rw,pids,release_agent=/run/cgmanager/agents/cgm-release-agent.pids +2:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:79 - cgroup cgroup rw,xattr,release_agent=/lib/systemd/systemd-cgroups-agent,name=systemd +2:27 / /sys/fs/fuse/connections rw,relatime master:80 - fusectl fusectl rw +2:28 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:81 - pstore pstore rw +2:29 / /sys/kernel/debug rw,relatime master:82 - debugfs debugfs rw +2:30 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:83 - securityfs securityfs rw +2:31 / /tmp rw,relatime master:84 - tmpfs tmpfs rw +2:31 /snap.test-snapd-mountinfo-core18/tmp /tmp rw,relatime - tmpfs tmpfs rw +0:0 /usr/lib/snapd /usr/lib/snapd ro,relatime master:1 - squashfs /dev/loop0 ro +2:34 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +0:3 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +2:35 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/extrausers /var/lib/extrausers rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:0 / /var/lib/snapd/hostfs ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda3 rw,data=ordered +1:0 / /var/lib/snapd/hostfs/boot/efi rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 /EFI/ubuntu /var/lib/snapd/hostfs/boot/grub rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:1 /system-data/etc/apparmor.d/cache /var/lib/snapd/hostfs/etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /var/lib/snapd/hostfs/etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /var/lib/snapd/hostfs/etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/keyboard /var/lib/snapd/hostfs/etc/default/keyboard rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /var/lib/snapd/hostfs/etc/default/swapfile rw,relatime master:12 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/environment /var/lib/snapd/hostfs/etc/environment rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /var/lib/snapd/hostfs/etc/fstab rw,nosuid,noexec,relatime master:14 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /var/lib/snapd/hostfs/etc/group ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /var/lib/snapd/hostfs/etc/gshadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /var/lib/snapd/hostfs/etc/hosts rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init /var/lib/snapd/hostfs/etc/init rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/init.d /var/lib/snapd/hostfs/etc/init.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /var/lib/snapd/hostfs/etc/iproute2 rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /var/lib/snapd/hostfs/etc/machine-id rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /var/lib/snapd/hostfs/etc/modprobe.d rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /var/lib/snapd/hostfs/etc/modules-load.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /var/lib/snapd/hostfs/etc/netplan rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /var/lib/snapd/hostfs/etc/network/if-up.d rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/interfaces.d /var/lib/snapd/hostfs/etc/network/interfaces.d rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/passwd /var/lib/snapd/hostfs/etc/passwd ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ppp /var/lib/snapd/hostfs/etc/ppp rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc0.d /var/lib/snapd/hostfs/etc/rc0.d rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc1.d /var/lib/snapd/hostfs/etc/rc1.d rw,relatime master:28 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc2.d /var/lib/snapd/hostfs/etc/rc2.d rw,relatime master:29 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc3.d /var/lib/snapd/hostfs/etc/rc3.d rw,relatime master:30 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc4.d /var/lib/snapd/hostfs/etc/rc4.d rw,relatime master:31 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc5.d /var/lib/snapd/hostfs/etc/rc5.d rw,relatime master:32 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rc6.d /var/lib/snapd/hostfs/etc/rc6.d rw,relatime master:33 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rcS.d /var/lib/snapd/hostfs/etc/rcS.d rw,relatime master:34 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/rsyslog.d /var/lib/snapd/hostfs/etc/rsyslog.d rw,relatime master:35 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /var/lib/snapd/hostfs/etc/shadow ro,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /var/lib/snapd/hostfs/etc/ssh rw,relatime master:36 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sudoers.d /var/lib/snapd/hostfs/etc/sudoers.d rw,relatime master:37 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /var/lib/snapd/hostfs/etc/sysctl.d rw,relatime master:38 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/logind.conf.d /var/lib/snapd/hostfs/etc/systemd/logind.conf.d rw,relatime master:39 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/network /var/lib/snapd/hostfs/etc/systemd/network rw,relatime master:40 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /var/lib/snapd/hostfs/etc/systemd/system rw,relatime master:41 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /var/lib/snapd/hostfs/etc/systemd/system rw,relatime master:42 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system.conf.d /var/lib/snapd/hostfs/etc/systemd/system.conf.d rw,relatime master:43 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/timesyncd.conf /var/lib/snapd/hostfs/etc/systemd/timesyncd.conf rw,relatime master:44 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/user.conf.d /var/lib/snapd/hostfs/etc/systemd/user.conf.d rw,relatime master:45 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /var/lib/snapd/hostfs/etc/udev/rules.d rw,relatime master:46 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/update-motd.d /var/lib/snapd/hostfs/etc/update-motd.d rw,relatime master:47 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /var/lib/snapd/hostfs/etc/writable rw,relatime master:48 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /var/lib/snapd/hostfs/home rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /var/lib/snapd/hostfs/lib/firmware ro,relatime master:49 - squashfs /dev/loop1 ro +0:1 /modules /var/lib/snapd/hostfs/lib/modules ro,relatime master:50 - squashfs /dev/loop1 ro +2:6 / /var/lib/snapd/hostfs/media rw,relatime master:51 - tmpfs tmpfs rw +2:7 / /var/lib/snapd/hostfs/mnt rw,relatime master:52 - tmpfs tmpfs rw +1:1 /system-data/root /var/lib/snapd/hostfs/root rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +2:5 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:56 - tmpfs tmpfs rw,mode=755 +2:10 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:55 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /var/lib/snapd/hostfs/run/cgmanager/fs rw,relatime master:57 - tmpfs cgmfs rw,size=VARIABLE,mode=755 +2:12 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:58 - tmpfs tmpfs rw,size=VARIABLE +2:10 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:13 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:59 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /var/lib/snapd/hostfs/snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /var/lib/snapd/hostfs/snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro +2:31 / /var/lib/snapd/hostfs/tmp rw,relatime master:84 - tmpfs tmpfs rw +1:1 /system-data/var/cache/apparmor /var/lib/snapd/hostfs/var/cache/apparmor rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/cache/snapd /var/lib/snapd/hostfs/var/cache/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/apparmor /var/lib/snapd/hostfs/var/lib/apparmor rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/cloud /var/lib/snapd/hostfs/var/lib/cloud rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/console-conf /var/lib/snapd/hostfs/var/lib/console-conf rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dbus /var/lib/snapd/hostfs/var/lib/dbus rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dhcp /var/lib/snapd/hostfs/var/lib/dhcp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/extrausers /var/lib/snapd/hostfs/var/lib/extrausers rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/initramfs-tools /var/lib/snapd/hostfs/var/lib/initramfs-tools rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/logrotate /var/lib/snapd/hostfs/var/lib/logrotate rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/misc /var/lib/snapd/hostfs/var/lib/misc rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd/hostfs/var/lib/snapd rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +2:32 / /var/lib/snapd/hostfs/var/lib/sudo rw,relatime master:85 - tmpfs tmpfs rw,mode=700 +1:1 /system-data/var/lib/systemd/random-seed /var/lib/snapd/hostfs/var/lib/systemd/random-seed rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/systemd/rfkill /var/lib/snapd/hostfs/var/lib/systemd/rfkill rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/waagent /var/lib/snapd/hostfs/var/lib/waagent rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/log /var/lib/snapd/hostfs/var/log rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/lib/snapd/hostfs/var/snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/lib/snapd/hostfs/var/tmp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 / /var/lib/snapd/hostfs/writable rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/writable/system-data/snap/core/1 ro,nodev,relatime master:60 - squashfs /dev/loop2 ro +0:3 / /var/lib/snapd/hostfs/writable/system-data/snap/core18/1 ro,nodev,relatime master:61 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/1 ro,nodev,relatime master:62 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/writable/system-data/snap/pc/1 ro,nodev,relatime master:63 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:64 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:65 - squashfs /dev/loop7 ro +0:8 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-rsync/1 ro,nodev,relatime master:66 - squashfs /dev/loop8 ro +1:1 /system-data/var/log /var/log rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/snap rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/tmp rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-core-18-64/HOST.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-18-64/HOST.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-core-18-64/HOST.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-18-64/HOST.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,98 @@ +0:0 / / ro,relatime shared:1 - squashfs /dev/loop0 ro +1:0 / /boot/efi rw,relatime shared:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 /EFI/ubuntu /boot/grub rw,relatime shared:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +2:0 / /dev rw,nosuid,relatime shared:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +2:1 / /dev/hugepages rw,relatime shared:4 - hugetlbfs hugetlbfs rw,pagesize=2M +2:2 / /dev/mqueue rw,relatime shared:5 - mqueue mqueue rw +2:3 / /dev/pts rw,nosuid,noexec,relatime shared:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +2:4 / /dev/shm rw,nosuid,nodev shared:7 - tmpfs tmpfs rw +1:1 /system-data/etc/apparmor.d/cache /etc/apparmor.d/cache rw,relatime shared:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /etc/cloud rw,relatime shared:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /etc/dbus-1/system.d rw,relatime shared:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /etc/default/swapfile rw,relatime shared:11 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /etc/fstab rw,nosuid,noexec,relatime shared:12 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /etc/group ro,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /etc/gshadow ro,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /etc/hosts rw,relatime shared:14 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /etc/iproute2 rw,relatime shared:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /etc/machine-id rw,relatime shared:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /etc/modprobe.d rw,relatime shared:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /etc/modules-load.d rw,relatime shared:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /etc/netplan rw,relatime shared:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /etc/network/if-up.d rw,relatime shared:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/passwd /etc/passwd ro,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /etc/shadow ro,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /etc/ssh rw,relatime shared:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sudoers.d /etc/sudoers.d rw,relatime shared:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /etc/sysctl.d rw,relatime shared:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd /etc/systemd rw,relatime shared:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime shared:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /etc/udev/rules.d rw,relatime shared:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /etc/writable rw,relatime shared:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /home rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /lib/firmware ro,relatime shared:28 - squashfs /dev/loop1 ro +0:1 /modules /lib/modules ro,relatime shared:29 - squashfs /dev/loop1 ro +2:6 / /media rw,relatime shared:30 - tmpfs tmpfs rw +2:7 / /mnt rw,relatime shared:31 - tmpfs tmpfs rw +2:8 / /proc rw,nosuid,nodev,noexec,relatime shared:32 - proc proc rw +2:9 / /proc/sys/fs/binfmt_misc rw,relatime shared:33 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +1:1 /system-data/root /root rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +2:10 / /run rw,nosuid,noexec,relatime shared:34 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:5 / /run rw,nosuid,noexec,relatime shared:35 - tmpfs tmpfs rw,mode=755 +2:11 / /run/lock rw,nosuid,nodev,noexec,relatime shared:36 - tmpfs tmpfs rw,size=VARIABLE +2:10 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:12 / /run/user/0 rw,nosuid,nodev,relatime shared:37 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /snap rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /snap/core/1 ro,nodev,relatime shared:38 - squashfs /dev/loop2 ro +0:0 / /snap/core18/1 ro,nodev,relatime shared:39 - squashfs /dev/loop0 ro +0:1 / /snap/pc-kernel/1 ro,nodev,relatime shared:40 - squashfs /dev/loop1 ro +0:3 / /snap/pc/1 ro,nodev,relatime shared:41 - squashfs /dev/loop3 ro +0:4 / /snap/snapd/1 ro,nodev,relatime shared:42 - squashfs /dev/loop4 ro +0:5 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime shared:43 - squashfs /dev/loop5 ro +0:6 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime shared:44 - squashfs /dev/loop6 ro +0:7 / /snap/test-snapd-rsync-core18/1 ro,nodev,relatime shared:45 - squashfs /dev/loop7 ro +2:13 / /sys rw,nosuid,nodev,noexec,relatime shared:46 - sysfs sysfs rw +2:14 / /sys/fs/cgroup ro,nosuid,nodev,noexec shared:47 - tmpfs tmpfs ro,mode=755 +2:15 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime shared:48 - cgroup cgroup rw,blkio +2:16 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime shared:49 - cgroup cgroup rw,cpu,cpuacct +2:17 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime shared:50 - cgroup cgroup rw,cpuset +2:18 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime shared:51 - cgroup cgroup rw,devices +2:19 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime shared:52 - cgroup cgroup rw,freezer +2:20 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime shared:53 - cgroup cgroup rw,hugetlb +2:21 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime shared:54 - cgroup cgroup rw,memory +2:22 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime shared:55 - cgroup cgroup rw,net_cls,net_prio +2:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime shared:56 - cgroup cgroup rw,perf_event +2:24 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime shared:57 - cgroup cgroup rw,pids +2:25 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime shared:58 - cgroup cgroup rw,rdma +2:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime shared:59 - cgroup cgroup rw,xattr,name=systemd +2:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime shared:60 - cgroup2 cgroup rw,nsdelegate +2:28 / /sys/fs/fuse/connections rw,relatime shared:61 - fusectl fusectl rw +2:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime shared:62 - pstore pstore rw +2:30 / /sys/kernel/config rw,relatime shared:63 - configfs configfs rw +2:31 / /sys/kernel/debug rw,relatime shared:64 - debugfs debugfs rw +2:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime shared:65 - securityfs securityfs rw +2:33 / /tmp rw,relatime shared:66 - tmpfs tmpfs rw +0:4 /usr/lib/snapd /usr/lib/snapd ro,nodev,relatime shared:42 - squashfs /dev/loop4 ro +1:1 /system-data/var/cache /var/cache rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/cloud /var/lib/cloud rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/console-conf /var/lib/console-conf rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dbus /var/lib/dbus rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dhcp /var/lib/dhcp rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/extrausers /var/lib/extrausers rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/misc /var/lib/misc rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/private/systemd /var/lib/private/systemd rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +2:34 / /var/lib/sudo rw,relatime shared:67 - tmpfs tmpfs rw,mode=700 +1:1 /system-data/var/lib/systemd /var/lib/systemd rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/log /var/log rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/snap rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/tmp rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +1:1 / /writable rw,relatime shared:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /writable/system-data/snap/core/1 ro,nodev,relatime shared:38 - squashfs /dev/loop2 ro +0:0 / /writable/system-data/snap/core18/1 ro,nodev,relatime shared:39 - squashfs /dev/loop0 ro +0:1 / /writable/system-data/snap/pc-kernel/1 ro,nodev,relatime shared:40 - squashfs /dev/loop1 ro +0:3 / /writable/system-data/snap/pc/1 ro,nodev,relatime shared:41 - squashfs /dev/loop3 ro +0:4 / /writable/system-data/snap/snapd/1 ro,nodev,relatime shared:42 - squashfs /dev/loop4 ro +0:5 / /writable/system-data/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime shared:43 - squashfs /dev/loop5 ro +0:6 / /writable/system-data/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime shared:44 - squashfs /dev/loop6 ro +0:7 / /writable/system-data/snap/test-snapd-rsync-core18/1 ro,nodev,relatime shared:45 - squashfs /dev/loop7 ro diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-core-18-64/PER-SNAP-16.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-18-64/PER-SNAP-16.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-core-18-64/PER-SNAP-16.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-18-64/PER-SNAP-16.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,160 @@ +0:2 / / ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +2:0 / /dev rw,nosuid,relatime master:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +2:1 / /dev/hugepages rw,relatime master:4 - hugetlbfs hugetlbfs rw,pagesize=2M +2:2 / /dev/mqueue rw,relatime master:5 - mqueue mqueue rw +2:35 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:3 / /dev/pts rw,nosuid,noexec,relatime master:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +2:35 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:4 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw +0:0 /etc /etc ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/etc/apparmor.d/cache /etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /etc/default/swapfile rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /etc/fstab rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /etc/group ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /etc/gshadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /etc/hosts rw,relatime master:14 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /etc/iproute2 rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /etc/machine-id rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /etc/modprobe.d rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /etc/modules-load.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /etc/netplan rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /etc/network/if-up.d rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +0:2 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +1:1 /system-data/root/test-etc/passwd /etc/passwd ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /etc/shadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /etc/ssh rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +0:2 /etc/ssl /etc/ssl ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +1:1 /system-data/etc/sudoers.d /etc/sudoers.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /etc/sysctl.d rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd /etc/systemd rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /etc/udev/rules.d rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /etc/writable rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /home rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /lib/firmware ro,relatime master:28 - squashfs /dev/loop1 ro +0:1 /modules /lib/modules ro,relatime master:29 - squashfs /dev/loop1 ro +2:6 / /media rw,relatime shared:30 - tmpfs tmpfs rw +2:7 / /mnt rw,relatime master:31 - tmpfs tmpfs rw +2:8 / /proc rw,nosuid,nodev,noexec,relatime master:32 - proc proc rw +2:9 / /proc/sys/fs/binfmt_misc rw,relatime master:33 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +1:1 /system-data/root /root rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:10 / /run rw,nosuid,noexec,relatime master:34 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /run/lock rw,nosuid,nodev,noexec,relatime master:36 - tmpfs tmpfs rw,size=VARIABLE +2:10 /netns /run/netns rw,nosuid,noexec,relatime shared:34 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:10 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:12 / /run/user/0 rw,nosuid,nodev,relatime master:37 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /snap/core/1 ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +0:0 / /snap/core18/1 ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +0:1 / /snap/pc-kernel/1 ro,nodev,relatime master:40 - squashfs /dev/loop1 ro +0:3 / /snap/pc/1 ro,nodev,relatime master:41 - squashfs /dev/loop3 ro +0:4 / /snap/snapd/1 ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +0:5 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:43 - squashfs /dev/loop5 ro +0:6 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:44 - squashfs /dev/loop6 ro +0:7 / /snap/test-snapd-rsync-core18/1 ro,nodev,relatime master:45 - squashfs /dev/loop7 ro +2:13 / /sys rw,nosuid,nodev,noexec,relatime master:46 - sysfs sysfs rw +2:14 / /sys/fs/cgroup ro,nosuid,nodev,noexec master:47 - tmpfs tmpfs ro,mode=755 +2:15 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:48 - cgroup cgroup rw,blkio +2:16 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:49 - cgroup cgroup rw,cpu,cpuacct +2:17 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:50 - cgroup cgroup rw,cpuset +2:18 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:51 - cgroup cgroup rw,devices +2:19 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:52 - cgroup cgroup rw,freezer +2:20 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:53 - cgroup cgroup rw,hugetlb +2:21 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:54 - cgroup cgroup rw,memory +2:22 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:55 - cgroup cgroup rw,net_cls,net_prio +2:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:56 - cgroup cgroup rw,perf_event +2:24 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:57 - cgroup cgroup rw,pids +2:25 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime master:58 - cgroup cgroup rw,rdma +2:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:59 - cgroup cgroup rw,xattr,name=systemd +2:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime master:60 - cgroup2 cgroup rw,nsdelegate +2:28 / /sys/fs/fuse/connections rw,relatime master:61 - fusectl fusectl rw +2:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:62 - pstore pstore rw +2:30 / /sys/kernel/config rw,relatime master:63 - configfs configfs rw +2:31 / /sys/kernel/debug rw,relatime master:64 - debugfs debugfs rw +2:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:65 - securityfs securityfs rw +2:33 / /tmp rw,relatime master:66 - tmpfs tmpfs rw +2:33 /snap.test-snapd-mountinfo-core16/tmp /tmp rw,relatime - tmpfs tmpfs rw +0:4 /usr/lib/snapd /usr/lib/snapd ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +2:36 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +0:2 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +2:37 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/extrausers /var/lib/extrausers rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:0 / /var/lib/snapd/hostfs ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda3 rw,data=ordered +1:0 / /var/lib/snapd/hostfs/boot/efi rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 /EFI/ubuntu /var/lib/snapd/hostfs/boot/grub rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:1 /system-data/etc/apparmor.d/cache /var/lib/snapd/hostfs/etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /var/lib/snapd/hostfs/etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /var/lib/snapd/hostfs/etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /var/lib/snapd/hostfs/etc/default/swapfile rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /var/lib/snapd/hostfs/etc/fstab rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /var/lib/snapd/hostfs/etc/group ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /var/lib/snapd/hostfs/etc/gshadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /var/lib/snapd/hostfs/etc/hosts rw,relatime master:14 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /var/lib/snapd/hostfs/etc/iproute2 rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /var/lib/snapd/hostfs/etc/machine-id rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /var/lib/snapd/hostfs/etc/modprobe.d rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /var/lib/snapd/hostfs/etc/modules-load.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /var/lib/snapd/hostfs/etc/netplan rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /var/lib/snapd/hostfs/etc/network/if-up.d rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/passwd /var/lib/snapd/hostfs/etc/passwd ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /var/lib/snapd/hostfs/etc/shadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /var/lib/snapd/hostfs/etc/ssh rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sudoers.d /var/lib/snapd/hostfs/etc/sudoers.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /var/lib/snapd/hostfs/etc/sysctl.d rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd /var/lib/snapd/hostfs/etc/systemd rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /var/lib/snapd/hostfs/etc/systemd/system rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /var/lib/snapd/hostfs/etc/udev/rules.d rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /var/lib/snapd/hostfs/etc/writable rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /var/lib/snapd/hostfs/home rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /var/lib/snapd/hostfs/lib/firmware ro,relatime master:28 - squashfs /dev/loop1 ro +0:1 /modules /var/lib/snapd/hostfs/lib/modules ro,relatime master:29 - squashfs /dev/loop1 ro +2:6 / /var/lib/snapd/hostfs/media rw,relatime master:30 - tmpfs tmpfs rw +2:7 / /var/lib/snapd/hostfs/mnt rw,relatime master:31 - tmpfs tmpfs rw +1:1 /system-data/root /var/lib/snapd/hostfs/root rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:5 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:35 - tmpfs tmpfs rw,mode=755 +2:10 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:34 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:36 - tmpfs tmpfs rw,size=VARIABLE +2:10 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:12 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:37 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /var/lib/snapd/hostfs/snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +0:0 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +0:1 / /var/lib/snapd/hostfs/snap/pc-kernel/1 ro,nodev,relatime master:40 - squashfs /dev/loop1 ro +0:3 / /var/lib/snapd/hostfs/snap/pc/1 ro,nodev,relatime master:41 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/snap/snapd/1 ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:43 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:44 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/snap/test-snapd-rsync-core18/1 ro,nodev,relatime master:45 - squashfs /dev/loop7 ro +2:33 / /var/lib/snapd/hostfs/tmp rw,relatime master:66 - tmpfs tmpfs rw +0:4 /usr/lib/snapd /var/lib/snapd/hostfs/usr/lib/snapd ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +1:1 /system-data/var/cache /var/lib/snapd/hostfs/var/cache rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/cloud /var/lib/snapd/hostfs/var/lib/cloud rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/console-conf /var/lib/snapd/hostfs/var/lib/console-conf rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dbus /var/lib/snapd/hostfs/var/lib/dbus rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dhcp /var/lib/snapd/hostfs/var/lib/dhcp rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/extrausers /var/lib/snapd/hostfs/var/lib/extrausers rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/misc /var/lib/snapd/hostfs/var/lib/misc rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/private/systemd /var/lib/snapd/hostfs/var/lib/private/systemd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd/hostfs/var/lib/snapd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:34 / /var/lib/snapd/hostfs/var/lib/sudo rw,relatime master:67 - tmpfs tmpfs rw,mode=700 +1:1 /system-data/var/lib/systemd /var/lib/snapd/hostfs/var/lib/systemd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/log /var/lib/snapd/hostfs/var/log rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/lib/snapd/hostfs/var/snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/lib/snapd/hostfs/var/tmp rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 / /var/lib/snapd/hostfs/writable rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/writable/system-data/snap/core/1 ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +0:0 / /var/lib/snapd/hostfs/writable/system-data/snap/core18/1 ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +0:1 / /var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/1 ro,nodev,relatime master:40 - squashfs /dev/loop1 ro +0:3 / /var/lib/snapd/hostfs/writable/system-data/snap/pc/1 ro,nodev,relatime master:41 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/writable/system-data/snap/snapd/1 ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:43 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:44 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-rsync-core18/1 ro,nodev,relatime master:45 - squashfs /dev/loop7 ro +1:1 /system-data/var/log /var/log rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/tmp rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-core-18-64/PER-SNAP-18.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-18-64/PER-SNAP-18.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-core-18-64/PER-SNAP-18.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-18-64/PER-SNAP-18.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,160 @@ +0:0 / / ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +2:0 / /dev rw,nosuid,relatime master:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +2:1 / /dev/hugepages rw,relatime master:4 - hugetlbfs hugetlbfs rw,pagesize=2M +2:2 / /dev/mqueue rw,relatime master:5 - mqueue mqueue rw +2:35 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:3 / /dev/pts rw,nosuid,noexec,relatime master:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +2:35 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:4 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw +0:0 /etc /etc ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/etc/apparmor.d/cache /etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /etc/default/swapfile rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /etc/fstab rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /etc/group ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /etc/gshadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /etc/hosts rw,relatime master:14 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /etc/iproute2 rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /etc/machine-id rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /etc/modprobe.d rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /etc/modules-load.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /etc/netplan rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /etc/network/if-up.d rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +0:0 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +1:1 /system-data/root/test-etc/passwd /etc/passwd ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /etc/shadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /etc/ssh rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +0:0 /etc/ssl /etc/ssl ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +1:1 /system-data/etc/sudoers.d /etc/sudoers.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /etc/sysctl.d rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd /etc/systemd rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /etc/udev/rules.d rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /etc/writable rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /home rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /lib/firmware ro,relatime master:28 - squashfs /dev/loop1 ro +0:1 /modules /lib/modules ro,relatime master:29 - squashfs /dev/loop1 ro +2:6 / /media rw,relatime shared:30 - tmpfs tmpfs rw +2:7 / /mnt rw,relatime master:31 - tmpfs tmpfs rw +2:8 / /proc rw,nosuid,nodev,noexec,relatime master:32 - proc proc rw +2:9 / /proc/sys/fs/binfmt_misc rw,relatime master:33 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +1:1 /system-data/root /root rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:10 / /run rw,nosuid,noexec,relatime master:34 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /run/lock rw,nosuid,nodev,noexec,relatime master:36 - tmpfs tmpfs rw,size=VARIABLE +2:10 /netns /run/netns rw,nosuid,noexec,relatime shared:34 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:10 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:12 / /run/user/0 rw,nosuid,nodev,relatime master:37 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /snap/core/1 ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +0:0 / /snap/core18/1 ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +0:1 / /snap/pc-kernel/1 ro,nodev,relatime master:40 - squashfs /dev/loop1 ro +0:3 / /snap/pc/1 ro,nodev,relatime master:41 - squashfs /dev/loop3 ro +0:4 / /snap/snapd/1 ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +0:5 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:43 - squashfs /dev/loop5 ro +0:6 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:44 - squashfs /dev/loop6 ro +0:7 / /snap/test-snapd-rsync-core18/1 ro,nodev,relatime master:45 - squashfs /dev/loop7 ro +2:13 / /sys rw,nosuid,nodev,noexec,relatime master:46 - sysfs sysfs rw +2:14 / /sys/fs/cgroup ro,nosuid,nodev,noexec master:47 - tmpfs tmpfs ro,mode=755 +2:15 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:48 - cgroup cgroup rw,blkio +2:16 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:49 - cgroup cgroup rw,cpu,cpuacct +2:17 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:50 - cgroup cgroup rw,cpuset +2:18 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:51 - cgroup cgroup rw,devices +2:19 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:52 - cgroup cgroup rw,freezer +2:20 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:53 - cgroup cgroup rw,hugetlb +2:21 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:54 - cgroup cgroup rw,memory +2:22 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:55 - cgroup cgroup rw,net_cls,net_prio +2:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:56 - cgroup cgroup rw,perf_event +2:24 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:57 - cgroup cgroup rw,pids +2:25 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime master:58 - cgroup cgroup rw,rdma +2:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:59 - cgroup cgroup rw,xattr,name=systemd +2:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime master:60 - cgroup2 cgroup rw,nsdelegate +2:28 / /sys/fs/fuse/connections rw,relatime master:61 - fusectl fusectl rw +2:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:62 - pstore pstore rw +2:30 / /sys/kernel/config rw,relatime master:63 - configfs configfs rw +2:31 / /sys/kernel/debug rw,relatime master:64 - debugfs debugfs rw +2:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:65 - securityfs securityfs rw +2:33 / /tmp rw,relatime master:66 - tmpfs tmpfs rw +2:33 /snap.test-snapd-mountinfo-core18/tmp /tmp rw,relatime - tmpfs tmpfs rw +0:4 /usr/lib/snapd /usr/lib/snapd ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +2:36 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +0:0 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +2:37 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/extrausers /var/lib/extrausers rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:0 / /var/lib/snapd/hostfs ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda3 rw,data=ordered +1:0 / /var/lib/snapd/hostfs/boot/efi rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 /EFI/ubuntu /var/lib/snapd/hostfs/boot/grub rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:1 /system-data/etc/apparmor.d/cache /var/lib/snapd/hostfs/etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /var/lib/snapd/hostfs/etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /var/lib/snapd/hostfs/etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /var/lib/snapd/hostfs/etc/default/swapfile rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /var/lib/snapd/hostfs/etc/fstab rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /var/lib/snapd/hostfs/etc/group ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /var/lib/snapd/hostfs/etc/gshadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /var/lib/snapd/hostfs/etc/hosts rw,relatime master:14 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /var/lib/snapd/hostfs/etc/iproute2 rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /var/lib/snapd/hostfs/etc/machine-id rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /var/lib/snapd/hostfs/etc/modprobe.d rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /var/lib/snapd/hostfs/etc/modules-load.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /var/lib/snapd/hostfs/etc/netplan rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /var/lib/snapd/hostfs/etc/network/if-up.d rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/passwd /var/lib/snapd/hostfs/etc/passwd ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /var/lib/snapd/hostfs/etc/shadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /var/lib/snapd/hostfs/etc/ssh rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sudoers.d /var/lib/snapd/hostfs/etc/sudoers.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /var/lib/snapd/hostfs/etc/sysctl.d rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd /var/lib/snapd/hostfs/etc/systemd rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /var/lib/snapd/hostfs/etc/systemd/system rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /var/lib/snapd/hostfs/etc/udev/rules.d rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /var/lib/snapd/hostfs/etc/writable rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /var/lib/snapd/hostfs/home rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /var/lib/snapd/hostfs/lib/firmware ro,relatime master:28 - squashfs /dev/loop1 ro +0:1 /modules /var/lib/snapd/hostfs/lib/modules ro,relatime master:29 - squashfs /dev/loop1 ro +2:6 / /var/lib/snapd/hostfs/media rw,relatime master:30 - tmpfs tmpfs rw +2:7 / /var/lib/snapd/hostfs/mnt rw,relatime master:31 - tmpfs tmpfs rw +1:1 /system-data/root /var/lib/snapd/hostfs/root rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:5 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:35 - tmpfs tmpfs rw,mode=755 +2:10 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:34 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:36 - tmpfs tmpfs rw,size=VARIABLE +2:10 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:12 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:37 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /var/lib/snapd/hostfs/snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +0:0 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +0:1 / /var/lib/snapd/hostfs/snap/pc-kernel/1 ro,nodev,relatime master:40 - squashfs /dev/loop1 ro +0:3 / /var/lib/snapd/hostfs/snap/pc/1 ro,nodev,relatime master:41 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/snap/snapd/1 ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:43 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:44 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/snap/test-snapd-rsync-core18/1 ro,nodev,relatime master:45 - squashfs /dev/loop7 ro +2:33 / /var/lib/snapd/hostfs/tmp rw,relatime master:66 - tmpfs tmpfs rw +0:4 /usr/lib/snapd /var/lib/snapd/hostfs/usr/lib/snapd ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +1:1 /system-data/var/cache /var/lib/snapd/hostfs/var/cache rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/cloud /var/lib/snapd/hostfs/var/lib/cloud rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/console-conf /var/lib/snapd/hostfs/var/lib/console-conf rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dbus /var/lib/snapd/hostfs/var/lib/dbus rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dhcp /var/lib/snapd/hostfs/var/lib/dhcp rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/extrausers /var/lib/snapd/hostfs/var/lib/extrausers rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/misc /var/lib/snapd/hostfs/var/lib/misc rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/private/systemd /var/lib/snapd/hostfs/var/lib/private/systemd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd/hostfs/var/lib/snapd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:34 / /var/lib/snapd/hostfs/var/lib/sudo rw,relatime master:67 - tmpfs tmpfs rw,mode=700 +1:1 /system-data/var/lib/systemd /var/lib/snapd/hostfs/var/lib/systemd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/log /var/lib/snapd/hostfs/var/log rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/lib/snapd/hostfs/var/snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/lib/snapd/hostfs/var/tmp rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 / /var/lib/snapd/hostfs/writable rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/writable/system-data/snap/core/1 ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +0:0 / /var/lib/snapd/hostfs/writable/system-data/snap/core18/1 ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +0:1 / /var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/1 ro,nodev,relatime master:40 - squashfs /dev/loop1 ro +0:3 / /var/lib/snapd/hostfs/writable/system-data/snap/pc/1 ro,nodev,relatime master:41 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/writable/system-data/snap/snapd/1 ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:43 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:44 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-rsync-core18/1 ro,nodev,relatime master:45 - squashfs /dev/loop7 ro +1:1 /system-data/var/log /var/log rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/tmp rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-core-18-64/PER-USER-16.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-18-64/PER-USER-16.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-core-18-64/PER-USER-16.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-18-64/PER-USER-16.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,160 @@ +0:2 / / ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +2:0 / /dev rw,nosuid,relatime master:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +2:1 / /dev/hugepages rw,relatime master:4 - hugetlbfs hugetlbfs rw,pagesize=2M +2:2 / /dev/mqueue rw,relatime master:5 - mqueue mqueue rw +2:35 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:3 / /dev/pts rw,nosuid,noexec,relatime master:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +2:35 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:4 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw +0:0 /etc /etc ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/etc/apparmor.d/cache /etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /etc/default/swapfile rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /etc/fstab rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /etc/group ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /etc/gshadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /etc/hosts rw,relatime master:14 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /etc/iproute2 rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /etc/machine-id rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /etc/modprobe.d rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /etc/modules-load.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /etc/netplan rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /etc/network/if-up.d rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +0:2 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +1:1 /system-data/root/test-etc/passwd /etc/passwd ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /etc/shadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /etc/ssh rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +0:2 /etc/ssl /etc/ssl ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +1:1 /system-data/etc/sudoers.d /etc/sudoers.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /etc/sysctl.d rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd /etc/systemd rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /etc/udev/rules.d rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /etc/writable rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /home rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /lib/firmware ro,relatime master:28 - squashfs /dev/loop1 ro +0:1 /modules /lib/modules ro,relatime master:29 - squashfs /dev/loop1 ro +2:6 / /media rw,relatime shared:30 - tmpfs tmpfs rw +2:7 / /mnt rw,relatime master:31 - tmpfs tmpfs rw +2:8 / /proc rw,nosuid,nodev,noexec,relatime master:32 - proc proc rw +2:9 / /proc/sys/fs/binfmt_misc rw,relatime master:33 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +1:1 /system-data/root /root rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:10 / /run rw,nosuid,noexec,relatime master:34 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /run/lock rw,nosuid,nodev,noexec,relatime master:36 - tmpfs tmpfs rw,size=VARIABLE +2:10 /netns /run/netns rw,nosuid,noexec,relatime shared:34 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:10 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:12 / /run/user/0 rw,nosuid,nodev,relatime master:37 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /snap/core/1 ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +0:0 / /snap/core18/1 ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +0:1 / /snap/pc-kernel/1 ro,nodev,relatime master:40 - squashfs /dev/loop1 ro +0:3 / /snap/pc/1 ro,nodev,relatime master:41 - squashfs /dev/loop3 ro +0:4 / /snap/snapd/1 ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +0:5 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:43 - squashfs /dev/loop5 ro +0:6 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:44 - squashfs /dev/loop6 ro +0:7 / /snap/test-snapd-rsync-core18/1 ro,nodev,relatime master:45 - squashfs /dev/loop7 ro +2:13 / /sys rw,nosuid,nodev,noexec,relatime master:46 - sysfs sysfs rw +2:14 / /sys/fs/cgroup ro,nosuid,nodev,noexec master:47 - tmpfs tmpfs ro,mode=755 +2:15 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:48 - cgroup cgroup rw,blkio +2:16 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:49 - cgroup cgroup rw,cpu,cpuacct +2:17 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:50 - cgroup cgroup rw,cpuset +2:18 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:51 - cgroup cgroup rw,devices +2:19 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:52 - cgroup cgroup rw,freezer +2:20 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:53 - cgroup cgroup rw,hugetlb +2:21 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:54 - cgroup cgroup rw,memory +2:22 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:55 - cgroup cgroup rw,net_cls,net_prio +2:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:56 - cgroup cgroup rw,perf_event +2:24 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:57 - cgroup cgroup rw,pids +2:25 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime master:58 - cgroup cgroup rw,rdma +2:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:59 - cgroup cgroup rw,xattr,name=systemd +2:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime master:60 - cgroup2 cgroup rw,nsdelegate +2:28 / /sys/fs/fuse/connections rw,relatime master:61 - fusectl fusectl rw +2:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:62 - pstore pstore rw +2:30 / /sys/kernel/config rw,relatime master:63 - configfs configfs rw +2:31 / /sys/kernel/debug rw,relatime master:64 - debugfs debugfs rw +2:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:65 - securityfs securityfs rw +2:33 / /tmp rw,relatime master:66 - tmpfs tmpfs rw +2:33 /snap.test-snapd-mountinfo-core16/tmp /tmp rw,relatime - tmpfs tmpfs rw +0:4 /usr/lib/snapd /usr/lib/snapd ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +2:36 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +0:2 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +2:37 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/extrausers /var/lib/extrausers rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:0 / /var/lib/snapd/hostfs ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda3 rw,data=ordered +1:0 / /var/lib/snapd/hostfs/boot/efi rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 /EFI/ubuntu /var/lib/snapd/hostfs/boot/grub rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:1 /system-data/etc/apparmor.d/cache /var/lib/snapd/hostfs/etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /var/lib/snapd/hostfs/etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /var/lib/snapd/hostfs/etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /var/lib/snapd/hostfs/etc/default/swapfile rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /var/lib/snapd/hostfs/etc/fstab rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /var/lib/snapd/hostfs/etc/group ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /var/lib/snapd/hostfs/etc/gshadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /var/lib/snapd/hostfs/etc/hosts rw,relatime master:14 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /var/lib/snapd/hostfs/etc/iproute2 rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /var/lib/snapd/hostfs/etc/machine-id rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /var/lib/snapd/hostfs/etc/modprobe.d rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /var/lib/snapd/hostfs/etc/modules-load.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /var/lib/snapd/hostfs/etc/netplan rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /var/lib/snapd/hostfs/etc/network/if-up.d rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/passwd /var/lib/snapd/hostfs/etc/passwd ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /var/lib/snapd/hostfs/etc/shadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /var/lib/snapd/hostfs/etc/ssh rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sudoers.d /var/lib/snapd/hostfs/etc/sudoers.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /var/lib/snapd/hostfs/etc/sysctl.d rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd /var/lib/snapd/hostfs/etc/systemd rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /var/lib/snapd/hostfs/etc/systemd/system rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /var/lib/snapd/hostfs/etc/udev/rules.d rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /var/lib/snapd/hostfs/etc/writable rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /var/lib/snapd/hostfs/home rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /var/lib/snapd/hostfs/lib/firmware ro,relatime master:28 - squashfs /dev/loop1 ro +0:1 /modules /var/lib/snapd/hostfs/lib/modules ro,relatime master:29 - squashfs /dev/loop1 ro +2:6 / /var/lib/snapd/hostfs/media rw,relatime master:30 - tmpfs tmpfs rw +2:7 / /var/lib/snapd/hostfs/mnt rw,relatime master:31 - tmpfs tmpfs rw +1:1 /system-data/root /var/lib/snapd/hostfs/root rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:5 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:35 - tmpfs tmpfs rw,mode=755 +2:10 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:34 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:36 - tmpfs tmpfs rw,size=VARIABLE +2:10 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:12 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:37 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /var/lib/snapd/hostfs/snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +0:0 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +0:1 / /var/lib/snapd/hostfs/snap/pc-kernel/1 ro,nodev,relatime master:40 - squashfs /dev/loop1 ro +0:3 / /var/lib/snapd/hostfs/snap/pc/1 ro,nodev,relatime master:41 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/snap/snapd/1 ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:43 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:44 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/snap/test-snapd-rsync-core18/1 ro,nodev,relatime master:45 - squashfs /dev/loop7 ro +2:33 / /var/lib/snapd/hostfs/tmp rw,relatime master:66 - tmpfs tmpfs rw +0:4 /usr/lib/snapd /var/lib/snapd/hostfs/usr/lib/snapd ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +1:1 /system-data/var/cache /var/lib/snapd/hostfs/var/cache rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/cloud /var/lib/snapd/hostfs/var/lib/cloud rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/console-conf /var/lib/snapd/hostfs/var/lib/console-conf rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dbus /var/lib/snapd/hostfs/var/lib/dbus rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dhcp /var/lib/snapd/hostfs/var/lib/dhcp rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/extrausers /var/lib/snapd/hostfs/var/lib/extrausers rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/misc /var/lib/snapd/hostfs/var/lib/misc rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/private/systemd /var/lib/snapd/hostfs/var/lib/private/systemd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd/hostfs/var/lib/snapd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:34 / /var/lib/snapd/hostfs/var/lib/sudo rw,relatime master:67 - tmpfs tmpfs rw,mode=700 +1:1 /system-data/var/lib/systemd /var/lib/snapd/hostfs/var/lib/systemd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/log /var/lib/snapd/hostfs/var/log rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/lib/snapd/hostfs/var/snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/lib/snapd/hostfs/var/tmp rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 / /var/lib/snapd/hostfs/writable rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/writable/system-data/snap/core/1 ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +0:0 / /var/lib/snapd/hostfs/writable/system-data/snap/core18/1 ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +0:1 / /var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/1 ro,nodev,relatime master:40 - squashfs /dev/loop1 ro +0:3 / /var/lib/snapd/hostfs/writable/system-data/snap/pc/1 ro,nodev,relatime master:41 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/writable/system-data/snap/snapd/1 ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:43 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:44 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-rsync-core18/1 ro,nodev,relatime master:45 - squashfs /dev/loop7 ro +1:1 /system-data/var/log /var/log rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/tmp rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/google.ubuntu-core-18-64/PER-USER-18.expected.txt snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-18-64/PER-USER-18.expected.txt --- snapd-2.40/tests/main/mount-ns/google.ubuntu-core-18-64/PER-USER-18.expected.txt 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/google.ubuntu-core-18-64/PER-USER-18.expected.txt 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,160 @@ +0:0 / / ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +2:0 / /dev rw,nosuid,relatime master:3 - devtmpfs udev rw,size=VARIABLE,nr_inodes=0,mode=755 +2:1 / /dev/hugepages rw,relatime master:4 - hugetlbfs hugetlbfs rw,pagesize=2M +2:2 / /dev/mqueue rw,relatime master:5 - mqueue mqueue rw +2:35 /ptmx /dev/ptmx rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:3 / /dev/pts rw,nosuid,noexec,relatime master:6 - devpts devpts rw,gid=5,mode=620,ptmxmode=000 +2:35 / /dev/pts rw,relatime - devpts devpts rw,gid=5,mode=620,ptmxmode=666 +2:4 / /dev/shm rw,nosuid,nodev master:7 - tmpfs tmpfs rw +0:0 /etc /etc ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/etc/apparmor.d/cache /etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /etc/default/swapfile rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /etc/fstab rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /etc/group ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /etc/gshadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /etc/hosts rw,relatime master:14 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /etc/iproute2 rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /etc/machine-id rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /etc/modprobe.d rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /etc/modules-load.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /etc/netplan rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /etc/network/if-up.d rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +0:0 /etc/nsswitch.conf /etc/nsswitch.conf ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +1:1 /system-data/root/test-etc/passwd /etc/passwd ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /etc/shadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /etc/ssh rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +0:0 /etc/ssl /etc/ssl ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +1:1 /system-data/etc/sudoers.d /etc/sudoers.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /etc/sysctl.d rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd /etc/systemd rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /etc/systemd/system rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /etc/udev/rules.d rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /etc/writable rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /home rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /lib/firmware ro,relatime master:28 - squashfs /dev/loop1 ro +0:1 /modules /lib/modules ro,relatime master:29 - squashfs /dev/loop1 ro +2:6 / /media rw,relatime shared:30 - tmpfs tmpfs rw +2:7 / /mnt rw,relatime master:31 - tmpfs tmpfs rw +2:8 / /proc rw,nosuid,nodev,noexec,relatime master:32 - proc proc rw +2:9 / /proc/sys/fs/binfmt_misc rw,relatime master:33 - autofs systemd-1 rw,fd=0,pgrp=1,timeout=0,minproto=5,maxproto=5,direct,pipe_ino=0 +1:1 /system-data/root /root rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:10 / /run rw,nosuid,noexec,relatime master:34 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /run/lock rw,nosuid,nodev,noexec,relatime master:36 - tmpfs tmpfs rw,size=VARIABLE +2:10 /netns /run/netns rw,nosuid,noexec,relatime shared:34 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:10 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:12 / /run/user/0 rw,nosuid,nodev,relatime master:37 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /snap/core/1 ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +0:0 / /snap/core18/1 ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +0:1 / /snap/pc-kernel/1 ro,nodev,relatime master:40 - squashfs /dev/loop1 ro +0:3 / /snap/pc/1 ro,nodev,relatime master:41 - squashfs /dev/loop3 ro +0:4 / /snap/snapd/1 ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +0:5 / /snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:43 - squashfs /dev/loop5 ro +0:6 / /snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:44 - squashfs /dev/loop6 ro +0:7 / /snap/test-snapd-rsync-core18/1 ro,nodev,relatime master:45 - squashfs /dev/loop7 ro +2:13 / /sys rw,nosuid,nodev,noexec,relatime master:46 - sysfs sysfs rw +2:14 / /sys/fs/cgroup ro,nosuid,nodev,noexec master:47 - tmpfs tmpfs ro,mode=755 +2:15 / /sys/fs/cgroup/blkio rw,nosuid,nodev,noexec,relatime master:48 - cgroup cgroup rw,blkio +2:16 / /sys/fs/cgroup/cpu,cpuacct rw,nosuid,nodev,noexec,relatime master:49 - cgroup cgroup rw,cpu,cpuacct +2:17 / /sys/fs/cgroup/cpuset rw,nosuid,nodev,noexec,relatime master:50 - cgroup cgroup rw,cpuset +2:18 / /sys/fs/cgroup/devices rw,nosuid,nodev,noexec,relatime master:51 - cgroup cgroup rw,devices +2:19 / /sys/fs/cgroup/freezer rw,nosuid,nodev,noexec,relatime master:52 - cgroup cgroup rw,freezer +2:20 / /sys/fs/cgroup/hugetlb rw,nosuid,nodev,noexec,relatime master:53 - cgroup cgroup rw,hugetlb +2:21 / /sys/fs/cgroup/memory rw,nosuid,nodev,noexec,relatime master:54 - cgroup cgroup rw,memory +2:22 / /sys/fs/cgroup/net_cls,net_prio rw,nosuid,nodev,noexec,relatime master:55 - cgroup cgroup rw,net_cls,net_prio +2:23 / /sys/fs/cgroup/perf_event rw,nosuid,nodev,noexec,relatime master:56 - cgroup cgroup rw,perf_event +2:24 / /sys/fs/cgroup/pids rw,nosuid,nodev,noexec,relatime master:57 - cgroup cgroup rw,pids +2:25 / /sys/fs/cgroup/rdma rw,nosuid,nodev,noexec,relatime master:58 - cgroup cgroup rw,rdma +2:26 / /sys/fs/cgroup/systemd rw,nosuid,nodev,noexec,relatime master:59 - cgroup cgroup rw,xattr,name=systemd +2:27 / /sys/fs/cgroup/unified rw,nosuid,nodev,noexec,relatime master:60 - cgroup2 cgroup rw,nsdelegate +2:28 / /sys/fs/fuse/connections rw,relatime master:61 - fusectl fusectl rw +2:29 / /sys/fs/pstore rw,nosuid,nodev,noexec,relatime master:62 - pstore pstore rw +2:30 / /sys/kernel/config rw,relatime master:63 - configfs configfs rw +2:31 / /sys/kernel/debug rw,relatime master:64 - debugfs debugfs rw +2:32 / /sys/kernel/security rw,nosuid,nodev,noexec,relatime master:65 - securityfs securityfs rw +2:33 / /tmp rw,relatime master:66 - tmpfs tmpfs rw +2:33 /snap.test-snapd-mountinfo-core18/tmp /tmp rw,relatime - tmpfs tmpfs rw +0:4 /usr/lib/snapd /usr/lib/snapd ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +2:36 / /usr/share/gdb rw,relatime - tmpfs tmpfs rw,mode=755 +0:0 /usr/share/gdb/auto-load /usr/share/gdb/auto-load ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +2:37 / /usr/share/gdb/test rw,relatime - tmpfs tmpfs rw +0:0 /usr/src /usr/src ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/extrausers /var/lib/extrausers rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:0 / /var/lib/snapd/hostfs ro,relatime master:1 - squashfs /dev/loop0 ro +1:1 /system-data/var/lib/snapd/hostfs /var/lib/snapd/hostfs rw,relatime - ext4 /dev/sda3 rw,data=ordered +1:0 / /var/lib/snapd/hostfs/boot/efi rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:0 /EFI/ubuntu /var/lib/snapd/hostfs/boot/grub rw,relatime master:2 - vfat /dev/sda2 rw,fmask=0022,dmask=0022,codepage=437,iocharset=iso8859-1,shortname=mixed,errors=remount-ro +1:1 /system-data/etc/apparmor.d/cache /var/lib/snapd/hostfs/etc/apparmor.d/cache rw,relatime master:8 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/cloud /var/lib/snapd/hostfs/etc/cloud rw,relatime master:9 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/dbus-1/system.d /var/lib/snapd/hostfs/etc/dbus-1/system.d rw,relatime master:10 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/default/swapfile /var/lib/snapd/hostfs/etc/default/swapfile rw,relatime master:11 - ext4 /dev/sda3 rw,data=ordered +2:5 /image.fstab /var/lib/snapd/hostfs/etc/fstab rw,nosuid,noexec,relatime master:12 - tmpfs tmpfs rw,mode=755 +1:1 /system-data/root/test-etc/group /var/lib/snapd/hostfs/etc/group ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/gshadow /var/lib/snapd/hostfs/etc/gshadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/hosts /var/lib/snapd/hostfs/etc/hosts rw,relatime master:14 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/iproute2 /var/lib/snapd/hostfs/etc/iproute2 rw,relatime master:15 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/machine-id /var/lib/snapd/hostfs/etc/machine-id rw,relatime master:16 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modprobe.d /var/lib/snapd/hostfs/etc/modprobe.d rw,relatime master:17 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/modules-load.d /var/lib/snapd/hostfs/etc/modules-load.d rw,relatime master:18 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/netplan /var/lib/snapd/hostfs/etc/netplan rw,relatime master:19 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/network/if-up.d /var/lib/snapd/hostfs/etc/network/if-up.d rw,relatime master:20 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/passwd /var/lib/snapd/hostfs/etc/passwd ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/root/test-etc/shadow /var/lib/snapd/hostfs/etc/shadow ro,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/ssh /var/lib/snapd/hostfs/etc/ssh rw,relatime master:21 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sudoers.d /var/lib/snapd/hostfs/etc/sudoers.d rw,relatime master:22 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/sysctl.d /var/lib/snapd/hostfs/etc/sysctl.d rw,relatime master:23 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd /var/lib/snapd/hostfs/etc/systemd rw,relatime master:24 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/systemd/system /var/lib/snapd/hostfs/etc/systemd/system rw,relatime master:25 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/udev/rules.d /var/lib/snapd/hostfs/etc/udev/rules.d rw,relatime master:26 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/etc/writable /var/lib/snapd/hostfs/etc/writable rw,relatime master:27 - ext4 /dev/sda3 rw,data=ordered +1:1 /user-data /var/lib/snapd/hostfs/home rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:1 /firmware /var/lib/snapd/hostfs/lib/firmware ro,relatime master:28 - squashfs /dev/loop1 ro +0:1 /modules /var/lib/snapd/hostfs/lib/modules ro,relatime master:29 - squashfs /dev/loop1 ro +2:6 / /var/lib/snapd/hostfs/media rw,relatime master:30 - tmpfs tmpfs rw +2:7 / /var/lib/snapd/hostfs/mnt rw,relatime master:31 - tmpfs tmpfs rw +1:1 /system-data/root /var/lib/snapd/hostfs/root rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:5 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:35 - tmpfs tmpfs rw,mode=755 +2:10 / /var/lib/snapd/hostfs/run rw,nosuid,noexec,relatime master:34 - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:11 / /var/lib/snapd/hostfs/run/lock rw,nosuid,nodev,noexec,relatime master:36 - tmpfs tmpfs rw,size=VARIABLE +2:10 /snapd/ns /var/lib/snapd/hostfs/run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=VARIABLE,mode=755 +2:12 / /var/lib/snapd/hostfs/run/user/0 rw,nosuid,nodev,relatime master:37 - tmpfs tmpfs rw,size=VARIABLE,mode=700 +1:1 /system-data/snap /var/lib/snapd/hostfs/snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/snap/core/1 ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +0:0 / /var/lib/snapd/hostfs/snap/core18/1 ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +0:1 / /var/lib/snapd/hostfs/snap/pc-kernel/1 ro,nodev,relatime master:40 - squashfs /dev/loop1 ro +0:3 / /var/lib/snapd/hostfs/snap/pc/1 ro,nodev,relatime master:41 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/snap/snapd/1 ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:43 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:44 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/snap/test-snapd-rsync-core18/1 ro,nodev,relatime master:45 - squashfs /dev/loop7 ro +2:33 / /var/lib/snapd/hostfs/tmp rw,relatime master:66 - tmpfs tmpfs rw +0:4 /usr/lib/snapd /var/lib/snapd/hostfs/usr/lib/snapd ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +1:1 /system-data/var/cache /var/lib/snapd/hostfs/var/cache rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/cloud /var/lib/snapd/hostfs/var/lib/cloud rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/console-conf /var/lib/snapd/hostfs/var/lib/console-conf rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dbus /var/lib/snapd/hostfs/var/lib/dbus rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/dhcp /var/lib/snapd/hostfs/var/lib/dhcp rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/extrausers /var/lib/snapd/hostfs/var/lib/extrausers rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/misc /var/lib/snapd/hostfs/var/lib/misc rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/private/systemd /var/lib/snapd/hostfs/var/lib/private/systemd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/lib/snapd /var/lib/snapd/hostfs/var/lib/snapd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +2:34 / /var/lib/snapd/hostfs/var/lib/sudo rw,relatime master:67 - tmpfs tmpfs rw,mode=700 +1:1 /system-data/var/lib/systemd /var/lib/snapd/hostfs/var/lib/systemd rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/log /var/lib/snapd/hostfs/var/log rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/lib/snapd/hostfs/var/snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/lib/snapd/hostfs/var/tmp rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 / /var/lib/snapd/hostfs/writable rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +0:2 / /var/lib/snapd/hostfs/writable/system-data/snap/core/1 ro,nodev,relatime master:38 - squashfs /dev/loop2 ro +0:0 / /var/lib/snapd/hostfs/writable/system-data/snap/core18/1 ro,nodev,relatime master:39 - squashfs /dev/loop0 ro +0:1 / /var/lib/snapd/hostfs/writable/system-data/snap/pc-kernel/1 ro,nodev,relatime master:40 - squashfs /dev/loop1 ro +0:3 / /var/lib/snapd/hostfs/writable/system-data/snap/pc/1 ro,nodev,relatime master:41 - squashfs /dev/loop3 ro +0:4 / /var/lib/snapd/hostfs/writable/system-data/snap/snapd/1 ro,nodev,relatime master:42 - squashfs /dev/loop4 ro +0:5 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core16/1 ro,nodev,relatime master:43 - squashfs /dev/loop5 ro +0:6 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-mountinfo-core18/1 ro,nodev,relatime master:44 - squashfs /dev/loop6 ro +0:7 / /var/lib/snapd/hostfs/writable/system-data/snap/test-snapd-rsync-core18/1 ro,nodev,relatime master:45 - squashfs /dev/loop7 ro +1:1 /system-data/var/log /var/log rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/snap /var/snap rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered +1:1 /system-data/var/tmp /var/tmp rw,relatime master:13 - ext4 /dev/sda3 rw,data=ordered diff -Nru snapd-2.40/tests/main/mount-ns/task.yaml snapd-2.42.1/tests/main/mount-ns/task.yaml --- snapd-2.40/tests/main/mount-ns/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,193 @@ +summary: The shape of the mount namespace on classic systems for non-classic snaps +systems: [ubuntu-16.04-64, ubuntu-18.04-64] +# The test itself works perfectly fine but in conjunction with our leaky test +# suite it often fails because it detects cruft left over by other tests in a +# way that was not detected before. Classic systems should be clear of mount +# side-effects now. The test should be _eventually_ enabled on +# ubuntu-core-16-64 and ubuntu-core-18-64. + +# set to manual in release/2.42 because core has changed and fixing +# the tests would require larger changes, i.e. +# https://github.com/snapcore/snapd/pull/7676 so it seems more expedient +# disable this test here +manual: true + +# The test is sensitive to backend type, which designates the used image. +# Backends are enabled one-by-one along with the matching data set. +backends: [google] +details: | + This test measures the mount table of the host, of the mount namespace for + a simple core16-based snap for the per-user mount namespace of a simple + core16-base snap as well as for a simple core18 based snap and finally of a + simple snap using classic confinement. + + The mount tables are obtained from /proc/pid/mountinfo interface. They are + then fed to mountinfo-tool with various determinism adjustment options and + compared to stock tables created when the test was first made. + + Naturally occurring mount tables will differ from invocation to invocation + for several reasons. Snap revisions seen in various paths will drift over + time. The order of mount operations that can be done in parallel will + differ from one boot to another. Good examples of that include + /snap/name/revision, which can be all started in parallel. Another one is + the set of control groups mounted in sysfs. Some mount options may be + created based on the amount of available memory. Some mount entries may + contain numeric IDs that are allocated and are hard to predict. Block + devices backing actual file systems may be on various disks, for instance + on /dev/vda or /dev/sdb. All of those are handled by mountinfo-tool + --rename and --renumber, along with sorting options that combat + non-deterministic mount order. + + Unfortunately this test need separate data sets for google compute engine + and for qemu. The images are just slightly different, containing small + tweaks that result in different initial host mount table. Such changes are + further reflected in per-snap and per-user mount namespaces, since they + contain the view of the host namespace. + + Individual backend / system hierarchies can be compared using tools like + meld or even diff. This test is very broad but which makes it somewhat + fragile. It is likely to fail on any change affecting mounts performed by + to snap-confine and snap-update-ns. This is by design. Mount propagation is + somewhat complex and precise tests may not capture broad behavior changes. + Of entire systems. + + Lastly, on core systems the test is somewhat artificial as it also measures + changes performed by the test preparation process. Some of the mount + entries are not present on real core systems. + + If you see this test randomly failing it may be because it has observed + state leaked by another test that ran on the same machine earlier in the + spread execution chain. +environment: + MACHINE_STATE/inherit: inherit + MACHINE_STATE/reboot: reboot +prepare: | + case "$MACHINE_STATE" in + inherit) + # The test will run with whatever the machine state was originally. + true + ;; + reboot) + # TODO: when https://github.com/snapcore/spread/pull/85 is merged + # and released this test can be allowed to run on bash 4.3. Without + # the workaround for a bug in bash REBOOT causes the spread test to + # fail instead of asking spread to reboot the machine. + if version-tool --strict "$(echo "$BASH_VERSION" | cut -d. -f 1-2)" -eq 4.3; then + echo "SKIP: this test cannot operate on bash 4.3.x" + touch please-skip-this-test + exit 0 + fi + # + # The test will reboot once before performing the test. This will + # remove any ephemeral state that may be left in the kernel by prior + # test cases or by project-wide prepare that is does not persist across + # boots. + if [ "$SPREAD_REBOOT" -eq 0 ]; then + REBOOT + fi + ;; + esac + # The --renumber and --rename options renumber and rename various + # non-deterministic elements of the mount table. The --ref-x1000 option + # sets a multiple of 1000 as the base value for allocated renumbered + # identifiers, depending on the depth of "nesting" (via --ref) that is + # used. This makes it easier to maintain the tables in face of changes to + # the host. + # + # The rewrite and display ordering helps with concurrently mounted + # file-systems. This way even if each element inside, say, /snap/... is + # mounted concurrently and may be mounted in different order the measured + # order is deterministic. The use of filesystem field is there so that + # autofs mounted filesystems (like binfmt_misc) don't have random ordering + # between the real thing and the automount entry. + deterministic-mountinfo-tool() { + mountinfo-tool \ + --renumber \ + --rename \ + --ref-x1000 \ + --rewrite-order mount_point \ + --rewrite-order mount_source \ + --rewrite-order fs_type \ + --display-order mount_point \ + --display-order mount_source \ + --display-order fs_type \ + .dev .root_dir .mount_point .mount_opts .opt_fields .fs_type .mount_source .sb_opts \ + "$@" + } + + echo "Install and connect all the test snaps" + # This way the renumbered peer group numbers won't suggest that core18 is + # somehow mounted only in the per-snap mount namespace and the mount + # namespaces of each variant will be more alike since there won't be small + # differences related to set of base snaps installed. + snap pack test-snapd-mountinfo-classic + snap pack test-snapd-mountinfo-core16 + snap pack test-snapd-mountinfo-core18 + + if snap debug sandbox-features --required confinement-options:classic; then + snap install --dangerous --classic test-snapd-mountinfo-classic_1_all.snap + fi + snap install --dangerous test-snapd-mountinfo-core16_1_all.snap + snap install --dangerous test-snapd-mountinfo-core18_1_all.snap + + snap connect test-snapd-mountinfo-core16:mount-observe + snap connect test-snapd-mountinfo-core18:mount-observe + + echo "Collect mountinfo from the host" + cat /proc/self/mountinfo >HOST.raw.txt + + if snap debug sandbox-features --required confinement-options:classic; then + echo "Collect mountinfo from classic, per-snap and per-snap, per-user mount namespaces" + su root -c "snap run test-snapd-mountinfo-classic" >PER-SNAP-C7.raw.txt + su test -c "snap run test-snapd-mountinfo-classic" >PER-USER-C7.raw.txt + fi + + echo "Collect mountinfo from core16-based, per-snap and per-snap, per-user mount namespaces" + su root -c "snap run test-snapd-mountinfo-core16" >PER-SNAP-16.raw.txt + su test -c "snap run test-snapd-mountinfo-core16" >PER-USER-16.raw.txt + + echo "Collect mountinfo from core18-based, per-snap and per-snap, per-user mount namespaces" + su root -c "snap run test-snapd-mountinfo-core18" >PER-SNAP-18.raw.txt + su test -c "snap run test-snapd-mountinfo-core18" >PER-USER-18.raw.txt + + echo "Transform mountinfo tables to make them deterministic" + deterministic-mountinfo-tool -f HOST.raw.txt >HOST.deterministic.txt + deterministic-mountinfo-tool --ref HOST.raw.txt -f PER-SNAP-16.raw.txt >PER-SNAP-16.deterministic.txt + deterministic-mountinfo-tool --ref HOST.raw.txt --ref PER-SNAP-16.raw.txt -f PER-USER-16.raw.txt >PER-USER-16.deterministic.txt + deterministic-mountinfo-tool --ref HOST.raw.txt -f PER-SNAP-18.raw.txt >PER-SNAP-18.deterministic.txt + deterministic-mountinfo-tool --ref HOST.raw.txt --ref PER-SNAP-18.raw.txt -f PER-USER-18.raw.txt >PER-USER-18.deterministic.txt + if snap debug sandbox-features --required confinement-options:classic; then + deterministic-mountinfo-tool --ref HOST.raw.txt -f PER-SNAP-C7.raw.txt >PER-SNAP-C7.deterministic.txt + deterministic-mountinfo-tool --ref HOST.raw.txt --ref PER-SNAP-C7.raw.txt -f PER-USER-C7.raw.txt >PER-USER-C7.deterministic.txt + fi + + if snap debug sandbox-features --required confinement-options:classic; then + snap remove test-snapd-mountinfo-classic + fi + snap remove test-snapd-mountinfo-core16 + snap remove test-snapd-mountinfo-core18 +execute: | + if [ -e please-skip-this-test ]; then + exit 0 + fi + diff -u "$SPREAD_BACKEND.$SPREAD_SYSTEM/HOST.expected.txt" HOST.deterministic.txt + diff -u "$SPREAD_BACKEND.$SPREAD_SYSTEM/PER-SNAP-16.expected.txt" PER-SNAP-16.deterministic.txt + diff -u "$SPREAD_BACKEND.$SPREAD_SYSTEM/PER-USER-16.expected.txt" PER-USER-16.deterministic.txt + diff -u "$SPREAD_BACKEND.$SPREAD_SYSTEM/PER-SNAP-18.expected.txt" PER-SNAP-18.deterministic.txt + diff -u "$SPREAD_BACKEND.$SPREAD_SYSTEM/PER-USER-18.expected.txt" PER-USER-18.deterministic.txt + if snap debug sandbox-features --required confinement-options:classic; then + diff -u "$SPREAD_BACKEND.$SPREAD_SYSTEM/PER-SNAP-C7.expected.txt" PER-SNAP-C7.deterministic.txt + diff -u "$SPREAD_BACKEND.$SPREAD_SYSTEM/PER-USER-C7.expected.txt" PER-USER-C7.deterministic.txt + fi +restore: | + rm -f ./*.raw.txt ./*.deterministic.txt + rm -f test-snapd-mountinfo-classic_1_all.snap + rm -f test-snapd-mountinfo-core16_1_all.snap + rm -f test-snapd-mountinfo-core18-1_all.snap + rm -f please-skip-this-test +debug: | + for fname in ./*.deterministic.txt; do + echo + cat "$fname" + echo + done diff -Nru snapd-2.40/tests/main/mount-ns/test-snapd-mountinfo-classic/bin/mountinfo snapd-2.42.1/tests/main/mount-ns/test-snapd-mountinfo-classic/bin/mountinfo --- snapd-2.40/tests/main/mount-ns/test-snapd-mountinfo-classic/bin/mountinfo 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/test-snapd-mountinfo-classic/bin/mountinfo 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,2 @@ +#!/bin/sh +exec cat /proc/self/mountinfo diff -Nru snapd-2.40/tests/main/mount-ns/test-snapd-mountinfo-classic/meta/snap.yaml snapd-2.42.1/tests/main/mount-ns/test-snapd-mountinfo-classic/meta/snap.yaml --- snapd-2.40/tests/main/mount-ns/test-snapd-mountinfo-classic/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/test-snapd-mountinfo-classic/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,8 @@ +name: test-snapd-mountinfo-classic +summary: app for displaying mountinfo inside the snap mount namespace +version: 1 +architecture: [all] +confinement: classic +apps: + test-snapd-mountinfo-classic: + command: bin/mountinfo diff -Nru snapd-2.40/tests/main/mount-ns/test-snapd-mountinfo-core16/bin/mountinfo snapd-2.42.1/tests/main/mount-ns/test-snapd-mountinfo-core16/bin/mountinfo --- snapd-2.40/tests/main/mount-ns/test-snapd-mountinfo-core16/bin/mountinfo 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/test-snapd-mountinfo-core16/bin/mountinfo 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,2 @@ +#!/bin/sh +exec cat /proc/self/mountinfo diff -Nru snapd-2.40/tests/main/mount-ns/test-snapd-mountinfo-core16/meta/snap.yaml snapd-2.42.1/tests/main/mount-ns/test-snapd-mountinfo-core16/meta/snap.yaml --- snapd-2.40/tests/main/mount-ns/test-snapd-mountinfo-core16/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/test-snapd-mountinfo-core16/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,17 @@ +name: test-snapd-mountinfo-core16 +summary: app for displaying mountinfo inside the snap mount namespace +version: 1 +architecture: [all] +plugs: + mount-observe: +# This layout is designed to create a writable mimic on top of production base +# snap, in this case core, without being extremely painful to analyze. The core +# snap contains the directory /usr/share/gdb/auto-load which will be re-created +# by the mimic at /usr/share/gdb. This allows us to have a test with just one +# re-created element and without engineering a custom core for this test. +layout: + /usr/share/gdb/test: + type: tmpfs +apps: + test-snapd-mountinfo-core16: + command: bin/mountinfo diff -Nru snapd-2.40/tests/main/mount-ns/test-snapd-mountinfo-core18/bin/mountinfo snapd-2.42.1/tests/main/mount-ns/test-snapd-mountinfo-core18/bin/mountinfo --- snapd-2.40/tests/main/mount-ns/test-snapd-mountinfo-core18/bin/mountinfo 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/test-snapd-mountinfo-core18/bin/mountinfo 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,2 @@ +#!/bin/sh +exec cat /proc/self/mountinfo diff -Nru snapd-2.40/tests/main/mount-ns/test-snapd-mountinfo-core18/meta/snap.yaml snapd-2.42.1/tests/main/mount-ns/test-snapd-mountinfo-core18/meta/snap.yaml --- snapd-2.40/tests/main/mount-ns/test-snapd-mountinfo-core18/meta/snap.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-ns/test-snapd-mountinfo-core18/meta/snap.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,14 @@ +name: test-snapd-mountinfo-core18 +summary: app for displaying mountinfo inside the snap mount namespace +version: 1 +architecture: [all] +base: core18 +plugs: + mount-observe: +# Please see the comment in test-snapd-mountinfo-core16 snap for rationale. +layout: + /usr/share/gdb/test: + type: tmpfs +apps: + test-snapd-mountinfo-core18: + command: bin/mountinfo diff -Nru snapd-2.40/tests/main/mount-protocol-error/task.yaml snapd-2.42.1/tests/main/mount-protocol-error/task.yaml --- snapd-2.40/tests/main/mount-protocol-error/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/mount-protocol-error/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -12,7 +12,7 @@ backends: [-autopkgtest] # only run on a subset of systems because this test takes a long time to run -systems: [ubuntu-18.04-64, ubuntu-18.10-64, arch-linux-64] +systems: [ubuntu-18.04-64, arch-linux-64] execute: | snap set system experimental.parallel-instances=true diff -Nru snapd-2.40/tests/main/netplan-apply/fake-netplan-apply-service.py snapd-2.42.1/tests/main/netplan-apply/fake-netplan-apply-service.py --- snapd-2.40/tests/main/netplan-apply/fake-netplan-apply-service.py 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/netplan-apply/fake-netplan-apply-service.py 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,50 @@ +#!/usr/bin/python3 + +from gi.repository import GLib +import dbus.mainloop.glib +import dbus.service +import sys + +BUS_NAME = "io.netplan.Netplan" +OBJECT_PATH = "/io/netplan/Netplan" +DOC_IFACE = "io.netplan.Netplan" + + +class NetplanApplyService(dbus.service.Object): + def __init__(self, connection, object_path, logfile): + super().__init__(connection, object_path) + self._logfile = logfile + + @dbus.service.method(dbus_interface=DOC_IFACE, in_signature="", + out_signature="b") + def Apply(self): + # log that we were called and always return True + with open(self._logfile, "a+") as fp: + fp.write("Apply called\n") + return True + + +def main(argv): + logfile = argv[1] + dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + main_loop = GLib.MainLoop() + + bus = dbus.SystemBus() + # Make sure we quit when the bus shuts down + bus.add_signal_receiver( + main_loop.quit, signal_name="Disconnected", + path="/org/freedesktop/DBus/Local", + dbus_interface="org.freedesktop.DBus.Local") + + NetplanApplyService(bus, OBJECT_PATH, logfile) + + # Allow other services to assume our bus name + dbus.service.BusName( + BUS_NAME, bus, allow_replacement=True, replace_existing=True, + do_not_queue=True) + + main_loop.run() + + +if __name__ == '__main__': + sys.exit(main(sys.argv)) diff -Nru snapd-2.40/tests/main/netplan-apply/io.netplan.Netplan.conf snapd-2.42.1/tests/main/netplan-apply/io.netplan.Netplan.conf --- snapd-2.40/tests/main/netplan-apply/io.netplan.Netplan.conf 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/netplan-apply/io.netplan.Netplan.conf 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + diff -Nru snapd-2.40/tests/main/netplan-apply/snapcraft.yaml snapd-2.42.1/tests/main/netplan-apply/snapcraft.yaml --- snapd-2.40/tests/main/netplan-apply/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/netplan-apply/snapcraft.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,50 @@ +name: test-snapd-netplan-apply +base: core18 +version: git +summary: Backend-agnostic network configuration in YAML +description: | + Netplan is a utility for easily configuring networking on a linux system. + You simply create a YAML description of the required network interfaces and + what each should be configured to do. From this description Netplan will + generate all the necessary configuration for your chosen renderer tool. +grade: devel +confinement: strict + +apps: + netplan: + command: usr/bin/python3 $SNAP/usr/sbin/netplan + environment: + PYTHONPATH: $SNAP/usr/lib/python3/dist-packages:$PYTHONPATH + adapter: full + plugs: + - network + - network-bind + - network-setup-control + +parts: + netplan: + source: https://github.com/CanonicalLtd/netplan.git + plugin: make + build-packages: + - bash-completion + - libglib2.0-dev + - libyaml-dev + - uuid-dev + - pandoc + - pkg-config + - python3 + - python3-coverage + - python3-yaml + - python3-netifaces + - python3-nose + - pyflakes3 + - pep8 + - systemd + - libsystemd-dev + stage-packages: + - iproute2 + - python3 + - python3-netifaces + - python3-yaml + - systemd + - libatm1 diff -Nru snapd-2.40/tests/main/netplan-apply/task.yaml snapd-2.42.1/tests/main/netplan-apply/task.yaml --- snapd-2.40/tests/main/netplan-apply/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/netplan-apply/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,87 @@ +summary: Ensure that netplan apply works with network-setup-control + +details: | + Netplan apply is used to apply network configuration to the system + +environment: + NETPLAN: io.netplan.Netplan + +# run on all classic ubuntu LTS and current dev systems 16.04+ +systems: + - ubuntu-16* + - ubuntu-18* + - ubuntu-19* + - ubuntu-20* + +prepare: | + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh + snap install test-snapd-netplan-apply --edge + + # backup the dbus service file and policy config if they exist before + # executing + for f in system-services/$NETPLAN.service system.d/$NETPLAN.conf; do + if [ -f /usr/share/dbus-1/$f ]; then + mv /usr/share/dbus-1/$f /usr/share/dbus-1/$f.backup + fi + done + + # install the dbus policy config file and service unit for our fake netplan + # system dbus service + echo "Install the netplan D-Bus activatable system service" + mkdir -p /usr/share/dbus-1/system.d + mkdir -p /usr/share/dbus-1/system-services + cp $NETPLAN.conf /usr/share/dbus-1/system.d/$NETPLAN.conf + # generate the service file here so that we can referece the python file and + # the log file in this directory + cat << EOF > /usr/share/dbus-1/system-services/$NETPLAN.service + [D-BUS Service] + Name=$NETPLAN + Exec=$(pwd)/fake-netplan-apply-service.py $(pwd)/dbus-netplan-apply.log + User=root + AssumedAppArmorLabel=unconfined + EOF + + touch dbus-netplan-apply.log + +restore: | + # kill the dbus service if it is running + set +e + if [ -n "$(pgrep --full fake-netplan-apply-service.py)" ]; then + for pid in $(pgrep --full fake-netplan-apply-service.py); do + kill -9 "$pid" + done + fi + set -e + + # restore the dbus service file and policy config file if the backup exists + for f in system-services/$NETPLAN.service system.d/$NETPLAN.conf; do + if [ -f /usr/share/dbus-1/$f.backup ]; then + mv /usr/share/dbus-1/$f.backup /usr/share/dbus-1/$f + fi + done + +execute: | + echo "The interface is disconnected by default" + snap connections test-snapd-netplan-apply | MATCH 'network-setup-control +test-snapd-netplan-apply:network-setup-control +- +-' + + echo "Running netplan apply without network-setup-control fails" + if test-snapd-netplan-apply.netplan apply; then + echo "Expected access denied error for netplan apply" + exit 1 + fi + + echo "The D-Bus service was not activated" + not MATCH "Apply called" < dbus-netplan-apply.log + + echo "When the interface is connected" + snap connect test-snapd-netplan-apply:network-setup-control + + echo "Running netplan apply now works" + if ! test-snapd-netplan-apply.netplan apply; then + echo "Unexpected error running netplan apply" + exit 1 + fi + + echo "And the D-Bus service was activated" + MATCH "Apply called" < dbus-netplan-apply.log diff -Nru snapd-2.40/tests/main/nfs-support/task.yaml snapd-2.42.1/tests/main/nfs-support/task.yaml --- snapd-2.40/tests/main/nfs-support/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/nfs-support/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -19,16 +19,29 @@ . "$TESTSLIB/snaps.sh" install_local test-snapd-sh + # If /proc/fs/nfsd is not initially mounted then ask the test to unmount it later. + if not mountinfo-tool /proc/fs/nfsd .fs_type=nfsd; then + touch /tmp/please-unmount-nfsd + echo "the test needs to unmount /proc/fs/nfsd if it becomes mounted" + fi + # If /var/lib/nfs/rpc_pipefs is not initially mounted then ask the test to unmount it later. + if not mountinfo-tool /var/lib/nfs/rpc_pipefs .fs_type=rpc_pipefs; then + touch /tmp/please-unmount-rpc-pipefs + echo "the test needs to unmount /var/lib/nfs/rpc_pipefs if it becomes mounted" + fi + restore: | #shellcheck source=tests/lib/pkgdb.sh . "$TESTSLIB/pkgdb.sh" # Unmount NFS mount over /home if one exists. - umount /home || true + if mountinfo-tool /home; then + umount /home + fi # Restore the fstab backup file if one exists. - if [ -e fstab.orig ]; then - mv fstab.orig /etc/fstab + if [ -e /tmp/fstab.orig ]; then + mv /tmp/fstab.orig /etc/fstab fi # Remove the NFS server and its configuration data. @@ -41,6 +54,46 @@ systemctl reset-failed snapd.service systemctl start snapd.service + # Depending on OS do cleanup appropriate for the host. + case "$SPREAD_SYSTEM" in + ubuntu-14.04-*) + # On Ubuntu 14.04 we started the NFS server so stop it here. On + # other versions of Ubuntu the server was pre-installed and + # running so we don't have to stop it. + service nfs-kernel-server stop + ;; + arch-*) + # The nfsdcld service may keep rpc_pipefs busy, as seen in this output from lsof. + # nfsdcld 5736 root 10u FIFO 0,47 0t0 114 /var/lib/nfs/rpc_pipefs/nfsd/cld (deleted) + systemctl stop nfsdcld.service + systemctl stop nfs-server.service + systemctl disable nfs-server.service + ;; + amazon-*|centos-*) + systemctl stop nfs + systemctl disable nfs + ;; + esac + if [ -e /tmp/please-unmount-nfsd ]; then + if mountinfo-tool /proc/fs/nfsd .fs_type=nfsd; then + umount /proc/fs/nfsd + fi + rm -f /tmp/please-unmount-nfsd + fi + if [ -e /tmp/please-unmount-rpc-pipefs ]; then + if mountinfo-tool /var/lib/nfs/rpc_pipefs .fs_type=rpc_pipefs; then + umount /var/lib/nfs/rpc_pipefs + fi + rm -f /tmp/please-unmount-rpc-pipefs + fi + # If the system originally had NFS installed, restore the status after + # changes made in switch-case above. + case "$SPREAD_SYSTEM" in + ubuntu-14.04-*) + service nfs-kernel-server start + ;; + esac + execute: | # only needed because we do it 11 times (!) restart_snapd() { @@ -65,7 +118,7 @@ # Export /home over NFS. mkdir -p /etc/exports.d/ echo '/home localhost(rw,no_subtree_check,no_root_squash)' > /etc/exports.d/test.exports - + # Make sure the nfs service is running case "$SPREAD_SYSTEM" in ubuntu-14.04-*) @@ -75,8 +128,9 @@ systemctl restart nfs-kernel-server ;; fedora-*) - # Enable udp protocol for nfs on fedora which is disable by default + # Enable udp protocol for nfs on fedora which is disabled by default sed -i -e 's/RPCNFSDARGS=.*/RPCNFSDARGS="--udp"/g' /etc/sysconfig/nfs + # FIXME: this is not restored anywhere. systemctl restart nfs ;; arch-*) @@ -176,7 +230,7 @@ ensure_normal_perms # Back up the /etc/fstab file and define a NFS mount mount there. - cp -a /etc/fstab fstab.orig + cp -a /etc/fstab /tmp/fstab.orig echo 'localhost:/home /home nfs defaults 0 0' >> /etc/fstab # Restart snapd and ensure that we have extra permissions again. diff -Nru snapd-2.40/tests/main/non-home/task.yaml snapd-2.42.1/tests/main/non-home/task.yaml --- snapd-2.40/tests/main/non-home/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/non-home/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -13,7 +13,7 @@ adduser --home "$THOME" "$TUSER" restore: | - deluser --remove-home "$TUSER" + user-tool remove-with-group "$TUSER" execute: | echo "Install a snap" diff -Nru snapd-2.40/tests/main/parallel-install-basic/task.yaml snapd-2.42.1/tests/main/parallel-install-basic/task.yaml --- snapd-2.40/tests/main/parallel-install-basic/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/parallel-install-basic/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -81,5 +81,5 @@ su -l -c "test-snapd-tools_foo.cmd sh -c 'cat \$SNAP_USER_COMMON/canary'" test | MATCH canary-instance-common su -l -c "test-snapd-tools_foo.cmd sh -c 'cat \$SNAP_USER_DATA/canary'" test | MATCH canary-instance-snap -restore: +restore: | snap set system experimental.parallel-instances=null diff -Nru snapd-2.40/tests/main/parallel-install-common-dirs/task.yaml snapd-2.42.1/tests/main/parallel-install-common-dirs/task.yaml --- snapd-2.40/tests/main/parallel-install-common-dirs/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/parallel-install-common-dirs/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -80,5 +80,5 @@ not test -d "$SNAP_MOUNT_DIR/test-snapd-tools" not test -d "/var/snap/test-snapd-tools" -restore: +restore: | snap set system experimental.parallel-instances=null diff -Nru snapd-2.40/tests/main/parallel-install-common-dirs-undo/task.yaml snapd-2.42.1/tests/main/parallel-install-common-dirs-undo/task.yaml --- snapd-2.40/tests/main/parallel-install-common-dirs-undo/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/parallel-install-common-dirs-undo/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -33,5 +33,5 @@ not test -d "$SNAP_MOUNT_DIR/test-snapd-service" not test -d "/var/snap/test-snapd-service" -restore: +restore: | snap set system experimental.parallel-instances=null diff -Nru snapd-2.40/tests/main/parallel-install-desktop/task.yaml snapd-2.42.1/tests/main/parallel-install-desktop/task.yaml --- snapd-2.40/tests/main/parallel-install-desktop/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/parallel-install-desktop/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -41,5 +41,5 @@ snap remove basic-desktop test -f /var/lib/snapd/desktop/applications/basic-desktop+longname_echo.desktop -restore: +restore: | snap set system experimental.parallel-instances=null diff -Nru snapd-2.40/tests/main/parallel-install-snap-icons/task.yaml snapd-2.42.1/tests/main/parallel-install-snap-icons/task.yaml --- snapd-2.40/tests/main/parallel-install-snap-icons/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/parallel-install-snap-icons/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,37 @@ +summary: Parallel installed snaps have non-conflicting icons + +restore: | + snap unset system experimental.parallel-instances + +execute: | + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh + + echo "Install a snap providing icons" + install_local test-snapd-icon-theme + + echo "Install additional instances of the snap" + snap set system experimental.parallel-instances=true + install_local_as test-snapd-icon-theme test-snapd-icon-theme_longname + install_local_as test-snapd-icon-theme test-snapd-icon-theme_foo + + echo "Each instance provides its own icons" + icondir=/var/lib/snapd/desktop/icons/hicolor/scalable/apps + [ -f "$icondir/snap.test-snapd-icon-theme.foo.svg" ] + [ -f "$icondir/snap.test-snapd-icon-theme_longname.foo.svg" ] + [ -f "$icondir/snap.test-snapd-icon-theme_foo.foo.svg" ] + + echo "Each instance's desktop file references its own icon" + desktopdir=/var/lib/snapd/desktop/applications + MATCH '^Icon=snap.test-snapd-icon-theme.foo$' < "$desktopdir/test-snapd-icon-theme_echo.desktop" + MATCH '^Icon=snap.test-snapd-icon-theme_longname.foo$' < "$desktopdir/test-snapd-icon-theme+longname_echo.desktop" + MATCH '^Icon=snap.test-snapd-icon-theme_foo.foo$' < "$desktopdir/test-snapd-icon-theme+foo_echo.desktop" + + echo "Removing once instance does not remove the other instances' icons" + snap remove test-snapd-icon-theme_foo + [ -f "$icondir/snap.test-snapd-icon-theme.foo.svg" ] + [ -f "$icondir/snap.test-snapd-icon-theme_longname.foo.svg" ] + [ ! -f "$icondir/snap.test-snapd-icon-theme_foo.foo.svg" ] + + snap remove test-snapd-icon-theme + [ -f "$icondir/snap.test-snapd-icon-theme_longname.foo.svg" ] diff -Nru snapd-2.40/tests/main/prepare-image-grub-core18/task.yaml snapd-2.42.1/tests/main/prepare-image-grub-core18/task.yaml --- snapd-2.40/tests/main/prepare-image-grub-core18/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/prepare-image-grub-core18/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -11,14 +11,14 @@ restore: | rm -rf "$ROOT" - + execute: | echo Running prepare-image - su -c "SNAPPY_USE_STAGING_STORE=$SNAPPY_USE_STAGING_STORE snap prepare-image --channel edge --snap test-snapd-tools $TESTSLIB/assertions/ubuntu-core-18-amd64.model $ROOT" test + su -c "SNAPPY_USE_STAGING_STORE=$SNAPPY_USE_STAGING_STORE snap prepare-image --channel edge --snap test-snapd-tools-core18 $TESTSLIB/assertions/ubuntu-core-18-amd64.model $ROOT" test echo Verifying the result ls -lR "$IMAGE" - for f in pc pc-kernel core18 snapd test-snapd-tools; do + for f in pc pc-kernel core18 snapd test-snapd-tools-core18; do ls "$IMAGE"/var/lib/snapd/seed/snaps/"${f}"*.snap done MATCH snap_core=core18 < "$IMAGE"/boot/grub/grubenv diff -Nru snapd-2.40/tests/main/proxy-no-core/task.yaml snapd-2.42.1/tests/main/proxy-no-core/task.yaml --- snapd-2.40/tests/main/proxy-no-core/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/proxy-no-core/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -52,4 +52,4 @@ # shellcheck source=tests/lib/journalctl.sh . "$TESTSLIB/journalctl.sh" - check_journalctl_log 'CONNECT fastly.cdn.snapcraft.io' -u tinyproxy + check_journalctl_log 'CONNECT fastly.*.cdn.snapcraft.io' -u tinyproxy diff -Nru snapd-2.40/tests/main/refresh-with-epoch-bump/task.yaml snapd-2.42.1/tests/main/refresh-with-epoch-bump/task.yaml --- snapd-2.40/tests/main/refresh-with-epoch-bump/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/refresh-with-epoch-bump/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -3,7 +3,7 @@ details: | This test installs a snap and then refreshes it to a channel that's on an incompatible epoch. The store and snapd should figure - this out and go through a revision that can read the curent epoch + this out and go through a revision that can read the current epoch before re-refreshing to the expected one. prepare: | diff -Nru snapd-2.40/tests/main/remodel/task.yaml snapd-2.42.1/tests/main/remodel/task.yaml --- snapd-2.40/tests/main/remodel/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/remodel/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,7 +1,7 @@ summary: | Test remodel -systems: [ubuntu-core-16-64] +systems: [ubuntu-core-1*-64] prepare: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -10,14 +10,25 @@ fi #shellcheck source=tests/lib/systemd.sh . "$TESTSLIB"/systemd.sh + #shellcheck source=tests/lib/systems.sh + . "$TESTSLIB"/systems.sh + systemctl stop snapd.service snapd.socket rm -rf /var/lib/snapd/assertions/* rm -rf /var/lib/snapd/device rm -rf /var/lib/snapd/state.json mv /var/lib/snapd/seed/assertions/model model.bak cp "$TESTSLIB"/assertions/developer1.account /var/lib/snapd/seed/assertions - cp "$TESTSLIB"/assertions/developer1.account-key /var/lib/snapd/seed/assertions - cp "$TESTSLIB"/assertions/developer1-pc.model /var/lib/snapd/seed/assertions + cp "$TESTSLIB"/assertions/developer1.account-key /var/lib/snapd/seed/assertions + if is_core18_system; then + cp "$TESTSLIB"/assertions/developer1-pc-18.model /var/lib/snapd/seed/assertions/developer1-pc.model + cp "$TESTSLIB"/assertions/developer1-pc-18-revno2.model developer1-pc-revno2.model + cp "$TESTSLIB"/assertions/developer1-pc-18-revno3.model developer1-pc-revno3.model + else + cp "$TESTSLIB"/assertions/developer1-pc.model /var/lib/snapd/seed/assertions + cp "$TESTSLIB"/assertions/developer1-pc-revno2.model . + cp "$TESTSLIB"/assertions/developer1-pc-revno3.model . + fi cp "$TESTSLIB"/assertions/testrootorg-store.account-key /var/lib/snapd/seed/assertions # kick first boot again systemctl start snapd.service snapd.socket @@ -61,8 +72,12 @@ exit fi + #shellcheck source=tests/lib/systems.sh + . "$TESTSLIB"/systems.sh + SNAP="$(get_snap_for_system test-snapd-tools)" + # sanity check - not snap list test-snapd-tools + not snap list "$SNAP" echo "Wait for first boot to be done" while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done @@ -70,26 +85,26 @@ snap debug model|MATCH "model: my-model" echo "Now we remodel" - snap remodel "$TESTSLIB"/assertions/developer1-pc-revno2.model + snap remodel developer1-pc-revno2.model echo "and we got the new required snap" - snap list test-snapd-tools + snap list "$SNAP" echo "and we got the new model assertion" snap debug model|MATCH "revision: 2" snap changes | MATCH "Refresh model assertion from revision 0 to 2" echo "and we cannot remove the new required snap" - not snap remove test-snapd-tools + not snap remove "$SNAP" echo "And we can remodel again this time test-snapd-tools is no longer required" - snap remodel "$TESTSLIB"/assertions/developer1-pc-revno3.model + snap remodel developer1-pc-revno3.model snap debug model|MATCH "revision: 3" snap changes | MATCH "Refresh model assertion from revision 2 to 3" - echo "and test-snapd-tools is still available" - snap list test-snapd-tools + echo "and $SNAP is still available" + snap list "$SNAP" echo "and we can clean it up here because it is no longer required" - snap remove test-snapd-tools + snap remove "$SNAP" echo "and test that the remodel shows up in 'snap changes'" diff -Nru snapd-2.40/tests/main/remodel-kernel/task.yaml snapd-2.42.1/tests/main/remodel-kernel/task.yaml --- snapd-2.40/tests/main/remodel-kernel/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/remodel-kernel/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,145 @@ +summary: | + Test a remodel that switches to a new kernel + +environment: + OLD_KERNEL: pc-kernel + NEW_KERNEL: test-snapd-pc-kernel + +# FIXME: add core18 test as well +systems: [ubuntu-core-16-64] + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + #shellcheck source=tests/lib/systemd.sh + . "$TESTSLIB"/systemd.sh + systemctl stop snapd.service snapd.socket + rm -rf /var/lib/snapd/assertions/* + rm -rf /var/lib/snapd/device + rm -rf /var/lib/snapd/state.json + mv /var/lib/snapd/seed/assertions/model model.bak + cp "$TESTSLIB"/assertions/developer1.account /var/lib/snapd/seed/assertions + cp "$TESTSLIB"/assertions/developer1.account-key /var/lib/snapd/seed/assertions + cp "$TESTSLIB"/assertions/developer1-pc.model /var/lib/snapd/seed/assertions + cp "$TESTSLIB"/assertions/testrootorg-store.account-key /var/lib/snapd/seed/assertions + # kick first boot again + systemctl start snapd.service snapd.socket + while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + #shellcheck source=tests/lib/systemd.sh + . "$TESTSLIB"/systemd.sh + systemctl stop snapd.service snapd.socket + rm -rf /var/lib/snapd/assertions/* + rm -rf /var/lib/snapd/device + rm -rf /var/lib/snapd/state.json + + rm -f /var/lib/snapd/seed/assertions/developer1.account + rm -f /var/lib/snapd/seed/assertions/developer1.account-key + rm -f /var/lib/snapd/seed/assertions/developer1-pc.model + rm -f /var/lib/snapd/seed/assertions/testrootorg-store.account-key + cp model.bak /var/lib/snapd/seed/assertions/model + rm -f ./*.bak + # kick first boot again + systemctl start snapd.service snapd.socket + # wait for first boot to be done + snap wait system seed.loaded + while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done + # extra paranoia because failure to cleanup earlier took us a long time + # to find + if [ -e /var/snap/$NEW_KERNEL/current ]; then + echo "Leftover $NEW_KERNEL data dir found, test does not " + echo "properly cleanup" + echo "see https://github.com/snapcore/snapd/pull/6620" + echo + find /var/snap + exit 1 + fi + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + #shellcheck source=tests/lib/boot.sh + . "$TESTSLIB"/boot.sh + + wait_change_done() { + chg_summary="$1" + for _ in $(seq 10); do + if snap changes | grep -qE "[0-9]+\ +Done\ +.* $chg_summary"; then + break + fi + # some debug output + snap changes + # wait a bit + sleep 5 + done + snap changes | MATCH "$chg_summary" + } + + # initial boot with the current model + if [ "$SPREAD_REBOOT" = 0 ]; then + # sanity check + snap list "$OLD_KERNEL" + + echo "We have the right model assertion" + snap debug model|MATCH "model: my-model" + + echo "Now we remodel" + snap remodel "$TESTSLIB"/assertions/developer1-pc-new-kernel.model + + echo "Double check that we boot into the right kernel" + grub-editenv list | MATCH "snap_try_kernel=$NEW_KERNEL" + + echo "reboot to finish the change" + REBOOT + fi + + # first boot with the new model kernel + if [ "$SPREAD_REBOOT" = 1 ]; then + echo "and we have the new kernel snap installed" + snap list "$NEW_KERNEL" + + echo "And are using it" + wait_core_post_boot + grub-editenv list | MATCH "snap_kernel=$NEW_KERNEL" + + echo "and we got the new model assertion" + wait_change_done "Refresh model assertion from revision 0 to 2" + snap debug model|MATCH "revision: 2" + + echo "and we cannot remove the kernel snap" + not snap remove "$NEW_KERNEL" + + # TODO: test when keeping the old kernel + echo "but we can remove the old kernel" + snap remove "$OLD_KERNEL" + + echo "And we can remodel again and remove the new kernel" + snap remodel "$TESTSLIB"/assertions/developer1-pc-revno3.model + REBOOT + fi + + # reboot from new model to undo the new model again (to not pollute tests) + if [ "$SPREAD_REBOOT" = 2 ]; then + wait_core_post_boot + grub-editenv list | MATCH "snap_kernel=$OLD_KERNEL" + + wait_change_done "Refresh model assertion from revision 2 to 3" + snap debug model|MATCH "revision: 3" + echo "cleanup" + snap remove "$NEW_KERNEL" + + echo "Ensure we are back to the original kernel channel and kernel" + snap refresh --channel="$KERNEL_CHANNEL" "$OLD_KERNEL" + REBOOT + fi diff -Nru snapd-2.40/tests/main/remove-errors/task.yaml snapd-2.42.1/tests/main/remove-errors/task.yaml --- snapd-2.40/tests/main/remove-errors/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/remove-errors/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,18 +1,25 @@ summary: Check remove command errors -# core18: on core18 the "core" snap is removable -systems: [-ubuntu-core-18-*] - execute: | - echo "Given a core snap is installed" #shellcheck source=tests/lib/snaps.sh . "$TESTSLIB/snaps.sh" - install_local test-snapd-tools - + #shellcheck source=tests/lib/systems.sh + . "$TESTSLIB"/systems.sh #shellcheck source=tests/lib/names.sh . "$TESTSLIB/names.sh" + + BASE_SNAP=core + TARGET_SNAP=test-snapd-tools + if is_core18_system; then + BASE_SNAP=core18 + TARGET_SNAP=test-snapd-tools-core18 + fi + + echo "Given a base snap, $BASE_SNAP, is installed" + install_local "$TARGET_SNAP" + echo "Ensure the important snaps can not be removed" - for sn in core $kernel_name $gadget_name; do + for sn in $BASE_SNAP $kernel_name $gadget_name; do if snap remove "$sn"; then echo "It should not be possible to remove $sn" exit 1 diff -Nru snapd-2.40/tests/main/retry-tool/task.yaml snapd-2.42.1/tests/main/retry-tool/task.yaml --- snapd-2.40/tests/main/retry-tool/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/retry-tool/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,15 @@ +summary: smoke test for the retry test tool +execute: | + # Retry runs the command that was passed as argument and returns the exit + # code of that command. + retry-tool true + not retry-tool -n 1 false + # If the command doesn't exist it is not re-tried multiple times. + ( not retry-tool this-command-does-not-exist ) 2>&1 \ + | grep -F 'retry: cannot execute command this-command-does-not-exist: [Errno 2] No such file or directory' + # On failure it tells us about it, showing progress. + retry-tool -n 2 --wait 0.1 false 2>&1 | grep -F "retry: command false failed with code 1" + retry-tool -n 2 --wait 0.1 false 2>&1 | grep -F "retry: next attempt in 0.1 second(s) (attempt 1 of 2)" + retry-tool -n 2 --wait 0.1 false 2>&1 | grep -F "retry: command false keeps failing after 2 attempts" + # Though all output is removed with the --quiet switch. + test "$(retry-tool -n 2 --wait 0.1 --quiet false 2>&1 | wc -l)" -eq 0 diff -Nru snapd-2.40/tests/main/sbuild/task.yaml snapd-2.42.1/tests/main/sbuild/task.yaml --- snapd-2.40/tests/main/sbuild/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/sbuild/task.yaml 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -summary: Ensure snapd builds correctly in sbuild - -# takes a while -priority: 500 - -kill-timeout: 30m - -systems: [debian-sid-*] - -execute: | - echo "Create a sid sbuild env" - eatmydata sbuild-createchroot --include=eatmydata,ccache,gnupg sid /srv/chroot/sid-amd64-sbuild http://deb.debian.org/debian - - echo "Allow test user to run sbuild" - sbuild-adduser test - - echo "And build it normally" - su -c "sbuild -d sid --run-autopkgtest $SPREAD_PATH/../*.dsc" test - echo "..and now just 'arch: any'" - su -c "sbuild --arch-any -d sid --run-autopkgtest $SPREAD_PATH/../*.dsc" test - -restore: | - rm --recursive --one-file-system /srv/chroot/sid-amd64-sbuild - rm -f /etc/schroot/chroot.d/sid-amd64-sbuild-* - -debug: | - cat <&1 | MATCH 'no matches' diff -Nru snapd-2.40/tests/main/snap-auto-import-asserts/task.yaml snapd-2.42.1/tests/main/snap-auto-import-asserts/task.yaml --- snapd-2.40/tests/main/snap-auto-import-asserts/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-auto-import-asserts/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,6 +1,6 @@ summary: Check that `snap auto-import` works as expected -systems: [ubuntu-core-16-64] +systems: [ubuntu-core-1*-64] prepare: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -17,7 +17,7 @@ #shellcheck source=tests/lib/ramdisk.sh . "$TESTSLIB/ramdisk.sh" setup_ramdisk - mkfs.vfat /dev/ram0 + mkfs.ext3 /dev/ram0 mount /dev/ram0 /mnt cp "$TESTSLIB"/assertions/testrootorg-store.account-key /mnt/auto-import.assert sync diff -Nru snapd-2.40/tests/main/snap-auto-import-asserts-spools/task.yaml snapd-2.42.1/tests/main/snap-auto-import-asserts-spools/task.yaml --- snapd-2.40/tests/main/snap-auto-import-asserts-spools/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-auto-import-asserts-spools/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,6 +1,6 @@ summary: Check that `snap auto-import` works as expected -systems: [ubuntu-core-16-64] +systems: [ubuntu-core-1*-64] prepare: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -17,7 +17,7 @@ #shellcheck source=tests/lib/ramdisk.sh . "$TESTSLIB/ramdisk.sh" setup_ramdisk - mkfs.vfat /dev/ram0 + mkfs.ext3 /dev/ram0 mount /dev/ram0 /mnt cp "$TESTSLIB"/assertions/testrootorg-store.account-key /mnt/auto-import.assert sync diff -Nru snapd-2.40/tests/main/snap-auto-mount/task.yaml snapd-2.42.1/tests/main/snap-auto-mount/task.yaml --- snapd-2.40/tests/main/snap-auto-mount/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-auto-mount/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,12 +1,15 @@ summary: Check that `snap auto-import` works as expected -systems: [ubuntu-core-16-64] +systems: [ubuntu-core-1*-64] prepare: | if [ "$TRUST_TEST_KEYS" = "false" ]; then echo "This test needs test keys to be trusted" exit fi + # shellcheck source=tests/lib/systems.sh + . "$TESTSLIB/systems.sh" + echo "Install dmsetup" snap install --devmode --edge dmsetup @@ -20,7 +23,14 @@ #shellcheck source=tests/lib/ramdisk.sh . "$TESTSLIB/ramdisk.sh" setup_ramdisk - mkfs.vfat /dev/ram0 + + # We use different filesystems to cover both: fat and ext. fat is the most + # common fs used and we also use ext3 because fat is not available on ubuntu core 18 + if is_core18_system; then + mkfs.ext3 /dev/ram0 + else + mkfs.vfat /dev/ram0 + fi mount /dev/ram0 /mnt cp "$TESTSLIB"/assertions/testrootorg-store.account-key /mnt/auto-import.assert sync diff -Nru snapd-2.40/tests/main/snap-confine/task.yaml snapd-2.42.1/tests/main/snap-confine/task.yaml --- snapd-2.40/tests/main/snap-confine/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-confine/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -42,12 +42,11 @@ not test -f /run/udev/tags/snap_test-snapd-tools_echo/+module:nvidia_modeset echo "Ensure apparmor profile for snap-confine is parsable" - for f in /etc/apparmor.d/usr.lib.snapd.snap-confine*; do + while IFS= read -r -d '' file; do if command -v apparmor_parser 2>/dev/null; then - apparmor_parser -QTK "$f" + apparmor_parser -QTK "$file" fi if command -v aa-enforce 2>/dev/null; then - aa-enforce "$f" + aa-enforce "$file" fi - done - + done < <(find /etc/apparmor.d -maxdepth 1 -name 'usr.lib.snapd.snap-confine*') diff -Nru snapd-2.40/tests/main/snap-core-fixup/task.yaml snapd-2.42.1/tests/main/snap-core-fixup/task.yaml --- snapd-2.40/tests/main/snap-core-fixup/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-core-fixup/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,6 +1,6 @@ summary: Check that the core-fixup-sh script works -systems: [ubuntu-core-16-*] +systems: [ubuntu-core-1*] restore: | umount /boot/uboot @@ -25,7 +25,8 @@ umount /boot/uboot echo "Now test with the real corrupted image" - unxz -c -d test.img.xz >test.img + # We use tar instead of unxz because it is available in all the core systems + tar zxvf test.img.tar.gz mount -t vfat test.img /boot/uboot n=$(find /boot/uboot -name uboot.env| wc -l) if [ "$n" != "2" ]; then Binary files /tmp/tmpqD7xhb/6u5KrkVo8z/snapd-2.40/tests/main/snap-core-fixup/test.img.tar.gz and /tmp/tmpqD7xhb/2JDV8QJ2c_/snapd-2.42.1/tests/main/snap-core-fixup/test.img.tar.gz differ Binary files /tmp/tmpqD7xhb/6u5KrkVo8z/snapd-2.40/tests/main/snap-core-fixup/test.img.xz and /tmp/tmpqD7xhb/2JDV8QJ2c_/snapd-2.42.1/tests/main/snap-core-fixup/test.img.xz differ diff -Nru snapd-2.40/tests/main/snap-core-symlinks/task.yaml snapd-2.42.1/tests/main/snap-core-symlinks/task.yaml --- snapd-2.40/tests/main/snap-core-symlinks/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-core-symlinks/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,12 +1,20 @@ summary: Check that the seed symlinks work -systems: [ubuntu-core-16-*] +systems: [ubuntu-core-1*] execute: | - echo "Ensure that the core snap is a symlink into the seed" - core_symlink="$(readlink -f /var/lib/snapd/snaps/core_*.snap)" + # shellcheck source=tests/lib/systems.sh + . "$TESTSLIB/systems.sh" + + TARGET_SNAP=core + if is_core18_system; then + TARGET_SNAP=core18 + fi + + echo "Ensure that the $TARGET_SNAP snap is a symlink into the seed" + core_symlink="$(readlink -f /var/lib/snapd/snaps/${TARGET_SNAP}_*.snap)" if [[ "${core_symlink}" != /var/lib/snapd/seed/snaps/* ]]; then - echo "The initial core snap should symlink into the seed directory" + echo "The initial $TARGET_SNAP snap should symlink into the seed directory" echo "but it does not." exit 1 fi diff -Nru snapd-2.40/tests/main/snapctl/task.yaml snapd-2.42.1/tests/main/snapctl/task.yaml --- snapd-2.40/tests/main/snapctl/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snapctl/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -9,6 +9,11 @@ snap alias test-snapd-curl.curl curl fi +restore: | + if snap list test-snapd-curl; then + snap remove test-snapd-curl + fi + execute: | echo "Verify that snapctl -h runs without a context" if ! snapctl -h; then diff -Nru snapd-2.40/tests/main/snap-debug-state/task.yaml snapd-2.42.1/tests/main/snap-debug-state/task.yaml --- snapd-2.40/tests/main/snap-debug-state/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-debug-state/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,35 @@ +summary: Ensure `snap debug changes|tasks|task` commands work + +prepare: | + snap install hello-world + # The debug commands access state.json directly, snapd should be stopped + systemctl stop snapd.{socket,service} + + if [[ "$SPREAD_SYSTEM" == ubuntu-1* ]]; then + apt install -y graphviz + fi + +restore: | + systemctl start snapd.{socket,service} + rm -f out.png + +execute: | + echo "Changes can be listed" + snap debug state --changes /var/lib/snapd/state.json | MATCH "seed .*Initialize system state" + snap debug state /var/lib/snapd/state.json | MATCH "seed .*Initialize system state" + + echo "Snap changes defaults to state.json in the current directory" + cd /var/lib/snapd + snap debug state --changes | MATCH "install-snap .*Install \"hello-world\" snap" + + echo "Tasks can be listed" + snap debug state --change=1 /var/lib/snapd/state.json | MATCH "mark-seeded .*Mark system seeded" + + echo "Individual task can be examined" + snap debug state --task=1 /var/lib/snapd/state.json | MATCH "kind: mark-seeded" + snap debug state --task=1 /var/lib/snapd/state.json | MATCH "summary: Mark system seeded" + + # sanity check, dot shouldn't fail + if [[ "$SPREAD_SYSTEM" == ubuntu-1* ]]; then + snap debug state --change=1 --dot /var/lib/snapd/state.json | dot -Tpng > out.png + fi diff -Nru snapd-2.40/tests/main/snapd-reexec/task.yaml snapd-2.42.1/tests/main/snapd-reexec/task.yaml --- snapd-2.40/tests/main/snapd-reexec/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snapd-reexec/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -6,6 +6,12 @@ restore: | #shellcheck source=tests/lib/dirs.sh . "$TESTSLIB"/dirs.sh + + # Remove the locale revision of core, if we installed one. + if [ "$(readlink "$SNAP_MOUNT_DIR/core/current")" = x1 ]; then + snap revert core + snap remove --revision=x1 core + fi # extra cleanup in case something in this test went wrong rm -f /etc/systemd/system/snapd.service.d/no-reexec.conf systemctl stop snapd.service snapd.socket @@ -42,7 +48,7 @@ mount --bind /tmp/old-info $SNAP_MOUNT_DIR/core/current/usr/lib/snapd/info systemctl start snapd.service snapd.socket snap list - check_journalctl_log 'core snap \(at .*\) is older \(.*\) than distribution package' + check_journalctl_log 'snap \(at .*\) is older \(.*\) than distribution package' echo "Revert back to normal" systemctl stop snapd.service snapd.socket diff -Nru snapd-2.40/tests/main/snapd-reexec-snapd-snap/task.yaml snapd-2.42.1/tests/main/snapd-reexec-snapd-snap/task.yaml --- snapd-2.40/tests/main/snapd-reexec-snapd-snap/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snapd-reexec-snapd-snap/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -12,9 +12,12 @@ exit 0 fi - echo "Enable installing the snapd snap" + echo "Enable installing the snapd snap, this happens automatically" snap set core experimental.snapd-snap=true - snap install --edge snapd + # give the state time to create the change and then start watching + # it + retry-tool -n 30 snap watch --last=transition-to-snapd-snap + snap list snapd # shellcheck source=tests/lib/journalctl.sh . "$TESTSLIB/journalctl.sh" diff -Nru snapd-2.40/tests/main/snapd-slow-startup/task.yaml snapd-2.42.1/tests/main/snapd-slow-startup/task.yaml --- snapd-2.40/tests/main/snapd-slow-startup/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/snapd-slow-startup/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,44 @@ +summary: Test that snapd is not terminated by systemd on a slow startup + +systems: [ubuntu-18.04-64,ubuntu-19.04-64] + +restore: | + # extra cleanup in case something in this test went wrong + rm -f /etc/systemd/system/snapd.service.d/slow-startup.conf + systemctl stop snapd.service snapd.socket + +debug: | + ls /etc/systemd/system/snapd.service.d + cat /etc/systemd/system/snapd.service.d/* + +execute: | + systemd_ver="$(systemctl --version|head -1|cut -d ' ' -f2)" + if [ "${systemd_ver}" -lt 236 ]; then + echo "systemd ${systemd_ver} too old, no EXTEND_TIMEOUT_USEC support" + exit 0 + fi + + # have 6 extra snaps installed, makes 7 with core + snap pack "$TESTSLIB"/snaps/basic + snap set system experimental.parallel-instances=true + for i in $(seq 6); do + snap install --dangerous --name="basic_$i" basic_1.0_all.snap + done + + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + + echo "Simulate a slow startup" + systemctl stop snapd.service snapd.socket + cat > /etc/systemd/system/snapd.service.d/slow-startup.conf <&1 | MATCH 'snap "snapctl-hooks" has no "root.key2" configuration option' + + echo "Test unsetting of root.key2 with via snapctl unset" + # init and sanity check + snap set snapctl-hooks command=noop root.key2="b" + snap get snapctl-hooks root.key2 | MATCH "b" + # note, unsetting happens in the configure hook in response to "test-unset-with-unset" value + snap set snapctl-hooks command=test-unset-with-unset + snap get snapctl-hooks root.key2 2>&1 | MATCH 'snap "snapctl-hooks" has no "root.key2" configuration option' + echo "Test that config values are not available once snap is removed" snap remove snapctl-hooks if output=$(snap get snapctl-hooks foo); then diff -Nru snapd-2.40/tests/main/snap-icons/task.yaml snapd-2.42.1/tests/main/snap-icons/task.yaml --- snapd-2.40/tests/main/snap-icons/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-icons/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,29 @@ +summary: Snaps can install icon theme icons + +execute: | + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh + + echo "Install a snap providing icons" + install_local test-snapd-icon-theme + + echo "Icons provided by the snap are installed to a shared location" + iconfile=/var/lib/snapd/desktop/icons/hicolor/scalable/apps/snap.test-snapd-icon-theme.foo.svg + [ -f "$iconfile" ] + MATCH "icon from test-snapd-icon-theme" < "$iconfile" + + echo "Desktop files can reference installed icons" + desktopfile=/var/lib/snapd/desktop/applications/test-snapd-icon-theme_echo.desktop + MATCH '^Icon=snap.test-snapd-icon-theme.foo$' < "$desktopfile" + + echo "Remove the snap" + snap remove test-snapd-icon-theme + + echo "The icon has been removed" + [ ! -f "$iconfile" ] + + echo "The empty icon theme subdirectories have also been removed" + [ ! -d /var/lib/snapd/desktop/icons/hicolor ] + + echo "But the base icons directory remains" + [ -d /var/lib/snapd/desktop/icons ] diff -Nru snapd-2.40/tests/main/snap-info/check.py snapd-2.42.1/tests/main/snap-info/check.py --- snapd-2.40/tests/main/snap-info/check.py 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-info/check.py 2019-10-30 12:17:43.000000000 +0000 @@ -94,6 +94,10 @@ ("candidate", equals, "↑"), ("beta", equals, "↑"), ("edge", matches, verRelRevNotesRx("-")), + ("2.0/stable", exists), + ("2.0/candidate", exists), + ("2.0/beta", exists), + ("2.0/edge", exists), ), ("snap-id", equals, snap_ids["test-snapd-tools"]), ("license", matches, r"(unknown|unset)"), # TODO: update once snap.yaml contains the right license diff -Nru snapd-2.40/tests/main/snap-info/task.yaml snapd-2.42.1/tests/main/snap-info/task.yaml --- snapd-2.40/tests/main/snap-info/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-info/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -2,12 +2,17 @@ # core18 has no python3-yaml # amazon: no PyYAML is not packaged for python3 -systems: [-ubuntu-core-18-*, -amazon-*, -centos-*] +systems: [-amazon-*, -centos-*] prepare: | snap pack "$TESTSLIB"/snaps/basic snap install test-snapd-tools snap install --channel beta --devmode test-snapd-devmode + # ensures that an empty directory doesn't confuse things + mkdir -v core + +restore: | + rmdir -v core execute: | echo "With no arguments, errors out" diff -Nru snapd-2.40/tests/main/snap-model/task.yaml snapd-2.42.1/tests/main/snap-model/task.yaml --- snapd-2.40/tests/main/snap-model/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-model/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,50 @@ +summary: Check that snap model works + +execute: | + knownCmdAssertion=$(snap known model) + modelCmdAssertion=$(snap model --assertion) + echo "Check that model assertion from \"snap known\" matches \"snap model\"" + if [ "$modelCmdAssertion" != "$knownCmdAssertion" ]; then + echo "model assertions not the same, difference is:" + diff -u <(echo "$modelCmdAssertion") <(echo "$knownCmdAssertion") + exit 1 + fi + + knownCmdAssertion=$(snap known serial) + modelCmdAssertion=$(snap model --serial --assertion) + echo "Check that serial assertion from \"snap known\" matches \"snap model\"" + if [ "$modelCmdAssertion" != "$knownCmdAssertion" ]; then + echo "serial assertions not the same, difference is:" + diff -u <(echo "$modelCmdAssertion") <(echo "$knownCmdAssertion") + exit 1 + fi + + modelCmdSerial="$(snap model --serial | grep -Po "serial:\s+\K(.*)")" + knownCmdSerial="$(snap known serial | grep -Po "serial:\s+\K(.*)")" + echo "Check that serial from \"snap known\" matches \"snap model\"" + if [ "$modelCmdSerial" != "$knownCmdSerial" ]; then + echo "serial numbers not the same, difference is:" + diff -u <(echo "$knownCmdSerial") <(echo "$knownCmdSerial") + exit 1 + fi + + modelCmdModel="$(snap model | grep -Po "model\s+\K(.*)")" + knownCmdModel="$(snap known model | grep -Po "model:\s+\K(.*)")" + echo "Check that model from \"snap known\" matches \"snap model\"" + if snap known model | grep -q -P '^display-name:'; then + # the model has a display-name so `snap model` output will be different + echo "note: model has a display-name in it" + modelDisplayName="$(snap known model | grep -Po "display-name:\s+\K(.*)")" + fullModelShown="$modelDisplayName ($knownCmdModel)" + if [ "$modelCmdModel" != "$fullModelShown" ]; then + echo "model not the same, difference is:" + diff -u <(echo "$modelCmdModel") <(echo "$fullModelShown") + exit 1 + fi + else + if [ "$modelCmdModel" != "$knownCmdModel" ]; then + echo "model not the same, difference is:" + diff -u <(echo "$modelCmdModel") <(echo "$knownCmdModel") + exit 1 + fi + fi diff -Nru snapd-2.40/tests/main/snap-run/task.yaml snapd-2.42.1/tests/main/snap-run/task.yaml --- snapd-2.40/tests/main/snap-run/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-run/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -10,13 +10,22 @@ install_local basic-run install_local test-snapd-tools -debug: - cat stderr +debug: | + if [ -f stderr ]; then + cat stderr + fi execute: | echo "Test that snap run use environments" basic-run.echo-data | MATCH ^/var/snap + if command -v gdb; then + echo "Test snap run --gdb works" + echo "c" | snap run --gdb test-snapd-tools.echo hello > stdout + MATCH 'Continuing.' < stdout + MATCH hello < stdout + fi + # the strace on 14.04 is too old if grep -q 'VERSION_ID="14.04"' /etc/os-release; then snap install strace-static @@ -30,6 +39,23 @@ snap install strace-static fi + echo "Test snap --strace invalid works" + if snap run --strace="invalid" test-snapd-tools.echo hello 2>stderr ; then + echo "snap run with an invalid strace option should fail but it did not" + exit 1 + fi + MATCH "Can't stat 'invalid': No such file or directory" < stderr + + if [[ "$SPREAD_SYSTEM" == arch-linux-* ]]; then + # Arch runs the mainline kernel, strace (with event filter or not) *may* + # randomly get stuck on the kernel side, see: + # - proposed patch: https://lore.kernel.org/patchwork/patch/719314/ + # - snap-exec & strace stuck: https://paste.ubuntu.com/p/8nVzj8Sqfq/ + echo "SKIP further tests due to know kernel/strace problems" + exit 0 + fi + # XXX: any tests that execute strace should be added below this point + echo "Test snap run --strace" snap run --strace test-snapd-tools.echo "hello-world" >stdout 2>stderr MATCH hello-world < stdout @@ -45,20 +71,6 @@ MATCH "strace -- version" < stdout [ ! -s stderr ] - echo "Test snap --strace invalid works" - if snap run --strace="invalid" test-snapd-tools.echo hello 2>stderr ; then - echo "snap run with an invalid strace option should fail but it did not" - exit 1 - fi - MATCH "Can't stat 'invalid': No such file or directory" < stderr - - if command -v gdb; then - echo "Test snap run --gdb works" - echo "c" | snap run --gdb test-snapd-tools.echo hello > stdout - MATCH 'Continuing.' < stdout - MATCH hello < stdout - fi - snap run --trace-exec test-snapd-tools.echo hello 2> stderr MATCH "Slowest [0-9]+ exec calls during snap run" < stderr MATCH " [0-9.]+s .*/snap-exec" < stderr diff -Nru snapd-2.40/tests/main/snap-session-agent-socket-activation/task.yaml snapd-2.42.1/tests/main/snap-session-agent-socket-activation/task.yaml --- snapd-2.40/tests/main/snap-session-agent-socket-activation/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-session-agent-socket-activation/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,72 @@ +summary: Check that root can activate the session agent via socket activation + +systems: + # Ubuntu 14.04 does not have a complete systemd implementation + - -ubuntu-14.04-* + # Systemd on CentOS 7/Amazon Linux 2 does not have the user@uid unit + - -amazon-linux-2-* + - -centos-7-* + +environment: + TEST_UID: $(id -u test) + USER_RUNTIME_DIR: /run/user/${TEST_UID} + +prepare: | + # Ensure that snapd.session-agent.socket is enabled. This may not + # be the case on distributions where presets have been used to + # disable it. + if [ ! -L /usr/lib/systemd/user/sockets.target.wants/snapd.session-agent.socket ] && + ! systemctl --user --global is-enabled snapd.session-agent.socket; then + systemctl --user --global enable snapd.session-agent.socket + touch agent-was-enabled + fi + mkdir -p "$USER_RUNTIME_DIR" + chmod u=rwX,go= "$USER_RUNTIME_DIR" + chown test:test "$USER_RUNTIME_DIR" + systemctl start "user@${TEST_UID}.service" + + # ensure curl is available (needed for e.g. core18) + if ! command -v curl; then + snap install --devmode --edge test-snapd-curl + snap alias test-snapd-curl.curl curl + fi + +restore: | + if snap list test-snapd-curl; then + snap remove test-snapd-curl + fi + systemctl stop "user@${TEST_UID}.service" + rm -rf "${USER_RUNTIME_DIR:?}"/* "${USER_RUNTIME_DIR:?}"/.[!.]* + if [ -f agent-was-enabled ]; then + systemctl --user --global disable snapd.session-agent.socket + rm agent-was-enabled + fi + +execute: | + systemctl_user() { + su -l -c "XDG_RUNTIME_DIR=\"${USER_RUNTIME_DIR}\" systemctl --user $*" test + } + echo "Initially snap session-agent is not running" + if systemctl_user is-active snapd.session-agent.service; then + exit 1 + fi + + echo "However its REST API socket exists" + test -S "${USER_RUNTIME_DIR}/snapd-session-agent.socket" + + echo "We can issue queries to the socket as root" + curl --unix-socket "${USER_RUNTIME_DIR}/snapd-session-agent.socket" \ + -D- http://localhost/v1/session-info | MATCH "HTTP/1.1 200 OK" + + echo "Now snap session-agent is running" + systemctl_user is-active snapd.session-agent.service + + echo "If we stop session-agent, it can be restarted via socket activation" + systemctl_user stop snapd.session-agent.service + if systemctl_user is-active snapd.session-agent.service; then + exit 1 + fi + + curl --unix-socket "${USER_RUNTIME_DIR}/snapd-session-agent.socket" \ + -D- http://localhost/v1/session-info | MATCH "HTTP/1.1 200 OK" + systemctl_user is-active snapd.session-agent.service diff -Nru snapd-2.40/tests/main/snap-set/task.yaml snapd-2.42.1/tests/main/snap-set/task.yaml --- snapd-2.40/tests/main/snap-set/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-set/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -32,13 +32,31 @@ exit 1 fi - echo "Test that the set value can be null and can be retrieved" - if ! snap set snapctl-hooks command=test-snapctl-get-foo-null foo=null; then + echo "Sanity check before we unset the value with null" + if ! obtained=$(snap get snapctl-hooks foo); then + echo "Expected snap get to be able to retrieve set value" + exit 1 + fi + [[ "$obtained" == "bar" ]] + + echo "Test that the set value can be null and it removes the option" + if ! snap set snapctl-hooks command=test-snapctl-foo-null foo=null; then echo "Expected hook to be able to retrieve set value" exit 1 fi - obtained=$(snap get snapctl-hooks foo) - [[ "$obtained" == "" ]] + snap get snapctl-hooks foo 2>&1 | MATCH 'snap "snapctl-hooks" has no "foo" configuration option' + + echo "Set foo back" + snap set snapctl-hooks command=test-snapctl-get-foo foo=bar + if ! obtained=$(snap get snapctl-hooks foo); then + echo "Expected snap get to be able to retrieve set value" + exit 1 + fi + test "$obtained" = "bar" + + echo "Test that the foo value can be unset" + snap set snapctl-hooks command=test-snapctl-foo-null foo! + snap get snapctl-hooks foo 2>&1 | MATCH 'snap "snapctl-hooks" has no "foo" configuration option' echo "Test that an invalid key results in an error" if obtained=$(snap set snapctl-hooks invalid_key=value 2>&1); then @@ -53,3 +71,11 @@ exit 1 fi [[ "$obtained" == *"error from within configure hook"* ]] + + echo "Test that the 'snap set' order is deterministic" + for _ in $(seq 50); do + snap set snapctl-hooks command=noop one! + snap set snapctl-hooks one.two=2 one='{"three":3}' + snap get snapctl-hooks -l one.two | MATCH "one.two[ ]*2" + snap get snapctl-hooks -l one.three | MATCH "one.three[ ]*3" + done diff -Nru snapd-2.40/tests/main/snap-set-core-w-no-core/task.yaml snapd-2.42.1/tests/main/snap-set-core-w-no-core/task.yaml --- snapd-2.40/tests/main/snap-set-core-w-no-core/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-set-core-w-no-core/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -27,3 +27,11 @@ echo "snap set core must error for unknown options" exit 1 fi + if snap set core unknown!; then + echo "snap set core must error for unknown options" + exit 1 + fi + if snap unset core unknown.option; then + echo "snap unset core must error for unknown options" + exit 1 + fi diff -Nru snapd-2.40/tests/main/snap-switch/task.yaml snapd-2.42.1/tests/main/snap-switch/task.yaml --- snapd-2.40/tests/main/snap-switch/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-switch/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -4,5 +4,8 @@ snap install test-snapd-tools snap info test-snapd-tools|MATCH "tracking: +stable" - snap switch --edge test-snapd-tools + snap switch --edge test-snapd-tools > stdout.txt snap info test-snapd-tools|MATCH "tracking: +edge" + + echo "Ensure we don't print incorrect and confusing information" + not grep "is closed; temporarily forwarding to stable." < stdout.txt diff -Nru snapd-2.40/tests/main/snap-system-env/task.yaml snapd-2.42.1/tests/main/snap-system-env/task.yaml --- snapd-2.40/tests/main/snap-system-env/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-system-env/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,7 +1,7 @@ summary: Ensure systemd environment generator works # systemd environment generators are only supported on 17.10+ -systems: [ubuntu-18.04-*, ubuntu-18.10-*, ubuntu-19.04-*, ubuntu-2*] +systems: [ubuntu-18.04-*, ubuntu-19*, ubuntu-2*] execute: | # integration test to ensure it works on the real system @@ -19,16 +19,8 @@ # ensure /usr/local/{,s}bin is still part of the PATH, LP: 1814355 systemd-run --pty --wait '/usr/bin/env' | MATCH 'PATH=.*/local/.*' systemd-run --pty --wait '/usr/bin/env' > env.out - if [ "$VERSION_ID" = "18.04" ];then - # Only 18.10+ is fully working with the systemd generator, so for 18.04 - # we account for /snap/bin not being in the PATH, until LP: #1771858 is - # fixed. - not MATCH 'PATH=.*/snap/bin' < env.out - exit 0 - else - # ensure PATH is updated (and check full PATH, see LP: #1814355) - MATCH 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin' < env.out - fi + # ensure PATH is updated (and check full PATH, see LP: #1814355) + MATCH 'PATH=/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:/snap/bin' < env.out # some unit tests SENV=/usr/lib/systemd/system-environment-generators/snapd-env-generator diff -Nru snapd-2.40/tests/main/snap-unset/task.yaml snapd-2.42.1/tests/main/snap-unset/task.yaml --- snapd-2.40/tests/main/snap-unset/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-unset/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,29 @@ +summary: Check that `snap unset` works and removes config options. + +prepare: | + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh + + echo "Build basic test snap" + install_local basic-hooks + +execute: | + echo "Setting up initial configuration" + snap set basic-hooks foo=abc bar=def baz=1234 + # sanity check + snap get basic-hooks foo bar baz + + echo "Unsetting unknown option currently doesn't give an error" + snap unset basic-hooks unknown.option + + echo "Unsetting foo option" + snap unset basic-hooks foo + snap get basic-hooks foo 2>&1 | MATCH 'snap "basic-hooks" has no "foo" configuration option' + + # sanity check, two options still available + snap get basic-hooks bar baz + + echo "Unsetting multiple options" + snap unset basic-hooks bar baz + snap get basic-hooks bar 2>&1 | MATCH 'snap "basic-hooks" has no "bar" configuration option' + snap get basic-hooks baz 2>&1 | MATCH 'snap "basic-hooks" has no "baz" configuration option' diff -Nru snapd-2.40/tests/main/snap-userd-reexec/task.yaml snapd-2.42.1/tests/main/snap-userd-reexec/task.yaml --- snapd-2.40/tests/main/snap-userd-reexec/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-userd-reexec/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -15,3 +15,11 @@ echo "Ensure the dbus service file got created" test -f /usr/share/dbus-1/services/io.snapcraft.Launcher.service diff -u /usr/share/dbus-1/services/io.snapcraft.Launcher.service.orig /usr/share/dbus-1/services/io.snapcraft.Launcher.service +restore: | + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh + # Remove the locale revision of core, if we installed one. + if [ "$(readlink "$SNAP_MOUNT_DIR/core/current")" = x1 ]; then + snap revert core + snap remove --revision=x1 core + fi diff -Nru snapd-2.40/tests/main/snap-wait/task.yaml snapd-2.42.1/tests/main/snap-wait/task.yaml --- snapd-2.40/tests/main/snap-wait/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/snap-wait/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,6 +1,6 @@ summary: Check that `snap wait` works -kill-timeout: 2m +kill-timeout: 5m prepare: | # shellcheck source=tests/lib/snaps.sh diff -Nru snapd-2.40/tests/main/stale-base-snap/task.yaml snapd-2.42.1/tests/main/stale-base-snap/task.yaml --- snapd-2.40/tests/main/stale-base-snap/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/stale-base-snap/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -37,6 +37,13 @@ restore: | rm -rf squashfs-root rm -f core-customized.snap + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB"/dirs.sh + # Remove the locale revision of core, if we installed one. + if [ "$(readlink "$SNAP_MOUNT_DIR/core/current")" = x1 ]; then + snap revert core + snap remove --revision=x1 core + fi execute: | # Start a "sleep" process in the background diff -Nru snapd-2.40/tests/main/svcs-disable-post-refresh-hook/task.yaml snapd-2.42.1/tests/main/svcs-disable-post-refresh-hook/task.yaml --- snapd-2.40/tests/main/svcs-disable-post-refresh-hook/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/svcs-disable-post-refresh-hook/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,24 @@ +summary: | + Check that `snapctl stop --disable` actually stops services on post-refresh + +execute: | + # shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh + + echo "Installing snap first time starts services" + install_local test-snapd-svcs-disable-refresh-hook + + echo "Services are running after install hook" + for service in simple forking; do + echo "Verify that the $service service isn't running" + snap services | MATCH "test-snapd-svcs-disable-refresh-hook\\.$service\\s+enabled\\s+active" + done + + echo "Refreshing the snap triggers post-refresh hook which disables the services" + install_local test-snapd-svcs-disable-refresh-hook + + echo "Services are now disabled" + for service in simple forking; do + echo "Verify that the $service service isn't running" + snap services | MATCH "test-snapd-svcs-disable-refresh-hook\\.$service\\s+disabled\\s+inactive" + done diff -Nru snapd-2.40/tests/main/system-usernames/task.yaml snapd-2.42.1/tests/main/system-usernames/task.yaml --- snapd-2.40/tests/main/system-usernames/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/system-usernames/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,760 @@ +summary: ensure seccomp rules for snap_daemon user work correctly + +environment: + # This is used to abbreviate some of the paths below. + P1: /var/snap/test-snapd-sh/common/test.chown + P2: /var/snap/test-snapd-daemon-user/common/test.chown + P3: /var/snap/test-snapd-daemon-user/common/test.chown32 + # List of expected snap install failures due to libseccomp/golang-seccomp + # being too old. This should only reduce with time since new systems should + # have newer libseccomp and golang-seccomp + EXFAIL: "amazon-linux-2-64 centos-7-64 debian-9-64 fedora-29-64 fedora-30-64 opensuse-15\\.0-64 ubuntu-14" + +prepare: | + echo "Install helper snaps with default confinement" + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh + install_local test-snapd-sh + +restore: | + # make sure this snap is removed in case it was installed + snap remove test-snapd-daemon-user || true + + # snapd will create this for us, but we'll remove it for consistency in + # test runs + user-tool remove-with-group snap_daemon + +execute: | + # to accommodate different distros, we pick the LSB required 'daemon' user + # to verify 'chown' doesn't work with arbitrary users + getent passwd daemon || exit 1 + getent group daemon || exit 1 + + # expect a seccomp denial with 'chown' operation + echo "Running 'chown' without any system-usernames" + touch "$P1" + snap run test-snapd-sh -c "chown daemon:daemon $P1" 2>&1 | MATCH 'Operation not permitted' + + # We want this test to run on all systems since the end goal is for all + # systems that have a new enough libseccomp (>= 2.4.0) and golang-seccomp + # (with ActLog support, or >= 0.9.1) to show the feature is enabled, and + # those not meeting these criteria to show the feature as disabled. We can + # check for support in an existing snap's seccomp profile header, then go + # from there. + supported="no" + header=$(head -2 /var/lib/snapd/seccomp/bpf/snap.test-snapd-sh.test-snapd-sh.src | grep -A1 'snap-seccomp version information' | tail -1) + echo "snap-seccomp version information is '$header'" + if [ -n "$header" ]; then + features=$(echo "$header" | cut -d ' ' -f 5) + if echo "$features" | grep -q "bpf-actlog" ; then + maj=$(echo "$header" | cut -d ' ' -f 3 | cut -d '.' -f 1) + min=$(echo "$header" | cut -d ' ' -f 3 | cut -d '.' -f 2) + if [ "$maj" -gt 2 ]; then + supported="yes" + elif [ "$maj" -eq 2 ] && [ "$min" -ge 4 ]; then + supported="yes" + fi + fi + fi + + ok_to_fail="no" + for regex in $EXFAIL ; do + if echo "$SPREAD_SYSTEM" | grep -Eq "$regex" ; then + ok_to_fail="yes" + break + fi + done + if [ "$supported" = "no" ] && [ "$ok_to_fail" = "no" ]; then + # Fail if have no support when we should + echo "'$SPREAD_SYSTEM' does not have support when it should!!" + exit 1 + elif [ "$supported" = "yes" ] && [ "$ok_to_fail" = "yes" ]; then + # if an old system gains, make sure we notice and adjust to not regress + # in the future + if [ "$ok_to_fail" = "yes" ]; then + echo "'$SPREAD_SYSTEM' now has support. Please update this test" + exit 1 + fi + fi + + # Now try to install the test snap. If we don't have support, we expect an + # error, if we do, then we can proceed + if [ "$supported" = "yes" ]; then + snap install --edge test-snapd-daemon-user + else + snap install --edge test-snapd-daemon-user 2>&1 | MATCH 'require a snapd built against (libseccomp >= 2.4|golang-seccomp >= 0.9.1)' + exit 0 + fi + + echo "Verify the snap_daemon user and group was created with the expected uid/gid" + user=$(getent passwd snap_daemon | cut -d : -f 3) + test "$user" -eq "584788" || exit 1 + group=$(getent group snap_daemon | cut -d : -f 3) + test "$group" -eq "584788" || exit 1 + + # + # Test typical drop operations though libc and raw syscalls + # + echo "Running 'drop' with system-usernames: [ snap_daemon ] as non-root" + # CAP_SETGID is dropped before running + su -l -c "snap run test-snapd-daemon-user.drop snap_daemon" test 2>&1 | MATCH 'Operation not permitted' + su -l -c "snap run test-snapd-daemon-user.drop32 snap_daemon" test 2>&1 | MATCH 'Operation not permitted' + + echo "Running 'drop' with system-usernames: [ snap_daemon ] as root" + snap run test-snapd-daemon-user.drop snap_daemon 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=$user, rgid=$group, egid=$group, sgid=$group, groups=$" + snap run test-snapd-daemon-user.drop32 snap_daemon 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=$user, rgid=$group, egid=$group, sgid=$group, groups=$" + + echo "Running 'drop-syscall' with system-usernames: [ snap_daemon ] as non-root" + # CAP_SETGID is dropped before running + su -l -c "snap run test-snapd-daemon-user.drop-syscall snap_daemon" test 2>&1 | MATCH 'Operation not permitted' + su -l -c "snap run test-snapd-daemon-user.drop-syscall32 snap_daemon" test 2>&1 | MATCH 'Operation not permitted' + + echo "Running 'drop-syscall' with system-usernames: [ snap_daemon ] as root" + snap run test-snapd-daemon-user.drop-syscall snap_daemon 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=$user, rgid=$group, egid=$group, sgid=$group, groups=$" + snap run test-snapd-daemon-user.drop-syscall32 snap_daemon 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=$user, rgid=$group, egid=$group, sgid=$group, groups=$" + + # + # test individual seccomp rules + # + echo "Testing individual syscalls" + + # setgid + echo "'setgid g:root' allowed" + snap run test-snapd-daemon-user.setgid root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setgid32 root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setgid g:snap_daemon' allowed" + snap run test-snapd-daemon-user.setgid snap_daemon 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=$group, sgid=$group, groups=" + snap run test-snapd-daemon-user.setgid32 snap_daemon 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=$group, sgid=$group, groups=" + + echo "'setgid g:test' denied" + snap run test-snapd-daemon-user.setgid test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setgid32 test 2>&1 | MATCH "Operation not permitted" + + + # setregid + echo "'setregid g:root g:root' allowed" + snap run test-snapd-daemon-user.setregid root root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setregid32 root root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setregid -1 g:root' allowed" + snap run test-snapd-daemon-user.setregid -1 root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setregid32 -1 root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setregid g:root -1' allowed" + snap run test-snapd-daemon-user.setregid root -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setregid32 root -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setregid g:snap_daemon g:snap_daemon' allowed" + snap run test-snapd-daemon-user.setregid snap_daemon snap_daemon 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=$group, sgid=$group, groups=" + snap run test-snapd-daemon-user.setregid32 snap_daemon snap_daemon 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=$group, sgid=$group, groups=" + + echo "'setregid -1 g:snap_daemon' allowed" + snap run test-snapd-daemon-user.setregid -1 snap_daemon 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=$group, sgid=$group, groups=" + snap run test-snapd-daemon-user.setregid32 -1 snap_daemon 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=$group, sgid=$group, groups=" + + echo "'setregid g:snap_daemon -1' allowed" + snap run test-snapd-daemon-user.setregid snap_daemon -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setregid32 snap_daemon -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=0, sgid=0, groups=" + + echo "'setregid g:root g:snap_daemon' allowed" + snap run test-snapd-daemon-user.setregid root snap_daemon 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=$group, sgid=$group, groups=" + snap run test-snapd-daemon-user.setregid32 root snap_daemon 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=$group, sgid=$group, groups=" + + echo "'setregid g:snap_daemon g:root' allowed" + snap run test-snapd-daemon-user.setregid snap_daemon root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setregid32 snap_daemon root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=0, sgid=0, groups=" + + echo "'setregid g:test g:test' denied" + snap run test-snapd-daemon-user.setregid test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setregid32 test test 2>&1 | MATCH "Operation not permitted" + + echo "'setregid -1 g:test' denied" + snap run test-snapd-daemon-user.setregid -1 test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setregid32 -1 test 2>&1 | MATCH "Operation not permitted" + + echo "'setregid g:test -1' denied" + snap run test-snapd-daemon-user.setregid test -1 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setregid32 test -1 2>&1 | MATCH "Operation not permitted" + + echo "'setregid g:root g:test' denied" + snap run test-snapd-daemon-user.setregid root test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setregid32 root test 2>&1 | MATCH "Operation not permitted" + + echo "'setregid g:test g:root' denied" + snap run test-snapd-daemon-user.setregid test root 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setregid32 test root 2>&1 | MATCH "Operation not permitted" + + echo "'setregid g:snap_daemon g:test' denied" + snap run test-snapd-daemon-user.setregid snap_daemon test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setregid32 snap_daemon test 2>&1 | MATCH "Operation not permitted" + + echo "'setregid g:test g:snap_daemon' denied" + snap run test-snapd-daemon-user.setregid test snap_daemon 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setregid32 test snap_daemon 2>&1 | MATCH "Operation not permitted" + + + # setresgid + echo "'setresgid g:root g:root g:root' allowed" + snap run test-snapd-daemon-user.setresgid root root root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setresgid32 root root root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setresgid -1 g:root -1' allowed" + snap run test-snapd-daemon-user.setresgid -1 root -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setresgid32 -1 root -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setresgid g:root g:root -1' allowed" + snap run test-snapd-daemon-user.setresgid root root -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setresgid32 root root -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setresgid g:snap_daemon g:snap_daemon g:snap_daemon' allowed" + snap run test-snapd-daemon-user.setresgid snap_daemon snap_daemon snap_daemon 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=$group, sgid=$group, groups=" + snap run test-snapd-daemon-user.setresgid32 snap_daemon snap_daemon snap_daemon 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=$group, sgid=$group, groups=" + + echo "'setresgid -1 g:snap_daemon -1' allowed" + snap run test-snapd-daemon-user.setresgid -1 snap_daemon -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=$group, sgid=0, groups=" + snap run test-snapd-daemon-user.setresgid32 -1 snap_daemon -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=$group, sgid=0, groups=" + + echo "'setresgid g:snap_daemon g:snap_daemon -1' allowed" + snap run test-snapd-daemon-user.setresgid snap_daemon snap_daemon -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=$group, sgid=0, groups=" + snap run test-snapd-daemon-user.setresgid32 snap_daemon snap_daemon -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=$group, sgid=0, groups=" + + echo "'setresgid g:snap_daemon g:snap_daemon g:root' allowed" + snap run test-snapd-daemon-user.setresgid snap_daemon snap_daemon root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=$group, sgid=0, groups=" + snap run test-snapd-daemon-user.setresgid32 snap_daemon snap_daemon root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=$group, sgid=0, groups=" + + echo "'setresgid g:snap_daemon g:root g:root' allowed" + snap run test-snapd-daemon-user.setresgid snap_daemon root root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setregid32 snap_daemon root root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=$group, egid=0, sgid=0, groups=" + + echo "'setresgid -1 -1 g:test' denied" + snap run test-snapd-daemon-user.setresgid -1 -1 test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 -1 -1 test 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid -1 g:test -1' denied" + snap run test-snapd-daemon-user.setresgid -1 test -1 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 -1 test -1 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid -1 g:test g:test' denied" + snap run test-snapd-daemon-user.setresgid -1 test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 -1 test test 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:test -1 -1' denied" + snap run test-snapd-daemon-user.setresgid test -1 -1 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 test -1 -1 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:test -1 g:test' denied" + snap run test-snapd-daemon-user.setresgid test -1 test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 test -1 test 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:test g:test -1' denied" + snap run test-snapd-daemon-user.setresgid test test -1 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 test test -1 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:test g:test g:test' denied" + snap run test-snapd-daemon-user.setresgid test test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 test test test 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:root g:root g:test' denied" + snap run test-snapd-daemon-user.setresgid root root test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 root root test 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:root g:test g:root' denied" + snap run test-snapd-daemon-user.setresgid root test root 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 root test root 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:root g:test g:test' denied" + snap run test-snapd-daemon-user.setresgid root test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 root test test 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:test g:root g:root' denied" + snap run test-snapd-daemon-user.setresgid test root root 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 test root root 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:test g:root g:test' denied" + snap run test-snapd-daemon-user.setresgid test root test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 test root test 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:test g:test g:root' denied" + snap run test-snapd-daemon-user.setresgid test test root 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 test test root 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:snap_daemon g:snap_daemon g:test' denied" + snap run test-snapd-daemon-user.setresgid snap_daemon snap_daemon test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 snap_daemon snap_daemon test 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:snap_daemon g:test g:snap_daemon' denied" + snap run test-snapd-daemon-user.setresgid snap_daemon test snap_daemon 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 snap_daemon test snap_daemon 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:snap_daemon g:test g:test' denied" + snap run test-snapd-daemon-user.setresgid snap_daemon test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 snap_daemon test test 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:test g:snap_daemon g:snap_daemon' denied" + snap run test-snapd-daemon-user.setresgid test snap_daemon snap_daemon 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 test snap_daemon snap_daemon 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:test g:snap_daemon g:test' denied" + snap run test-snapd-daemon-user.setresgid test snap_daemon test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 test snap_daemon test 2>&1 | MATCH "Operation not permitted" + + echo "'setresgid g:test g:test g:snap_daemon' denied" + snap run test-snapd-daemon-user.setresgid test test snap_daemon 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresgid32 test test snap_daemon 2>&1 | MATCH "Operation not permitted" + + + # setuid + echo "'setuid u:root' allowed" + snap run test-snapd-daemon-user.setuid root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setuid32 root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setuid u:snap_daemon' allowed" + snap run test-snapd-daemon-user.setuid snap_daemon 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=$user, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setuid32 snap_daemon 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=$user, rgid=0, egid=0, sgid=0, groups=" + + echo "'setuid u:test' denied" + snap run test-snapd-daemon-user.setuid test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setuid32 test 2>&1 | MATCH "Operation not permitted" + + + # setreuid + echo "'setreuid u:root u:root' allowed" + snap run test-snapd-daemon-user.setreuid root root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setreuid32 root root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setreuid -1 u:root' allowed" + snap run test-snapd-daemon-user.setreuid -1 root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setreuid32 -1 root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setreuid u:root -1' allowed" + snap run test-snapd-daemon-user.setreuid root -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setreuid32 root -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setreuid u:snap_daemon u:snap_daemon' allowed" + snap run test-snapd-daemon-user.setreuid snap_daemon snap_daemon 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=$user, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setreuid32 snap_daemon snap_daemon 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=$user, rgid=0, egid=0, sgid=0, groups=" + + echo "'setreuid -1 u:snap_daemon' allowed" + snap run test-snapd-daemon-user.setreuid -1 snap_daemon 2>&1 | MATCH "After: ruid=0, euid=$user, suid=$user, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setreuid32 -1 snap_daemon 2>&1 | MATCH "After: ruid=0, euid=$user, suid=$user, rgid=0, egid=0, sgid=0, groups=" + + echo "'setreuid u:snap_daemon -1' allowed" + snap run test-snapd-daemon-user.setreuid snap_daemon -1 2>&1 | MATCH "After: ruid=$user, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setreuid32 snap_daemon -1 2>&1 | MATCH "After: ruid=$user, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setreuid u:root u:snap_daemon' allowed" + snap run test-snapd-daemon-user.setreuid root snap_daemon 2>&1 | MATCH "After: ruid=0, euid=$user, suid=$user, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setreuid32 root snap_daemon 2>&1 | MATCH "After: ruid=0, euid=$user, suid=$user, rgid=0, egid=0, sgid=0, groups=" + + echo "'setreuid u:snap_daemon u:root' allowed" + snap run test-snapd-daemon-user.setreuid snap_daemon root 2>&1 | MATCH "After: ruid=$user, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setreuid32 snap_daemon root 2>&1 | MATCH "After: ruid=$user, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setreuid u:test u:test' denied" + snap run test-snapd-daemon-user.setreuid test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setreuid32 test test 2>&1 | MATCH "Operation not permitted" + + echo "'setreuid -1 u:test' denied" + snap run test-snapd-daemon-user.setreuid -1 test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setreuid32 -1 test 2>&1 | MATCH "Operation not permitted" + + echo "'setreuid u:test -1' denied" + snap run test-snapd-daemon-user.setreuid test -1 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setreuid32 test -1 2>&1 | MATCH "Operation not permitted" + + echo "'setreuid u:root u:test' denied" + snap run test-snapd-daemon-user.setreuid root test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setreuid32 root test 2>&1 | MATCH "Operation not permitted" + + echo "'setreuid u:test u:root' denied" + snap run test-snapd-daemon-user.setreuid test root 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setreuid32 test root 2>&1 | MATCH "Operation not permitted" + + echo "'setreuid u:snap_daemon u:test' denied" + snap run test-snapd-daemon-user.setreuid snap_daemon test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setreuid32 snap_daemon test 2>&1 | MATCH "Operation not permitted" + + echo "'setreuid u:test u:snap_daemon' denied" + snap run test-snapd-daemon-user.setreuid test snap_daemon 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setreuid32 test snap_daemon 2>&1 | MATCH "Operation not permitted" + + + # setresuid + echo "'setresuid u:root u:root u:root' allowed" + snap run test-snapd-daemon-user.setresuid root root root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setresuid32 root root root 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setresuid -1 u:root -1' allowed" + snap run test-snapd-daemon-user.setresuid -1 root -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setresuid32 -1 root -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setresuid u:root u:root -1' allowed" + snap run test-snapd-daemon-user.setresuid root root -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setresuid32 root root -1 2>&1 | MATCH "After: ruid=0, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setresuid u:snap_daemon u:snap_daemon u:snap_daemon' allowed" + snap run test-snapd-daemon-user.setresuid snap_daemon snap_daemon snap_daemon 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=$user, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setresuid32 snap_daemon snap_daemon snap_daemon 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=$user, rgid=0, egid=0, sgid=0, groups=" + + echo "'setresuid -1 u:snap_daemon -1' allowed" + snap run test-snapd-daemon-user.setresuid -1 snap_daemon -1 2>&1 | MATCH "After: ruid=0, euid=$user, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setresuid32 -1 snap_daemon -1 2>&1 | MATCH "After: ruid=0, euid=$user, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setresuid u:daemon u:daemon -1' allowed" + snap run test-snapd-daemon-user.setresuid snap_daemon snap_daemon -1 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setresuid32 snap_daemon snap_daemon -1 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setresuid u:daemon u:daemon u:root' allowed" + snap run test-snapd-daemon-user.setresuid snap_daemon snap_daemon root 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setresuid32 snap_daemon snap_daemon root 2>&1 | MATCH "After: ruid=$user, euid=$user, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setresuid u:daemon u:root u:root' allowed" + snap run test-snapd-daemon-user.setresuid snap_daemon root root 2>&1 | MATCH "After: ruid=$user, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + snap run test-snapd-daemon-user.setreuid32 snap_daemon root root 2>&1 | MATCH "After: ruid=$user, euid=0, suid=0, rgid=0, egid=0, sgid=0, groups=" + + echo "'setresuid -1 -1 u:test' denied" + snap run test-snapd-daemon-user.setresuid -1 -1 test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 -1 -1 test 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid -1 u:test -1' denied" + snap run test-snapd-daemon-user.setresuid -1 test -1 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 -1 test -1 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid -1 u:test u:test' denied" + snap run test-snapd-daemon-user.setresuid -1 test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 -1 test test 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:test -1 -1' denied" + snap run test-snapd-daemon-user.setresuid test -1 -1 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 test -1 -1 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:test -1 u:test' denied" + snap run test-snapd-daemon-user.setresuid test -1 test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 test -1 test 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:test u:test -1' denied" + snap run test-snapd-daemon-user.setresuid test test -1 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 test test -1 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:test u:test u:test' denied" + snap run test-snapd-daemon-user.setresuid test test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 test test test 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:root u:root u:test' denied" + snap run test-snapd-daemon-user.setresuid root root test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 root root test 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:root u:test u:root' denied" + snap run test-snapd-daemon-user.setresuid root test root 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 root test root 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:root u:test u:test' denied" + snap run test-snapd-daemon-user.setresuid root test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 root test test 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:test u:root u:root' denied" + snap run test-snapd-daemon-user.setresuid test root root 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 test root root 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:test u:root u:test' denied" + snap run test-snapd-daemon-user.setresuid test root test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 test root test 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:test u:test u:root' denied" + snap run test-snapd-daemon-user.setresuid test test root 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 test test root 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:daemon u:daemon u:test' denied" + snap run test-snapd-daemon-user.setresuid snap_daemon snap_daemon test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 snap_daemon snap_daemon test 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:daemon u:test u:daemon' denied" + snap run test-snapd-daemon-user.setresuid snap_daemon test snap_daemon 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 snap_daemon test snap_daemon 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:daemon u:test u:test' denied" + snap run test-snapd-daemon-user.setresuid snap_daemon test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 snap_daemon test test 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:test u:daemon u:daemon' denied" + snap run test-snapd-daemon-user.setresuid test snap_daemon snap_daemon 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 test snap_daemon snap_daemon 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:test u:daemon u:test' denied" + snap run test-snapd-daemon-user.setresuid test snap_daemon test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 test snap_daemon test 2>&1 | MATCH "Operation not permitted" + + echo "'setresuid u:test u:test u:daemon' denied" + snap run test-snapd-daemon-user.setresuid test test snap_daemon 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.setresuid32 test test snap_daemon 2>&1 | MATCH "Operation not permitted" + + + # chown + touch "$P2" "$P3" + + echo "'chown - u:root g:root' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.chown "$P2" root root 2>&1 | MATCH "After: .* uid=0, gid=0" + snap run test-snapd-daemon-user.chown32 "$P3" root root 2>&1 | MATCH "After: .* uid=0, gid=0" + + echo "'chown - u:root -1' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.chown "$P2" root -1 2>&1 | MATCH "After: .* uid=0, gid=0" + snap run test-snapd-daemon-user.chown32 "$P3" root -1 2>&1 | MATCH "After: .* uid=0, gid=0" + + echo "'chown - -1 g:root' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.chown "$P2" -1 root 2>&1 | MATCH "After: .* uid=0, gid=0" + snap run test-snapd-daemon-user.chown32 "$P3" -1 root 2>&1 | MATCH "After: .* uid=0, gid=0" + + echo "'chown - u:daemon g:daemon' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.chown "$P2" snap_daemon snap_daemon 2>&1 | MATCH "After: .* uid=$user, gid=$group" + snap run test-snapd-daemon-user.chown32 "$P3" snap_daemon snap_daemon 2>&1 | MATCH "After: .* uid=$user, gid=$group" + + echo "'chown - u:daemon -1' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.chown "$P2" snap_daemon -1 2>&1 | MATCH "After: .* uid=$user, gid=0" + snap run test-snapd-daemon-user.chown32 "$P3" snap_daemon -1 2>&1 | MATCH "After: .* uid=$user, gid=0" + + echo "'chown - -1 g:daemon' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.chown "$P2" -1 snap_daemon 2>&1 | MATCH "After: .* uid=0, gid=$group" + snap run test-snapd-daemon-user.chown32 "$P3" -1 snap_daemon 2>&1 | MATCH "After: .* uid=0, gid=$group" + + echo "'chown - u:test g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.chown "$P2" test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.chown32 "$P3" test test 2>&1 | MATCH "Operation not permitted" + + echo "'chown - -1 g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.chown "$P2" -1 test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.chown32 "$P3" -1 test 2>&1 | MATCH "Operation not permitted" + + echo "'chown - u:test -1' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.chown "$P2" test -1 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.chown32 "$P3" test -1 2>&1 | MATCH "Operation not permitted" + + echo "'chown - u:test g:root' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.chown "$P2" test root 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.chown32 "$P3" test root 2>&1 | MATCH "Operation not permitted" + + echo "'chown - u:root g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.chown "$P2" root test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.chown32 "$P3" root test 2>&1 | MATCH "Operation not permitted" + + echo "'chown - u:test g:daemon' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.chown "$P2" test snap_daemon 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.chown32 "$P3" test snap_daemon 2>&1 | MATCH "Operation not permitted" + + echo "'chown - u:daemon g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.chown "$P2" snap_daemon test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.chown32 "$P3" snap_daemon test 2>&1 | MATCH "Operation not permitted" + + + # lchown + echo "'lchown - u:root g:root' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.lchown "$P2" root root 2>&1 | MATCH "After: .* uid=0, gid=0" + snap run test-snapd-daemon-user.lchown32 "$P3" root root 2>&1 | MATCH "After: .* uid=0, gid=0" + + echo "'lchown - u:root -1' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.lchown "$P2" root -1 2>&1 | MATCH "After: .* uid=0, gid=0" + snap run test-snapd-daemon-user.lchown32 "$P3" root -1 2>&1 | MATCH "After: .* uid=0, gid=0" + + echo "'lchown - -1 g:root' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.lchown "$P2" -1 root 2>&1 | MATCH "After: .* uid=0, gid=0" + snap run test-snapd-daemon-user.lchown32 "$P3" -1 root 2>&1 | MATCH "After: .* uid=0, gid=0" + + echo "'lchown - u:daemon g:daemon' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.lchown "$P2" snap_daemon snap_daemon 2>&1 | MATCH "After: .* uid=$user, gid=$group" + snap run test-snapd-daemon-user.lchown32 "$P3" snap_daemon snap_daemon 2>&1 | MATCH "After: .* uid=$user, gid=$group" + + echo "'lchown - u:daemon -1' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.lchown "$P2" snap_daemon -1 2>&1 | MATCH "After: .* uid=$user, gid=0" + snap run test-snapd-daemon-user.lchown32 "$P3" snap_daemon -1 2>&1 | MATCH "After: .* uid=$user, gid=0" + + echo "'lchown - -1 g:daemon' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.lchown "$P2" -1 snap_daemon 2>&1 | MATCH "After: .* uid=0, gid=$group" + snap run test-snapd-daemon-user.lchown32 "$P3" -1 snap_daemon 2>&1 | MATCH "After: .* uid=0, gid=$group" + + echo "'lchown - u:test g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.lchown "$P2" test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.lchown32 "$P3" test test 2>&1 | MATCH "Operation not permitted" + + echo "'lchown - -1 g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.lchown "$P2" -1 test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.lchown32 "$P3" -1 test 2>&1 | MATCH "Operation not permitted" + + echo "'lchown - u:test -1' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.lchown "$P2" test -1 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.lchown32 "$P3" test -1 2>&1 | MATCH "Operation not permitted" + + echo "'lchown - u:test g:root' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.lchown "$P2" test root 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.lchown32 "$P3" test root 2>&1 | MATCH "Operation not permitted" + + echo "'lchown - u:root g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.lchown "$P2" root test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.lchown32 "$P3" root test 2>&1 | MATCH "Operation not permitted" + + echo "'lchown - u:test g:daemon' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.lchown "$P2" test snap_daemon 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.lchown32 "$P3" test snap_daemon 2>&1 | MATCH "Operation not permitted" + + echo "'lchown - u:daemon g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.lchown "$P2" snap_daemon test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.lchown32 "$P3" snap_daemon test 2>&1 | MATCH "Operation not permitted" + + + # fchown + echo "'fchown - u:root g:root' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchown "$P2" root root 2>&1 | MATCH "After: .* uid=0, gid=0" + snap run test-snapd-daemon-user.fchown32 "$P3" root root 2>&1 | MATCH "After: .* uid=0, gid=0" + + echo "'fchown - u:root -1' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchown "$P2" root -1 2>&1 | MATCH "After: .* uid=0, gid=0" + snap run test-snapd-daemon-user.fchown32 "$P3" root -1 2>&1 | MATCH "After: .* uid=0, gid=0" + + echo "'fchown - -1 g:root' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchown "$P2" -1 root 2>&1 | MATCH "After: .* uid=0, gid=0" + snap run test-snapd-daemon-user.fchown32 "$P3" -1 root 2>&1 | MATCH "After: .* uid=0, gid=0" + + echo "'fchown - u:daemon g:daemon' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchown "$P2" snap_daemon snap_daemon 2>&1 | MATCH "After: .* uid=$user, gid=$group" + snap run test-snapd-daemon-user.fchown32 "$P3" snap_daemon snap_daemon 2>&1 | MATCH "After: .* uid=$user, gid=$group" + + echo "'fchown - u:daemon -1' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchown "$P2" snap_daemon -1 2>&1 | MATCH "After: .* uid=$user, gid=0" + snap run test-snapd-daemon-user.fchown32 "$P3" snap_daemon -1 2>&1 | MATCH "After: .* uid=$user, gid=0" + + echo "'fchown - -1 g:daemon' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchown "$P2" -1 snap_daemon 2>&1 | MATCH "After: .* uid=0, gid=$group" + snap run test-snapd-daemon-user.fchown32 "$P3" -1 snap_daemon 2>&1 | MATCH "After: .* uid=0, gid=$group" + + echo "'fchown - u:test g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchown "$P2" test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchown32 "$P3" test test 2>&1 | MATCH "Operation not permitted" + + echo "'fchown - -1 g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchown "$P2" -1 test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchown32 "$P3" -1 test 2>&1 | MATCH "Operation not permitted" + + echo "'fchown - u:test -1' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchown "$P2" test -1 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchown32 "$P3" test -1 2>&1 | MATCH "Operation not permitted" + + echo "'fchown - u:test g:root' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchown "$P2" test root 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchown32 "$P3" test root 2>&1 | MATCH "Operation not permitted" + + echo "'fchown - u:root g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchown "$P2" root test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchown32 "$P3" root test 2>&1 | MATCH "Operation not permitted" + + echo "'fchown - u:test g:daemon' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchown "$P2" test snap_daemon 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchown32 "$P3" test snap_daemon 2>&1 | MATCH "Operation not permitted" + + echo "'fchown - u:daemon g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchown "$P2" snap_daemon test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchown32 "$P3" snap_daemon test 2>&1 | MATCH "Operation not permitted" + + + # fchownat + echo "'fchownat - u:root g:root' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchownat "$P2" root root 2>&1 | MATCH "After: .* uid=0, gid=0" + snap run test-snapd-daemon-user.fchownat32 "$P3" root root 2>&1 | MATCH "After: .* uid=0, gid=0" + + echo "'fchownat - u:root -1' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchownat "$P2" root -1 2>&1 | MATCH "After: .* uid=0, gid=0" + snap run test-snapd-daemon-user.fchownat32 "$P3" root -1 2>&1 | MATCH "After: .* uid=0, gid=0" + + echo "'fchownat - -1 g:root' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchownat "$P2" -1 root 2>&1 | MATCH "After: .* uid=0, gid=0" + snap run test-snapd-daemon-user.fchownat32 "$P3" -1 root 2>&1 | MATCH "After: .* uid=0, gid=0" + + echo "'fchownat - u:daemon g:daemon' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchownat "$P2" snap_daemon snap_daemon 2>&1 | MATCH "After: .* uid=$user, gid=$group" + snap run test-snapd-daemon-user.fchownat32 "$P3" snap_daemon snap_daemon 2>&1 | MATCH "After: .* uid=$user, gid=$group" + + echo "'fchownat - u:daemon -1' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchownat "$P2" snap_daemon -1 2>&1 | MATCH "After: .* uid=$user, gid=0" + snap run test-snapd-daemon-user.fchownat32 "$P3" snap_daemon -1 2>&1 | MATCH "After: .* uid=$user, gid=0" + + echo "'fchownat - -1 g:daemon' allowed" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchownat "$P2" -1 snap_daemon 2>&1 | MATCH "After: .* uid=0, gid=$group" + snap run test-snapd-daemon-user.fchownat32 "$P3" -1 snap_daemon 2>&1 | MATCH "After: .* uid=0, gid=$group" + + echo "'fchownat - u:test g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchownat "$P2" test test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchownat32 "$P3" test test 2>&1 | MATCH "Operation not permitted" + + echo "'fchownat - -1 g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchownat "$P2" -1 test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchownat32 "$P3" -1 test 2>&1 | MATCH "Operation not permitted" + + echo "'fchownat - u:test -1' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchownat "$P2" test -1 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchownat32 "$P3" test -1 2>&1 | MATCH "Operation not permitted" + + echo "'fchownat - u:test g:root' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchownat "$P2" test root 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchownat32 "$P3" test root 2>&1 | MATCH "Operation not permitted" + + echo "'fchownat - u:root g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchownat "$P2" root test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchownat32 "$P3" root test 2>&1 | MATCH "Operation not permitted" + + echo "'fchownat - u:test g:daemon' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchownat "$P2" test snap_daemon 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchownat32 "$P3" test snap_daemon 2>&1 | MATCH "Operation not permitted" + + echo "'fchownat - u:daemon g:test' denied" + chown root:root "$P2" "$P3" + snap run test-snapd-daemon-user.fchownat "$P2" snap_daemon test 2>&1 | MATCH "Operation not permitted" + snap run test-snapd-daemon-user.fchownat32 "$P3" snap_daemon test 2>&1 | MATCH "Operation not permitted" diff -Nru snapd-2.40/tests/main/system-usernames-illegal/task.yaml snapd-2.42.1/tests/main/system-usernames-illegal/task.yaml --- snapd-2.40/tests/main/system-usernames-illegal/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/system-usernames-illegal/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,17 @@ +summary: ensure unapproved user cannot be used with system-usernames + +# List of expected snap install failures due to libseccomp/golang-seccomp being +# too old. Since the illegal name check happens after verifying system support, +# we can ignore these. +systems: [-amazon-linux-2-*, -centos-7-*, -debian-9-*, -fedora-29-*, -fedora-30-*, -opensuse-15.0-*, -opensuse-15.1-*, -ubuntu-14.04-*] + +execute: | + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB"/snaps.sh + snap_path=$(make_snap test-snapd-illegal-system-username) + echo "Try to install a snap with an illegal user in 'system-usernames'" + snap install --dangerous "${snap_path}" 2>&1 | MATCH 'requires unsupported system username "daemon"' + + # Make sure neiter snap_deaemon user nor group are created + not getent passwd snap_daemon + not getent group snap_daemon diff -Nru snapd-2.40/tests/main/system-usernames-install-twice/task.yaml snapd-2.42.1/tests/main/system-usernames-install-twice/task.yaml --- snapd-2.40/tests/main/system-usernames-install-twice/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/system-usernames-install-twice/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,28 @@ +summary: ensure snap can be installed twice (reusing the created groups) + +# List of expected snap install failures due to libseccomp/golang-seccomp being +# too old. Since the illegal name check happens after verifying system support, +# we can ignore these. Ignore ubuntu-core since groupdel doesn't support +# --extrausers +systems: [-amazon-linux-2-*, -centos-7-*, -debian-9-*, -fedora-29-*, -fedora-30-*, -opensuse-15.0-*, -opensuse-15.1-*, -ubuntu-14.04-*, -ubuntu-core-*] + +prepare: | + snap install --edge test-snapd-daemon-user + +restore: | + snap remove test-snapd-daemon-user || true + + # snapd will create this for us, but we'll remove it for consistency in + # test runs + user-tool remove-with-group snap_daemon + +execute: | + echo "When the snap is removed" + snap remove test-snapd-daemon-user + + echo "Then the snap_daemon user and group remain" + getent passwd snap_daemon || exit 1 + getent group snap_daemon || exit 1 + + echo "And the snap can be installed again" + snap install --edge test-snapd-daemon-user diff -Nru snapd-2.40/tests/main/system-usernames-missing-user/task.yaml snapd-2.42.1/tests/main/system-usernames-missing-user/task.yaml --- snapd-2.40/tests/main/system-usernames-missing-user/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/system-usernames-missing-user/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,28 @@ +summary: ensure snap fails to install if one of user or group doesn't exist + +# List of expected snap install failures due to libseccomp/golang-seccomp being +# too old. Since the illegal name check happens after verifying system support, +# we can ignore these. Ignore ubuntu-core since groupdel doesn't support +# --extrausers +systems: [-amazon-linux-2-*, -centos-7-*, -debian-9-*, -fedora-29-*, -fedora-30-*, -opensuse-15.0-*, -opensuse-15.1-*, -ubuntu-14.04-*, -ubuntu-core-*] + +prepare: | + groupadd --system snap_daemon + +restore: | + # Make sure the snap is removed if the test failed and the snap was + # installed + snap remove test-snapd-daemon-user || true + + # snapd will create this for us, but we'll remove it for consistency in + # test runs + groupdel snap_daemon || true + not getent group snap_daemon + +execute: | + echo "When the snap_daemon group exists but not the user" + getent passwd snap_daemon && exit 1 + getent group snap_daemon || exit 1 + + echo "Then the snap cannot be installed" + snap install --edge test-snapd-daemon-user 2>&1 | MATCH 'cannot add user/group "snap_daemon": group exists and user does not' diff -Nru snapd-2.40/tests/main/ubuntu-core-classic/task.yaml snapd-2.42.1/tests/main/ubuntu-core-classic/task.yaml --- snapd-2.40/tests/main/ubuntu-core-classic/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-classic/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -14,6 +14,11 @@ restore: | rm -f /etc/sudoers.d/create-test + # Undo the change done by the classic snap. + # FIXME: https://github.com/snapcore/classic-snap/issues/32 + if [ "$(mountinfo-tool /dev/pts .sb_opts)" = "rw,mode=666,ptmxmode=666" ]; then + mount devpts -t devpts /dev/pts -o remount,nosuid,noexec,relatime,gid=5,mode=620,ptmxmode=000 + fi execute: | echo "Ensure classic can be installed" diff -Nru snapd-2.40/tests/main/ubuntu-core-create-user/task.yaml snapd-2.42.1/tests/main/ubuntu-core-create-user/task.yaml --- snapd-2.40/tests/main/ubuntu-core-create-user/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-create-user/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,6 +1,6 @@ summary: Ensure that snap create-user works in ubuntu-core -systems: [ubuntu-core-16-*] +systems: [ubuntu-core-1*] restore: | # meh, deluser has no --extrausers support diff -Nru snapd-2.40/tests/main/ubuntu-core-custom-device-reg/task.yaml snapd-2.42.1/tests/main/ubuntu-core-custom-device-reg/task.yaml --- snapd-2.40/tests/main/ubuntu-core-custom-device-reg/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-custom-device-reg/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -2,7 +2,7 @@ Test that device initialisation and registration can be customized with the prepare-device gadget hook -systems: [ubuntu-core-16-64] +systems: [ubuntu-core-1*-64] prepare: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -11,6 +11,9 @@ fi #shellcheck source=tests/lib/systemd.sh . "$TESTSLIB"/systemd.sh + #shellcheck source=tests/lib/systems.sh + . "$TESTSLIB"/systems.sh + systemctl stop snapd.service snapd.socket rm -rf /var/lib/snapd/assertions/* rm -rf /var/lib/snapd/device @@ -26,7 +29,11 @@ python3 ./manip_seed.py /var/lib/snapd/seed/seed.yaml cp "$TESTSLIB"/assertions/developer1.account /var/lib/snapd/seed/assertions cp "$TESTSLIB"/assertions/developer1.account-key /var/lib/snapd/seed/assertions - cp "$TESTSLIB"/assertions/developer1-pc.model /var/lib/snapd/seed/assertions + if is_core18_system; then + cp "$TESTSLIB"/assertions/developer1-pc-18.model /var/lib/snapd/seed/assertions/developer1-pc.model + else + cp "$TESTSLIB"/assertions/developer1-pc.model /var/lib/snapd/seed/assertions + fi cp "$TESTSLIB"/assertions/testrootorg-store.account-key /var/lib/snapd/seed/assertions # start fake device svc systemd_create_and_start_unit fakedevicesvc "$(command -v fakedevicesvc) localhost:11029" @@ -40,6 +47,7 @@ fi #shellcheck source=tests/lib/systemd.sh . "$TESTSLIB"/systemd.sh + systemctl stop snapd.service snapd.socket systemd_stop_and_destroy_unit fakedevicesvc rm -rf /var/lib/snapd/assertions/* diff -Nru snapd-2.40/tests/main/ubuntu-core-custom-device-reg-extras/task.yaml snapd-2.42.1/tests/main/ubuntu-core-custom-device-reg-extras/task.yaml --- snapd-2.40/tests/main/ubuntu-core-custom-device-reg-extras/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-custom-device-reg-extras/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -3,7 +3,7 @@ with the prepare-device gadget hook and this can set request headers, a proposed serial and the body of the serial assertion -systems: [ubuntu-core-16-64] +systems: [ubuntu-core-1*-64] prepare: | if [ "$TRUST_TEST_KEYS" = "false" ]; then @@ -12,6 +12,9 @@ fi #shellcheck source=tests/lib/systemd.sh . "$TESTSLIB"/systemd.sh + #shellcheck source=tests/lib/systems.sh + . "$TESTSLIB"/systems.sh + systemctl stop snapd.service snapd.socket rm -rf /var/lib/snapd/assertions/* rm -rf /var/lib/snapd/device @@ -27,7 +30,11 @@ python3 ./manip_seed.py /var/lib/snapd/seed/seed.yaml cp "$TESTSLIB"/assertions/developer1.account /var/lib/snapd/seed/assertions cp "$TESTSLIB"/assertions/developer1.account-key /var/lib/snapd/seed/assertions - cp "$TESTSLIB"/assertions/developer1-pc.model /var/lib/snapd/seed/assertions + if is_core18_system; then + cp "$TESTSLIB"/assertions/developer1-pc-18.model /var/lib/snapd/seed/assertions/developer1-pc.model + else + cp "$TESTSLIB"/assertions/developer1-pc.model /var/lib/snapd/seed/assertions + fi cp "$TESTSLIB"/assertions/testrootorg-store.account-key /var/lib/snapd/seed/assertions # start fake device svc systemd_create_and_start_unit fakedevicesvc "$(command -v fakedevicesvc) localhost:11029" diff -Nru snapd-2.40/tests/main/ubuntu-core-device-reg/task.yaml snapd-2.42.1/tests/main/ubuntu-core-device-reg/task.yaml --- snapd-2.40/tests/main/ubuntu-core-device-reg/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-device-reg/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -2,13 +2,19 @@ Ensure after device initialisation registration worked and we have a serial and can acquire a session macaroon -systems: [ubuntu-core-16-*] +systems: [ubuntu-core-1*] execute: | + #shellcheck source=tests/lib/names.sh + . "$TESTSLIB"/names.sh + #shellcheck source=tests/lib/systems.sh + . "$TESTSLIB"/systems.sh + echo "Wait for first boot to be done" while ! snap changes | grep -q "Done.*Initialize system state"; do sleep 1; done + echo "We have a model assertion" - snap known model|MATCH "series: 16" + snap known model | MATCH "series: 16" if ! snap known model|grep "brand-id: canonical" ; then echo "Not a canonical model. Skipping." @@ -21,9 +27,19 @@ echo "Check we have a serial" snap known serial|MATCH "authority-id: canonical" snap known serial|MATCH "brand-id: canonical" - if [ "$SPREAD_SYSTEM" = "ubuntu-core-16-64" ]; then - snap known serial|MATCH "model: pc" - fi + case "$SPREAD_SYSTEM" in + ubuntu-core-18-64) + snap known serial | MATCH "model: ubuntu-core-18-amd64" + ;; + ubuntu-core-18-arm-*) + snap known serial | MATCH "model: ubuntu-core-18-$gadget_name" + ;; + ubuntu-core-16-64) + snap known serial | MATCH "model: pc" + ;; + *) + snap known serial | MATCH "model: $gadget_name" + esac echo "Make sure we could acquire a session macaroon" snap find pc diff -Nru snapd-2.40/tests/main/ubuntu-core-fan/task.yaml snapd-2.42.1/tests/main/ubuntu-core-fan/task.yaml --- snapd-2.40/tests/main/ubuntu-core-fan/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-fan/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,5 +1,6 @@ summary: Test ubuntu-fan +# Ubuntu fan not available on ubuntu-core-18 systems: [ubuntu-core-16-*] prepare: | diff -Nru snapd-2.40/tests/main/ubuntu-core-gadget-config-defaults/task.yaml snapd-2.42.1/tests/main/ubuntu-core-gadget-config-defaults/task.yaml --- snapd-2.40/tests/main/ubuntu-core-gadget-config-defaults/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-gadget-config-defaults/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -34,7 +34,7 @@ TEST_SNAP_ID=aLcJorEJZgJNUGL2GMb3WR9SoVyHUNAd fi - # Update hte gadget config file + # Update the gadget config file cat "$GADGET_FILE" >> squashfs-root/meta/gadget.yaml sed -i -e "s/TEST_SNAP_ID/$TEST_SNAP_ID/" squashfs-root/meta/gadget.yaml @@ -86,8 +86,8 @@ rm /var/lib/snapd/seed/snaps/pc_x1.snap TEST_REVNO=$(awk "/^snap-revision: / {print \$2}" test-snapd-with-configure_*.assert) - if systemctl status "snap-test-snapd-with-configure-${TEST_REVNO}.mount" ; then - systemctl stop "snap-test-snapd-with-configure-${TEST_REVNO}.mount" + if systemctl status "$(systemd-escape --path /snap/test-snapd-with-configure/"$TEST_REVNO".mount)"; then + systemctl stop "$(systemd-escape --path /snap/test-snapd-with-configure/"$TEST_REVNO".mount)" rm -f "/etc/systemd/system/snap-test-snapd-with-configure-${TEST_REVNO}.mount" rm -f "/etc/systemd/system/multi-user.target.wants/snap-test-snapd-with-configure-${TEST_REVNO}.mount" rm -f /var/lib/snapd/snaps/test-snapd-with-configure_*.snap diff -Nru snapd-2.40/tests/main/ubuntu-core-grub/task.yaml snapd-2.42.1/tests/main/ubuntu-core-grub/task.yaml --- snapd-2.40/tests/main/ubuntu-core-grub/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-grub/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,6 +1,6 @@ summary: Ensure we have no unpacked kernel.img/initrd.img on grub systems -systems: [ubuntu-core-16-64] +systems: [ubuntu-core-1*-64] environment: NAME/initrdimg: initrd.img* diff -Nru snapd-2.40/tests/main/ubuntu-core-os-release/task.yaml snapd-2.42.1/tests/main/ubuntu-core-os-release/task.yaml --- snapd-2.40/tests/main/ubuntu-core-os-release/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-os-release/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,7 +1,14 @@ summary: check that os-release is correct -systems: [ubuntu-core-16-*] +systems: [ubuntu-core-1*] execute: | + #shellcheck source=tests/lib/systems.sh + . "$TESTSLIB"/systems.sh + + if is_core18_system; then + MATCH "DISTRIB_RELEASE=18" < /etc/lsb-release + else + MATCH "DISTRIB_RELEASE=16" < /etc/lsb-release + fi MATCH "ID=ubuntu-core" < /etc/os-release - MATCH "DISTRIB_RELEASE=16" < /etc/lsb-release diff -Nru snapd-2.40/tests/main/ubuntu-core-reboot/task.yaml snapd-2.42.1/tests/main/ubuntu-core-reboot/task.yaml --- snapd-2.40/tests/main/ubuntu-core-reboot/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-reboot/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,10 +1,6 @@ summary: Ensure that service and apparmor profiles work after a reboot -systems: - - ubuntu-core-16-64 - - ubuntu-core-16-32 - - ubuntu-core-16-arm-64 - - ubuntu-core-16-arm-32 +systems: [ubuntu-core-1*] # Start early as it takes a long time. priority: 100 diff -Nru snapd-2.40/tests/main/ubuntu-core-refresh/task.yaml snapd-2.42.1/tests/main/ubuntu-core-refresh/task.yaml --- snapd-2.40/tests/main/ubuntu-core-refresh/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-refresh/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,82 @@ +summary: Check that the ubuntu-core system is rebooted after the core snap is refreshed + +details: | + This test checks that when invoking a manual refresh/revert for core or core18 snaps, + a reboot is triggered and the command would exit after the first phase of the installation + reporting "snapd is about to reboot the system" + +systems: [ubuntu-core-*] + +execute: | + #shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB"/journalctl.sh + #shellcheck source=tests/lib/systems.sh + . "$TESTSLIB"/systems.sh + + TARGET_SNAP_NAME=core + if is_core18_system; then + TARGET_SNAP_NAME=core18 + fi + + # After installing a new version of the core/core18 snap the system is rebooted + if [ "$SPREAD_REBOOT" = 0 ]; then + currRev="$(readlink /snap/${TARGET_SNAP_NAME}/current)" + echo "$currRev" > initialRev + + # use journalctl wrapper to grep only the logs collected while the test is running + if get_journalctl_log | MATCH "Waiting for system reboot"; then + echo "Already waiting for system reboot, exiting..." + exit 1 + fi + + # install new target snap + + snap install --dangerous "/var/lib/snapd/snaps/${TARGET_SNAP_NAME}_${currRev}.snap" &> refresh.log + MATCH "snapd is about to reboot the system" < refresh.log + + # Detect in the logs when the reboot can been triggered + for _ in $(seq 50); do + if check_journalctl_log "Waiting for system reboot"; then + break + fi + sleep 2 + done + + if ! check_journalctl_log "Waiting for system reboot"; then + echo "Taking too long to reach waiting for reboot" + exit 1 + fi + + REBOOT + elif [ "$SPREAD_REBOOT" = 1 ]; then + while ! snap changes | MATCH "Done.*Install \"${TARGET_SNAP_NAME}\" snap from file.*"; do + sleep 1 + done + + # Check the current revision has changed + currRev="$(readlink /snap/${TARGET_SNAP_NAME}/current)" + [ "$(cat initialRev)" != "$currRev" ] + + # revert the target snap + snap revert "$TARGET_SNAP_NAME" &> revert.log + MATCH "snapd is about to reboot the system" < revert.log + + # Detect in the logs when the reboot can been triggered + for _ in $(seq 50); do + if check_journalctl_log "Waiting for system reboot" -b; then + break + fi + sleep 2 + done + + if ! check_journalctl_log "Waiting for system reboot" -b; then + echo "Taking too long to reach waiting for reboot" + exit 1 + fi + + REBOOT + elif [ "$SPREAD_REBOOT" = 2 ]; then + # Check the current revision is the same than the original + currRev="$(readlink /snap/${TARGET_SNAP_NAME}/current)" + [ "$(cat initialRev)" == "$currRev" ] + fi diff -Nru snapd-2.40/tests/main/ubuntu-core-services/task.yaml snapd-2.42.1/tests/main/ubuntu-core-services/task.yaml --- snapd-2.40/tests/main/ubuntu-core-services/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-services/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,6 +1,6 @@ summary: Ensure all services on Core are active -systems: [ubuntu-core-16-*] +systems: [ubuntu-core-1*] execute: | echo "Ensure one-shot services are working" diff -Nru snapd-2.40/tests/main/ubuntu-core-upgrade/task.yaml snapd-2.42.1/tests/main/ubuntu-core-upgrade/task.yaml --- snapd-2.40/tests/main/ubuntu-core-upgrade/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-upgrade/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,6 +1,9 @@ summary: Upgrade the core snap and revert a few times -systems: [ubuntu-core-16-*] +# ARM devices are not supported on ubuntu-core-18 due to fw_printenv/setenv are +# not provided by the system and as the devices boot with uboot so it is not +# possible to get any grub information as it is done with non arm devices. +systems: [ubuntu-core-16-*, ubuntu-core-18-32*, ubuntu-core-18-64*] # Start early as it takes a long time. priority: 100 @@ -18,17 +21,36 @@ if [ -f curChg ] ; then snap abort "$(cat curChg)" || true fi + # Remove the revisions installed during the test. + # The x1 revision is the one we use initially. + snap remove core --revision=x2 + snap remove core --revision=x3 prepare: | - snap list | awk "/^core / {print(\$3)}" > nextBoot + #shellcheck source=tests/lib/systems.sh + . "$TESTSLIB"/systems.sh + + TARGET_SNAP=core + if is_core18_system; then + TARGET_SNAP=core18 + fi + + snap list | awk "/^${TARGET_SNAP} / {print(\$3)}" > nextBoot snap install test-snapd-tools execute: | #shellcheck source=tests/lib/boot.sh . "$TESTSLIB"/boot.sh + #shellcheck source=tests/lib/systems.sh + . "$TESTSLIB"/systems.sh + + TARGET_SNAP=core + if is_core18_system; then + TARGET_SNAP=core18 + fi # FIXME Why it starting with snap_mode=try the first time? - # Perhaps because core is installed after seeding? Do we + # Perhaps because $TARGET_SNAP is installed after seeding? Do we # want that on pristine images? if [ "$SPREAD_REBOOT" != 0 ]; then echo "Waiting for snapd to clean snap_mode" @@ -37,12 +59,12 @@ done echo "Ensure the bootloader is correct after reboot" - test "$(bootenv snap_core)" = "core_$(cat nextBoot).snap" + test "$(bootenv snap_core)" = "${TARGET_SNAP}_$(cat nextBoot).snap" test "$(bootenv snap_try_core)" = "" test "$(bootenv snap_mode)" = "" fi - snap list | awk "/^core / {print(\$3)}" > prevBoot + snap list | awk "/^${TARGET_SNAP} / {print(\$3)}" > prevBoot # wait for ongoing change if there is one if [ -f curChg ] ; then @@ -52,10 +74,10 @@ case "$SPREAD_REBOOT" in - 0) cmd="snap install --dangerous /var/lib/snapd/snaps/core_$(cat prevBoot).snap" ;; - 1) cmd="snap revert core" ;; - 2) cmd="snap install --dangerous /var/lib/snapd/snaps/core_$(cat prevBoot).snap" ;; - 3) cmd="snap revert core" ;; + 0) cmd="snap install --dangerous /var/lib/snapd/snaps/${TARGET_SNAP}_$(cat prevBoot).snap" ;; + 1) cmd="snap revert $TARGET_SNAP" ;; + 2) cmd="snap install --dangerous /var/lib/snapd/snaps/${TARGET_SNAP}_$(cat prevBoot).snap" ;; + 3) cmd="snap revert $TARGET_SNAP" ;; 4) exit 0 ;; esac @@ -74,9 +96,9 @@ test-snapd-tools.echo hello | MATCH hello echo "Ensure the bootloader is correct before reboot" - readlink /snap/core/current > nextBoot + readlink "/snap/${TARGET_SNAP}/current" > nextBoot test "$(cat prevBoot)" != "$(cat nextBoot)" - test "$(bootenv snap_try_core)" = "core_$(cat nextBoot).snap" + test "$(bootenv snap_try_core)" = "${TARGET_SNAP}_$(cat nextBoot).snap" test "$(bootenv snap_mode)" = "try" echo "Ensure the device is scheduled for auto-reboot" diff -Nru snapd-2.40/tests/main/ubuntu-core-writablepaths/task.yaml snapd-2.42.1/tests/main/ubuntu-core-writablepaths/task.yaml --- snapd-2.40/tests/main/ubuntu-core-writablepaths/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/ubuntu-core-writablepaths/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -1,6 +1,6 @@ summary: Ensure that the writable paths on the image are correct -systems: [ubuntu-core-16-*] +systems: [ubuntu-core-1*] execute: | echo "Ensure everything in writable-paths is actually writable" diff -Nru snapd-2.40/tests/main/upgrade-from-2.15/task.yaml snapd-2.42.1/tests/main/upgrade-from-2.15/task.yaml --- snapd-2.40/tests/main/upgrade-from-2.15/task.yaml 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/tests/main/upgrade-from-2.15/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -3,10 +3,22 @@ systems: [ubuntu-16.04-64] prepare: | - dpkg --purge snapd + apt-tool checkpoint > installed.pkgs + #shellcheck source=tests/lib/pkgdb.sh + . "$TESTSLIB/pkgdb.sh" + distro_purge_package snapd restore: | - dpkg --purge ubuntu-core-launcher snap-confine + #shellcheck source=tests/lib/pkgdb.sh + . "$TESTSLIB/pkgdb.sh" + if [ -e old_install_done ]; then + echo "Ensure core is gone and we have ubuntu-core instead" + distro_purge_package snapd + fi + distro_install_build_snapd + + apt-tool restore installed.pkgs + rm -f installed.pkgs execute: | #shellcheck source=tests/lib/systemd.sh @@ -19,6 +31,9 @@ echo "Install snapd 2.15.2" apt install -y ./ubuntu-core-launcher_1.0.38-0ubuntu0.16.04.8_amd64.deb ./snap-confine_1.0.38-0ubuntu0.16.04.8_amd64.deb ./snapd_2.15.2ubuntu1_amd64.deb + echo "Installation completed" + touch old_install_done + echo "install a service snap and check its active" snap install go-example-webserver @@ -58,5 +73,8 @@ MATCH "^/usr/lib/snapd/snap-confine \\(enforce\\)" < /sys/kernel/security/apparmor/profiles MATCH "^/snap/core/.*/usr/lib/snapd/snap-confine \\(enforce\\)" < /sys/kernel/security/apparmor/profiles + echo "ensure that the old/obsolete snap-confine appamor profile got removed" + test ! -f /etc/apparmor.d/usr.lib.snapd.snap-confine + echo "Smoke test, this should fail if profiles were wrong" test-snapd-tools.echo hello | MATCH hello diff -Nru snapd-2.40/tests/main/user-libnss/findid.go snapd-2.42.1/tests/main/user-libnss/findid.go --- snapd-2.40/tests/main/user-libnss/findid.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/user-libnss/findid.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,29 @@ +package main + +import ( + "fmt" + "log" + "os" + + "github.com/snapcore/snapd/osutil" +) + +func main() { + fnName := os.Args[1] + userOrGroupName := os.Args[2] + + var fn func(string) (uint64, error) + switch fnName { + case "uid": + fn = osutil.FindUid + case "gid": + fn = osutil.FindGid + default: + log.Fatalf("unknown fnName: %q", fnName) + } + id, err := fn(userOrGroupName) + if err != nil { + log.Fatalf("fn failed: %q", err) + } + fmt.Println(id) +} diff -Nru snapd-2.40/tests/main/user-libnss/task.yaml snapd-2.42.1/tests/main/user-libnss/task.yaml --- snapd-2.40/tests/main/user-libnss/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/user-libnss/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,53 @@ +summary: Ensure our osutil.Find{Uid,Gid} code work with libnss + +description: | + The os/user code in go will behave differently when compiled with + or without cgo. This is confusing so we created the helpers + osutil.Find{Uid,Gid} that automatically fall back to calling + getent(1) when build without cgo. This test ensures these are + working correctly. + +# only run on a well defined system where we know how to setup libnss +systems: [ubuntu-18.04-64] + +prepare: | + echo "Save nsswitch.conf" + cp /etc/nsswitch.conf /etc/nsswitch.conf.save + echo "Install extrausers" + apt install libnss-extrausers + echo "Enable libnss-extrusers" + sed -i 's/^group:.*compat/\0 extrausers/' /etc/nsswitch.conf + sed -i 's/^passwd:.*compat/\0 extrausers/' /etc/nsswitch.conf + sed -i 's/^shadow:.*compat/\0 extrausers/' /etc/nsswitch.conf + echo "Workaround silly bug that causes extrausers to crash when missing" + for name in gshadow shadow; do + touch /var/lib/extrausers/$name + chmod 640 /var/lib/extrausers/$name + chown root:shadow /var/lib/extrausers/$name + done + echo "Add user" + adduser --extrausers --disabled-login --no-create-home --gecos '' --uid 9876 --shell /bin/false extratest + +restore: | + mv /etc/nsswitch.conf.save /etc/nsswitch.conf + apt autoremove -y libnss-extrausers + rm -rf /var/lib/extrausers + +execute: | + echo "Ensure tests run with both CGO and without" + su test -c 'CGO_ENABLED=1 go test github.com/snapcore/snapd/osutil' + su test -c 'CGO_ENABLED=0 go test github.com/snapcore/snapd/osutil' + + CGO_ENABLED=1 go build -o findid-cgo ./findid.go + CGO_ENABLED=0 go build -o findid-no-cgo ./findid.go + + # sanity check + getent passwd extratest + getent group extratest + + echo "Run binaries (CGO, without) exercising the helpers" + test "$(./findid-cgo uid extratest)" = "9876" + test "$(./findid-cgo gid extratest)" = "9876" + + test "$(./findid-no-cgo uid extratest)" = "9876" + test "$(./findid-no-cgo gid extratest)" = "9876" diff -Nru snapd-2.40/tests/main/version-tool/task.yaml snapd-2.42.1/tests/main/version-tool/task.yaml --- snapd-2.40/tests/main/version-tool/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/main/version-tool/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,43 @@ +summary: integration tests for version-tool +execute: | + # == + version-tool --strict 1 -eq 1 + version-tool --strict 1 -eq 1.0 + version-tool --strict 1.0 -eq 1 + not version-tool --strict 1 -eq 2 + + # != + not version-tool --strict 1.2 -ne 1.2 + version-tool --strict 1 -ne 2 + version-tool --strict 2 -ne 1 + + # < and <= + version-tool --strict 1 -lt 2 + not version-tool --strict 2 -lt 1 + version-tool --strict 1 -le 2 + version-tool --strict 2 -le 2 + not version-tool --strict 2 -le 1 + + # > and >= + version-tool --strict 2 -gt 1 + not version-tool --strict 1 -gt 2 + version-tool --strict 2 -ge 1 + version-tool --strict 2 -ge 2 + not version-tool --strict 1 -ge 2 + + # --verbose + version-tool --verbose --strict 1 -eq 2 | MATCH 'delta between 1 and 2 is: -1' + version-tool --verbose --strict 1 -eq 2 | MATCH 'delta -1 is inconsistent with ==' + + # --version + # NOTE: older python versions print the version string to stderr + version-tool --version 2>&1 | MATCH 1.0 + + # Strict requires all version components to be integers. + version-tool --strict 1.2 -eq 1.2-foo 2>&1 | MATCH 'error: version 1.2-foo is not purely numeric' + # Such invalid comparison also returns a distinct error code. + set +e + version-tool --strict 1.2 -eq 1.2-foo + error_code=$? + set -e + test "$error_code" -eq 2 diff -Nru snapd-2.40/tests/nightly/classic-ubuntu-core-transition/task.yaml snapd-2.42.1/tests/nightly/classic-ubuntu-core-transition/task.yaml --- snapd-2.40/tests/nightly/classic-ubuntu-core-transition/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/nightly/classic-ubuntu-core-transition/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,128 @@ +summary: Ensure that the ubuntu-core -> core transition works + +# we never test on core because the transition can only happen on "classic" we +# disable on ppc64el because the downloads are very slow there Fedora, openSUSE, +# Arch, CentOS are disabled at the moment as there is something fishy going on +# and the snapd service gets terminated during the process. +systems: [-ubuntu-core-*, -ubuntu-*-ppc64el, -fedora-*, -opensuse-*, -ubuntu-*-i386, -arch-*, -amazon-*, -centos-*, -debian-sid-*] + +# autopkgtest run only a subset of tests that deals with the integration +# with the distro +backends: [-autopkgtest] + +warn-timeout: 1m +kill-timeout: 5m + +debug: | + snap list || true + snap info core || true + snap info ubuntu-core || true + snap changes + #shellcheck source=tests/lib/changes.sh + . "$TESTSLIB/changes.sh" + snap change "$(change_id 'Transition ubuntu-core to core')" || true + +execute: | + #shellcheck source=tests/lib/pkgdb.sh + . "$TESTSLIB/pkgdb.sh" + #shellcheck source=tests/lib/systemd.sh + . "$TESTSLIB"/systemd.sh + curl() { + local url="$1" + # sadly systemd active means not that its really ready so we wait + # here for the socket to be available + while ! ss -t -l -n|grep :80; do + ss -l -l -n + sleep 1 + done + python3 -c "import urllib.request; print(urllib.request.urlopen(\"$url\").read().decode(\"utf-8\"))" + } + + #shellcheck source=tests/lib/pkgdb.sh + . "$TESTSLIB/pkgdb.sh" + echo "Ensure core is gone and we have ubuntu-core instead" + distro_purge_package snapd + distro_install_build_snapd + + # need to be seeded to allow snap install + snap wait system seed.loaded + + # modify daemon state to set ubuntu-core-transition-last-retry-time to the + # current time to prevent the ubuntu-core transition before the test snap is + # installed + systemctl stop snapd.{service,socket} + now="$(date --utc -Ins)" + jq -c '. + {data: (.data + {"ubuntu-core-transition-last-retry-time": "'"$now"'"})}' < /var/lib/snapd/state.json > state.json.new + mv state.json.new /var/lib/snapd/state.json + systemctl start snapd.{service,socket} + + snap download "--${CORE_CHANNEL}" ubuntu-core + snap ack ./ubuntu-core_*.assert + snap install ./ubuntu-core_*.snap + + snap install test-snapd-python-webserver + snap interfaces -i network | MATCH ":network +test-snapd-python-webserver" + snap interfaces -i network-bind | MATCH ":network-bind +.*test-snapd-python-webserver" + + echo "Ensure the webserver is working" + wait_for_service snap.test-snapd-python-webserver.test-snapd-python-webserver + curl http://localhost | MATCH "XKCD rocks" + + # restore ubuntu-core-transition-last-retry-time to its previous value and restart the daemon + systemctl stop snapd.{service,socket} + jq -c 'del(.["data"]["ubuntu-core-transition-last-retry-time"])' < /var/lib/snapd/state.json > state.json.new + mv state.json.new /var/lib/snapd/state.json + systemctl start snapd.{service,socket} + + echo "Ensure transition is triggered" + # wait for steady state or ensure-state-soon will be pointless + ok=0 + for i in $(seq 40); do + if ! snap changes|grep -q ".*.Doing.*" ; then + ok=1 + break + fi + sleep .5 + done + if [ $ok -ne 1 ] ; then + echo "Did not reach steady state" + exit 1 + fi + snap debug ensure-state-soon + + # wait for transition + ok=0 + for i in $(seq 240); do + if snap changes|grep -q ".*Done.*Transition ubuntu-core to core" ; then + ok=1 + break + fi + sleep 1 + done + if [ $ok -ne 1 ] ; then + echo "Transition did not start or finish" + exit 1 + fi + + if snap list|grep ubuntu-core; then + echo "ubuntu-core still installed, transition failed" + exit 1 + fi + snap interfaces -i network | MATCH ":network +test-snapd-python-webserver" + snap interfaces -i network-bind | MATCH ":network-bind +.*test-snapd-python-webserver" + echo "Ensure the webserver is still working" + wait_for_service snap.test-snapd-python-webserver.test-snapd-python-webserver + curl http://localhost | MATCH "XKCD rocks" + + systemctl restart snap.test-snapd-python-webserver.test-snapd-python-webserver + wait_for_service snap.test-snapd-python-webserver.test-snapd-python-webserver + echo "Ensure the webserver is working after a snap restart" + curl http://localhost | MATCH "XKCD rocks" + + echo "Ensure snap set core works" + snap set core system.power-key-action=ignore + if [ "$(snap get core system.power-key-action)" != "ignore" ]; then + echo "snap get did not return the expected result: " + snap get core system.power-key-action + exit 1 + fi diff -Nru snapd-2.40/tests/nightly/classic-ubuntu-core-transition-auth/task.yaml snapd-2.42.1/tests/nightly/classic-ubuntu-core-transition-auth/task.yaml --- snapd-2.40/tests/nightly/classic-ubuntu-core-transition-auth/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/nightly/classic-ubuntu-core-transition-auth/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,76 @@ +summary: Ensure that the ubuntu-core -> core transition works with auth.json + +# we never test on core because the transition can only happen on "classic" +# we disable on ppc64el because the downloads are very slow there +# Fedora, openSUSE and Arch are disabled at the moment as there is something +# fishy going on and the snapd service gets terminated during the process. +systems: [-ubuntu-core-*, -ubuntu-*-ppc64el, -fedora-*, -opensuse-*, -arch-*] + +# autopkgtest run only a subset of tests that deals with the integration +# with the distro +backends: [-autopkgtest] + +warn-timeout: 1m + +kill-timeout: 5m + +debug: | + snap changes + #shellcheck source=tests/lib/changes.sh + . "$TESTSLIB/changes.sh" + snap change "$(change_id 'Transition ubuntu-core to core')" || true + +execute: | + #shellcheck source=tests/lib/pkgdb.sh + . "$TESTSLIB/pkgdb.sh" + echo "Ensure core is gone and we have ubuntu-core instead" + distro_purge_package snapd + distro_install_build_snapd + + # need to be seeded to allow snap install + snap wait system seed.loaded + + snap download "--${CORE_CHANNEL}" ubuntu-core + snap ack ./ubuntu-core_*.assert + snap install ./ubuntu-core_*.snap + + mkdir -p /root/.snap/ + echo '{}' > /root/.snap/auth.json + mkdir -p /home/test/.snap/ + echo '{}' > /home/test/.snap/auth.json + + echo "Ensure transition is triggered" + # wait for steady state or ensure-state-soon will be pointless + ok=0 + for i in $(seq 40); do + if ! snap changes|grep -q ".*.Doing.*" ; then + ok=1 + break + fi + sleep .5 + done + if [ $ok -ne 1 ] ; then + echo "Did not reach steady state" + exit 1 + fi + snap debug ensure-state-soon + + + # wait for transition + ok=0 + for i in $(seq 240); do + if snap changes|grep -q ".*Done.*Transition ubuntu-core to core" ; then + ok=1 + break + fi + sleep 1 + done + if [ $ok -ne 1 ] ; then + echo "Transition did not start or finish" + exit 1 + fi + + if snap list|grep ubuntu-core; then + echo "ubuntu-core still installed, transition failed" + exit 1 + fi diff -Nru snapd-2.40/tests/nightly/classic-ubuntu-core-transition-two-cores/task.yaml snapd-2.42.1/tests/nightly/classic-ubuntu-core-transition-two-cores/task.yaml --- snapd-2.40/tests/nightly/classic-ubuntu-core-transition-two-cores/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/nightly/classic-ubuntu-core-transition-two-cores/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,78 @@ +summary: Ensure that the ubuntu-core -> core transition works with two cores + +# we never test on core because the transition can only happen on "classic" +# we disable on ppc64el because the downloads are very slow there +systems: [-ubuntu-core-*, -ubuntu-*-ppc64el] + +# autopkgtest run only a subset of tests that deals with the integration +# with the distro +backends: [-autopkgtest] + +warn-timeout: 1m + +kill-timeout: 5m + +debug: | + snap changes + #shellcheck source=tests/lib/changes.sh + . "$TESTSLIB"/changes.sh + snap change "$(change_id 'Transition ubuntu-core to core')" || true + +execute: | + echo "install a snap" + snap install test-snapd-python-webserver + snap interfaces -i network | MATCH ":network.*test-snapd-python-webserver" + + #shellcheck source=tests/lib/names.sh + . "$TESTSLIB/names.sh" + cp /var/lib/snapd/state.json /var/lib/snapd/state.json.old + jq -r '.data.snaps["core"].type="xxx"' < /var/lib/snapd/state.json.old > /var/lib/snapd/state.json + + systemctl stop snapd.service snapd.socket + systemctl start snapd.service snapd.socket + + snap download "--${CORE_CHANNEL}" ubuntu-core + snap ack ./ubuntu-core_*.assert + snap install ./ubuntu-core_*.snap + + cp /var/lib/snapd/state.json /var/lib/snapd/state.json.old + jq -r '.data.snaps["core"].type="os"' < /var/lib/snapd/state.json.old > /var/lib/snapd/state.json + + snap list | MATCH "ubuntu-core " + snap list | MATCH "core " + + echo "Ensure transition is triggered" + # wait for steady state or ensure-state-soon will be pointless + ok=0 + for _ in $(seq 40); do + if ! snap changes|grep -q ".*.Doing.*" ; then + ok=1 + break + fi + sleep .5 + done + if [ $ok -ne 1 ] ; then + echo "Did not reach steady state" + exit 1 + fi + snap debug ensure-state-soon + + # wait for transition + ok=0 + for _ in $(seq 240); do + if snap changes|grep -q ".*Done.*Transition ubuntu-core to core" ; then + ok=1 + break + fi + sleep 1 + done + if [ $ok -ne 1 ] ; then + echo "Transition did not start or finish" + exit 1 + fi + + if ! snap list|MATCH -v ubuntu-core; then + echo "ubuntu-core still installed, transition failed" + exit 1 + fi + snap interfaces -i network | MATCH ":network.*test-snapd-python-webserver" diff -Nru snapd-2.40/tests/nightly/install-snaps/task.yaml snapd-2.42.1/tests/nightly/install-snaps/task.yaml --- snapd-2.40/tests/nightly/install-snaps/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/nightly/install-snaps/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,136 @@ +summary: Check install popular snaps + +details: | + This test is intended to install some popular snaps from + different channels. The idea is to detect any problem + installing that are currently published and some of them + with many revisions. + The execution of this test is made on the nightly build. + +environment: + # High Profile + SNAP/azurecli: azure-cli + SNAP/awscli: aws-cli + SNAP/heroku: heroku + SNAP/hiri: hiri + SNAP/kubectl: kubectl + SNAP/rocketchatserver: rocketchat-server + # Selected from recent insights posts + SNAP/corebird: corebird + SNAP/gitterdesktop: gitter-desktop + SNAP/helm: helm + SNAP/mattermostdesktop: mattermost-desktop + SNAP/mentaplexmediaserver: menta-plexmediaserver + SNAP/openspades: openspades + SNAP/pintown: pin-town + SNAP/postgresql10: postgresql10 + SNAP/storjshare: storjshare + SNAP/slackterm: slack-term + SNAP/vectr: vectr + SNAP/wekan: wekan + SNAP/wormhole: wormhole + # Featured snaps in Ubuntu Software + SNAP/anboxinstaller: anbox-installer + SNAP/lxd: lxd + # Top non canonical snaps + SNAP/atom: atom + SNAP/discord: discord + SNAP/docker: docker + SNAP/etcd: etcd + SNAP/geocoder: geocoder + SNAP/gimp: gimp + SNAP/huggle: huggle + SNAP/hugo: hugo + SNAP/ia: ia + SNAP/kurly: kurly + SNAP/micro: micro + SNAP/nikola: nikola + SNAP/parity: parity + SNAP/paritybitcoin: parity-bitcoin + SNAP/remmina: remmina + SNAP/skype: skype + SNAP/slack: slack + SNAP/spotify: spotify + SNAP/telegramsergiusens: telegram-sergiusens + SNAP/zeronet: zeronet + SNAP/zeronetjs: zeronet-js + # Top canonical snaps + SNAP/bare: bare + SNAP/bluez: bluez + SNAP/conjureup: conjure-up + SNAP/gedit: gedit + SNAP/go: go + SNAP/juju: juju + SNAP/neutron: neutron + SNAP/nova: nova + SNAP/snapcraft: snapcraft + SNAP/solc: solc + SNAP/vault: vault + +prepare: | + cp /etc/systemd/system/snapd.service.d/local.conf /etc/systemd/system/snapd.service.d/local.conf.bak + sed 's/SNAPD_CONFIGURE_HOOK_TIMEOUT=.*s/SNAPD_CONFIGURE_HOOK_TIMEOUT=180s/g' -i /etc/systemd/system/snapd.service.d/local.conf + systemctl daemon-reload + systemctl restart snapd.socket + + if [ ! -d '/snap' ]; then + #shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB/dirs.sh" + ln -s "$SNAP_MOUNT_DIR" /snap + fi + +restore: | + mv /etc/systemd/system/snapd.service.d/local.conf.bak /etc/systemd/system/snapd.service.d/local.conf + systemctl daemon-reload + systemctl restart snapd.socket + + if [ -L /snap ]; then + unlink /snap + fi + + if [ "$SNAP" = lxd ]; then + lxd-tool undo-lxd-mount-changes + fi + +execute: | + #shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB/snaps.sh" + + CHANNELS="stable candidate beta edge" + for CHANNEL in $CHANNELS; do + # shellcheck disable=SC2153 + if ! CHANNEL_INFO="$(snap info --unicode=never "$SNAP" | grep " $CHANNEL: ")"; then + echo "Snap $SNAP not found" + exit + fi + if echo "$CHANNEL_INFO" | MATCH "$CHANNEL:.*--"; then + continue + fi + + if echo "$CHANNEL_INFO" | MATCH "$CHANNEL:.*classic"; then + if is_classic_confinement_supported; then + snap install "$SNAP" "--$CHANNEL" --classic + else + echo "The snap $SNAP requires classic confinement which is not supported yet" + exit + fi + elif echo "$CHANNEL_INFO" | MATCH "$CHANNEL:.*jailmode"; then + snap install "$SNAP" "--$CHANNEL" --jailmode + elif echo "$CHANNEL_INFO" | MATCH "$CHANNEL:.*devmode"; then + snap install "$SNAP" "--$CHANNEL" --devmode + else + snap install "$SNAP" "--$CHANNEL" + fi + break + done + + echo "Check the snap is properly installed" + snap list | MATCH "$SNAP" + + echo "Check the snap is properly removed" + snap remove "$SNAP" + + if snap list | MATCH "$SNAP"; then + echo "Snap $SNAP not removed properly" + exit 1 + fi diff -Nru snapd-2.40/tests/nightly/interfaces-openvswitch/task.yaml snapd-2.42.1/tests/nightly/interfaces-openvswitch/task.yaml --- snapd-2.40/tests/nightly/interfaces-openvswitch/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/nightly/interfaces-openvswitch/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,111 @@ +summary: Ensure that the openvswitch interface works. + +# Openvswitch getting stuck during installation sporadically on ubuntu-14.04-64 +# Openvswitch service not available on the following systems +systems: [-ubuntu-core-*, -opensuse-*, -amazon-*, -arch-linux-*, -ubuntu-14.04-64] + +details: | + The openvswitch interface allows to task to the openvswitch socket (rw mode). + + A snap which defines a openvswitch plug must be shown in the interfaces list. + The plug must not be autoconnected on install and, as usual, must be able to be + reconnected. + + A snap declaring a plug on this interface must be able to do all the operations that + are carried through the socket, in this test we exercise bridge and port creation, + list and deletion. + +prepare: | + #shellcheck source=tests/lib/pkgdb.sh + . "$TESTSLIB/pkgdb.sh" + + echo "Given openvswitch is installed" + distro_install_package --no-install-recommends openvswitch-switch + + # Ensure the openvswitch service is started which isn't the case by + # default on all distributions + if systemctl status openvswitch-switch.service | MATCH "Loaded:.*loaded"; then + systemctl enable --now openvswitch-switch.service + elif systemctl status openvswitch.service | MATCH "Loaded:.*loaded"; then + systemctl enable --now openvswitch.service + fi + + echo "And a snap declaring a plug on the openvswitch interface is installed" + snap install --edge test-snapd-openvswitch-consumer + + echo "And a tap interface is defined" + ip tuntap add tap1 mode tap + +restore: | + #shellcheck source=tests/lib/pkgdb.sh + . "$TESTSLIB/pkgdb.sh" + + ovs-vsctl del-port br0 tap1 || true + ovs-vsctl del-br br0 || true + + distro_purge_package openvswitch-switch + distro_auto_remove_packages + + ip link delete tap1 || true + +execute: | + echo "The interface is disconnected by default" + snap interfaces -i openvswitch | MATCH -- '^- +test-snapd-openvswitch-consumer:openvswitch' + + echo "When the plug is connected" + snap connect test-snapd-openvswitch-consumer:openvswitch + + echo "Then the snap is able to create a bridge" + test-snapd-openvswitch-consumer.ovs-vsctl add-br br0 + ovs-vsctl list-br | MATCH br0 + + echo "And the snap is able to create a port" + test-snapd-openvswitch-consumer.ovs-vsctl add-port br0 tap1 + ovs-vsctl list-ports br0 | MATCH tap1 + + echo "And the snap is able to delete a port" + test-snapd-openvswitch-consumer.ovs-vsctl del-port br0 tap1 + ovs-vsctl list-ports br0 | MATCH -v tap1 + + echo "And the snap is able to delete a bridge" + test-snapd-openvswitch-consumer.ovs-vsctl del-br br0 + ovs-vsctl list-br | MATCH -v br0 + + if [ "$(snap debug confinement)" = partial ] ; then + exit 0 + fi + + echo "When the plug is disconnected" + snap disconnect test-snapd-openvswitch-consumer:openvswitch + + echo "Then the snap is not able to create a bridge" + if test-snapd-openvswitch-consumer.ovs-vsctl add-br br0 2> bridge-creation.error; then + echo "Expected permission error accessing openvswitch socket with disconnected plug" + exit 1 + fi + MATCH 'database connection failed \(Permission denied\)' < bridge-creation.error + + ovs-vsctl add-br br0 + + echo "And the snap is not able to create a port" + if test-snapd-openvswitch-consumer.ovs-vsctl add-port br0 tap1 2> port-creation.error; then + echo "Expected permission error accessing openvswitch socket with disconnected plug" + exit 1 + fi + MATCH 'database connection failed \(Permission denied\)' < port-creation.error + + ovs-vsctl add-port br0 tap1 + + echo "And the snap is not able to delete a port" + if test-snapd-openvswitch-consumer.ovs-vsctl del-port br0 tap1 2> port-deletion.error; then + echo "Expected permission error accessing openvswitch socket with disconnected plug" + exit 1 + fi + MATCH 'database connection failed \(Permission denied\)' < port-deletion.error + + echo "And the snap is not able to delete a bridge" + if test-snapd-openvswitch-consumer.ovs-vsctl del-br br0 2> br-creation.error; then + echo "Expected permission error accessing openvswitch socket with disconnected plug" + exit 1 + fi + MATCH 'database connection failed \(Permission denied\)' < br-creation.error diff -Nru snapd-2.40/tests/nightly/sbuild/task.yaml snapd-2.42.1/tests/nightly/sbuild/task.yaml --- snapd-2.40/tests/nightly/sbuild/task.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/tests/nightly/sbuild/task.yaml 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,44 @@ +summary: Ensure snapd builds correctly in sbuild + +# takes a while +priority: 500 + +environment: + BUILD_MODE/normal: normal + BUILD_MODE/any: any + +systems: [debian-sid-*] + +execute: | + echo "Create a sid sbuild env" + eatmydata sbuild-createchroot --include=eatmydata,ccache,gnupg sid /srv/chroot/sid-amd64-sbuild http://deb.debian.org/debian + + echo "Allow test user to run sbuild" + sbuild-adduser test + + BUILD_PARAM="" + if [ "$BUILD_MODE" == "any" ]; then + BUILD_PARAM="--arch-any" + fi + + echo "Build mode: $BUILD_MODE" + su -c "sbuild $BUILD_PARAM -d sid --run-autopkgtest $SPREAD_PATH/../*.dsc" test + +restore: | + rm --recursive --one-file-system /srv/chroot/sid-amd64-sbuild + rm -f /etc/schroot/chroot.d/sid-amd64-sbuild-* + +debug: | + # Test that there's a log file and a symbolic link pointing to it. + # The non-symlink has a time-stamp and we can match on the "Z" timezone + # marker to find it. + test "$(find . -maxdepth 1 -name '*Z.build' | wc -l)" -ge 1 && tail -n 100 ./*Z.build + cat < -// patterns. '%%' expands to '%', all other patterns expand to empty strings. -func expandDesktopFields(in string) string { - raw := []rune(in) - out := make([]rune, 0, len(raw)) - - var hasKey bool - for _, r := range raw { - if hasKey { - hasKey = false - // only allow %% -> % expansion, drop other keys - if r == '%' { - out = append(out, r) - } - continue - } else if r == '%' { - hasKey = true - continue - } - out = append(out, r) - } - return string(out) -} - -type skipDesktopFileError struct { - reason string -} - -func (s *skipDesktopFileError) Error() string { - return s.reason -} - -func isOneOfIn(of []string, other []string) bool { - for _, one := range of { - if strutil.ListContains(other, one) { - return true - } - } - return false -} - -func loadAutostartDesktopFile(path string) (command string, err error) { - f, err := os.Open(path) - if err != nil { - return "", err - } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - bline := scanner.Bytes() - if bytes.HasPrefix(bline, []byte("#")) { - continue - } - split := bytes.SplitN(bline, []byte("="), 2) - if len(split) != 2 { - continue - } - // See https://standards.freedesktop.org/autostart-spec/autostart-spec-latest.html - // for details on how Hidden, OnlyShowIn, NotShownIn are handled. - switch string(split[0]) { - case "Exec": - command = strings.TrimSpace(expandDesktopFields(string(split[1]))) - case "Hidden": - if bytes.Equal(split[1], []byte("true")) { - return "", &skipDesktopFileError{"desktop file is hidden"} - } - case "OnlyShowIn": - onlyIn := splitSkippingEmpty(string(split[1]), ';') - if !isOneOfIn(currentDesktop, onlyIn) { - return "", &skipDesktopFileError{fmt.Sprintf("current desktop %q not included in %q", currentDesktop, onlyIn)} - } - case "NotShownIn": - notIn := splitSkippingEmpty(string(split[1]), ';') - if isOneOfIn(currentDesktop, notIn) { - return "", &skipDesktopFileError{fmt.Sprintf("current desktop %q excluded by %q", currentDesktop, notIn)} - } - case "X-GNOME-Autostart-enabled": - // GNOME specific extension, see gnome-session: - // https://github.com/GNOME/gnome-session/blob/c449df5269e02c59ae83021a3110ec1b338a2bba/gnome-session/gsm-autostart-app.c#L110..L145 - if !strutil.ListContains(currentDesktop, "GNOME") { - // not GNOME - continue - } - if !bytes.Equal(split[1], []byte("true")) { - return "", &skipDesktopFileError{"desktop file is hidden by X-GNOME-Autostart-enabled extension"} - } - } - } - if err := scanner.Err(); err != nil { - return "", err - } - - command = strings.TrimSpace(command) - if command == "" { - return "", fmt.Errorf("Exec not found or invalid") - } - return command, nil - -} - -func autostartCmd(snapName, desktopFilePath string) (*exec.Cmd, error) { - desktopFile := filepath.Base(desktopFilePath) - - info, err := snap.ReadCurrentInfo(snapName) - if err != nil { - return nil, err - } - - var app *snap.AppInfo - for _, candidate := range info.Apps { - if candidate.Autostart == desktopFile { - app = candidate - break - } - } - if app == nil { - return nil, fmt.Errorf("cannot match desktop file with snap %s applications", snapName) - } - - command, err := loadAutostartDesktopFile(desktopFilePath) - if err != nil { - if _, ok := err.(*skipDesktopFileError); ok { - return nil, fmt.Errorf("skipped: %v", err) - } - return nil, fmt.Errorf("cannot determine startup command for application %s in snap %s: %v", app.Name, snapName, err) - } - logger.Debugf("exec line: %v", command) - - split, err := shlex.Split(command) - if err != nil { - return nil, fmt.Errorf("invalid application startup command: %v", err) - } - - // NOTE: Ignore the actual argv[0] in Exec=.. line and replace it with a - // command of the snap application. Any arguments passed in the Exec=.. - // line to the original command are preserved. - cmd := exec.Command(app.WrapperPath(), split[1:]...) - return cmd, nil -} - -// failedAutostartError keeps track of errors that occurred when starting an -// application for a specific desktop file, desktop file name is as a key -type failedAutostartError map[string]error - -func (f failedAutostartError) Error() string { - var out bytes.Buffer - - dfiles := make([]string, 0, len(f)) - for desktopFile := range f { - dfiles = append(dfiles, desktopFile) - } - sort.Strings(dfiles) - for _, desktopFile := range dfiles { - fmt.Fprintf(&out, "- %q: %v\n", desktopFile, f[desktopFile]) - } - return out.String() -} - -func makeStdStreams(identifier string) (stdout *os.File, stderr *os.File) { - var err error - - stdout, err = systemd.NewJournalStreamFile(identifier, syslog.LOG_INFO, false) - if err != nil { - logger.Noticef("failed to set up stdout journal stream for %q: %v", identifier, err) - stdout = os.Stdout - } - - stderr, err = systemd.NewJournalStreamFile(identifier, syslog.LOG_WARNING, false) - if err != nil { - logger.Noticef("failed to set up stderr journal stream for %q: %v", identifier, err) - stderr = os.Stderr - } - - return stdout, stderr -} - -var userCurrent = user.Current - -// AutostartSessionApps starts applications which have placed their desktop -// files in $SNAP_USER_DATA/.config/autostart -// -// NOTE: By the spec, the actual path is $SNAP_USER_DATA/${XDG_CONFIG_DIR}/autostart -func AutostartSessionApps() error { - usr, err := userCurrent() - if err != nil { - return err - } - - usrSnapDir := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) - - glob := filepath.Join(usrSnapDir, "*/current/.config/autostart/*.desktop") - matches, err := filepath.Glob(glob) - if err != nil { - return err - } - - failedApps := make(failedAutostartError) - for _, desktopFilePath := range matches { - desktopFile := filepath.Base(desktopFilePath) - logger.Debugf("autostart desktop file %v", desktopFile) - - // /home/foo/snap/some-snap/current/.config/autostart/some-app.desktop -> - // some-snap/current/.config/autostart/some-app.desktop - noHomePrefix := strings.TrimPrefix(desktopFilePath, usrSnapDir+"/") - // some-snap/current/.config/autostart/some-app.desktop -> some-snap - snapName := noHomePrefix[0:strings.IndexByte(noHomePrefix, '/')] - - logger.Debugf("snap name: %q", snapName) - - cmd, err := autostartCmd(snapName, desktopFilePath) - if err != nil { - failedApps[desktopFile] = err - continue - } - - // similarly to gnome-session, use the desktop file name as - // identifier, see: - // https://github.com/GNOME/gnome-session/blob/099c19099de8e351f6cc0f2110ad27648780a0fe/gnome-session/gsm-autostart-app.c#L948 - cmd.Stdout, cmd.Stderr = makeStdStreams(desktopFile) - if err := cmd.Start(); err != nil { - failedApps[desktopFile] = fmt.Errorf("cannot autostart %q: %v", desktopFile, err) - } - } - if len(failedApps) > 0 { - return failedApps - } - return nil -} diff -Nru snapd-2.40/userd/autostart_test.go snapd-2.42.1/userd/autostart_test.go --- snapd-2.40/userd/autostart_test.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/autostart_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,325 +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 userd_test - -import ( - "io/ioutil" - "os" - "os/user" - "path" - "path/filepath" - "strings" - - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/snap" - "github.com/snapcore/snapd/snap/snaptest" - "github.com/snapcore/snapd/testutil" - "github.com/snapcore/snapd/userd" -) - -type autostartSuite struct { - dir string - autostartDir string - userDir string - userCurrentRestore func() -} - -var _ = Suite(&autostartSuite{}) - -func (s *autostartSuite) SetUpTest(c *C) { - s.dir = c.MkDir() - dirs.SetRootDir(s.dir) - snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) - - s.userDir = path.Join(s.dir, "home") - s.autostartDir = path.Join(s.userDir, ".config", "autostart") - s.userCurrentRestore = userd.MockUserCurrent(func() (*user.User, error) { - return &user.User{HomeDir: s.userDir}, nil - }) - - err := os.MkdirAll(s.autostartDir, 0755) - c.Assert(err, IsNil) -} - -func (s *autostartSuite) TearDownTest(c *C) { - s.dir = c.MkDir() - dirs.SetRootDir("/") - if s.userCurrentRestore != nil { - s.userCurrentRestore() - } -} - -func (s *autostartSuite) TestLoadAutostartDesktopFile(c *C) { - allGood := `[Desktop Entry] -Exec=foo --bar -` - allGoodWithFlags := `[Desktop Entry] -Exec=foo --bar "%%p" %U %D +%s %% -` - noExec := `[Desktop Entry] -Type=Application -` - emptyExec := `[Desktop Entry] -Exec= -` - onlySpacesExec := `[Desktop Entry] -Exec= -` - hidden := `[Desktop Entry] -Exec=foo --bar -Hidden=true -` - hiddenFalse := `[Desktop Entry] -Exec=foo --bar -Hidden=false -` - justGNOME := `[Desktop Entry] -Exec=foo --bar -OnlyShowIn=GNOME; -` - notInGNOME := `[Desktop Entry] -Exec=foo --bar -NotShownIn=GNOME; -` - notInGNOMEAndKDE := `[Desktop Entry] -Exec=foo --bar -NotShownIn=GNOME;KDE; -` - hiddenGNOMEextension := `[Desktop Entry] -Exec=foo --bar -X-GNOME-Autostart-enabled=false -` - GNOMEextension := `[Desktop Entry] -Exec=foo --bar -X-GNOME-Autostart-enabled=true -` - - for i, tc := range []struct { - in string - out string - err string - current string - }{{ - in: allGood, - out: "foo --bar", - }, { - in: noExec, - err: "Exec not found or invalid", - }, { - in: emptyExec, - err: "Exec not found or invalid", - }, { - in: onlySpacesExec, - err: "Exec not found or invalid", - }, { - in: allGoodWithFlags, - out: `foo --bar "%p" + %`, - }, { - in: hidden, - err: `desktop file is hidden`, - }, { - in: hiddenFalse, - out: `foo --bar`, - }, { - in: justGNOME, - out: "foo --bar", - current: "GNOME", - }, { - in: justGNOME, - current: "KDE", - err: `current desktop \["KDE"\] not included in \["GNOME"\]`, - }, { - in: notInGNOME, - current: "GNOME", - err: `current desktop \["GNOME"\] excluded by \["GNOME"\]`, - }, { - in: notInGNOME, - current: "KDE", - out: "foo --bar", - }, { - in: notInGNOMEAndKDE, - current: "XFCE", - out: "foo --bar", - }, { - in: hiddenGNOMEextension, - current: "KDE", - out: "foo --bar", - }, { - in: hiddenGNOMEextension, - current: "GNOME", - err: `desktop file is hidden by X-GNOME-Autostart-enabled extension`, - }, { - in: GNOMEextension, - current: "GNOME", - out: "foo --bar", - }, { - in: GNOMEextension, - current: "KDE", - out: "foo --bar", - }} { - c.Logf("tc %d", i) - - path := filepath.Join(c.MkDir(), "foo.desktop") - err := ioutil.WriteFile(path, []byte(tc.in), 0644) - c.Assert(err, IsNil) - - run := func() { - defer userd.MockCurrentDesktop(tc.current)() - - cmd, err := userd.LoadAutostartDesktopFile(path) - if tc.err != "" { - c.Check(cmd, Equals, "") - c.Check(err, ErrorMatches, tc.err) - } else { - c.Check(err, IsNil) - c.Check(cmd, Equals, tc.out) - } - } - run() - } -} - -var mockYaml = `name: snapname -version: 1.0 -apps: - foo: - command: run-app - autostart: foo-stable.desktop -` - -func (s *autostartSuite) TestTryAutostartAppValid(c *C) { - si := snaptest.MockSnapCurrent(c, mockYaml, &snap.SideInfo{Revision: snap.R("x2")}) - - appWrapperPath := si.Apps["foo"].WrapperPath() - err := os.MkdirAll(filepath.Dir(appWrapperPath), 0755) - c.Assert(err, IsNil) - - appCmd := testutil.MockCommand(c, appWrapperPath, "") - defer appCmd.Restore() - - fooDesktopFile := filepath.Join(s.autostartDir, "foo-stable.desktop") - writeFile(c, fooDesktopFile, - []byte(`[Desktop Entry] -Exec=this-is-ignored -a -b --foo="a b c" -z "dev" -`)) - - cmd, err := userd.AutostartCmd("snapname", fooDesktopFile) - c.Assert(err, IsNil) - c.Assert(cmd.Path, Equals, appWrapperPath) - - err = cmd.Start() - c.Assert(err, IsNil) - cmd.Wait() - - c.Assert(appCmd.Calls(), DeepEquals, - [][]string{ - { - filepath.Base(appWrapperPath), - "-a", - "-b", - "--foo=a b c", - "-z", - "dev", - }, - }) -} - -func (s *autostartSuite) TestTryAutostartAppNoMatchingApp(c *C) { - snaptest.MockSnapCurrent(c, mockYaml, &snap.SideInfo{Revision: snap.R("x2")}) - - fooDesktopFile := filepath.Join(s.autostartDir, "foo-no-match.desktop") - writeFile(c, fooDesktopFile, - []byte(`[Desktop Entry] -Exec=this-is-ignored -a -b --foo="a b c" -z "dev" -`)) - - cmd, err := userd.AutostartCmd("snapname", fooDesktopFile) - c.Assert(cmd, IsNil) - c.Assert(err, ErrorMatches, `cannot match desktop file with snap snapname applications`) -} - -func (s *autostartSuite) TestTryAutostartAppNoSnap(c *C) { - fooDesktopFile := filepath.Join(s.autostartDir, "foo-stable.desktop") - writeFile(c, fooDesktopFile, - []byte(`[Desktop Entry] -Exec=this-is-ignored -a -b --foo="a b c" -z "dev" -`)) - - cmd, err := userd.AutostartCmd("snapname", fooDesktopFile) - c.Assert(cmd, IsNil) - c.Assert(err, ErrorMatches, `cannot find current revision for snap snapname.*`) -} - -func (s *autostartSuite) TestTryAutostartAppBadExec(c *C) { - snaptest.MockSnapCurrent(c, mockYaml, &snap.SideInfo{Revision: snap.R("x2")}) - - fooDesktopFile := filepath.Join(s.autostartDir, "foo-stable.desktop") - writeFile(c, fooDesktopFile, - []byte(`[Desktop Entry] -Foo=bar -`)) - - cmd, err := userd.AutostartCmd("snapname", fooDesktopFile) - c.Assert(cmd, IsNil) - c.Assert(err, ErrorMatches, `cannot determine startup command for application foo in snap snapname: Exec not found or invalid`) -} - -func writeFile(c *C, path string, content []byte) { - err := os.MkdirAll(filepath.Dir(path), 0755) - c.Assert(err, IsNil) - err = ioutil.WriteFile(path, content, 0644) - c.Assert(err, IsNil) -} - -func (s *autostartSuite) TestTryAutostartMany(c *C) { - var mockYamlTemplate = `name: {snap} -version: 1.0 -apps: - foo: - command: run-app - autostart: foo-stable.desktop -` - - snaptest.MockSnapCurrent(c, strings.Replace(mockYamlTemplate, "{snap}", "a-foo", -1), - &snap.SideInfo{Revision: snap.R("x2")}) - snaptest.MockSnapCurrent(c, strings.Replace(mockYamlTemplate, "{snap}", "b-foo", -1), - &snap.SideInfo{Revision: snap.R("x2")}) - writeFile(c, filepath.Join(s.userDir, "snap/a-foo/current/.config/autostart/foo-stable.desktop"), - []byte(`[Desktop Entry] -Foo=bar -`)) - writeFile(c, filepath.Join(s.userDir, "snap/b-foo/current/.config/autostart/no-match.desktop"), - []byte(`[Desktop Entry] -Exec=no-snap -`)) - writeFile(c, filepath.Join(s.userDir, "snap/c-foo/current/.config/autostart/no-snap.desktop"), - []byte(`[Desktop Entry] -Exec=no-snap -`)) - - err := userd.AutostartSessionApps() - c.Assert(err, NotNil) - c.Check(err, ErrorMatches, `- "foo-stable.desktop": cannot determine startup command for application foo in snap a-foo: Exec not found or invalid -- "no-match.desktop": cannot match desktop file with snap b-foo applications -- "no-snap.desktop": cannot find current revision for snap c-foo: readlink.*no such file or directory -`) -} diff -Nru snapd-2.40/userd/export_test.go snapd-2.42.1/userd/export_test.go --- snapd-2.40/userd/export_test.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/export_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,57 +0,0 @@ -// -*- 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 userd - -import ( - "os/user" - - "github.com/godbus/dbus" -) - -var ( - SnapFromPid = snapFromPid - - LoadAutostartDesktopFile = loadAutostartDesktopFile - AutostartCmd = autostartCmd -) - -func MockSnapFromSender(f func(*dbus.Conn, dbus.Sender) (string, error)) func() { - origSnapFromSender := snapFromSender - snapFromSender = f - return func() { - snapFromSender = origSnapFromSender - } -} - -func MockUserCurrent(f func() (*user.User, error)) func() { - origUserCurrent := userCurrent - userCurrent = f - return func() { - userCurrent = origUserCurrent - } -} - -func MockCurrentDesktop(current string) func() { - old := currentDesktop - currentDesktop = splitSkippingEmpty(current, ':') - return func() { - currentDesktop = old - } -} diff -Nru snapd-2.40/userd/helpers.go snapd-2.42.1/userd/helpers.go --- snapd-2.40/userd/helpers.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/helpers.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,109 +0,0 @@ -// -*- 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 userd - -import ( - "bufio" - "fmt" - "os" - "path/filepath" - "strings" - - "github.com/godbus/dbus" - - "github.com/snapcore/snapd/dirs" -) - -var snapFromSender = snapFromSenderImpl - -func snapFromSenderImpl(conn *dbus.Conn, sender dbus.Sender) (string, error) { - pid, err := connectionPid(conn, sender) - if err != nil { - return "", fmt.Errorf("cannot get connection pid: %v", err) - } - snap, err := snapFromPid(pid) - if err != nil { - return "", fmt.Errorf("cannot find snap for connection: %v", err) - } - // Check that the sender is still connected to the bus: if it - // has disconnected between the GetConnectionUnixProcessID - // call and when we poked around in /proc, then it is possible - // that the process ID was reused. - if !nameHasOwner(conn, sender) { - return "", fmt.Errorf("sender is no longer connected to the bus") - } - return snap, nil -} - -func connectionPid(conn *dbus.Conn, sender dbus.Sender) (pid int, err error) { - call := conn.BusObject().Call("org.freedesktop.DBus.GetConnectionUnixProcessID", 0, sender) - if call.Err != nil { - return 0, call.Err - } - call.Store(&pid) - return pid, nil -} - -func nameHasOwner(conn *dbus.Conn, sender dbus.Sender) bool { - call := conn.BusObject().Call("org.freedesktop.DBus.NameHasOwner", 0, sender) - if call.Err != nil { - return false - } - var hasOwner bool - call.Store(&hasOwner) - return hasOwner -} - -// FIXME: move to osutil? -func snapFromPid(pid int) (string, error) { - f, err := os.Open(fmt.Sprintf("%s/proc/%d/cgroup", dirs.GlobalRootDir, pid)) - if err != nil { - return "", err - } - defer f.Close() - - scanner := bufio.NewScanner(f) - for scanner.Scan() { - // we need to find a string like: - // ... - // 7:freezer:/snap.hello-world - // ... - // See cgroup(7) for details about the /proc/[pid]/cgroup - // format. - l := strings.Split(scanner.Text(), ":") - if len(l) < 3 { - continue - } - controllerList := l[1] - cgroupPath := l[2] - if !strings.Contains(controllerList, "freezer") { - continue - } - if strings.HasPrefix(cgroupPath, "/snap.") { - snap := strings.SplitN(filepath.Base(cgroupPath), ".", 2)[1] - return snap, nil - } - } - if scanner.Err() != nil { - return "", scanner.Err() - } - - return "", fmt.Errorf("cannot find a snap for pid %v", pid) -} diff -Nru snapd-2.40/userd/helpers_test.go snapd-2.42.1/userd/helpers_test.go --- snapd-2.40/userd/helpers_test.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/helpers_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,63 +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 userd_test - -import ( - "io/ioutil" - "os" - "path/filepath" - - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/userd" -) - -type helpersSuite struct{} - -var _ = Suite(&helpersSuite{}) - -var mockCgroup = []byte(` -10:devices:/user.slice -9:cpuset:/ -8:net_cls,net_prio:/ -7:freezer:/snap.hello-world -6:perf_event:/ -5:pids:/user.slice/user-1000.slice/user@1000.service -4:cpu,cpuacct:/ -3:memory:/ -2:blkio:/ -1:name=systemd:/user.slice/user-1000.slice/user@1000.service/gnome-terminal-server.service -0::/user.slice/user-1000.slice/user@1000.service/gnome-terminal-server.service -`) - -func (s *helpersSuite) TestSnapFromPid(c *C) { - root := c.MkDir() - dirs.SetRootDir(root) - - err := os.MkdirAll(filepath.Join(root, "proc/333"), 0755) - c.Assert(err, IsNil) - err = ioutil.WriteFile(filepath.Join(root, "proc/333/cgroup"), mockCgroup, 0755) - c.Assert(err, IsNil) - - snap, err := userd.SnapFromPid(333) - c.Assert(err, IsNil) - c.Check(snap, Equals, "hello-world") -} diff -Nru snapd-2.40/userd/launcher.go snapd-2.42.1/userd/launcher.go --- snapd-2.40/userd/launcher.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/launcher.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,206 +0,0 @@ -// -*- 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 userd - -import ( - "fmt" - "net/url" - "os" - "os/exec" - "path/filepath" - "strings" - "syscall" - "time" - - "github.com/godbus/dbus" - - "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/i18n" - "github.com/snapcore/snapd/osutil/sys" - "github.com/snapcore/snapd/strutil" - "github.com/snapcore/snapd/userd/ui" -) - -const launcherIntrospectionXML = ` - - - - - - - - - - - - - - - -` - -var ( - allowedURLSchemes = []string{"http", "https", "mailto", "snap", "help"} -) - -// Launcher implements the 'io.snapcraft.Launcher' DBus interface. -type Launcher struct { - conn *dbus.Conn -} - -// Name returns the name of the interface this object implements -func (s *Launcher) Name() string { - return "io.snapcraft.Launcher" -} - -// BasePath returns the base path of the object -func (s *Launcher) BasePath() dbus.ObjectPath { - return "/io/snapcraft/Launcher" -} - -// IntrospectionData gives the XML formatted introspection description -// of the DBus service. -func (s *Launcher) IntrospectionData() string { - return launcherIntrospectionXML -} - -func makeAccessDeniedError(err error) *dbus.Error { - return &dbus.Error{ - Name: "org.freedesktop.DBus.Error.AccessDenied", - Body: []interface{}{err.Error()}, - } -} - -// OpenURL implements the 'OpenURL' method of the 'io.snapcraft.Launcher' -// DBus interface. Before the provided url is passed to xdg-open the scheme is -// validated against a list of allowed schemes. All other schemes are denied. -func (s *Launcher) OpenURL(addr string, sender dbus.Sender) *dbus.Error { - u, err := url.Parse(addr) - if err != nil { - return &dbus.ErrMsgInvalidArg - } - - if !strutil.ListContains(allowedURLSchemes, u.Scheme) { - return makeAccessDeniedError(fmt.Errorf("Supplied URL scheme %q is not allowed", u.Scheme)) - } - - snap, err := snapFromSender(s.conn, sender) - if err != nil { - return dbus.MakeFailedError(err) - } - - xdg_data_dirs := []string{} - xdg_data_dirs = append(xdg_data_dirs, fmt.Sprintf(filepath.Join(dirs.SnapMountDir, snap, "current/usr/share"))) - for _, dir := range strings.Split(os.Getenv("XDG_DATA_DIRS"), ":") { - xdg_data_dirs = append(xdg_data_dirs, dir) - } - - cmd := exec.Command("xdg-open", addr) - cmd.Env = os.Environ() - cmd.Env = append(cmd.Env, fmt.Sprintf("XDG_DATA_DIRS=%s", strings.Join(xdg_data_dirs, ":"))) - - if err := cmd.Run(); err != nil { - return dbus.MakeFailedError(fmt.Errorf("cannot open supplied URL")) - } - - return nil -} - -// fdToFilename determines the path associated with an open file descriptor. -// -// The file descriptor cannot be opened using O_PATH and must refer to -// a regular file or to a directory. The symlink at /proc/self/fd/ -// is read to determine the filename. The descriptor is also fstat'ed -// and the resulting device number and inode number are compared to -// stat on the path determined earlier. The numbers must match. -func fdToFilename(fd int) (string, error) { - flags, err := sys.FcntlGetFl(fd) - if err != nil { - return "", err - } - // File descriptors opened with O_PATH do not imply access to - // the file in question. - if flags&sys.O_PATH != 0 { - return "", fmt.Errorf("cannot use file descriptors opened using O_PATH") - } - - // Determine the file name associated with the passed file descriptor. - filename, err := os.Readlink(fmt.Sprintf("/proc/self/fd/%d", fd)) - if err != nil { - return "", err - } - - var fileStat, fdStat syscall.Stat_t - if err := syscall.Stat(filename, &fileStat); err != nil { - return "", err - } - if err := syscall.Fstat(fd, &fdStat); err != nil { - return "", err - } - - // Sanity check to ensure we've got the right file - if fdStat.Dev != fileStat.Dev || fdStat.Ino != fileStat.Ino { - return "", fmt.Errorf("cannot determine file name") - } - - fileType := fileStat.Mode & syscall.S_IFMT - if fileType != syscall.S_IFREG && fileType != syscall.S_IFDIR { - return "", fmt.Errorf("cannot open anything other than regular files or directories") - } - - return filename, nil -} - -func (s *Launcher) OpenFile(parentWindow string, clientFd dbus.UnixFD, sender dbus.Sender) *dbus.Error { - // godbus transfers ownership of this file descriptor to us - fd := int(clientFd) - defer syscall.Close(fd) - - filename, err := fdToFilename(fd) - if err != nil { - return dbus.MakeFailedError(err) - } - - snap, err := snapFromSender(s.conn, sender) - if err != nil { - return dbus.MakeFailedError(err) - } - dialog, err := ui.New() - if err != nil { - return dbus.MakeFailedError(err) - } - answeredYes := dialog.YesNo( - i18n.G("Allow opening file?"), - fmt.Sprintf(i18n.G("Allow snap %q to open file %q?"), snap, filename), - &ui.DialogOptions{ - Timeout: 5 * 60 * time.Second, - Footer: i18n.G("This dialog will close automatically after 5 minutes of inactivity."), - }, - ) - if !answeredYes { - return dbus.MakeFailedError(fmt.Errorf("permission denied")) - } - - if err = exec.Command("xdg-open", filename).Run(); err != nil { - return dbus.MakeFailedError(fmt.Errorf("cannot open supplied URL")) - } - - return nil -} diff -Nru snapd-2.40/userd/launcher_test.go snapd-2.42.1/userd/launcher_test.go --- snapd-2.40/userd/launcher_test.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/launcher_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,199 +0,0 @@ -// -*- 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 userd_test - -import ( - "io/ioutil" - "os" - "path/filepath" - "syscall" - "testing" - - "github.com/godbus/dbus" - - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/osutil/sys" - "github.com/snapcore/snapd/testutil" - "github.com/snapcore/snapd/userd" -) - -func Test(t *testing.T) { TestingT(t) } - -type launcherSuite struct { - launcher *userd.Launcher - - mockXdgOpen *testutil.MockCmd - restoreSnapFromSender func() -} - -var _ = Suite(&launcherSuite{}) - -func (s *launcherSuite) SetUpTest(c *C) { - s.launcher = &userd.Launcher{} - s.mockXdgOpen = testutil.MockCommand(c, "xdg-open", "") - s.restoreSnapFromSender = userd.MockSnapFromSender(func(*dbus.Conn, dbus.Sender) (string, error) { - return "some-snap", nil - }) -} - -func (s *launcherSuite) TearDownTest(c *C) { - s.mockXdgOpen.Restore() - s.restoreSnapFromSender() -} - -func (s *launcherSuite) TestOpenURLWithNotAllowedScheme(c *C) { - for _, t := range []struct { - url string - errMatcher string - }{ - {"tel://049112233445566", "Supplied URL scheme \"tel\" is not allowed"}, - {"aabbccdd0011", "Supplied URL scheme \"\" is not allowed"}, - {"invälid:%url", dbus.ErrMsgInvalidArg.Error()}, - } { - err := s.launcher.OpenURL(t.url, ":some-dbus-sender") - c.Assert(err, ErrorMatches, t.errMatcher) - c.Assert(s.mockXdgOpen.Calls(), IsNil) - } -} - -func (s *launcherSuite) TestOpenURLWithAllowedSchemeHappy(c *C) { - for _, schema := range []string{"http", "https", "mailto", "snap", "help"} { - err := s.launcher.OpenURL(schema+"://snapcraft.io", ":some-dbus-sender") - c.Assert(err, IsNil) - c.Assert(s.mockXdgOpen.Calls(), DeepEquals, [][]string{ - {"xdg-open", schema + "://snapcraft.io"}, - }) - s.mockXdgOpen.ForgetCalls() - } -} - -func (s *launcherSuite) TestOpenURLWithFailingXdgOpen(c *C) { - cmd := testutil.MockCommand(c, "xdg-open", "false") - defer cmd.Restore() - - err := s.launcher.OpenURL("https://snapcraft.io", ":some-dbus-sender") - c.Assert(err, NotNil) - c.Assert(err, ErrorMatches, "cannot open supplied URL") -} - -func mockUICommands(c *C, script string) (restore func()) { - mock := testutil.MockCommand(c, "zenity", script) - mock.Also("kdialog", script) - - return func() { - mock.Restore() - } -} - -func (s *launcherSuite) TestOpenFileUserAccepts(c *C) { - restore := mockUICommands(c, "true") - defer restore() - - path := filepath.Join(c.MkDir(), "test.txt") - c.Assert(ioutil.WriteFile(path, []byte("Hello world"), 0644), IsNil) - - file, err := os.Open(path) - c.Assert(err, IsNil) - defer file.Close() - - dupFd, err := syscall.Dup(int(file.Fd())) - c.Assert(err, IsNil) - - err = s.launcher.OpenFile("", dbus.UnixFD(dupFd), ":some-dbus-sender") - c.Assert(err, IsNil) - c.Assert(s.mockXdgOpen.Calls(), DeepEquals, [][]string{ - {"xdg-open", path}, - }) -} - -func (s *launcherSuite) TestOpenFileUserDeclines(c *C) { - restore := mockUICommands(c, "false") - defer restore() - - path := filepath.Join(c.MkDir(), "test.txt") - c.Assert(ioutil.WriteFile(path, []byte("Hello world"), 0644), IsNil) - - file, err := os.Open(path) - c.Assert(err, IsNil) - defer file.Close() - - dupFd, err := syscall.Dup(int(file.Fd())) - c.Assert(err, IsNil) - - err = s.launcher.OpenFile("", dbus.UnixFD(dupFd), ":some-dbus-sender") - c.Assert(err, NotNil) - c.Assert(err, ErrorMatches, "permission denied") - c.Assert(s.mockXdgOpen.Calls(), IsNil) -} - -func (s *launcherSuite) TestOpenFileSucceedsWithDirectory(c *C) { - restore := mockUICommands(c, "true") - defer restore() - - dir := c.MkDir() - fd, err := syscall.Open(dir, syscall.O_RDONLY|syscall.O_DIRECTORY, 0755) - c.Assert(err, IsNil) - defer syscall.Close(fd) - - dupFd, err := syscall.Dup(fd) - c.Assert(err, IsNil) - - err = s.launcher.OpenFile("", dbus.UnixFD(dupFd), ":some-dbus-sender") - c.Assert(err, IsNil) - c.Assert(s.mockXdgOpen.Calls(), DeepEquals, [][]string{ - {"xdg-open", dir}, - }) -} - -func (s *launcherSuite) TestOpenFileFailsWithDeviceFile(c *C) { - restore := mockUICommands(c, "true") - defer restore() - - file, err := os.Open("/dev/null") - c.Assert(err, IsNil) - defer file.Close() - - dupFd, err := syscall.Dup(int(file.Fd())) - c.Assert(err, IsNil) - - err = s.launcher.OpenFile("", dbus.UnixFD(dupFd), ":some-dbus-sender") - c.Assert(err, NotNil) - c.Assert(err, ErrorMatches, "cannot open anything other than regular files or directories") - c.Assert(s.mockXdgOpen.Calls(), IsNil) -} - -func (s *launcherSuite) TestOpenFileFailsWithPathDescriptor(c *C) { - restore := mockUICommands(c, "true") - defer restore() - - dir := c.MkDir() - fd, err := syscall.Open(dir, sys.O_PATH, 0755) - c.Assert(err, IsNil) - defer syscall.Close(fd) - - dupFd, err := syscall.Dup(fd) - c.Assert(err, IsNil) - - err = s.launcher.OpenFile("", dbus.UnixFD(dupFd), ":some-dbus-sender") - c.Assert(err, NotNil) - c.Assert(err, ErrorMatches, "cannot use file descriptors opened using O_PATH") - c.Assert(s.mockXdgOpen.Calls(), IsNil) -} diff -Nru snapd-2.40/userd/settings.go snapd-2.42.1/userd/settings.go --- snapd-2.40/userd/settings.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/settings.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,226 +0,0 @@ -// -*- 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 userd - -import ( - "fmt" - "os/exec" - "path/filepath" - "strings" - "time" - - "github.com/godbus/dbus" - "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/i18n" - "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/snap" - "github.com/snapcore/snapd/userd/ui" -) - -// Timeout when the confirmation dialog for an xdg-setging -// automatically closes. Keep in sync with the core snaps -// xdg-settings wrapper which also sets this value to 300. -var defaultConfirmDialogTimeout = 300 * time.Second - -const settingsIntrospectionXML = ` - - - - - - - - - - - - - - - - - - - - - -` - -// TODO: allow setting default-url-scheme-handler ? -var settingsWhitelist = []string{ - "default-web-browser", -} - -func allowedSetting(setting string) bool { - if !strings.HasSuffix(setting, ".desktop") { - return false - } - base := strings.TrimSuffix(setting, ".desktop") - - return snap.ValidAppName(base) -} - -func settingWhitelisted(setting string) *dbus.Error { - for _, whitelisted := range settingsWhitelist { - if setting == whitelisted { - return nil - } - } - return dbus.MakeFailedError(fmt.Errorf("cannot use setting %q: not allowed", setting)) -} - -// Settings implements the 'io.snapcraft.Settings' DBus interface. -type Settings struct { - conn *dbus.Conn -} - -// Name returns the name of the interface this object implements -func (s *Settings) Name() string { - return "io.snapcraft.Settings" -} - -// BasePath returns the base path of the object -func (s *Settings) BasePath() dbus.ObjectPath { - return "/io/snapcraft/Settings" -} - -// IntrospectionData gives the XML formatted introspection description -// of the DBus service. -func (s *Settings) IntrospectionData() string { - return settingsIntrospectionXML -} - -// some notes: -// - we only set/get desktop files -// - all desktop files of snaps are prefixed with: ${snap}_ -// - on get/check/set we need to add/strip this prefix - -// Check implements the 'Check' method of the 'io.snapcraft.Settings' -// DBus interface. -// -// Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.Check string:'default-web-browser' string:'firefox.desktop' -func (s *Settings) Check(setting, check string, sender dbus.Sender) (string, *dbus.Error) { - // avoid information leak: see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758 - snap, err := snapFromSender(s.conn, sender) - if err != nil { - return "", dbus.MakeFailedError(err) - } - if err := settingWhitelisted(setting); err != nil { - return "", err - } - if !allowedSetting(check) { - return "", dbus.MakeFailedError(fmt.Errorf("cannot check setting %q to value %q: value not allowed", setting, check)) - } - - // FIXME: this works only for desktop files - desktopFile := fmt.Sprintf("%s_%s", snap, check) - - cmd := exec.Command("xdg-settings", "check", setting, desktopFile) - output, err := cmd.CombinedOutput() - if err != nil { - return "", dbus.MakeFailedError(fmt.Errorf("cannot check setting %s: %s", setting, osutil.OutputErr(output, err))) - } - - return strings.TrimSpace(string(output)), nil -} - -// Get implements the 'Get' method of the 'io.snapcraft.Settings' -// DBus interface. -// -// Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.Get string:'default-web-browser' -func (s *Settings) Get(setting string, sender dbus.Sender) (string, *dbus.Error) { - if err := settingWhitelisted(setting); err != nil { - return "", err - } - - cmd := exec.Command("xdg-settings", "get", setting) - output, err := cmd.CombinedOutput() - if err != nil { - return "", dbus.MakeFailedError(fmt.Errorf("cannot get setting %s: %s", setting, osutil.OutputErr(output, err))) - } - - // avoid information leak: see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758 - snap, err := snapFromSender(s.conn, sender) - if err != nil { - return "", dbus.MakeFailedError(err) - } - if !strings.HasPrefix(string(output), snap+"_") { - return "NOT-THIS-SNAP.desktop", nil - } - - desktopFile := strings.SplitN(string(output), "_", 2)[1] - return strings.TrimSpace(desktopFile), nil -} - -// Set implements the 'Set' method of the 'io.snapcraft.Settings' -// DBus interface. -// -// Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.Set string:'default-web-browser' string:'chromium-browser.desktop' -func (s *Settings) Set(setting, new string, sender dbus.Sender) *dbus.Error { - if err := settingWhitelisted(setting); err != nil { - return err - } - // see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758 - snap, err := snapFromSender(s.conn, sender) - if err != nil { - return dbus.MakeFailedError(err) - } - - if !allowedSetting(new) { - return dbus.MakeFailedError(fmt.Errorf("cannot set setting %q to value %q: value not allowed", setting, new)) - } - new = fmt.Sprintf("%s_%s", snap, new) - df := filepath.Join(dirs.SnapDesktopFilesDir, new) - if !osutil.FileExists(df) { - return dbus.MakeFailedError(fmt.Errorf("cannot find desktop file %q", df)) - } - - // FIXME: we need to know the parent PID or our dialog may pop under - // the existing windows. We might get it with the help of - // the xdg-settings tool inside the core snap. It would have - // to get the PID of the process asking for the settings - // then xdg-settings can sent this to us and we can intospect - // the X windows for _NET_WM_PID and use the windowID to - // attach to zenity - not sure how this translate to the - // wayland world though :/ - dialog, err := ui.New() - if err != nil { - return dbus.MakeFailedError(fmt.Errorf("cannot ask for settings change: %v", err)) - } - answeredYes := dialog.YesNo( - i18n.G("Allow settings change?"), - fmt.Sprintf(i18n.G("Allow snap %q to change %q to %q ?"), snap, setting, new), - &ui.DialogOptions{ - Timeout: defaultConfirmDialogTimeout, - Footer: i18n.G("This dialog will close automatically after 5 minutes of inactivity."), - }, - ) - if !answeredYes { - return dbus.MakeFailedError(fmt.Errorf("cannot change configuration: user declined change")) - } - - cmd := exec.Command("xdg-settings", "set", setting, new) - output, err := cmd.CombinedOutput() - if err != nil { - return dbus.MakeFailedError(fmt.Errorf("cannot set setting %s: %s", setting, osutil.OutputErr(output, err))) - } - - return nil -} diff -Nru snapd-2.40/userd/settings_test.go snapd-2.42.1/userd/settings_test.go --- snapd-2.40/userd/settings_test.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/settings_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,220 +0,0 @@ -// -*- 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 userd_test - -import ( - "io/ioutil" - "os" - "path/filepath" - - "github.com/godbus/dbus" - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/testutil" - "github.com/snapcore/snapd/userd" - "github.com/snapcore/snapd/userd/ui" -) - -type settingsSuite struct { - settings *userd.Settings - - mockXdgSettings *testutil.MockCmd - restoreSnapFromSender func() -} - -var _ = Suite(&settingsSuite{}) - -func (s *settingsSuite) SetUpTest(c *C) { - s.restoreSnapFromSender = userd.MockSnapFromSender(func(*dbus.Conn, dbus.Sender) (string, error) { - return "some-snap", nil - }) - - s.settings = &userd.Settings{} - s.mockXdgSettings = testutil.MockCommand(c, "xdg-settings", ` -if [ "$1" = "get" ] && [ "$2" = "default-web-browser" ]; then - echo "some-snap_foo.desktop" -elif [ "$1" = "check" ] && [ "$2" = "default-web-browser" ] && [ "$3" = "some-snap_foo.desktop" ]; then - echo yes -elif [ "$1" = "check" ] && [ "$2" = "default-web-browser" ]; then - echo no -elif [ "$1" = "set" ] && [ "$2" = "default-web-browser" ]; then - # nothing to do - exit 0 -else - echo "mock called with unsupported arguments $*" - exit 1 -fi -`) -} - -func (s *settingsSuite) TearDownTest(c *C) { - s.mockXdgSettings.Restore() - s.restoreSnapFromSender() -} - -func mockUIcommands(c *C, script string) func() { - mockZenity := testutil.MockCommand(c, "zenity", script) - mockKDialog := testutil.MockCommand(c, "kdialog", script) - return func() { - mockZenity.Restore() - mockKDialog.Restore() - } -} - -func (s *settingsSuite) TestGetUnhappy(c *C) { - for _, t := range []struct { - setting string - errMatcher string - }{ - {"random-setting", `cannot use setting "random-setting": not allowed`}, - {"invälid", `cannot use setting "invälid": not allowed`}, - {"", `cannot use setting "": not allowed`}, - } { - _, err := s.settings.Get(t.setting, ":some-dbus-sender") - c.Assert(err, ErrorMatches, t.errMatcher) - c.Assert(s.mockXdgSettings.Calls(), IsNil) - } -} - -func (s *settingsSuite) TestGetHappy(c *C) { - defaultBrowser, err := s.settings.Get("default-web-browser", ":some-dbus-sender") - c.Assert(err, IsNil) - c.Check(defaultBrowser, Equals, "foo.desktop") - c.Check(s.mockXdgSettings.Calls(), DeepEquals, [][]string{ - {"xdg-settings", "get", "default-web-browser"}, - }) -} - -func (s *settingsSuite) TestCheckInvalidSetting(c *C) { - _, err := s.settings.Check("random-setting", "foo.desktop", ":some-dbus-sender") - c.Assert(err, ErrorMatches, `cannot use setting "random-setting": not allowed`) - c.Assert(s.mockXdgSettings.Calls(), IsNil) -} - -func (s *settingsSuite) TestCheckIsDefault(c *C) { - isDefault, err := s.settings.Check("default-web-browser", "foo.desktop", ":some-dbus-sender") - c.Assert(err, IsNil) - c.Check(isDefault, Equals, "yes") - c.Check(s.mockXdgSettings.Calls(), DeepEquals, [][]string{ - {"xdg-settings", "check", "default-web-browser", "some-snap_foo.desktop"}, - }) -} - -func (s *settingsSuite) TestCheckNoDefault(c *C) { - isDefault, err := s.settings.Check("default-web-browser", "bar.desktop", ":some-dbus-sender") - c.Assert(err, IsNil) - c.Check(isDefault, Equals, "no") - c.Check(s.mockXdgSettings.Calls(), DeepEquals, [][]string{ - {"xdg-settings", "check", "default-web-browser", "some-snap_bar.desktop"}, - }) -} - -func (s *settingsSuite) TestSetInvalidSetting(c *C) { - err := s.settings.Set("random-setting", "foo.desktop", ":some-dbus-sender") - c.Assert(err, ErrorMatches, `cannot use setting "random-setting": not allowed`) - c.Assert(s.mockXdgSettings.Calls(), IsNil) -} - -func (s *settingsSuite) testSetUserDeclined(c *C) { - df := filepath.Join(dirs.SnapDesktopFilesDir, "some-snap_bar.desktop") - err := os.MkdirAll(filepath.Dir(df), 0755) - c.Assert(err, IsNil) - err = ioutil.WriteFile(df, nil, 0644) - c.Assert(err, IsNil) - - err = s.settings.Set("default-web-browser", "bar.desktop", ":some-dbus-sender") - c.Assert(err, ErrorMatches, `cannot change configuration: user declined change`) - c.Check(s.mockXdgSettings.Calls(), IsNil) - // FIXME: this needs PR#4342 - /* - c.Check(mockZenity.Calls(), DeepEquals, [][]string{ - {"zenity", "--question", "--text=Allow changing setting \"default-web-browser\" to \"bar.desktop\" ?"}, - }) - */ -} - -func (s *settingsSuite) TestSetUserDeclinedKDialog(c *C) { - // force zenity exec missing - restoreZenity := ui.MockHasZenityExecutable(func() bool { return false }) - restoreCmds := mockUIcommands(c, "false") - defer func() { - restoreZenity() - restoreCmds() - }() - - s.testSetUserDeclined(c) -} - -func (s *settingsSuite) TestSetUserDeclinedZenity(c *C) { - // force kdialog exec missing - restoreKDialog := ui.MockHasKDialogExecutable(func() bool { return false }) - restoreCmds := mockUIcommands(c, "false") - defer func() { - restoreKDialog() - restoreCmds() - }() - - s.testSetUserDeclined(c) -} - -func (s *settingsSuite) testSetUserAccepts(c *C) { - df := filepath.Join(dirs.SnapDesktopFilesDir, "some-snap_foo.desktop") - err := os.MkdirAll(filepath.Dir(df), 0755) - c.Assert(err, IsNil) - err = ioutil.WriteFile(df, nil, 0644) - c.Assert(err, IsNil) - - err = s.settings.Set("default-web-browser", "foo.desktop", ":some-dbus-sender") - c.Assert(err, IsNil) - c.Check(s.mockXdgSettings.Calls(), DeepEquals, [][]string{ - {"xdg-settings", "set", "default-web-browser", "some-snap_foo.desktop"}, - }) - // FIXME: this needs PR#4342 - /* - c.Check(mockZenity.Calls(), DeepEquals, [][]string{ - {"zenity", "--question", "--text=Allow changing setting \"default-web-browser\" to \"foo.desktop\" ?"}, - }) - */ -} - -func (s *settingsSuite) TestSetUserAcceptsZenity(c *C) { - // force kdialog exec missing - restoreKDialog := ui.MockHasKDialogExecutable(func() bool { return false }) - restoreCmds := mockUIcommands(c, "true") - defer func() { - restoreKDialog() - restoreCmds() - }() - - s.testSetUserAccepts(c) -} - -func (s *settingsSuite) TestSetUserAcceptsKDialog(c *C) { - // force zenity exec missing - restoreZenity := ui.MockHasZenityExecutable(func() bool { return false }) - restoreCmds := mockUIcommands(c, "true") - defer func() { - restoreZenity() - restoreCmds() - }() - - s.testSetUserAccepts(c) -} diff -Nru snapd-2.40/userd/ui/kdialog.go snapd-2.42.1/userd/ui/kdialog.go --- snapd-2.40/userd/ui/kdialog.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/ui/kdialog.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,68 +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 ui - -import ( - "fmt" - "html" - "os/exec" - "time" -) - -// KDialog provides a kdialog based UI interface -type KDialog struct{} - -// YesNo asks a yes/no question using kdialog -func (*KDialog) YesNo(primary, secondary string, options *DialogOptions) bool { - if options == nil { - options = &DialogOptions{} - } - - txt := fmt.Sprintf(`

%s

%s

`, html.EscapeString(primary), html.EscapeString(secondary)) - if options.Footer != "" { - txt += fmt.Sprintf(`

%s

`, html.EscapeString(options.Footer)) - } - cmd := exec.Command("kdialog", "--yesno="+txt) - if err := cmd.Start(); err != nil { - return false - } - - var err error - if options.Timeout > 0 { - done := make(chan error, 1) - go func() { done <- cmd.Wait() }() - select { - case err = <-done: - // normal exit - case <-time.After(options.Timeout): - // timeout do nothing, the other side will have timed - // out as well, no need to send a reply. - cmd.Process.Kill() - return false - } - } else { - err = cmd.Wait() - } - if err == nil { - return true - } - - return false -} diff -Nru snapd-2.40/userd/ui/kdialog_test.go snapd-2.42.1/userd/ui/kdialog_test.go --- snapd-2.40/userd/ui/kdialog_test.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/ui/kdialog_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,84 +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 ui_test - -import ( - "time" - - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/testutil" - "github.com/snapcore/snapd/userd/ui" -) - -type kdialogSuite struct { - mock *testutil.MockCmd -} - -var _ = Suite(&kdialogSuite{}) - -func (s *kdialogSuite) TestYesNoSimpleYes(c *C) { - mock := testutil.MockCommand(c, "kdialog", "") - defer mock.Restore() - - z := &ui.KDialog{} - answeredYes := z.YesNo("primary", "secondary", nil) - c.Check(answeredYes, Equals, true) - c.Check(mock.Calls(), DeepEquals, [][]string{ - {"kdialog", "--yesno=

primary

secondary

"}, - }) -} - -func (s *kdialogSuite) TestYesNoSimpleNo(c *C) { - mock := testutil.MockCommand(c, "kdialog", "false") - defer mock.Restore() - - z := &ui.KDialog{} - answeredYes := z.YesNo("primary", "secondary", nil) - c.Check(answeredYes, Equals, false) - c.Check(mock.Calls(), DeepEquals, [][]string{ - {"kdialog", "--yesno=

primary

secondary

"}, - }) -} - -func (s *kdialogSuite) TestYesNoSimpleFooter(c *C) { - mock := testutil.MockCommand(c, "kdialog", "") - defer mock.Restore() - - z := &ui.KDialog{} - answeredYes := z.YesNo("primary", "secondary", &ui.DialogOptions{Footer: "footer"}) - c.Check(answeredYes, Equals, true) - c.Check(mock.Calls(), DeepEquals, [][]string{ - {"kdialog", "--yesno=

primary

secondary

footer

"}, - }) -} - -func (s *kdialogSuite) TestYesNoSimpleTimeout(c *C) { - mock := testutil.MockCommand(c, "kdialog", "sleep 9999999") - defer mock.Restore() - - z := &ui.KDialog{} - answeredYes := z.YesNo("primary", "secondary", &ui.DialogOptions{Timeout: 1 * time.Second}) - // this got auto-canceled after 1sec - c.Check(answeredYes, Equals, false) - c.Check(mock.Calls(), DeepEquals, [][]string{ - {"kdialog", "--yesno=

primary

secondary

"}, - }) -} diff -Nru snapd-2.40/userd/ui/ui.go snapd-2.42.1/userd/ui/ui.go --- snapd-2.40/userd/ui/ui.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/ui/ui.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,94 +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 ui - -import ( - "fmt" - "os" - "strings" - "time" - - "github.com/snapcore/snapd/osutil" -) - -// UI is an interface for user interaction -type UI interface { - // YesNo asks a yes/no question. The primary text - // will be printed in a larger font, the secondary text - // in the standard font and the (optional) footer will - // be printed in a small font. - // - // The value "true" is returned if the user clicks "yes", - // otherwise "false". - YesNo(primary, secondary string, options *DialogOptions) bool -} - -// Options for the UI interface -type DialogOptions struct { - Footer string - Timeout time.Duration -} - -var hasZenityExecutable = func() bool { - return osutil.ExecutableExists("zenity") -} - -var hasKDialogExecutable = func() bool { - return osutil.ExecutableExists("kdialog") -} - -func MockHasZenityExecutable(f func() bool) func() { - oldHasZenityExecutable := hasZenityExecutable - hasZenityExecutable = f - return func() { - hasZenityExecutable = oldHasZenityExecutable - } -} - -func MockHasKDialogExecutable(f func() bool) func() { - oldHasKDialogExecutable := hasKDialogExecutable - hasKDialogExecutable = f - return func() { - hasKDialogExecutable = oldHasKDialogExecutable - } -} - -// New returns the best matching UI interface for the given system -// or an error if no ui can be created. -func New() (UI, error) { - hasZenity := hasZenityExecutable() - hasKDialog := hasKDialogExecutable() - - switch { - case hasZenity && hasKDialog: - // prefer kdialog on KDE, otherwise use zenity - currentDesktop := os.Getenv("XDG_CURRENT_DESKTOP") - if strings.Contains("kde", strings.ToLower(currentDesktop)) { - return &KDialog{}, nil - } - return &Zenity{}, nil - case hasZenity: - return &Zenity{}, nil - case hasKDialog: - return &KDialog{}, nil - default: - return nil, fmt.Errorf("cannot create a UI: please install zenity or kdialog") - } -} diff -Nru snapd-2.40/userd/ui/zenity.go snapd-2.42.1/userd/ui/zenity.go --- snapd-2.40/userd/ui/zenity.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/ui/zenity.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,64 +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 ui - -import ( - "fmt" - "os/exec" - "time" -) - -// Zenity provides a zenity based UI interface -type Zenity struct{} - -// YesNo asks a yes/no question using zenity -func (*Zenity) YesNo(primary, secondary string, options *DialogOptions) bool { - if options == nil { - options = &DialogOptions{} - } - - txt := fmt.Sprintf("%s\n\n%s", primary, secondary) - if options.Footer != "" { - txt += fmt.Sprintf("\n\n%s", options.Footer) - } - args := []string{"--question", "--modal", "--text=" + txt} - if options.Timeout > 0 { - args = append(args, fmt.Sprintf("--timeout=%d", int(options.Timeout/time.Second))) - } - // Gtk is not doing a good job with long labels. It will - // create a very narrow and long window (minimal width to fit - // the buttons). So force a bigger width here. See also LP: - // 1778484. The primary number is lower because the header is - // in a bigger font. - if len(primary) > 10 || len(secondary) > 20 { - args = append(args, "--width=500") - } - - cmd := exec.Command("zenity", args...) - if err := cmd.Start(); err != nil { - return false - } - - if err := cmd.Wait(); err != nil { - return false - } - - return true -} diff -Nru snapd-2.40/userd/ui/zenity_test.go snapd-2.42.1/userd/ui/zenity_test.go --- snapd-2.40/userd/ui/zenity_test.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/ui/zenity_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,102 +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 ui_test - -import ( - "time" - - "testing" - - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/testutil" - "github.com/snapcore/snapd/userd/ui" -) - -func Test(t *testing.T) { TestingT(t) } - -type zenitySuite struct { - mock *testutil.MockCmd -} - -var _ = Suite(&zenitySuite{}) - -func (s *zenitySuite) TestYesNoSimpleYes(c *C) { - mock := testutil.MockCommand(c, "zenity", "") - defer mock.Restore() - - z := &ui.Zenity{} - answeredYes := z.YesNo("primary", "secondary", nil) - c.Check(answeredYes, Equals, true) - c.Check(mock.Calls(), DeepEquals, [][]string{ - {"zenity", "--question", "--modal", "--text=primary\n\nsecondary"}, - }) -} - -func (s *zenitySuite) TestYesNoSimpleNo(c *C) { - mock := testutil.MockCommand(c, "zenity", "false") - defer mock.Restore() - - z := &ui.Zenity{} - answeredYes := z.YesNo("primary", "secondary", nil) - c.Check(answeredYes, Equals, false) - c.Check(mock.Calls(), DeepEquals, [][]string{ - {"zenity", "--question", "--modal", "--text=primary\n\nsecondary"}, - }) -} - -func (s *zenitySuite) TestYesNoLong(c *C) { - mock := testutil.MockCommand(c, "zenity", "true") - defer mock.Restore() - - z := &ui.Zenity{} - _ = z.YesNo("01234567890", "01234567890", nil) - c.Check(mock.Calls(), DeepEquals, [][]string{ - {"zenity", "--question", "--modal", "--text=01234567890\n\n01234567890", "--width=500"}, - }) -} - -func (s *zenitySuite) TestYesNoSimpleFooter(c *C) { - mock := testutil.MockCommand(c, "zenity", "") - defer mock.Restore() - - z := &ui.Zenity{} - answeredYes := z.YesNo("primary", "secondary", &ui.DialogOptions{Footer: "footer"}) - c.Check(answeredYes, Equals, true) - c.Check(mock.Calls(), DeepEquals, [][]string{ - {"zenity", "--question", "--modal", `--text=primary - -secondary - -footer`}, - }) -} - -func (s *zenitySuite) TestYesNoSimpleTimeout(c *C) { - mock := testutil.MockCommand(c, "zenity", "true") - defer mock.Restore() - - z := &ui.Zenity{} - answeredYes := z.YesNo("primary", "secondary", &ui.DialogOptions{Timeout: 60 * time.Second}) - c.Check(answeredYes, Equals, true) - c.Check(mock.Calls(), DeepEquals, [][]string{ - {"zenity", "--question", "--modal", "--text=primary\n\nsecondary", "--timeout=60"}, - }) -} diff -Nru snapd-2.40/userd/userd.go snapd-2.42.1/userd/userd.go --- snapd-2.40/userd/userd.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/userd/userd.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,116 +0,0 @@ -// -*- 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 userd - -import ( - "fmt" - - "github.com/godbus/dbus" - "github.com/godbus/dbus/introspect" - "gopkg.in/tomb.v2" - - "github.com/snapcore/snapd/logger" -) - -type dbusInterface interface { - Name() string - BasePath() dbus.ObjectPath - IntrospectionData() string -} - -type Userd struct { - tomb tomb.Tomb - conn *dbus.Conn - dbusIfaces []dbusInterface -} - -func dbusSessionBus() (*dbus.Conn, error) { - // use a private connection to the session bus, this way we can manage - // its lifetime without worrying of breaking other code - conn, err := dbus.SessionBusPrivate() - if err != nil { - return nil, err - } - if err := conn.Auth(nil); err != nil { - conn.Close() - return nil, err - } - if err := conn.Hello(); err != nil { - conn.Close() - return nil, err - } - return conn, nil -} - -func (ud *Userd) Init() error { - var err error - - ud.conn, err = dbusSessionBus() - if err != nil { - return err - } - - ud.dbusIfaces = []dbusInterface{ - &Launcher{ud.conn}, - &Settings{ud.conn}, - } - for _, iface := range ud.dbusIfaces { - reply, err := ud.conn.RequestName(iface.Name(), dbus.NameFlagDoNotQueue) - if err != nil { - return err - } - - if reply != dbus.RequestNameReplyPrimaryOwner { - return fmt.Errorf("cannot obtain bus name '%s'", iface.Name()) - } - - xml := "" + iface.IntrospectionData() + introspect.IntrospectDataString + "" - ud.conn.Export(iface, iface.BasePath(), iface.Name()) - ud.conn.Export(introspect.Introspectable(xml), iface.BasePath(), "org.freedesktop.DBus.Introspectable") - } - return nil -} - -func (ud *Userd) Start() { - logger.Noticef("Starting snap userd") - - ud.tomb.Go(func() error { - // Listen to keep our thread up and running. All DBus bits - // are running in the background - select { - case <-ud.tomb.Dying(): - ud.conn.Close() - } - err := ud.tomb.Err() - if err != nil && err != tomb.ErrStillAlive { - return err - } - return nil - }) -} - -func (ud *Userd) Stop() error { - ud.tomb.Kill(nil) - return ud.tomb.Wait() -} - -func (ud *Userd) Dying() <-chan struct{} { - return ud.tomb.Dying() -} diff -Nru snapd-2.40/usersession/agent/export_test.go snapd-2.42.1/usersession/agent/export_test.go --- snapd-2.40/usersession/agent/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/agent/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 agent + +var ( + SessionInfoCmd = sessionInfoCmd +) diff -Nru snapd-2.40/usersession/agent/response.go snapd-2.42.1/usersession/agent/response.go --- snapd-2.40/usersession/agent/response.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/agent/response.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,167 @@ +// -*- 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 agent + +import ( + "encoding/json" + "fmt" + "net/http" + + "github.com/snapcore/snapd/logger" +) + +// TODO: clean up unused code further after we have progressed enough +// to have a clear sense of what is untested and uneeded here + +// ResponseType is the response type +type ResponseType string + +// "there are three standard return types: Standard return value, +// Background operation, Error", each returning a JSON object with the +// following "type" field: +const ( + ResponseTypeSync ResponseType = "sync" + ResponseTypeAsync ResponseType = "async" + ResponseTypeError ResponseType = "error" +) + +// Response knows how to serve itself, and how to find itself +type Response interface { + ServeHTTP(w http.ResponseWriter, r *http.Request) +} + +type resp struct { + Status int // HTTP status code + Type ResponseType + Result interface{} +} + +type respJSON struct { + Type ResponseType `json:"type"` + Result interface{} `json:"result"` +} + +func (r *resp) MarshalJSON() ([]byte, error) { + return json.Marshal(respJSON{ + Type: r.Type, + Result: r.Result, + }) +} + +func (r *resp) ServeHTTP(w http.ResponseWriter, _ *http.Request) { + status := r.Status + bs, err := r.MarshalJSON() + if err != nil { + logger.Noticef("cannot marshal %#v to JSON: %v", *r, err) + bs = nil + status = 500 + } + + hdr := w.Header() + if r.Status == 202 || r.Status == 201 { + if m, ok := r.Result.(map[string]interface{}); ok { + if location, ok := m["resource"]; ok { + if location, ok := location.(string); ok && location != "" { + hdr.Set("Location", location) + } + } + } + } + + hdr.Set("Content-Type", "application/json") + w.WriteHeader(status) + w.Write(bs) +} + +type errorKind string + +const ( + errorKindLoginRequired = errorKind("login-required") +) + +type errorValue interface{} + +type errorResult struct { + Message string `json:"message"` // mandatory in error responses + Kind errorKind `json:"kind,omitempty"` + Value errorValue `json:"value,omitempty"` +} + +// SyncResponse builds a "sync" response from the given result. +func SyncResponse(result interface{}) Response { + if err, ok := result.(error); ok { + return InternalError("internal error: %v", err) + } + + if rsp, ok := result.(Response); ok { + return rsp + } + + return &resp{ + Type: ResponseTypeSync, + Status: 200, + Result: result, + } +} + +// AsyncResponse builds an "async" response from the given *Task +func AsyncResponse(result map[string]interface{}) Response { + return &resp{ + Type: ResponseTypeAsync, + Status: 202, + Result: result, + } +} + +// makeErrorResponder builds an errorResponder from the given error status. +func makeErrorResponder(status int) errorResponder { + return func(format string, v ...interface{}) Response { + res := &errorResult{} + if len(v) == 0 { + res.Message = format + } else { + res.Message = fmt.Sprintf(format, v...) + } + if status == 401 { + res.Kind = errorKindLoginRequired + } + return &resp{ + Type: ResponseTypeError, + Result: res, + Status: status, + } + } +} + +// errorResponder is a callable that produces an error Response. +// e.g., InternalError("something broke: %v", err), etc. +type errorResponder func(string, ...interface{}) Response + +// standard error responses +var ( + Unauthorized = makeErrorResponder(401) + NotFound = makeErrorResponder(404) + BadRequest = makeErrorResponder(400) + MethodNotAllowed = makeErrorResponder(405) + InternalError = makeErrorResponder(500) + NotImplemented = makeErrorResponder(501) + Forbidden = makeErrorResponder(403) + Conflict = makeErrorResponder(409) +) diff -Nru snapd-2.40/usersession/agent/rest_api.go snapd-2.42.1/usersession/agent/rest_api.go --- snapd-2.40/usersession/agent/rest_api.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/agent/rest_api.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,48 @@ +// -*- 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 agent + +import ( + "net/http" +) + +var restApi = []*Command{ + rootCmd, + sessionInfoCmd, +} + +var ( + rootCmd = &Command{ + Path: "/", + GET: nil, + } + + sessionInfoCmd = &Command{ + Path: "/v1/session-info", + GET: sessionInfo, + } +) + +func sessionInfo(c *Command, r *http.Request) Response { + m := map[string]interface{}{ + "version": c.s.Version, + } + return SyncResponse(m) +} diff -Nru snapd-2.40/usersession/agent/rest_api_test.go snapd-2.42.1/usersession/agent/rest_api_test.go --- snapd-2.40/usersession/agent/rest_api_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/agent/rest_api_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,76 @@ +// -*- 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 agent_test + +import ( + "encoding/json" + "fmt" + "net/http/httptest" + "os" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/usersession/agent" +) + +type restSuite struct{} + +var _ = Suite(&restSuite{}) + +func (s *restSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) + xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid()) + c.Assert(os.MkdirAll(xdgRuntimeDir, 0700), IsNil) +} + +func (s *restSuite) TearDownTest(c *C) { + dirs.SetRootDir("") +} + +type resp struct { + Type agent.ResponseType `json:"type"` + Result interface{} `json:"result"` +} + +func (s *restSuite) TestSessionInfo(c *C) { + // the agent.SessionInfo end point only supports GET requests + c.Check(agent.SessionInfoCmd.PUT, IsNil) + c.Check(agent.SessionInfoCmd.POST, IsNil) + c.Check(agent.SessionInfoCmd.DELETE, IsNil) + c.Assert(agent.SessionInfoCmd.GET, NotNil) + + c.Check(agent.SessionInfoCmd.Path, Equals, "/v1/session-info") + + a, err := agent.New() + c.Assert(err, IsNil) + a.Version = "42b1" + rec := httptest.NewRecorder() + agent.SessionInfoCmd.GET(agent.SessionInfoCmd, nil).ServeHTTP(rec, nil) + c.Check(rec.Code, Equals, 200) + c.Check(rec.HeaderMap.Get("Content-Type"), Equals, "application/json") + + var rsp resp + c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), IsNil) + c.Check(rsp.Type, Equals, agent.ResponseTypeSync) + c.Check(rsp.Result, DeepEquals, map[string]interface{}{ + "version": "42b1", + }) +} diff -Nru snapd-2.40/usersession/agent/session_agent.go snapd-2.42.1/usersession/agent/session_agent.go --- snapd-2.40/usersession/agent/session_agent.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/agent/session_agent.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,143 @@ +// -*- 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 agent + +import ( + "context" + "fmt" + "net" + "net/http" + "os" + "time" + + "github.com/gorilla/mux" + "gopkg.in/tomb.v2" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/netutil" + "github.com/snapcore/snapd/systemd" +) + +type SessionAgent struct { + Version string + listener net.Listener + serve *http.Server + tomb tomb.Tomb + router *mux.Router +} + +// A ResponseFunc handles one of the individual verbs for a method +type ResponseFunc func(*Command, *http.Request) Response + +// A Command routes a request to an individual per-verb ResponseFunc +type Command struct { + Path string + + GET ResponseFunc + PUT ResponseFunc + POST ResponseFunc + DELETE ResponseFunc + + s *SessionAgent +} + +func (c *Command) ServeHTTP(w http.ResponseWriter, r *http.Request) { + var rspf ResponseFunc + var rsp = MethodNotAllowed("method %q not allowed", r.Method) + + switch r.Method { + case "GET": + rspf = c.GET + case "PUT": + rspf = c.PUT + case "POST": + rspf = c.POST + case "DELETE": + rspf = c.DELETE + } + + if rspf != nil { + rsp = rspf(c, r) + } + rsp.ServeHTTP(w, r) +} + +func (s *SessionAgent) Init() error { + listenerMap, err := netutil.ActivationListeners() + if err != nil { + return err + } + agentSocket := fmt.Sprintf("%s/%d/snapd-session-agent.socket", dirs.XdgRuntimeDirBase, os.Getuid()) + if s.listener, err = netutil.GetListener(agentSocket, listenerMap); err != nil { + return fmt.Errorf("cannot listen on socket %s: %v", agentSocket, err) + } + s.addRoutes() + s.serve = &http.Server{Handler: s.router} + return nil +} + +func (s *SessionAgent) addRoutes() { + s.router = mux.NewRouter() + for _, c := range restApi { + c.s = s + s.router.Handle(c.Path, c).Name(c.Path) + } + s.router.NotFoundHandler = NotFound("not found") +} + +func (s *SessionAgent) Start() { + s.tomb.Go(func() error { + err := s.serve.Serve(s.listener) + if err == http.ErrServerClosed { + return nil + } + if err != nil && s.tomb.Err() == tomb.ErrStillAlive { + return err + } + return nil + }) + systemd.SdNotify("READY=1") +} + +var ( + shutdownTimeout = 5 * time.Second +) + +// Stop performs a graceful shutdown of the session agent and waits up to 5 +// seconds for it to complete. +func (s *SessionAgent) Stop() error { + systemd.SdNotify("STOPPING=1") + ctx, cancel := context.WithTimeout(context.Background(), shutdownTimeout) + defer cancel() + s.tomb.Kill(s.serve.Shutdown(ctx)) + return s.tomb.Wait() +} + +func (s *SessionAgent) Dying() <-chan struct{} { + return s.tomb.Dying() +} + +func New() (*SessionAgent, error) { + agent := &SessionAgent{} + if err := agent.Init(); err != nil { + return nil, err + } + return agent, nil +} diff -Nru snapd-2.40/usersession/agent/session_agent_test.go snapd-2.42.1/usersession/agent/session_agent_test.go --- snapd-2.40/usersession/agent/session_agent_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/agent/session_agent_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,106 @@ +// -*- 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 agent_test + +import ( + "encoding/json" + "fmt" + "net" + "net/http" + "os" + "testing" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/usersession/agent" +) + +func Test(t *testing.T) { TestingT(t) } + +type sessionAgentSuite struct { + socketPath string + client *http.Client +} + +var _ = Suite(&sessionAgentSuite{}) + +func (s *sessionAgentSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) + xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid()) + c.Assert(os.MkdirAll(xdgRuntimeDir, 0700), IsNil) + s.socketPath = fmt.Sprintf("%s/snapd-session-agent.socket", xdgRuntimeDir) + + transport := &http.Transport{ + Dial: func(_, _ string) (net.Conn, error) { + return net.Dial("unix", s.socketPath) + }, + DisableKeepAlives: true, + } + s.client = &http.Client{Transport: transport} +} + +func (s *sessionAgentSuite) TearDownTest(c *C) { + dirs.SetRootDir("") +} + +func (s *sessionAgentSuite) TestStartStop(c *C) { + agent, err := agent.New() + c.Assert(err, IsNil) + agent.Version = "42" + agent.Start() + defer func() { c.Check(agent.Stop(), IsNil) }() + + response, err := s.client.Get("http://localhost/v1/session-info") + c.Assert(err, IsNil) + defer response.Body.Close() + c.Check(response.StatusCode, Equals, 200) + + var rst struct { + Result struct { + Version string `json:"version"` + } `json:"result"` + } + c.Assert(json.NewDecoder(response.Body).Decode(&rst), IsNil) + c.Check(rst.Result.Version, Equals, "42") + + c.Check(agent.Stop(), IsNil) +} + +func (s *sessionAgentSuite) TestDying(c *C) { + agent, err := agent.New() + c.Assert(err, IsNil) + agent.Start() + select { + case <-agent.Dying(): + c.Error("agent.Dying() channel closed prematurely") + default: + } + go func() { + time.Sleep(5 * time.Millisecond) + c.Check(agent.Stop(), IsNil) + }() + select { + case <-agent.Dying(): + case <-time.After(2 * time.Second): + c.Error("agent.Dying() channel was not closed when agent stopped") + } +} diff -Nru snapd-2.40/usersession/autostart/autostart.go snapd-2.42.1/usersession/autostart/autostart.go --- snapd-2.40/usersession/autostart/autostart.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/autostart/autostart.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,278 @@ +// -*- 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 autostart + +import ( + "bufio" + "bytes" + "fmt" + "log/syslog" + "os" + "os/exec" + "os/user" + "path/filepath" + "sort" + "strings" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" + "github.com/snapcore/snapd/strutil/shlex" + "github.com/snapcore/snapd/systemd" +) + +var ( + currentDesktop = splitSkippingEmpty(os.Getenv("XDG_CURRENT_DESKTOP"), ':') +) + +func splitSkippingEmpty(s string, sep rune) []string { + return strings.FieldsFunc(s, func(r rune) bool { return r == sep }) +} + +// expandDesktopFields processes the input string and expands any % +// patterns. '%%' expands to '%', all other patterns expand to empty strings. +func expandDesktopFields(in string) string { + raw := []rune(in) + out := make([]rune, 0, len(raw)) + + var hasKey bool + for _, r := range raw { + if hasKey { + hasKey = false + // only allow %% -> % expansion, drop other keys + if r == '%' { + out = append(out, r) + } + continue + } else if r == '%' { + hasKey = true + continue + } + out = append(out, r) + } + return string(out) +} + +type skipDesktopFileError struct { + reason string +} + +func (s *skipDesktopFileError) Error() string { + return s.reason +} + +func isOneOfIn(of []string, other []string) bool { + for _, one := range of { + if strutil.ListContains(other, one) { + return true + } + } + return false +} + +func loadAutostartDesktopFile(path string) (command string, err error) { + f, err := os.Open(path) + if err != nil { + return "", err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + bline := scanner.Bytes() + if bytes.HasPrefix(bline, []byte("#")) { + continue + } + split := bytes.SplitN(bline, []byte("="), 2) + if len(split) != 2 { + continue + } + // See https://standards.freedesktop.org/autostart-spec/autostart-spec-latest.html + // for details on how Hidden, OnlyShowIn, NotShownIn are handled. + switch string(split[0]) { + case "Exec": + command = strings.TrimSpace(expandDesktopFields(string(split[1]))) + case "Hidden": + if bytes.Equal(split[1], []byte("true")) { + return "", &skipDesktopFileError{"desktop file is hidden"} + } + case "OnlyShowIn": + onlyIn := splitSkippingEmpty(string(split[1]), ';') + if !isOneOfIn(currentDesktop, onlyIn) { + return "", &skipDesktopFileError{fmt.Sprintf("current desktop %q not included in %q", currentDesktop, onlyIn)} + } + case "NotShownIn": + notIn := splitSkippingEmpty(string(split[1]), ';') + if isOneOfIn(currentDesktop, notIn) { + return "", &skipDesktopFileError{fmt.Sprintf("current desktop %q excluded by %q", currentDesktop, notIn)} + } + case "X-GNOME-Autostart-enabled": + // GNOME specific extension, see gnome-session: + // https://github.com/GNOME/gnome-session/blob/c449df5269e02c59ae83021a3110ec1b338a2bba/gnome-session/gsm-autostart-app.c#L110..L145 + if !strutil.ListContains(currentDesktop, "GNOME") { + // not GNOME + continue + } + if !bytes.Equal(split[1], []byte("true")) { + return "", &skipDesktopFileError{"desktop file is hidden by X-GNOME-Autostart-enabled extension"} + } + } + } + if err := scanner.Err(); err != nil { + return "", err + } + + command = strings.TrimSpace(command) + if command == "" { + return "", fmt.Errorf("Exec not found or invalid") + } + return command, nil + +} + +func autostartCmd(snapName, desktopFilePath string) (*exec.Cmd, error) { + desktopFile := filepath.Base(desktopFilePath) + + info, err := snap.ReadCurrentInfo(snapName) + if err != nil { + return nil, err + } + + var app *snap.AppInfo + for _, candidate := range info.Apps { + if candidate.Autostart == desktopFile { + app = candidate + break + } + } + if app == nil { + return nil, fmt.Errorf("cannot match desktop file with snap %s applications", snapName) + } + + command, err := loadAutostartDesktopFile(desktopFilePath) + if err != nil { + if _, ok := err.(*skipDesktopFileError); ok { + return nil, fmt.Errorf("skipped: %v", err) + } + return nil, fmt.Errorf("cannot determine startup command for application %s in snap %s: %v", app.Name, snapName, err) + } + logger.Debugf("exec line: %v", command) + + split, err := shlex.Split(command) + if err != nil { + return nil, fmt.Errorf("invalid application startup command: %v", err) + } + + // NOTE: Ignore the actual argv[0] in Exec=.. line and replace it with a + // command of the snap application. Any arguments passed in the Exec=.. + // line to the original command are preserved. + cmd := exec.Command(app.WrapperPath(), split[1:]...) + return cmd, nil +} + +// failedAutostartError keeps track of errors that occurred when starting an +// application for a specific desktop file, desktop file name is as a key +type failedAutostartError map[string]error + +func (f failedAutostartError) Error() string { + var out bytes.Buffer + + dfiles := make([]string, 0, len(f)) + for desktopFile := range f { + dfiles = append(dfiles, desktopFile) + } + sort.Strings(dfiles) + for _, desktopFile := range dfiles { + fmt.Fprintf(&out, "- %q: %v\n", desktopFile, f[desktopFile]) + } + return out.String() +} + +func makeStdStreams(identifier string) (stdout *os.File, stderr *os.File) { + var err error + + stdout, err = systemd.NewJournalStreamFile(identifier, syslog.LOG_INFO, false) + if err != nil { + logger.Noticef("failed to set up stdout journal stream for %q: %v", identifier, err) + stdout = os.Stdout + } + + stderr, err = systemd.NewJournalStreamFile(identifier, syslog.LOG_WARNING, false) + if err != nil { + logger.Noticef("failed to set up stderr journal stream for %q: %v", identifier, err) + stderr = os.Stderr + } + + return stdout, stderr +} + +var userCurrent = user.Current + +// AutostartSessionApps starts applications which have placed their desktop +// files in $SNAP_USER_DATA/.config/autostart +// +// NOTE: By the spec, the actual path is $SNAP_USER_DATA/${XDG_CONFIG_DIR}/autostart +func AutostartSessionApps() error { + usr, err := userCurrent() + if err != nil { + return err + } + + usrSnapDir := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) + + glob := filepath.Join(usrSnapDir, "*/current/.config/autostart/*.desktop") + matches, err := filepath.Glob(glob) + if err != nil { + return err + } + + failedApps := make(failedAutostartError) + for _, desktopFilePath := range matches { + desktopFile := filepath.Base(desktopFilePath) + logger.Debugf("autostart desktop file %v", desktopFile) + + // /home/foo/snap/some-snap/current/.config/autostart/some-app.desktop -> + // some-snap/current/.config/autostart/some-app.desktop + noHomePrefix := strings.TrimPrefix(desktopFilePath, usrSnapDir+"/") + // some-snap/current/.config/autostart/some-app.desktop -> some-snap + snapName := noHomePrefix[0:strings.IndexByte(noHomePrefix, '/')] + + logger.Debugf("snap name: %q", snapName) + + cmd, err := autostartCmd(snapName, desktopFilePath) + if err != nil { + failedApps[desktopFile] = err + continue + } + + // similarly to gnome-session, use the desktop file name as + // identifier, see: + // https://github.com/GNOME/gnome-session/blob/099c19099de8e351f6cc0f2110ad27648780a0fe/gnome-session/gsm-autostart-app.c#L948 + cmd.Stdout, cmd.Stderr = makeStdStreams(desktopFile) + if err := cmd.Start(); err != nil { + failedApps[desktopFile] = fmt.Errorf("cannot autostart %q: %v", desktopFile, err) + } + } + if len(failedApps) > 0 { + return failedApps + } + return nil +} diff -Nru snapd-2.40/usersession/autostart/autostart_test.go snapd-2.42.1/usersession/autostart/autostart_test.go --- snapd-2.40/usersession/autostart/autostart_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/autostart/autostart_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,328 @@ +// -*- 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 autostart_test + +import ( + "io/ioutil" + "os" + "os/user" + "path" + "path/filepath" + "strings" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/usersession/autostart" +) + +func Test(t *testing.T) { TestingT(t) } + +type autostartSuite struct { + dir string + autostartDir string + userDir string + userCurrentRestore func() +} + +var _ = Suite(&autostartSuite{}) + +func (s *autostartSuite) SetUpTest(c *C) { + s.dir = c.MkDir() + dirs.SetRootDir(s.dir) + snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) + + s.userDir = path.Join(s.dir, "home") + s.autostartDir = path.Join(s.userDir, ".config", "autostart") + s.userCurrentRestore = autostart.MockUserCurrent(func() (*user.User, error) { + return &user.User{HomeDir: s.userDir}, nil + }) + + err := os.MkdirAll(s.autostartDir, 0755) + c.Assert(err, IsNil) +} + +func (s *autostartSuite) TearDownTest(c *C) { + s.dir = c.MkDir() + dirs.SetRootDir("/") + if s.userCurrentRestore != nil { + s.userCurrentRestore() + } +} + +func (s *autostartSuite) TestLoadAutostartDesktopFile(c *C) { + allGood := `[Desktop Entry] +Exec=foo --bar +` + allGoodWithFlags := `[Desktop Entry] +Exec=foo --bar "%%p" %U %D +%s %% +` + noExec := `[Desktop Entry] +Type=Application +` + emptyExec := `[Desktop Entry] +Exec= +` + onlySpacesExec := `[Desktop Entry] +Exec= +` + hidden := `[Desktop Entry] +Exec=foo --bar +Hidden=true +` + hiddenFalse := `[Desktop Entry] +Exec=foo --bar +Hidden=false +` + justGNOME := `[Desktop Entry] +Exec=foo --bar +OnlyShowIn=GNOME; +` + notInGNOME := `[Desktop Entry] +Exec=foo --bar +NotShownIn=GNOME; +` + notInGNOMEAndKDE := `[Desktop Entry] +Exec=foo --bar +NotShownIn=GNOME;KDE; +` + hiddenGNOMEextension := `[Desktop Entry] +Exec=foo --bar +X-GNOME-Autostart-enabled=false +` + GNOMEextension := `[Desktop Entry] +Exec=foo --bar +X-GNOME-Autostart-enabled=true +` + + for i, tc := range []struct { + in string + out string + err string + current string + }{{ + in: allGood, + out: "foo --bar", + }, { + in: noExec, + err: "Exec not found or invalid", + }, { + in: emptyExec, + err: "Exec not found or invalid", + }, { + in: onlySpacesExec, + err: "Exec not found or invalid", + }, { + in: allGoodWithFlags, + out: `foo --bar "%p" + %`, + }, { + in: hidden, + err: `desktop file is hidden`, + }, { + in: hiddenFalse, + out: `foo --bar`, + }, { + in: justGNOME, + out: "foo --bar", + current: "GNOME", + }, { + in: justGNOME, + current: "KDE", + err: `current desktop \["KDE"\] not included in \["GNOME"\]`, + }, { + in: notInGNOME, + current: "GNOME", + err: `current desktop \["GNOME"\] excluded by \["GNOME"\]`, + }, { + in: notInGNOME, + current: "KDE", + out: "foo --bar", + }, { + in: notInGNOMEAndKDE, + current: "XFCE", + out: "foo --bar", + }, { + in: hiddenGNOMEextension, + current: "KDE", + out: "foo --bar", + }, { + in: hiddenGNOMEextension, + current: "GNOME", + err: `desktop file is hidden by X-GNOME-Autostart-enabled extension`, + }, { + in: GNOMEextension, + current: "GNOME", + out: "foo --bar", + }, { + in: GNOMEextension, + current: "KDE", + out: "foo --bar", + }} { + c.Logf("tc %d", i) + + path := filepath.Join(c.MkDir(), "foo.desktop") + err := ioutil.WriteFile(path, []byte(tc.in), 0644) + c.Assert(err, IsNil) + + run := func() { + defer autostart.MockCurrentDesktop(tc.current)() + + cmd, err := autostart.LoadAutostartDesktopFile(path) + if tc.err != "" { + c.Check(cmd, Equals, "") + c.Check(err, ErrorMatches, tc.err) + } else { + c.Check(err, IsNil) + c.Check(cmd, Equals, tc.out) + } + } + run() + } +} + +var mockYaml = `name: snapname +version: 1.0 +apps: + foo: + command: run-app + autostart: foo-stable.desktop +` + +func (s *autostartSuite) TestTryAutostartAppValid(c *C) { + si := snaptest.MockSnapCurrent(c, mockYaml, &snap.SideInfo{Revision: snap.R("x2")}) + + appWrapperPath := si.Apps["foo"].WrapperPath() + err := os.MkdirAll(filepath.Dir(appWrapperPath), 0755) + c.Assert(err, IsNil) + + appCmd := testutil.MockCommand(c, appWrapperPath, "") + defer appCmd.Restore() + + fooDesktopFile := filepath.Join(s.autostartDir, "foo-stable.desktop") + writeFile(c, fooDesktopFile, + []byte(`[Desktop Entry] +Exec=this-is-ignored -a -b --foo="a b c" -z "dev" +`)) + + cmd, err := autostart.AutostartCmd("snapname", fooDesktopFile) + c.Assert(err, IsNil) + c.Assert(cmd.Path, Equals, appWrapperPath) + + err = cmd.Start() + c.Assert(err, IsNil) + cmd.Wait() + + c.Assert(appCmd.Calls(), DeepEquals, + [][]string{ + { + filepath.Base(appWrapperPath), + "-a", + "-b", + "--foo=a b c", + "-z", + "dev", + }, + }) +} + +func (s *autostartSuite) TestTryAutostartAppNoMatchingApp(c *C) { + snaptest.MockSnapCurrent(c, mockYaml, &snap.SideInfo{Revision: snap.R("x2")}) + + fooDesktopFile := filepath.Join(s.autostartDir, "foo-no-match.desktop") + writeFile(c, fooDesktopFile, + []byte(`[Desktop Entry] +Exec=this-is-ignored -a -b --foo="a b c" -z "dev" +`)) + + cmd, err := autostart.AutostartCmd("snapname", fooDesktopFile) + c.Assert(cmd, IsNil) + c.Assert(err, ErrorMatches, `cannot match desktop file with snap snapname applications`) +} + +func (s *autostartSuite) TestTryAutostartAppNoSnap(c *C) { + fooDesktopFile := filepath.Join(s.autostartDir, "foo-stable.desktop") + writeFile(c, fooDesktopFile, + []byte(`[Desktop Entry] +Exec=this-is-ignored -a -b --foo="a b c" -z "dev" +`)) + + cmd, err := autostart.AutostartCmd("snapname", fooDesktopFile) + c.Assert(cmd, IsNil) + c.Assert(err, ErrorMatches, `cannot find current revision for snap snapname.*`) +} + +func (s *autostartSuite) TestTryAutostartAppBadExec(c *C) { + snaptest.MockSnapCurrent(c, mockYaml, &snap.SideInfo{Revision: snap.R("x2")}) + + fooDesktopFile := filepath.Join(s.autostartDir, "foo-stable.desktop") + writeFile(c, fooDesktopFile, + []byte(`[Desktop Entry] +Foo=bar +`)) + + cmd, err := autostart.AutostartCmd("snapname", fooDesktopFile) + c.Assert(cmd, IsNil) + c.Assert(err, ErrorMatches, `cannot determine startup command for application foo in snap snapname: Exec not found or invalid`) +} + +func writeFile(c *C, path string, content []byte) { + err := os.MkdirAll(filepath.Dir(path), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(path, content, 0644) + c.Assert(err, IsNil) +} + +func (s *autostartSuite) TestTryAutostartMany(c *C) { + var mockYamlTemplate = `name: {snap} +version: 1.0 +apps: + foo: + command: run-app + autostart: foo-stable.desktop +` + + snaptest.MockSnapCurrent(c, strings.Replace(mockYamlTemplate, "{snap}", "a-foo", -1), + &snap.SideInfo{Revision: snap.R("x2")}) + snaptest.MockSnapCurrent(c, strings.Replace(mockYamlTemplate, "{snap}", "b-foo", -1), + &snap.SideInfo{Revision: snap.R("x2")}) + writeFile(c, filepath.Join(s.userDir, "snap/a-foo/current/.config/autostart/foo-stable.desktop"), + []byte(`[Desktop Entry] +Foo=bar +`)) + writeFile(c, filepath.Join(s.userDir, "snap/b-foo/current/.config/autostart/no-match.desktop"), + []byte(`[Desktop Entry] +Exec=no-snap +`)) + writeFile(c, filepath.Join(s.userDir, "snap/c-foo/current/.config/autostart/no-snap.desktop"), + []byte(`[Desktop Entry] +Exec=no-snap +`)) + + err := autostart.AutostartSessionApps() + c.Assert(err, NotNil) + c.Check(err, ErrorMatches, `- "foo-stable.desktop": cannot determine startup command for application foo in snap a-foo: Exec not found or invalid +- "no-match.desktop": cannot match desktop file with snap b-foo applications +- "no-snap.desktop": cannot find current revision for snap c-foo: readlink.*no such file or directory +`) +} diff -Nru snapd-2.40/usersession/autostart/export_test.go snapd-2.42.1/usersession/autostart/export_test.go --- snapd-2.40/usersession/autostart/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/autostart/export_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,45 @@ +// -*- 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 autostart + +import ( + "os/user" +) + +var ( + LoadAutostartDesktopFile = loadAutostartDesktopFile + AutostartCmd = autostartCmd +) + +func MockUserCurrent(f func() (*user.User, error)) func() { + origUserCurrent := userCurrent + userCurrent = f + return func() { + userCurrent = origUserCurrent + } +} + +func MockCurrentDesktop(current string) func() { + old := currentDesktop + currentDesktop = splitSkippingEmpty(current, ':') + return func() { + currentDesktop = old + } +} diff -Nru snapd-2.40/usersession/userd/export_test.go snapd-2.42.1/usersession/userd/export_test.go --- snapd-2.40/usersession/userd/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/userd/export_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,36 @@ +// -*- 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 userd + +import ( + "github.com/godbus/dbus" +) + +var ( + SnapFromPid = snapFromPid +) + +func MockSnapFromSender(f func(*dbus.Conn, dbus.Sender) (string, error)) func() { + origSnapFromSender := snapFromSender + snapFromSender = f + return func() { + snapFromSender = origSnapFromSender + } +} diff -Nru snapd-2.40/usersession/userd/helpers.go snapd-2.42.1/usersession/userd/helpers.go --- snapd-2.40/usersession/userd/helpers.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/userd/helpers.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,109 @@ +// -*- 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 userd + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/godbus/dbus" + + "github.com/snapcore/snapd/dirs" +) + +var snapFromSender = snapFromSenderImpl + +func snapFromSenderImpl(conn *dbus.Conn, sender dbus.Sender) (string, error) { + pid, err := connectionPid(conn, sender) + if err != nil { + return "", fmt.Errorf("cannot get connection pid: %v", err) + } + snap, err := snapFromPid(pid) + if err != nil { + return "", fmt.Errorf("cannot find snap for connection: %v", err) + } + // Check that the sender is still connected to the bus: if it + // has disconnected between the GetConnectionUnixProcessID + // call and when we poked around in /proc, then it is possible + // that the process ID was reused. + if !nameHasOwner(conn, sender) { + return "", fmt.Errorf("sender is no longer connected to the bus") + } + return snap, nil +} + +func connectionPid(conn *dbus.Conn, sender dbus.Sender) (pid int, err error) { + call := conn.BusObject().Call("org.freedesktop.DBus.GetConnectionUnixProcessID", 0, sender) + if call.Err != nil { + return 0, call.Err + } + call.Store(&pid) + return pid, nil +} + +func nameHasOwner(conn *dbus.Conn, sender dbus.Sender) bool { + call := conn.BusObject().Call("org.freedesktop.DBus.NameHasOwner", 0, sender) + if call.Err != nil { + return false + } + var hasOwner bool + call.Store(&hasOwner) + return hasOwner +} + +// FIXME: move to osutil? +func snapFromPid(pid int) (string, error) { + f, err := os.Open(fmt.Sprintf("%s/proc/%d/cgroup", dirs.GlobalRootDir, pid)) + if err != nil { + return "", err + } + defer f.Close() + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + // we need to find a string like: + // ... + // 7:freezer:/snap.hello-world + // ... + // See cgroup(7) for details about the /proc/[pid]/cgroup + // format. + l := strings.Split(scanner.Text(), ":") + if len(l) < 3 { + continue + } + controllerList := l[1] + cgroupPath := l[2] + if !strings.Contains(controllerList, "freezer") { + continue + } + if strings.HasPrefix(cgroupPath, "/snap.") { + snap := strings.SplitN(filepath.Base(cgroupPath), ".", 2)[1] + return snap, nil + } + } + if scanner.Err() != nil { + return "", scanner.Err() + } + + return "", fmt.Errorf("cannot find a snap for pid %v", pid) +} diff -Nru snapd-2.40/usersession/userd/helpers_test.go snapd-2.42.1/usersession/userd/helpers_test.go --- snapd-2.40/usersession/userd/helpers_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/userd/helpers_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,63 @@ +// -*- 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 userd_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/usersession/userd" +) + +type helpersSuite struct{} + +var _ = Suite(&helpersSuite{}) + +var mockCgroup = []byte(` +10:devices:/user.slice +9:cpuset:/ +8:net_cls,net_prio:/ +7:freezer:/snap.hello-world +6:perf_event:/ +5:pids:/user.slice/user-1000.slice/user@1000.service +4:cpu,cpuacct:/ +3:memory:/ +2:blkio:/ +1:name=systemd:/user.slice/user-1000.slice/user@1000.service/gnome-terminal-server.service +0::/user.slice/user-1000.slice/user@1000.service/gnome-terminal-server.service +`) + +func (s *helpersSuite) TestSnapFromPid(c *C) { + root := c.MkDir() + dirs.SetRootDir(root) + + err := os.MkdirAll(filepath.Join(root, "proc/333"), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(root, "proc/333/cgroup"), mockCgroup, 0755) + c.Assert(err, IsNil) + + snap, err := userd.SnapFromPid(333) + c.Assert(err, IsNil) + c.Check(snap, Equals, "hello-world") +} diff -Nru snapd-2.40/usersession/userd/launcher.go snapd-2.42.1/usersession/userd/launcher.go --- snapd-2.40/usersession/userd/launcher.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/userd/launcher.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,206 @@ +// -*- 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 userd + +import ( + "fmt" + "net/url" + "os" + "os/exec" + "path/filepath" + "strings" + "syscall" + "time" + + "github.com/godbus/dbus" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil/sys" + "github.com/snapcore/snapd/strutil" + "github.com/snapcore/snapd/usersession/userd/ui" +) + +const launcherIntrospectionXML = ` + + + + + + + + + + + + + + + +` + +var ( + allowedURLSchemes = []string{"http", "https", "mailto", "snap", "help"} +) + +// Launcher implements the 'io.snapcraft.Launcher' DBus interface. +type Launcher struct { + conn *dbus.Conn +} + +// Name returns the name of the interface this object implements +func (s *Launcher) Name() string { + return "io.snapcraft.Launcher" +} + +// BasePath returns the base path of the object +func (s *Launcher) BasePath() dbus.ObjectPath { + return "/io/snapcraft/Launcher" +} + +// IntrospectionData gives the XML formatted introspection description +// of the DBus service. +func (s *Launcher) IntrospectionData() string { + return launcherIntrospectionXML +} + +func makeAccessDeniedError(err error) *dbus.Error { + return &dbus.Error{ + Name: "org.freedesktop.DBus.Error.AccessDenied", + Body: []interface{}{err.Error()}, + } +} + +// OpenURL implements the 'OpenURL' method of the 'io.snapcraft.Launcher' +// DBus interface. Before the provided url is passed to xdg-open the scheme is +// validated against a list of allowed schemes. All other schemes are denied. +func (s *Launcher) OpenURL(addr string, sender dbus.Sender) *dbus.Error { + u, err := url.Parse(addr) + if err != nil { + return &dbus.ErrMsgInvalidArg + } + + if !strutil.ListContains(allowedURLSchemes, u.Scheme) { + return makeAccessDeniedError(fmt.Errorf("Supplied URL scheme %q is not allowed", u.Scheme)) + } + + snap, err := snapFromSender(s.conn, sender) + if err != nil { + return dbus.MakeFailedError(err) + } + + xdg_data_dirs := []string{} + xdg_data_dirs = append(xdg_data_dirs, fmt.Sprintf(filepath.Join(dirs.SnapMountDir, snap, "current/usr/share"))) + for _, dir := range strings.Split(os.Getenv("XDG_DATA_DIRS"), ":") { + xdg_data_dirs = append(xdg_data_dirs, dir) + } + + cmd := exec.Command("xdg-open", addr) + cmd.Env = os.Environ() + cmd.Env = append(cmd.Env, fmt.Sprintf("XDG_DATA_DIRS=%s", strings.Join(xdg_data_dirs, ":"))) + + if err := cmd.Run(); err != nil { + return dbus.MakeFailedError(fmt.Errorf("cannot open supplied URL")) + } + + return nil +} + +// fdToFilename determines the path associated with an open file descriptor. +// +// The file descriptor cannot be opened using O_PATH and must refer to +// a regular file or to a directory. The symlink at /proc/self/fd/ +// is read to determine the filename. The descriptor is also fstat'ed +// and the resulting device number and inode number are compared to +// stat on the path determined earlier. The numbers must match. +func fdToFilename(fd int) (string, error) { + flags, err := sys.FcntlGetFl(fd) + if err != nil { + return "", err + } + // File descriptors opened with O_PATH do not imply access to + // the file in question. + if flags&sys.O_PATH != 0 { + return "", fmt.Errorf("cannot use file descriptors opened using O_PATH") + } + + // Determine the file name associated with the passed file descriptor. + filename, err := os.Readlink(fmt.Sprintf("/proc/self/fd/%d", fd)) + if err != nil { + return "", err + } + + var fileStat, fdStat syscall.Stat_t + if err := syscall.Stat(filename, &fileStat); err != nil { + return "", err + } + if err := syscall.Fstat(fd, &fdStat); err != nil { + return "", err + } + + // Sanity check to ensure we've got the right file + if fdStat.Dev != fileStat.Dev || fdStat.Ino != fileStat.Ino { + return "", fmt.Errorf("cannot determine file name") + } + + fileType := fileStat.Mode & syscall.S_IFMT + if fileType != syscall.S_IFREG && fileType != syscall.S_IFDIR { + return "", fmt.Errorf("cannot open anything other than regular files or directories") + } + + return filename, nil +} + +func (s *Launcher) OpenFile(parentWindow string, clientFd dbus.UnixFD, sender dbus.Sender) *dbus.Error { + // godbus transfers ownership of this file descriptor to us + fd := int(clientFd) + defer syscall.Close(fd) + + filename, err := fdToFilename(fd) + if err != nil { + return dbus.MakeFailedError(err) + } + + snap, err := snapFromSender(s.conn, sender) + if err != nil { + return dbus.MakeFailedError(err) + } + dialog, err := ui.New() + if err != nil { + return dbus.MakeFailedError(err) + } + answeredYes := dialog.YesNo( + i18n.G("Allow opening file?"), + fmt.Sprintf(i18n.G("Allow snap %q to open file %q?"), snap, filename), + &ui.DialogOptions{ + Timeout: 5 * 60 * time.Second, + Footer: i18n.G("This dialog will close automatically after 5 minutes of inactivity."), + }, + ) + if !answeredYes { + return dbus.MakeFailedError(fmt.Errorf("permission denied")) + } + + if err = exec.Command("xdg-open", filename).Run(); err != nil { + return dbus.MakeFailedError(fmt.Errorf("cannot open supplied URL")) + } + + return nil +} diff -Nru snapd-2.40/usersession/userd/launcher_test.go snapd-2.42.1/usersession/userd/launcher_test.go --- snapd-2.40/usersession/userd/launcher_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/userd/launcher_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,199 @@ +// -*- 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 userd_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "syscall" + "testing" + + "github.com/godbus/dbus" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/osutil/sys" + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/usersession/userd" +) + +func Test(t *testing.T) { TestingT(t) } + +type launcherSuite struct { + launcher *userd.Launcher + + mockXdgOpen *testutil.MockCmd + restoreSnapFromSender func() +} + +var _ = Suite(&launcherSuite{}) + +func (s *launcherSuite) SetUpTest(c *C) { + s.launcher = &userd.Launcher{} + s.mockXdgOpen = testutil.MockCommand(c, "xdg-open", "") + s.restoreSnapFromSender = userd.MockSnapFromSender(func(*dbus.Conn, dbus.Sender) (string, error) { + return "some-snap", nil + }) +} + +func (s *launcherSuite) TearDownTest(c *C) { + s.mockXdgOpen.Restore() + s.restoreSnapFromSender() +} + +func (s *launcherSuite) TestOpenURLWithNotAllowedScheme(c *C) { + for _, t := range []struct { + url string + errMatcher string + }{ + {"tel://049112233445566", "Supplied URL scheme \"tel\" is not allowed"}, + {"aabbccdd0011", "Supplied URL scheme \"\" is not allowed"}, + {"invälid:%url", dbus.ErrMsgInvalidArg.Error()}, + } { + err := s.launcher.OpenURL(t.url, ":some-dbus-sender") + c.Assert(err, ErrorMatches, t.errMatcher) + c.Assert(s.mockXdgOpen.Calls(), IsNil) + } +} + +func (s *launcherSuite) TestOpenURLWithAllowedSchemeHappy(c *C) { + for _, schema := range []string{"http", "https", "mailto", "snap", "help"} { + err := s.launcher.OpenURL(schema+"://snapcraft.io", ":some-dbus-sender") + c.Assert(err, IsNil) + c.Assert(s.mockXdgOpen.Calls(), DeepEquals, [][]string{ + {"xdg-open", schema + "://snapcraft.io"}, + }) + s.mockXdgOpen.ForgetCalls() + } +} + +func (s *launcherSuite) TestOpenURLWithFailingXdgOpen(c *C) { + cmd := testutil.MockCommand(c, "xdg-open", "false") + defer cmd.Restore() + + err := s.launcher.OpenURL("https://snapcraft.io", ":some-dbus-sender") + c.Assert(err, NotNil) + c.Assert(err, ErrorMatches, "cannot open supplied URL") +} + +func mockUICommands(c *C, script string) (restore func()) { + mock := testutil.MockCommand(c, "zenity", script) + mock.Also("kdialog", script) + + return func() { + mock.Restore() + } +} + +func (s *launcherSuite) TestOpenFileUserAccepts(c *C) { + restore := mockUICommands(c, "true") + defer restore() + + path := filepath.Join(c.MkDir(), "test.txt") + c.Assert(ioutil.WriteFile(path, []byte("Hello world"), 0644), IsNil) + + file, err := os.Open(path) + c.Assert(err, IsNil) + defer file.Close() + + dupFd, err := syscall.Dup(int(file.Fd())) + c.Assert(err, IsNil) + + err = s.launcher.OpenFile("", dbus.UnixFD(dupFd), ":some-dbus-sender") + c.Assert(err, IsNil) + c.Assert(s.mockXdgOpen.Calls(), DeepEquals, [][]string{ + {"xdg-open", path}, + }) +} + +func (s *launcherSuite) TestOpenFileUserDeclines(c *C) { + restore := mockUICommands(c, "false") + defer restore() + + path := filepath.Join(c.MkDir(), "test.txt") + c.Assert(ioutil.WriteFile(path, []byte("Hello world"), 0644), IsNil) + + file, err := os.Open(path) + c.Assert(err, IsNil) + defer file.Close() + + dupFd, err := syscall.Dup(int(file.Fd())) + c.Assert(err, IsNil) + + err = s.launcher.OpenFile("", dbus.UnixFD(dupFd), ":some-dbus-sender") + c.Assert(err, NotNil) + c.Assert(err, ErrorMatches, "permission denied") + c.Assert(s.mockXdgOpen.Calls(), IsNil) +} + +func (s *launcherSuite) TestOpenFileSucceedsWithDirectory(c *C) { + restore := mockUICommands(c, "true") + defer restore() + + dir := c.MkDir() + fd, err := syscall.Open(dir, syscall.O_RDONLY|syscall.O_DIRECTORY, 0755) + c.Assert(err, IsNil) + defer syscall.Close(fd) + + dupFd, err := syscall.Dup(fd) + c.Assert(err, IsNil) + + err = s.launcher.OpenFile("", dbus.UnixFD(dupFd), ":some-dbus-sender") + c.Assert(err, IsNil) + c.Assert(s.mockXdgOpen.Calls(), DeepEquals, [][]string{ + {"xdg-open", dir}, + }) +} + +func (s *launcherSuite) TestOpenFileFailsWithDeviceFile(c *C) { + restore := mockUICommands(c, "true") + defer restore() + + file, err := os.Open("/dev/null") + c.Assert(err, IsNil) + defer file.Close() + + dupFd, err := syscall.Dup(int(file.Fd())) + c.Assert(err, IsNil) + + err = s.launcher.OpenFile("", dbus.UnixFD(dupFd), ":some-dbus-sender") + c.Assert(err, NotNil) + c.Assert(err, ErrorMatches, "cannot open anything other than regular files or directories") + c.Assert(s.mockXdgOpen.Calls(), IsNil) +} + +func (s *launcherSuite) TestOpenFileFailsWithPathDescriptor(c *C) { + restore := mockUICommands(c, "true") + defer restore() + + dir := c.MkDir() + fd, err := syscall.Open(dir, sys.O_PATH, 0755) + c.Assert(err, IsNil) + defer syscall.Close(fd) + + dupFd, err := syscall.Dup(fd) + c.Assert(err, IsNil) + + err = s.launcher.OpenFile("", dbus.UnixFD(dupFd), ":some-dbus-sender") + c.Assert(err, NotNil) + c.Assert(err, ErrorMatches, "cannot use file descriptors opened using O_PATH") + c.Assert(s.mockXdgOpen.Calls(), IsNil) +} diff -Nru snapd-2.40/usersession/userd/settings.go snapd-2.42.1/usersession/userd/settings.go --- snapd-2.40/usersession/userd/settings.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/userd/settings.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,226 @@ +// -*- 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 userd + +import ( + "fmt" + "os/exec" + "path/filepath" + "strings" + "time" + + "github.com/godbus/dbus" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/usersession/userd/ui" +) + +// Timeout when the confirmation dialog for an xdg-setging +// automatically closes. Keep in sync with the core snaps +// xdg-settings wrapper which also sets this value to 300. +var defaultConfirmDialogTimeout = 300 * time.Second + +const settingsIntrospectionXML = ` + + + + + + + + + + + + + + + + + + + + + +` + +// TODO: allow setting default-url-scheme-handler ? +var settingsWhitelist = []string{ + "default-web-browser", +} + +func allowedSetting(setting string) bool { + if !strings.HasSuffix(setting, ".desktop") { + return false + } + base := strings.TrimSuffix(setting, ".desktop") + + return snap.ValidAppName(base) +} + +func settingWhitelisted(setting string) *dbus.Error { + for _, whitelisted := range settingsWhitelist { + if setting == whitelisted { + return nil + } + } + return dbus.MakeFailedError(fmt.Errorf("cannot use setting %q: not allowed", setting)) +} + +// Settings implements the 'io.snapcraft.Settings' DBus interface. +type Settings struct { + conn *dbus.Conn +} + +// Name returns the name of the interface this object implements +func (s *Settings) Name() string { + return "io.snapcraft.Settings" +} + +// BasePath returns the base path of the object +func (s *Settings) BasePath() dbus.ObjectPath { + return "/io/snapcraft/Settings" +} + +// IntrospectionData gives the XML formatted introspection description +// of the DBus service. +func (s *Settings) IntrospectionData() string { + return settingsIntrospectionXML +} + +// some notes: +// - we only set/get desktop files +// - all desktop files of snaps are prefixed with: ${snap}_ +// - on get/check/set we need to add/strip this prefix + +// Check implements the 'Check' method of the 'io.snapcraft.Settings' +// DBus interface. +// +// Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.Check string:'default-web-browser' string:'firefox.desktop' +func (s *Settings) Check(setting, check string, sender dbus.Sender) (string, *dbus.Error) { + // avoid information leak: see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758 + snap, err := snapFromSender(s.conn, sender) + if err != nil { + return "", dbus.MakeFailedError(err) + } + if err := settingWhitelisted(setting); err != nil { + return "", err + } + if !allowedSetting(check) { + return "", dbus.MakeFailedError(fmt.Errorf("cannot check setting %q to value %q: value not allowed", setting, check)) + } + + // FIXME: this works only for desktop files + desktopFile := fmt.Sprintf("%s_%s", snap, check) + + cmd := exec.Command("xdg-settings", "check", setting, desktopFile) + output, err := cmd.CombinedOutput() + if err != nil { + return "", dbus.MakeFailedError(fmt.Errorf("cannot check setting %s: %s", setting, osutil.OutputErr(output, err))) + } + + return strings.TrimSpace(string(output)), nil +} + +// Get implements the 'Get' method of the 'io.snapcraft.Settings' +// DBus interface. +// +// Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.Get string:'default-web-browser' +func (s *Settings) Get(setting string, sender dbus.Sender) (string, *dbus.Error) { + if err := settingWhitelisted(setting); err != nil { + return "", err + } + + cmd := exec.Command("xdg-settings", "get", setting) + output, err := cmd.CombinedOutput() + if err != nil { + return "", dbus.MakeFailedError(fmt.Errorf("cannot get setting %s: %s", setting, osutil.OutputErr(output, err))) + } + + // avoid information leak: see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758 + snap, err := snapFromSender(s.conn, sender) + if err != nil { + return "", dbus.MakeFailedError(err) + } + if !strings.HasPrefix(string(output), snap+"_") { + return "NOT-THIS-SNAP.desktop", nil + } + + desktopFile := strings.SplitN(string(output), "_", 2)[1] + return strings.TrimSpace(desktopFile), nil +} + +// Set implements the 'Set' method of the 'io.snapcraft.Settings' +// DBus interface. +// +// Example usage: dbus-send --session --dest=io.snapcraft.Settings --type=method_call --print-reply /io/snapcraft/Settings io.snapcraft.Settings.Set string:'default-web-browser' string:'chromium-browser.desktop' +func (s *Settings) Set(setting, new string, sender dbus.Sender) *dbus.Error { + if err := settingWhitelisted(setting); err != nil { + return err + } + // see https://github.com/snapcore/snapd/pull/4073#discussion_r146682758 + snap, err := snapFromSender(s.conn, sender) + if err != nil { + return dbus.MakeFailedError(err) + } + + if !allowedSetting(new) { + return dbus.MakeFailedError(fmt.Errorf("cannot set setting %q to value %q: value not allowed", setting, new)) + } + new = fmt.Sprintf("%s_%s", snap, new) + df := filepath.Join(dirs.SnapDesktopFilesDir, new) + if !osutil.FileExists(df) { + return dbus.MakeFailedError(fmt.Errorf("cannot find desktop file %q", df)) + } + + // FIXME: we need to know the parent PID or our dialog may pop under + // the existing windows. We might get it with the help of + // the xdg-settings tool inside the core snap. It would have + // to get the PID of the process asking for the settings + // then xdg-settings can sent this to us and we can intospect + // the X windows for _NET_WM_PID and use the windowID to + // attach to zenity - not sure how this translate to the + // wayland world though :/ + dialog, err := ui.New() + if err != nil { + return dbus.MakeFailedError(fmt.Errorf("cannot ask for settings change: %v", err)) + } + answeredYes := dialog.YesNo( + i18n.G("Allow settings change?"), + fmt.Sprintf(i18n.G("Allow snap %q to change %q to %q ?"), snap, setting, new), + &ui.DialogOptions{ + Timeout: defaultConfirmDialogTimeout, + Footer: i18n.G("This dialog will close automatically after 5 minutes of inactivity."), + }, + ) + if !answeredYes { + return dbus.MakeFailedError(fmt.Errorf("cannot change configuration: user declined change")) + } + + cmd := exec.Command("xdg-settings", "set", setting, new) + output, err := cmd.CombinedOutput() + if err != nil { + return dbus.MakeFailedError(fmt.Errorf("cannot set setting %s: %s", setting, osutil.OutputErr(output, err))) + } + + return nil +} diff -Nru snapd-2.40/usersession/userd/settings_test.go snapd-2.42.1/usersession/userd/settings_test.go --- snapd-2.40/usersession/userd/settings_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/userd/settings_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,220 @@ +// -*- 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 userd_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + "github.com/godbus/dbus" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/usersession/userd" + "github.com/snapcore/snapd/usersession/userd/ui" +) + +type settingsSuite struct { + settings *userd.Settings + + mockXdgSettings *testutil.MockCmd + restoreSnapFromSender func() +} + +var _ = Suite(&settingsSuite{}) + +func (s *settingsSuite) SetUpTest(c *C) { + s.restoreSnapFromSender = userd.MockSnapFromSender(func(*dbus.Conn, dbus.Sender) (string, error) { + return "some-snap", nil + }) + + s.settings = &userd.Settings{} + s.mockXdgSettings = testutil.MockCommand(c, "xdg-settings", ` +if [ "$1" = "get" ] && [ "$2" = "default-web-browser" ]; then + echo "some-snap_foo.desktop" +elif [ "$1" = "check" ] && [ "$2" = "default-web-browser" ] && [ "$3" = "some-snap_foo.desktop" ]; then + echo yes +elif [ "$1" = "check" ] && [ "$2" = "default-web-browser" ]; then + echo no +elif [ "$1" = "set" ] && [ "$2" = "default-web-browser" ]; then + # nothing to do + exit 0 +else + echo "mock called with unsupported arguments $*" + exit 1 +fi +`) +} + +func (s *settingsSuite) TearDownTest(c *C) { + s.mockXdgSettings.Restore() + s.restoreSnapFromSender() +} + +func mockUIcommands(c *C, script string) func() { + mockZenity := testutil.MockCommand(c, "zenity", script) + mockKDialog := testutil.MockCommand(c, "kdialog", script) + return func() { + mockZenity.Restore() + mockKDialog.Restore() + } +} + +func (s *settingsSuite) TestGetUnhappy(c *C) { + for _, t := range []struct { + setting string + errMatcher string + }{ + {"random-setting", `cannot use setting "random-setting": not allowed`}, + {"invälid", `cannot use setting "invälid": not allowed`}, + {"", `cannot use setting "": not allowed`}, + } { + _, err := s.settings.Get(t.setting, ":some-dbus-sender") + c.Assert(err, ErrorMatches, t.errMatcher) + c.Assert(s.mockXdgSettings.Calls(), IsNil) + } +} + +func (s *settingsSuite) TestGetHappy(c *C) { + defaultBrowser, err := s.settings.Get("default-web-browser", ":some-dbus-sender") + c.Assert(err, IsNil) + c.Check(defaultBrowser, Equals, "foo.desktop") + c.Check(s.mockXdgSettings.Calls(), DeepEquals, [][]string{ + {"xdg-settings", "get", "default-web-browser"}, + }) +} + +func (s *settingsSuite) TestCheckInvalidSetting(c *C) { + _, err := s.settings.Check("random-setting", "foo.desktop", ":some-dbus-sender") + c.Assert(err, ErrorMatches, `cannot use setting "random-setting": not allowed`) + c.Assert(s.mockXdgSettings.Calls(), IsNil) +} + +func (s *settingsSuite) TestCheckIsDefault(c *C) { + isDefault, err := s.settings.Check("default-web-browser", "foo.desktop", ":some-dbus-sender") + c.Assert(err, IsNil) + c.Check(isDefault, Equals, "yes") + c.Check(s.mockXdgSettings.Calls(), DeepEquals, [][]string{ + {"xdg-settings", "check", "default-web-browser", "some-snap_foo.desktop"}, + }) +} + +func (s *settingsSuite) TestCheckNoDefault(c *C) { + isDefault, err := s.settings.Check("default-web-browser", "bar.desktop", ":some-dbus-sender") + c.Assert(err, IsNil) + c.Check(isDefault, Equals, "no") + c.Check(s.mockXdgSettings.Calls(), DeepEquals, [][]string{ + {"xdg-settings", "check", "default-web-browser", "some-snap_bar.desktop"}, + }) +} + +func (s *settingsSuite) TestSetInvalidSetting(c *C) { + err := s.settings.Set("random-setting", "foo.desktop", ":some-dbus-sender") + c.Assert(err, ErrorMatches, `cannot use setting "random-setting": not allowed`) + c.Assert(s.mockXdgSettings.Calls(), IsNil) +} + +func (s *settingsSuite) testSetUserDeclined(c *C) { + df := filepath.Join(dirs.SnapDesktopFilesDir, "some-snap_bar.desktop") + err := os.MkdirAll(filepath.Dir(df), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(df, nil, 0644) + c.Assert(err, IsNil) + + err = s.settings.Set("default-web-browser", "bar.desktop", ":some-dbus-sender") + c.Assert(err, ErrorMatches, `cannot change configuration: user declined change`) + c.Check(s.mockXdgSettings.Calls(), IsNil) + // FIXME: this needs PR#4342 + /* + c.Check(mockZenity.Calls(), DeepEquals, [][]string{ + {"zenity", "--question", "--text=Allow changing setting \"default-web-browser\" to \"bar.desktop\" ?"}, + }) + */ +} + +func (s *settingsSuite) TestSetUserDeclinedKDialog(c *C) { + // force zenity exec missing + restoreZenity := ui.MockHasZenityExecutable(func() bool { return false }) + restoreCmds := mockUIcommands(c, "false") + defer func() { + restoreZenity() + restoreCmds() + }() + + s.testSetUserDeclined(c) +} + +func (s *settingsSuite) TestSetUserDeclinedZenity(c *C) { + // force kdialog exec missing + restoreKDialog := ui.MockHasKDialogExecutable(func() bool { return false }) + restoreCmds := mockUIcommands(c, "false") + defer func() { + restoreKDialog() + restoreCmds() + }() + + s.testSetUserDeclined(c) +} + +func (s *settingsSuite) testSetUserAccepts(c *C) { + df := filepath.Join(dirs.SnapDesktopFilesDir, "some-snap_foo.desktop") + err := os.MkdirAll(filepath.Dir(df), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(df, nil, 0644) + c.Assert(err, IsNil) + + err = s.settings.Set("default-web-browser", "foo.desktop", ":some-dbus-sender") + c.Assert(err, IsNil) + c.Check(s.mockXdgSettings.Calls(), DeepEquals, [][]string{ + {"xdg-settings", "set", "default-web-browser", "some-snap_foo.desktop"}, + }) + // FIXME: this needs PR#4342 + /* + c.Check(mockZenity.Calls(), DeepEquals, [][]string{ + {"zenity", "--question", "--text=Allow changing setting \"default-web-browser\" to \"foo.desktop\" ?"}, + }) + */ +} + +func (s *settingsSuite) TestSetUserAcceptsZenity(c *C) { + // force kdialog exec missing + restoreKDialog := ui.MockHasKDialogExecutable(func() bool { return false }) + restoreCmds := mockUIcommands(c, "true") + defer func() { + restoreKDialog() + restoreCmds() + }() + + s.testSetUserAccepts(c) +} + +func (s *settingsSuite) TestSetUserAcceptsKDialog(c *C) { + // force zenity exec missing + restoreZenity := ui.MockHasZenityExecutable(func() bool { return false }) + restoreCmds := mockUIcommands(c, "true") + defer func() { + restoreZenity() + restoreCmds() + }() + + s.testSetUserAccepts(c) +} diff -Nru snapd-2.40/usersession/userd/ui/kdialog.go snapd-2.42.1/usersession/userd/ui/kdialog.go --- snapd-2.40/usersession/userd/ui/kdialog.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/userd/ui/kdialog.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,68 @@ +// -*- 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 ui + +import ( + "fmt" + "html" + "os/exec" + "time" +) + +// KDialog provides a kdialog based UI interface +type KDialog struct{} + +// YesNo asks a yes/no question using kdialog +func (*KDialog) YesNo(primary, secondary string, options *DialogOptions) bool { + if options == nil { + options = &DialogOptions{} + } + + txt := fmt.Sprintf(`

%s

%s

`, html.EscapeString(primary), html.EscapeString(secondary)) + if options.Footer != "" { + txt += fmt.Sprintf(`

%s

`, html.EscapeString(options.Footer)) + } + cmd := exec.Command("kdialog", "--yesno="+txt) + if err := cmd.Start(); err != nil { + return false + } + + var err error + if options.Timeout > 0 { + done := make(chan error, 1) + go func() { done <- cmd.Wait() }() + select { + case err = <-done: + // normal exit + case <-time.After(options.Timeout): + // timeout do nothing, the other side will have timed + // out as well, no need to send a reply. + cmd.Process.Kill() + return false + } + } else { + err = cmd.Wait() + } + if err == nil { + return true + } + + return false +} diff -Nru snapd-2.40/usersession/userd/ui/kdialog_test.go snapd-2.42.1/usersession/userd/ui/kdialog_test.go --- snapd-2.40/usersession/userd/ui/kdialog_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/userd/ui/kdialog_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,84 @@ +// -*- 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 ui_test + +import ( + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/usersession/userd/ui" +) + +type kdialogSuite struct { + mock *testutil.MockCmd +} + +var _ = Suite(&kdialogSuite{}) + +func (s *kdialogSuite) TestYesNoSimpleYes(c *C) { + mock := testutil.MockCommand(c, "kdialog", "") + defer mock.Restore() + + z := &ui.KDialog{} + answeredYes := z.YesNo("primary", "secondary", nil) + c.Check(answeredYes, Equals, true) + c.Check(mock.Calls(), DeepEquals, [][]string{ + {"kdialog", "--yesno=

primary

secondary

"}, + }) +} + +func (s *kdialogSuite) TestYesNoSimpleNo(c *C) { + mock := testutil.MockCommand(c, "kdialog", "false") + defer mock.Restore() + + z := &ui.KDialog{} + answeredYes := z.YesNo("primary", "secondary", nil) + c.Check(answeredYes, Equals, false) + c.Check(mock.Calls(), DeepEquals, [][]string{ + {"kdialog", "--yesno=

primary

secondary

"}, + }) +} + +func (s *kdialogSuite) TestYesNoSimpleFooter(c *C) { + mock := testutil.MockCommand(c, "kdialog", "") + defer mock.Restore() + + z := &ui.KDialog{} + answeredYes := z.YesNo("primary", "secondary", &ui.DialogOptions{Footer: "footer"}) + c.Check(answeredYes, Equals, true) + c.Check(mock.Calls(), DeepEquals, [][]string{ + {"kdialog", "--yesno=

primary

secondary

footer

"}, + }) +} + +func (s *kdialogSuite) TestYesNoSimpleTimeout(c *C) { + mock := testutil.MockCommand(c, "kdialog", "sleep 9999999") + defer mock.Restore() + + z := &ui.KDialog{} + answeredYes := z.YesNo("primary", "secondary", &ui.DialogOptions{Timeout: 1 * time.Second}) + // this got auto-canceled after 1sec + c.Check(answeredYes, Equals, false) + c.Check(mock.Calls(), DeepEquals, [][]string{ + {"kdialog", "--yesno=

primary

secondary

"}, + }) +} diff -Nru snapd-2.40/usersession/userd/ui/ui.go snapd-2.42.1/usersession/userd/ui/ui.go --- snapd-2.40/usersession/userd/ui/ui.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/userd/ui/ui.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,94 @@ +// -*- 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 ui + +import ( + "fmt" + "os" + "strings" + "time" + + "github.com/snapcore/snapd/osutil" +) + +// UI is an interface for user interaction +type UI interface { + // YesNo asks a yes/no question. The primary text + // will be printed in a larger font, the secondary text + // in the standard font and the (optional) footer will + // be printed in a small font. + // + // The value "true" is returned if the user clicks "yes", + // otherwise "false". + YesNo(primary, secondary string, options *DialogOptions) bool +} + +// Options for the UI interface +type DialogOptions struct { + Footer string + Timeout time.Duration +} + +var hasZenityExecutable = func() bool { + return osutil.ExecutableExists("zenity") +} + +var hasKDialogExecutable = func() bool { + return osutil.ExecutableExists("kdialog") +} + +func MockHasZenityExecutable(f func() bool) func() { + oldHasZenityExecutable := hasZenityExecutable + hasZenityExecutable = f + return func() { + hasZenityExecutable = oldHasZenityExecutable + } +} + +func MockHasKDialogExecutable(f func() bool) func() { + oldHasKDialogExecutable := hasKDialogExecutable + hasKDialogExecutable = f + return func() { + hasKDialogExecutable = oldHasKDialogExecutable + } +} + +// New returns the best matching UI interface for the given system +// or an error if no ui can be created. +func New() (UI, error) { + hasZenity := hasZenityExecutable() + hasKDialog := hasKDialogExecutable() + + switch { + case hasZenity && hasKDialog: + // prefer kdialog on KDE, otherwise use zenity + currentDesktop := os.Getenv("XDG_CURRENT_DESKTOP") + if strings.Contains("kde", strings.ToLower(currentDesktop)) { + return &KDialog{}, nil + } + return &Zenity{}, nil + case hasZenity: + return &Zenity{}, nil + case hasKDialog: + return &KDialog{}, nil + default: + return nil, fmt.Errorf("cannot create a UI: please install zenity or kdialog") + } +} diff -Nru snapd-2.40/usersession/userd/ui/zenity.go snapd-2.42.1/usersession/userd/ui/zenity.go --- snapd-2.40/usersession/userd/ui/zenity.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/userd/ui/zenity.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,64 @@ +// -*- 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 ui + +import ( + "fmt" + "os/exec" + "time" +) + +// Zenity provides a zenity based UI interface +type Zenity struct{} + +// YesNo asks a yes/no question using zenity +func (*Zenity) YesNo(primary, secondary string, options *DialogOptions) bool { + if options == nil { + options = &DialogOptions{} + } + + txt := fmt.Sprintf("%s\n\n%s", primary, secondary) + if options.Footer != "" { + txt += fmt.Sprintf("\n\n%s", options.Footer) + } + args := []string{"--question", "--modal", "--text=" + txt} + if options.Timeout > 0 { + args = append(args, fmt.Sprintf("--timeout=%d", int(options.Timeout/time.Second))) + } + // Gtk is not doing a good job with long labels. It will + // create a very narrow and long window (minimal width to fit + // the buttons). So force a bigger width here. See also LP: + // 1778484. The primary number is lower because the header is + // in a bigger font. + if len(primary) > 10 || len(secondary) > 20 { + args = append(args, "--width=500") + } + + cmd := exec.Command("zenity", args...) + if err := cmd.Start(); err != nil { + return false + } + + if err := cmd.Wait(); err != nil { + return false + } + + return true +} diff -Nru snapd-2.40/usersession/userd/ui/zenity_test.go snapd-2.42.1/usersession/userd/ui/zenity_test.go --- snapd-2.40/usersession/userd/ui/zenity_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/userd/ui/zenity_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,102 @@ +// -*- 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 ui_test + +import ( + "time" + + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/usersession/userd/ui" +) + +func Test(t *testing.T) { TestingT(t) } + +type zenitySuite struct { + mock *testutil.MockCmd +} + +var _ = Suite(&zenitySuite{}) + +func (s *zenitySuite) TestYesNoSimpleYes(c *C) { + mock := testutil.MockCommand(c, "zenity", "") + defer mock.Restore() + + z := &ui.Zenity{} + answeredYes := z.YesNo("primary", "secondary", nil) + c.Check(answeredYes, Equals, true) + c.Check(mock.Calls(), DeepEquals, [][]string{ + {"zenity", "--question", "--modal", "--text=primary\n\nsecondary"}, + }) +} + +func (s *zenitySuite) TestYesNoSimpleNo(c *C) { + mock := testutil.MockCommand(c, "zenity", "false") + defer mock.Restore() + + z := &ui.Zenity{} + answeredYes := z.YesNo("primary", "secondary", nil) + c.Check(answeredYes, Equals, false) + c.Check(mock.Calls(), DeepEquals, [][]string{ + {"zenity", "--question", "--modal", "--text=primary\n\nsecondary"}, + }) +} + +func (s *zenitySuite) TestYesNoLong(c *C) { + mock := testutil.MockCommand(c, "zenity", "true") + defer mock.Restore() + + z := &ui.Zenity{} + _ = z.YesNo("01234567890", "01234567890", nil) + c.Check(mock.Calls(), DeepEquals, [][]string{ + {"zenity", "--question", "--modal", "--text=01234567890\n\n01234567890", "--width=500"}, + }) +} + +func (s *zenitySuite) TestYesNoSimpleFooter(c *C) { + mock := testutil.MockCommand(c, "zenity", "") + defer mock.Restore() + + z := &ui.Zenity{} + answeredYes := z.YesNo("primary", "secondary", &ui.DialogOptions{Footer: "footer"}) + c.Check(answeredYes, Equals, true) + c.Check(mock.Calls(), DeepEquals, [][]string{ + {"zenity", "--question", "--modal", `--text=primary + +secondary + +footer`}, + }) +} + +func (s *zenitySuite) TestYesNoSimpleTimeout(c *C) { + mock := testutil.MockCommand(c, "zenity", "true") + defer mock.Restore() + + z := &ui.Zenity{} + answeredYes := z.YesNo("primary", "secondary", &ui.DialogOptions{Timeout: 60 * time.Second}) + c.Check(answeredYes, Equals, true) + c.Check(mock.Calls(), DeepEquals, [][]string{ + {"zenity", "--question", "--modal", "--text=primary\n\nsecondary", "--timeout=60"}, + }) +} diff -Nru snapd-2.40/usersession/userd/userd.go snapd-2.42.1/usersession/userd/userd.go --- snapd-2.40/usersession/userd/userd.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/usersession/userd/userd.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,122 @@ +// -*- 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 userd + +import ( + "fmt" + + "github.com/godbus/dbus" + "github.com/godbus/dbus/introspect" + "gopkg.in/tomb.v2" + + "github.com/snapcore/snapd/logger" +) + +type dbusInterface interface { + Name() string + BasePath() dbus.ObjectPath + IntrospectionData() string +} + +type Userd struct { + tomb tomb.Tomb + conn *dbus.Conn + dbusIfaces []dbusInterface +} + +func dbusSessionBus() (*dbus.Conn, error) { + // use a private connection to the session bus, this way we can manage + // its lifetime without worrying of breaking other code + conn, err := dbus.SessionBusPrivate() + if err != nil { + return nil, err + } + if err := conn.Auth(nil); err != nil { + conn.Close() + return nil, err + } + if err := conn.Hello(); err != nil { + conn.Close() + return nil, err + } + return conn, nil +} + +func (ud *Userd) Init() error { + var err error + + ud.conn, err = dbusSessionBus() + if err != nil { + return err + } + + ud.dbusIfaces = []dbusInterface{ + &Launcher{ud.conn}, + &Settings{ud.conn}, + } + for _, iface := range ud.dbusIfaces { + // export the interfaces at the godbus API level first to avoid + // the race between being able to handle a call to an interface + // at the object level and the actual well-known object name + // becoming available on the bus + xml := "" + iface.IntrospectionData() + introspect.IntrospectDataString + "" + ud.conn.Export(iface, iface.BasePath(), iface.Name()) + ud.conn.Export(introspect.Introspectable(xml), iface.BasePath(), "org.freedesktop.DBus.Introspectable") + + // beyond this point the name is available and all handlers must + // have been set up + reply, err := ud.conn.RequestName(iface.Name(), dbus.NameFlagDoNotQueue) + if err != nil { + return err + } + + if reply != dbus.RequestNameReplyPrimaryOwner { + return fmt.Errorf("cannot obtain bus name '%s'", iface.Name()) + } + } + return nil +} + +func (ud *Userd) Start() { + logger.Noticef("Starting snap userd") + + ud.tomb.Go(func() error { + // Listen to keep our thread up and running. All DBus bits + // are running in the background + select { + case <-ud.tomb.Dying(): + ud.conn.Close() + } + err := ud.tomb.Err() + if err != nil && err != tomb.ErrStillAlive { + return err + } + return nil + }) +} + +func (ud *Userd) Stop() error { + ud.tomb.Kill(nil) + return ud.tomb.Wait() +} + +func (ud *Userd) Dying() <-chan struct{} { + return ud.tomb.Dying() +} diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/auth_anonymous.go snapd-2.42.1/vendor/github.com/godbus/dbus/auth_anonymous.go --- snapd-2.40/vendor/github.com/godbus/dbus/auth_anonymous.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/auth_anonymous.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,16 @@ +package dbus + +// AuthAnonymous returns an Auth that uses the ANONYMOUS mechanism. +func AuthAnonymous() Auth { + return &authAnonymous{} +} + +type authAnonymous struct{} + +func (a *authAnonymous) FirstData() (name, resp []byte, status AuthStatus) { + return []byte("ANONYMOUS"), nil, AuthOk +} + +func (a *authAnonymous) HandleData(data []byte) (resp []byte, status AuthStatus) { + return nil, AuthError +} diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/auth.go snapd-2.42.1/vendor/github.com/godbus/dbus/auth.go --- snapd-2.40/vendor/github.com/godbus/dbus/auth.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/auth.go 2019-10-30 12:17:43.000000000 +0000 @@ -77,7 +77,7 @@ for _, m := range methods { if name, data, status := m.FirstData(); bytes.Equal(v, name) { var ok bool - err = authWriteLine(conn.transport, []byte("AUTH"), []byte(v), data) + err = authWriteLine(conn.transport, []byte("AUTH"), v, data) if err != nil { return err } @@ -116,7 +116,6 @@ return err } go conn.inWorker() - go conn.outWorker() return nil } } diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/call.go snapd-2.42.1/vendor/github.com/godbus/dbus/call.go --- snapd-2.40/vendor/github.com/godbus/dbus/call.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/call.go 2019-10-30 12:17:43.000000000 +0000 @@ -1,9 +1,12 @@ package dbus import ( + "context" "errors" ) +var errSignature = errors.New("dbus: mismatched signature") + // Call represents a pending or completed method call. type Call struct { Destination string @@ -20,9 +23,25 @@ // Holds the response once the call is done. Body []interface{} + + // tracks context and canceler + ctx context.Context + ctxCanceler context.CancelFunc } -var errSignature = errors.New("dbus: mismatched signature") +func (c *Call) Context() context.Context { + if c.ctx == nil { + return context.Background() + } + + return c.ctx +} + +func (c *Call) ContextCancel() { + if c.ctxCanceler != nil { + c.ctxCanceler() + } +} // Store stores the body of the reply into the provided pointers. It returns // an error if the signatures of the body and retvalues don't match, or if @@ -34,3 +53,8 @@ return Store(c.Body, retvalues...) } + +func (c *Call) done() { + c.Done <- c + c.ContextCancel() +} diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/conn_darwin.go snapd-2.42.1/vendor/github.com/godbus/dbus/conn_darwin.go --- snapd-2.40/vendor/github.com/godbus/dbus/conn_darwin.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/conn_darwin.go 2019-10-30 12:17:43.000000000 +0000 @@ -31,3 +31,7 @@ } return defaultSystemBusAddress } + +func tryDiscoverDbusSessionBusAddress() string { + return "" +} diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/conn.go snapd-2.42.1/vendor/github.com/godbus/dbus/conn.go --- snapd-2.40/vendor/github.com/godbus/dbus/conn.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/conn.go 2019-10-30 12:17:43.000000000 +0000 @@ -1,6 +1,7 @@ package dbus import ( + "context" "errors" "io" "os" @@ -14,7 +15,6 @@ systemBusLck sync.Mutex sessionBus *Conn sessionBusLck sync.Mutex - sessionEnvLck sync.Mutex ) // ErrClosed is the error returned by calls on a closed connection. @@ -35,23 +35,13 @@ unixFD bool uuid string - names []string - namesLck sync.RWMutex - - serialLck sync.Mutex - nextSerial uint32 - serialUsed map[uint32]bool - - calls map[uint32]*Call - callsLck sync.RWMutex - - handler Handler - - out chan *Message - closed bool - outLck sync.RWMutex - + handler Handler signalHandler SignalHandler + serialGen SerialGenerator + + names *nameTracker + calls *callTracker + outHandler *outputHandler eavesdropped chan<- *Message eavesdroppedLck sync.Mutex @@ -87,32 +77,31 @@ } func getSessionBusAddress() (string, error) { - sessionEnvLck.Lock() - defer sessionEnvLck.Unlock() - address := os.Getenv("DBUS_SESSION_BUS_ADDRESS") - if address != "" && address != "autolaunch:" { + if address := os.Getenv("DBUS_SESSION_BUS_ADDRESS"); address != "" && address != "autolaunch:" { + return address, nil + + } else if address := tryDiscoverDbusSessionBusAddress(); address != "" { + os.Setenv("DBUS_SESSION_BUS_ADDRESS", address) return address, nil } return getSessionBusPlatformAddress() } // SessionBusPrivate returns a new private connection to the session bus. -func SessionBusPrivate() (*Conn, error) { +func SessionBusPrivate(opts ...ConnOption) (*Conn, error) { address, err := getSessionBusAddress() if err != nil { return nil, err } - return Dial(address) + return Dial(address, opts...) } // SessionBusPrivate returns a new private connection to the session bus. +// +// Deprecated: use SessionBusPrivate with options instead. func SessionBusPrivateHandler(handler Handler, signalHandler SignalHandler) (*Conn, error) { - address, err := getSessionBusAddress() - if err != nil { - return nil, err - } - return DialHandler(address, handler, signalHandler) + return SessionBusPrivate(WithHandler(handler), WithSignalHandler(signalHandler)) } // SystemBus returns a shared connection to the system bus, connecting to it if @@ -145,53 +134,95 @@ } // SystemBusPrivate returns a new private connection to the system bus. -func SystemBusPrivate() (*Conn, error) { - return Dial(getSystemBusPlatformAddress()) +// Note: this connection is not ready to use. One must perform Auth and Hello +// on the connection before it is useable. +func SystemBusPrivate(opts ...ConnOption) (*Conn, error) { + return Dial(getSystemBusPlatformAddress(), opts...) } // SystemBusPrivateHandler returns a new private connection to the system bus, using the provided handlers. +// +// Deprecated: use SystemBusPrivate with options instead. func SystemBusPrivateHandler(handler Handler, signalHandler SignalHandler) (*Conn, error) { - return DialHandler(getSystemBusPlatformAddress(), handler, signalHandler) + return SystemBusPrivate(WithHandler(handler), WithSignalHandler(signalHandler)) } // Dial establishes a new private connection to the message bus specified by address. -func Dial(address string) (*Conn, error) { +func Dial(address string, opts ...ConnOption) (*Conn, error) { tr, err := getTransport(address) if err != nil { return nil, err } - return newConn(tr, newDefaultHandler(), newDefaultSignalHandler()) + return newConn(tr, opts...) } // DialHandler establishes a new private connection to the message bus specified by address, using the supplied handlers. +// +// Deprecated: use Dial with options instead. func DialHandler(address string, handler Handler, signalHandler SignalHandler) (*Conn, error) { - tr, err := getTransport(address) - if err != nil { - return nil, err + return Dial(address, WithSignalHandler(signalHandler)) +} + +// ConnOption is a connection option. +type ConnOption func(conn *Conn) error + +// WithHandler overrides the default handler. +func WithHandler(handler Handler) ConnOption { + return func(conn *Conn) error { + conn.handler = handler + return nil + } +} + +// WithSignalHandler overrides the default signal handler. +func WithSignalHandler(handler SignalHandler) ConnOption { + return func(conn *Conn) error { + conn.signalHandler = handler + return nil + } +} + +// WithSerialGenerator overrides the default signals generator. +func WithSerialGenerator(gen SerialGenerator) ConnOption { + return func(conn *Conn) error { + conn.serialGen = gen + return nil } - return newConn(tr, handler, signalHandler) } // NewConn creates a new private *Conn from an already established connection. -func NewConn(conn io.ReadWriteCloser) (*Conn, error) { - return NewConnHandler(conn, newDefaultHandler(), newDefaultSignalHandler()) +func NewConn(conn io.ReadWriteCloser, opts ...ConnOption) (*Conn, error) { + return newConn(genericTransport{conn}, opts...) } // NewConnHandler creates a new private *Conn from an already established connection, using the supplied handlers. +// +// Deprecated: use NewConn with options instead. func NewConnHandler(conn io.ReadWriteCloser, handler Handler, signalHandler SignalHandler) (*Conn, error) { - return newConn(genericTransport{conn}, handler, signalHandler) + return NewConn(genericTransport{conn}, WithHandler(handler), WithSignalHandler(signalHandler)) } // newConn creates a new *Conn from a transport. -func newConn(tr transport, handler Handler, signalHandler SignalHandler) (*Conn, error) { +func newConn(tr transport, opts ...ConnOption) (*Conn, error) { conn := new(Conn) conn.transport = tr - conn.calls = make(map[uint32]*Call) - conn.out = make(chan *Message, 10) - conn.handler = handler - conn.signalHandler = signalHandler - conn.nextSerial = 1 - conn.serialUsed = map[uint32]bool{0: true} + for _, opt := range opts { + if err := opt(conn); err != nil { + return nil, err + } + } + conn.calls = newCallTracker() + if conn.handler == nil { + conn.handler = NewDefaultHandler() + } + if conn.signalHandler == nil { + conn.signalHandler = NewDefaultSignalHandler() + } + if conn.serialGen == nil { + conn.serialGen = newSerialGenerator() + } + conn.outHandler = &outputHandler{conn: conn} + conn.names = newNameTracker() conn.busObj = conn.Object("org.freedesktop.DBus", "/org/freedesktop/DBus") return conn, nil } @@ -206,18 +237,7 @@ // and the channels passed to Eavesdrop and Signal are closed. This method must // not be called on shared connections. func (conn *Conn) Close() error { - conn.outLck.Lock() - if conn.closed { - // inWorker calls Close on read error, the read error may - // be caused by another caller calling Close to shutdown the - // dbus connection, a double-close scenario we prevent here. - conn.outLck.Unlock() - return nil - } - close(conn.out) - conn.closed = true - conn.outLck.Unlock() - + conn.outHandler.close() if term, ok := conn.signalHandler.(Terminator); ok { term.Terminate() } @@ -251,15 +271,7 @@ // getSerial returns an unused serial. func (conn *Conn) getSerial() uint32 { - conn.serialLck.Lock() - defer conn.serialLck.Unlock() - n := conn.nextSerial - for conn.serialUsed[n] { - n++ - } - conn.serialUsed[n] = true - conn.nextSerial = n + 1 - return n + return conn.serialGen.GetSerial() } // Hello sends the initial org.freedesktop.DBus.Hello call. This method must be @@ -271,10 +283,7 @@ if err != nil { return err } - conn.namesLck.Lock() - conn.names = make([]string, 1) - conn.names[0] = s - conn.namesLck.Unlock() + conn.names.acquireUniqueConnectionName(s) return nil } @@ -283,109 +292,48 @@ func (conn *Conn) inWorker() { for { msg, err := conn.ReadMessage() - if err == nil { - conn.eavesdroppedLck.Lock() - if conn.eavesdropped != nil { - select { - case conn.eavesdropped <- msg: - default: - } - conn.eavesdroppedLck.Unlock() - continue - } - conn.eavesdroppedLck.Unlock() - dest, _ := msg.Headers[FieldDestination].value.(string) - found := false - if dest == "" { - found = true - } else { - conn.namesLck.RLock() - if len(conn.names) == 0 { - found = true - } - for _, v := range conn.names { - if dest == v { - found = true - break - } - } - conn.namesLck.RUnlock() - } - if !found { - // Eavesdropped a message, but no channel for it is registered. - // Ignore it. - continue - } - switch msg.Type { - case TypeMethodReply, TypeError: - serial := msg.Headers[FieldReplySerial].value.(uint32) - conn.callsLck.Lock() - if c, ok := conn.calls[serial]; ok { - if msg.Type == TypeError { - name, _ := msg.Headers[FieldErrorName].value.(string) - c.Err = Error{name, msg.Body} - } else { - c.Body = msg.Body - } - c.Done <- c - conn.serialLck.Lock() - delete(conn.serialUsed, serial) - conn.serialLck.Unlock() - delete(conn.calls, serial) - } - conn.callsLck.Unlock() - case TypeSignal: - iface := msg.Headers[FieldInterface].value.(string) - member := msg.Headers[FieldMember].value.(string) - // as per http://dbus.freedesktop.org/doc/dbus-specification.html , - // sender is optional for signals. - sender, _ := msg.Headers[FieldSender].value.(string) - if iface == "org.freedesktop.DBus" && sender == "org.freedesktop.DBus" { - if member == "NameLost" { - // If we lost the name on the bus, remove it from our - // tracking list. - name, ok := msg.Body[0].(string) - if !ok { - panic("Unable to read the lost name") - } - conn.namesLck.Lock() - for i, v := range conn.names { - if v == name { - conn.names = append(conn.names[:i], - conn.names[i+1:]...) - } - } - conn.namesLck.Unlock() - } else if member == "NameAcquired" { - // If we acquired the name on the bus, add it to our - // tracking list. - name, ok := msg.Body[0].(string) - if !ok { - panic("Unable to read the acquired name") - } - conn.namesLck.Lock() - conn.names = append(conn.names, name) - conn.namesLck.Unlock() - } - } - go conn.handleSignal(msg) - case TypeMethodCall: - go conn.handleCall(msg) + if err != nil { + if _, ok := err.(InvalidMessageError); !ok { + // Some read error occured (usually EOF); we can't really do + // anything but to shut down all stuff and returns errors to all + // pending replies. + conn.Close() + conn.calls.finalizeAllWithError(err) + return } - } else if _, ok := err.(InvalidMessageError); !ok { - // Some read error occured (usually EOF); we can't really do - // anything but to shut down all stuff and returns errors to all - // pending replies. - conn.Close() - conn.callsLck.RLock() - for _, v := range conn.calls { - v.Err = err - v.Done <- v + // invalid messages are ignored + continue + } + conn.eavesdroppedLck.Lock() + if conn.eavesdropped != nil { + select { + case conn.eavesdropped <- msg: + default: } - conn.callsLck.RUnlock() - return + conn.eavesdroppedLck.Unlock() + continue + } + conn.eavesdroppedLck.Unlock() + dest, _ := msg.Headers[FieldDestination].value.(string) + found := dest == "" || + !conn.names.uniqueNameIsKnown() || + conn.names.isKnownName(dest) + if !found { + // Eavesdropped a message, but no channel for it is registered. + // Ignore it. + continue + } + switch msg.Type { + case TypeError: + conn.serialGen.RetireSerial(conn.calls.handleDBusError(msg)) + case TypeMethodReply: + conn.serialGen.RetireSerial(conn.calls.handleReply(msg)) + case TypeSignal: + conn.handleSignal(msg) + case TypeMethodCall: + go conn.handleCall(msg) } - // invalid messages are ignored + } } @@ -395,6 +343,25 @@ // as per http://dbus.freedesktop.org/doc/dbus-specification.html , // sender is optional for signals. sender, _ := msg.Headers[FieldSender].value.(string) + if iface == "org.freedesktop.DBus" && sender == "org.freedesktop.DBus" { + if member == "NameLost" { + // If we lost the name on the bus, remove it from our + // tracking list. + name, ok := msg.Body[0].(string) + if !ok { + panic("Unable to read the lost name") + } + conn.names.loseName(name) + } else if member == "NameAcquired" { + // If we acquired the name on the bus, add it to our + // tracking list. + name, ok := msg.Body[0].(string) + if !ok { + panic("Unable to read the acquired name") + } + conn.names.acquireName(name) + } + } signal := &Signal{ Sender: sender, Path: msg.Headers[FieldPath].value.(ObjectPath), @@ -408,12 +375,7 @@ // connection. The slice is always at least one element long, the first element // being the unique name of the connection. func (conn *Conn) Names() []string { - conn.namesLck.RLock() - // copy the slice so it can't be modified - s := make([]string, len(conn.names)) - copy(s, conn.names) - conn.namesLck.RUnlock() - return s + return conn.names.listKnownNames() } // Object returns the object identified by the given destination name and path. @@ -421,26 +383,17 @@ return &Object{conn, dest, path} } -// outWorker runs in an own goroutine, encoding and sending messages that are -// sent to conn.out. -func (conn *Conn) outWorker() { - for msg := range conn.out { - err := conn.SendMessage(msg) - conn.callsLck.RLock() - if err != nil { - if c := conn.calls[msg.serial]; c != nil { - c.Err = err - c.Done <- c - } - conn.serialLck.Lock() - delete(conn.serialUsed, msg.serial) - conn.serialLck.Unlock() - } else if msg.Type != TypeMethodCall { - conn.serialLck.Lock() - delete(conn.serialUsed, msg.serial) - conn.serialLck.Unlock() - } - conn.callsLck.RUnlock() +func (conn *Conn) sendMessage(msg *Message) { + conn.sendMessageAndIfClosed(msg, func() {}) +} + +func (conn *Conn) sendMessageAndIfClosed(msg *Message, ifClosed func()) { + err := conn.outHandler.sendAndIfClosed(msg, ifClosed) + conn.calls.handleSendError(msg, err) + if err != nil { + conn.serialGen.RetireSerial(msg.serial) + } else if msg.Type != TypeMethodCall { + conn.serialGen.RetireSerial(msg.serial) } } @@ -451,8 +404,21 @@ // once the call is complete. Otherwise, ch is ignored and a Call structure is // returned of which only the Err member is valid. func (conn *Conn) Send(msg *Message, ch chan *Call) *Call { - var call *Call + return conn.send(context.Background(), msg, ch) +} + +// SendWithContext acts like Send but takes a context +func (conn *Conn) SendWithContext(ctx context.Context, msg *Message, ch chan *Call) *Call { + return conn.send(ctx, msg, ch) +} +func (conn *Conn) send(ctx context.Context, msg *Message, ch chan *Call) *Call { + if ctx == nil { + panic("nil context") + } + + var call *Call + ctx, canceler := context.WithCancel(ctx) msg.serial = conn.getSerial() if msg.Type == TypeMethodCall && msg.Flags&FlagNoReplyExpected == 0 { if ch == nil { @@ -468,26 +434,23 @@ call.Method = iface + "." + member call.Args = msg.Body call.Done = ch - conn.callsLck.Lock() - conn.calls[msg.serial] = call - conn.callsLck.Unlock() - conn.outLck.RLock() - if conn.closed { - call.Err = ErrClosed - call.Done <- call - } else { - conn.out <- msg - } - conn.outLck.RUnlock() + call.ctx = ctx + call.ctxCanceler = canceler + conn.calls.track(msg.serial, call) + go func() { + <-ctx.Done() + conn.calls.handleSendError(msg, ctx.Err()) + }() + conn.sendMessageAndIfClosed(msg, func() { + conn.calls.handleSendError(msg, ErrClosed) + canceler() + }) } else { - conn.outLck.RLock() - if conn.closed { + canceler() + call = &Call{Err: nil} + conn.sendMessageAndIfClosed(msg, func() { call = &Call{Err: ErrClosed} - } else { - conn.out <- msg - call = &Call{Err: nil} - } - conn.outLck.RUnlock() + }) } return call } @@ -520,11 +483,7 @@ if len(e.Body) > 0 { msg.Headers[FieldSignature] = MakeVariant(SignatureOf(e.Body...)) } - conn.outLck.RLock() - if !conn.closed { - conn.out <- msg - } - conn.outLck.RUnlock() + conn.sendMessage(msg) } // sendReply creates a method reply message corresponding to the parameters and @@ -542,37 +501,54 @@ if len(values) > 0 { msg.Headers[FieldSignature] = MakeVariant(SignatureOf(values...)) } - conn.outLck.RLock() - if !conn.closed { - conn.out <- msg - } - conn.outLck.RUnlock() + conn.sendMessage(msg) } -func (conn *Conn) defaultSignalAction(fn func(h *defaultSignalHandler, ch chan<- *Signal), ch chan<- *Signal) { - if !isDefaultSignalHandler(conn.signalHandler) { - return - } - handler := conn.signalHandler.(*defaultSignalHandler) - fn(handler, ch) +// AddMatchSignal registers the given match rule to receive broadcast +// signals based on their contents. +func (conn *Conn) AddMatchSignal(options ...MatchOption) error { + options = append([]MatchOption{withMatchType("signal")}, options...) + return conn.busObj.Call( + "org.freedesktop.DBus.AddMatch", 0, + formatMatchOptions(options), + ).Store() +} + +// RemoveMatchSignal removes the first rule that matches previously registered with AddMatchSignal. +func (conn *Conn) RemoveMatchSignal(options ...MatchOption) error { + options = append([]MatchOption{withMatchType("signal")}, options...) + return conn.busObj.Call( + "org.freedesktop.DBus.RemoveMatch", 0, + formatMatchOptions(options), + ).Store() } // Signal registers the given channel to be passed all received signal messages. -// The caller has to make sure that ch is sufficiently buffered; if a message -// arrives when a write to c is not possible, it is discarded. // // Multiple of these channels can be registered at the same time. // // These channels are "overwritten" by Eavesdrop; i.e., if there currently is a // channel for eavesdropped messages, this channel receives all signals, and // none of the channels passed to Signal will receive any signals. +// +// Panics if the signal handler is not a `SignalRegistrar`. func (conn *Conn) Signal(ch chan<- *Signal) { - conn.defaultSignalAction((*defaultSignalHandler).addSignal, ch) + handler, ok := conn.signalHandler.(SignalRegistrar) + if !ok { + panic("cannot use this method with a non SignalRegistrar handler") + } + handler.AddSignal(ch) } // RemoveSignal removes the given channel from the list of the registered channels. +// +// Panics if the signal handler is not a `SignalRegistrar`. func (conn *Conn) RemoveSignal(ch chan<- *Signal) { - conn.defaultSignalAction((*defaultSignalHandler).removeSignal, ch) + handler, ok := conn.signalHandler.(SignalRegistrar) + if !ok { + panic("cannot use this method with a non SignalRegistrar handler") + } + handler.RemoveSignal(ch) } // SupportsUnixFDs returns whether the underlying transport supports passing of @@ -681,3 +657,209 @@ } return "" } + +type outputHandler struct { + conn *Conn + sendLck sync.Mutex + closed struct { + isClosed bool + lck sync.RWMutex + } +} + +func (h *outputHandler) sendAndIfClosed(msg *Message, ifClosed func()) error { + h.closed.lck.RLock() + defer h.closed.lck.RUnlock() + if h.closed.isClosed { + ifClosed() + return nil + } + h.sendLck.Lock() + defer h.sendLck.Unlock() + return h.conn.SendMessage(msg) +} + +func (h *outputHandler) close() { + h.closed.lck.Lock() + defer h.closed.lck.Unlock() + h.closed.isClosed = true +} + +type serialGenerator struct { + lck sync.Mutex + nextSerial uint32 + serialUsed map[uint32]bool +} + +func newSerialGenerator() *serialGenerator { + return &serialGenerator{ + serialUsed: map[uint32]bool{0: true}, + nextSerial: 1, + } +} + +func (gen *serialGenerator) GetSerial() uint32 { + gen.lck.Lock() + defer gen.lck.Unlock() + n := gen.nextSerial + for gen.serialUsed[n] { + n++ + } + gen.serialUsed[n] = true + gen.nextSerial = n + 1 + return n +} + +func (gen *serialGenerator) RetireSerial(serial uint32) { + gen.lck.Lock() + defer gen.lck.Unlock() + delete(gen.serialUsed, serial) +} + +type nameTracker struct { + lck sync.RWMutex + unique string + names map[string]struct{} +} + +func newNameTracker() *nameTracker { + return &nameTracker{names: map[string]struct{}{}} +} +func (tracker *nameTracker) acquireUniqueConnectionName(name string) { + tracker.lck.Lock() + defer tracker.lck.Unlock() + tracker.unique = name +} +func (tracker *nameTracker) acquireName(name string) { + tracker.lck.Lock() + defer tracker.lck.Unlock() + tracker.names[name] = struct{}{} +} +func (tracker *nameTracker) loseName(name string) { + tracker.lck.Lock() + defer tracker.lck.Unlock() + delete(tracker.names, name) +} + +func (tracker *nameTracker) uniqueNameIsKnown() bool { + tracker.lck.RLock() + defer tracker.lck.RUnlock() + return tracker.unique != "" +} +func (tracker *nameTracker) isKnownName(name string) bool { + tracker.lck.RLock() + defer tracker.lck.RUnlock() + _, ok := tracker.names[name] + return ok || name == tracker.unique +} +func (tracker *nameTracker) listKnownNames() []string { + tracker.lck.RLock() + defer tracker.lck.RUnlock() + out := make([]string, 0, len(tracker.names)+1) + out = append(out, tracker.unique) + for k := range tracker.names { + out = append(out, k) + } + return out +} + +type callTracker struct { + calls map[uint32]*Call + lck sync.RWMutex +} + +func newCallTracker() *callTracker { + return &callTracker{calls: map[uint32]*Call{}} +} + +func (tracker *callTracker) track(sn uint32, call *Call) { + tracker.lck.Lock() + tracker.calls[sn] = call + tracker.lck.Unlock() +} + +func (tracker *callTracker) handleReply(msg *Message) uint32 { + serial := msg.Headers[FieldReplySerial].value.(uint32) + tracker.lck.RLock() + _, ok := tracker.calls[serial] + tracker.lck.RUnlock() + if ok { + tracker.finalizeWithBody(serial, msg.Body) + } + return serial +} + +func (tracker *callTracker) handleDBusError(msg *Message) uint32 { + serial := msg.Headers[FieldReplySerial].value.(uint32) + tracker.lck.RLock() + _, ok := tracker.calls[serial] + tracker.lck.RUnlock() + if ok { + name, _ := msg.Headers[FieldErrorName].value.(string) + tracker.finalizeWithError(serial, Error{name, msg.Body}) + } + return serial +} + +func (tracker *callTracker) handleSendError(msg *Message, err error) { + if err == nil { + return + } + tracker.lck.RLock() + _, ok := tracker.calls[msg.serial] + tracker.lck.RUnlock() + if ok { + tracker.finalizeWithError(msg.serial, err) + } +} + +// finalize was the only func that did not strobe Done +func (tracker *callTracker) finalize(sn uint32) { + tracker.lck.Lock() + defer tracker.lck.Unlock() + c, ok := tracker.calls[sn] + if ok { + delete(tracker.calls, sn) + c.ContextCancel() + } +} + +func (tracker *callTracker) finalizeWithBody(sn uint32, body []interface{}) { + tracker.lck.Lock() + c, ok := tracker.calls[sn] + if ok { + delete(tracker.calls, sn) + } + tracker.lck.Unlock() + if ok { + c.Body = body + c.done() + } +} + +func (tracker *callTracker) finalizeWithError(sn uint32, err error) { + tracker.lck.Lock() + c, ok := tracker.calls[sn] + if ok { + delete(tracker.calls, sn) + } + tracker.lck.Unlock() + if ok { + c.Err = err + c.done() + } +} + +func (tracker *callTracker) finalizeAllWithError(err error) { + tracker.lck.Lock() + closedCalls := make([]*Call, 0, len(tracker.calls)) + for sn := range tracker.calls { + closedCalls = append(closedCalls, tracker.calls[sn]) + } + tracker.calls = map[uint32]*Call{} + tracker.lck.Unlock() + for _, call := range closedCalls { + call.Err = err + call.done() + } +} diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/conn_other.go snapd-2.42.1/vendor/github.com/godbus/dbus/conn_other.go --- snapd-2.40/vendor/github.com/godbus/dbus/conn_other.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/conn_other.go 2019-10-30 12:17:43.000000000 +0000 @@ -6,14 +6,18 @@ "bytes" "errors" "fmt" + "io/ioutil" "os" "os/exec" + "os/user" + "path" + "strings" ) -const defaultSystemBusAddress = "unix:path=/var/run/dbus/system_bus_socket" +var execCommand = exec.Command func getSessionBusPlatformAddress() (string, error) { - cmd := exec.Command("dbus-launch") + cmd := execCommand("dbus-launch") b, err := cmd.CombinedOutput() if err != nil { @@ -23,7 +27,7 @@ i := bytes.IndexByte(b, '=') j := bytes.IndexByte(b, '\n') - if i == -1 || j == -1 { + if i == -1 || j == -1 || i > j { return "", errors.New("dbus: couldn't determine address of session bus") } @@ -33,10 +37,57 @@ return addr, nil } -func getSystemBusPlatformAddress() string { - address := os.Getenv("DBUS_SYSTEM_BUS_ADDRESS") - if address != "" { - return fmt.Sprintf("unix:path=%s", address) +// tryDiscoverDbusSessionBusAddress tries to discover an existing dbus session +// and return the value of its DBUS_SESSION_BUS_ADDRESS. +// It tries different techniques employed by different operating systems, +// returning the first valid address it finds, or an empty string. +// +// * /run/user//bus if this exists, it *is* the bus socket. present on +// Ubuntu 18.04 +// * /run/user//dbus-session: if this exists, it can be parsed for the bus +// address. present on Ubuntu 16.04 +// +// See https://dbus.freedesktop.org/doc/dbus-launch.1.html +func tryDiscoverDbusSessionBusAddress() string { + if runtimeDirectory, err := getRuntimeDirectory(); err == nil { + + if runUserBusFile := path.Join(runtimeDirectory, "bus"); fileExists(runUserBusFile) { + // if /run/user//bus exists, that file itself + // *is* the unix socket, so return its path + return fmt.Sprintf("unix:path=%s", runUserBusFile) + } + if runUserSessionDbusFile := path.Join(runtimeDirectory, "dbus-session"); fileExists(runUserSessionDbusFile) { + // if /run/user//dbus-session exists, it's a + // text file // containing the address of the socket, e.g.: + // DBUS_SESSION_BUS_ADDRESS=unix:abstract=/tmp/dbus-E1c73yNqrG + + if f, err := ioutil.ReadFile(runUserSessionDbusFile); err == nil { + fileContent := string(f) + + prefix := "DBUS_SESSION_BUS_ADDRESS=" + + if strings.HasPrefix(fileContent, prefix) { + address := strings.TrimRight(strings.TrimPrefix(fileContent, prefix), "\n\r") + return address + } + } + } + } + return "" +} + +func getRuntimeDirectory() (string, error) { + if currentUser, err := user.Current(); err != nil { + return "", err + } else { + return fmt.Sprintf("/run/user/%s", currentUser.Uid), nil + } +} + +func fileExists(filename string) bool { + if _, err := os.Stat(filename); !os.IsNotExist(err) { + return true + } else { + return false } - return defaultSystemBusAddress } diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/conn_unix.go snapd-2.42.1/vendor/github.com/godbus/dbus/conn_unix.go --- snapd-2.40/vendor/github.com/godbus/dbus/conn_unix.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/conn_unix.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,17 @@ +//+build !windows,!solaris,!darwin + +package dbus + +import ( + "os" +) + +const defaultSystemBusAddress = "unix:path=/var/run/dbus/system_bus_socket" + +func getSystemBusPlatformAddress() string { + address := os.Getenv("DBUS_SYSTEM_BUS_ADDRESS") + if address != "" { + return address + } + return defaultSystemBusAddress +} diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/conn_windows.go snapd-2.42.1/vendor/github.com/godbus/dbus/conn_windows.go --- snapd-2.40/vendor/github.com/godbus/dbus/conn_windows.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/conn_windows.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,15 @@ +//+build windows + +package dbus + +import "os" + +const defaultSystemBusAddress = "tcp:host=127.0.0.1,port=12434" + +func getSystemBusPlatformAddress() string { + address := os.Getenv("DBUS_SYSTEM_BUS_ADDRESS") + if address != "" { + return address + } + return defaultSystemBusAddress +} diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/decoder.go snapd-2.42.1/vendor/github.com/godbus/dbus/decoder.go --- snapd-2.40/vendor/github.com/godbus/dbus/decoder.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/decoder.go 2019-10-30 12:17:43.000000000 +0000 @@ -188,10 +188,23 @@ if depth >= 64 { panic(FormatError("input exceeds container depth limit")) } + sig := s[1:] length := dec.decode("u", depth).(uint32) - v := reflect.MakeSlice(reflect.SliceOf(typeFor(s[1:])), 0, int(length)) + // capacity can be determined only for fixed-size element types + var capacity int + if s := sigByteSize(sig); s != 0 { + capacity = int(length) / s + } + v := reflect.MakeSlice(reflect.SliceOf(typeFor(sig)), 0, capacity) // Even for empty arrays, the correct padding must be included - dec.align(alignment(typeFor(s[1:]))) + align := alignment(typeFor(s[1:])) + if len(s) > 1 && s[1] == '(' { + //Special case for arrays of structs + //structs decode as a slice of interface{} values + //but the dbus alignment does not match this + align = 8 + } + dec.align(align) spos := dec.pos for dec.pos < spos+int(length) { ev := dec.decode(s[1:], depth+1) @@ -220,6 +233,51 @@ } } +// sigByteSize tries to calculates size of the given signature in bytes. +// +// It returns zero when it can't, for example when it contains non-fixed size +// types such as strings, maps and arrays that require reading of the transmitted +// data, for that we would need to implement the unread method for Decoder first. +func sigByteSize(sig string) int { + var total int + for offset := 0; offset < len(sig); { + switch sig[offset] { + case 'y': + total += 1 + offset += 1 + case 'n', 'q': + total += 2 + offset += 1 + case 'b', 'i', 'u', 'h': + total += 4 + offset += 1 + case 'x', 't', 'd': + total += 8 + offset += 1 + case '(': + i := 1 + depth := 1 + for i < len(sig[offset:]) && depth != 0 { + if sig[offset+i] == '(' { + depth++ + } else if sig[offset+i] == ')' { + depth-- + } + i++ + } + s := sigByteSize(sig[offset+1 : offset+i-1]) + if s == 0 { + return 0 + } + total += s + offset += i + default: + return 0 + } + } + return total +} + // A FormatError is an error in the wire format. type FormatError string diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/default_handler.go snapd-2.42.1/vendor/github.com/godbus/dbus/default_handler.go --- snapd-2.40/vendor/github.com/godbus/dbus/default_handler.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/default_handler.go 2019-10-30 12:17:43.000000000 +0000 @@ -18,7 +18,12 @@ return newExportedIntf(methods, true) } -func newDefaultHandler() *defaultHandler { +//NewDefaultHandler returns an instance of the default +//call handler. This is useful if you want to implement only +//one of the two handlers but not both. +// +// Deprecated: this is the default value, don't use it, it will be unexported. +func NewDefaultHandler() *defaultHandler { h := &defaultHandler{ objects: make(map[ObjectPath]*exportedObj), defaultIntf: make(map[string]*exportedIntf), @@ -42,7 +47,7 @@ subpath := make(map[string]struct{}) var xml bytes.Buffer xml.WriteString("") - for obj, _ := range h.objects { + for obj := range h.objects { p := string(path) if p != "/" { p += "/" @@ -52,7 +57,7 @@ subpath[node_name] = struct{}{} } } - for s, _ := range subpath { + for s := range subpath { xml.WriteString("\n\t") } xml.WriteString("\n") @@ -158,6 +163,7 @@ } type exportedObj struct { + mu sync.RWMutex interfaces map[string]*exportedIntf } @@ -165,19 +171,27 @@ if name == "" { return obj, true } + obj.mu.RLock() + defer obj.mu.RUnlock() intf, exists := obj.interfaces[name] return intf, exists } func (obj *exportedObj) AddInterface(name string, iface *exportedIntf) { + obj.mu.Lock() + defer obj.mu.Unlock() obj.interfaces[name] = iface } func (obj *exportedObj) DeleteInterface(name string) { + obj.mu.Lock() + defer obj.mu.Unlock() delete(obj.interfaces, name) } func (obj *exportedObj) LookupMethod(name string) (Method, bool) { + obj.mu.RLock() + defer obj.mu.RUnlock() for _, intf := range obj.interfaces { method, exists := intf.LookupMethod(name) if exists { @@ -214,70 +228,101 @@ return obj.includeSubtree } -func newDefaultSignalHandler() *defaultSignalHandler { +//NewDefaultSignalHandler returns an instance of the default +//signal handler. This is useful if you want to implement only +//one of the two handlers but not both. +// +// Deprecated: this is the default value, don't use it, it will be unexported. +func NewDefaultSignalHandler() *defaultSignalHandler { return &defaultSignalHandler{} } -func isDefaultSignalHandler(handler SignalHandler) bool { - _, ok := handler.(*defaultSignalHandler) - return ok -} - type defaultSignalHandler struct { - sync.RWMutex + mu sync.RWMutex closed bool - signals []chan<- *Signal + signals []*signalChannelData } func (sh *defaultSignalHandler) DeliverSignal(intf, name string, signal *Signal) { - sh.RLock() - defer sh.RUnlock() + sh.mu.RLock() + defer sh.mu.RUnlock() if sh.closed { return } - for _, ch := range sh.signals { - ch <- signal + for _, scd := range sh.signals { + scd.deliver(signal) } } -func (sh *defaultSignalHandler) Init() error { - sh.Lock() - sh.signals = make([]chan<- *Signal, 0) - sh.Unlock() - return nil -} - func (sh *defaultSignalHandler) Terminate() { - sh.Lock() - sh.closed = true - for _, ch := range sh.signals { - close(ch) + sh.mu.Lock() + defer sh.mu.Unlock() + if sh.closed { + return } + + for _, scd := range sh.signals { + scd.close() + close(scd.ch) + } + sh.closed = true sh.signals = nil - sh.Unlock() } -func (sh *defaultSignalHandler) addSignal(ch chan<- *Signal) { - sh.Lock() - defer sh.Unlock() +func (sh *defaultSignalHandler) AddSignal(ch chan<- *Signal) { + sh.mu.Lock() + defer sh.mu.Unlock() if sh.closed { return } - sh.signals = append(sh.signals, ch) - + sh.signals = append(sh.signals, &signalChannelData{ + ch: ch, + done: make(chan struct{}), + }) } -func (sh *defaultSignalHandler) removeSignal(ch chan<- *Signal) { - sh.Lock() - defer sh.Unlock() +func (sh *defaultSignalHandler) RemoveSignal(ch chan<- *Signal) { + sh.mu.Lock() + defer sh.mu.Unlock() if sh.closed { return } for i := len(sh.signals) - 1; i >= 0; i-- { - if ch == sh.signals[i] { + if ch == sh.signals[i].ch { + sh.signals[i].close() copy(sh.signals[i:], sh.signals[i+1:]) sh.signals[len(sh.signals)-1] = nil sh.signals = sh.signals[:len(sh.signals)-1] } } } + +type signalChannelData struct { + wg sync.WaitGroup + ch chan<- *Signal + done chan struct{} +} + +func (scd *signalChannelData) deliver(signal *Signal) { + select { + case scd.ch <- signal: + case <-scd.done: + return + default: + scd.wg.Add(1) + go scd.deferredDeliver(signal) + } +} + +func (scd *signalChannelData) deferredDeliver(signal *Signal) { + select { + case scd.ch <- signal: + case <-scd.done: + } + scd.wg.Done() +} + +func (scd *signalChannelData) close() { + close(scd.done) + scd.wg.Wait() // wait until all spawned goroutines return +} diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/export.go snapd-2.42.1/vendor/github.com/godbus/dbus/export.go --- snapd-2.40/vendor/github.com/godbus/dbus/export.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/export.go 2019-10-30 12:17:43.000000000 +0000 @@ -170,11 +170,8 @@ reply.Body[i] = ret[i] } reply.Headers[FieldSignature] = MakeVariant(SignatureOf(reply.Body...)) - conn.outLck.RLock() - if !conn.closed { - conn.out <- reply - } - conn.outLck.RUnlock() + + conn.sendMessage(reply) } } @@ -207,12 +204,14 @@ if len(values) > 0 { msg.Headers[FieldSignature] = MakeVariant(SignatureOf(values...)) } - conn.outLck.RLock() - defer conn.outLck.RUnlock() - if conn.closed { + + var closed bool + conn.sendMessageAndIfClosed(msg, func() { + closed = true + }) + if closed { return ErrClosed } - conn.out <- msg return nil } diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/go.mod snapd-2.42.1/vendor/github.com/godbus/dbus/go.mod --- snapd-2.40/vendor/github.com/godbus/dbus/go.mod 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/go.mod 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,3 @@ +module github.com/godbus/dbus + +go 1.12 diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/match.go snapd-2.42.1/vendor/github.com/godbus/dbus/match.go --- snapd-2.40/vendor/github.com/godbus/dbus/match.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/match.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,62 @@ +package dbus + +import ( + "strings" +) + +// MatchOption specifies option for dbus routing match rule. Options can be constructed with WithMatch* helpers. +// For full list of available options consult +// https://dbus.freedesktop.org/doc/dbus-specification.html#message-bus-routing-match-rules +type MatchOption struct { + key string + value string +} + +func formatMatchOptions(options []MatchOption) string { + items := make([]string, 0, len(options)) + for _, option := range options { + items = append(items, option.key+"='"+option.value+"'") + } + return strings.Join(items, ",") +} + +// WithMatchOption creates match option with given key and value +func WithMatchOption(key, value string) MatchOption { + return MatchOption{key, value} +} + +// doesn't make sense to export this option because clients can only +// subscribe to messages with signal type. +func withMatchType(typ string) MatchOption { + return WithMatchOption("type", typ) +} + +// WithMatchSender sets sender match option. +func WithMatchSender(sender string) MatchOption { + return WithMatchOption("sender", sender) +} + +// WithMatchSender sets interface match option. +func WithMatchInterface(iface string) MatchOption { + return WithMatchOption("interface", iface) +} + +// WithMatchMember sets member match option. +func WithMatchMember(member string) MatchOption { + return WithMatchOption("member", member) +} + +// WithMatchObjectPath creates match option that filters events based on given path +func WithMatchObjectPath(path ObjectPath) MatchOption { + return WithMatchOption("path", string(path)) +} + +// WithMatchPathNamespace sets path_namespace match option. +func WithMatchPathNamespace(namespace ObjectPath) MatchOption { + return WithMatchOption("path_namespace", string(namespace)) +} + +// WithMatchDestination sets destination match option. +func WithMatchDestination(destination string) MatchOption { + return WithMatchOption("destination", destination) +} diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/object.go snapd-2.42.1/vendor/github.com/godbus/dbus/object.go --- snapd-2.40/vendor/github.com/godbus/dbus/object.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/object.go 2019-10-30 12:17:43.000000000 +0000 @@ -1,6 +1,7 @@ package dbus import ( + "context" "errors" "strings" ) @@ -9,8 +10,13 @@ // invoked. type BusObject interface { Call(method string, flags Flags, args ...interface{}) *Call + CallWithContext(ctx context.Context, method string, flags Flags, args ...interface{}) *Call Go(method string, flags Flags, ch chan *Call, args ...interface{}) *Call + GoWithContext(ctx context.Context, method string, flags Flags, ch chan *Call, args ...interface{}) *Call + AddMatchSignal(iface, member string, options ...MatchOption) *Call + RemoveMatchSignal(iface, member string, options ...MatchOption) *Call GetProperty(p string) (Variant, error) + SetProperty(p string, v interface{}) error Destination() string Path() ObjectPath } @@ -24,16 +30,50 @@ // Call calls a method with (*Object).Go and waits for its reply. func (o *Object) Call(method string, flags Flags, args ...interface{}) *Call { - return <-o.Go(method, flags, make(chan *Call, 1), args...).Done + return <-o.createCall(context.Background(), method, flags, make(chan *Call, 1), args...).Done } -// AddMatchSignal subscribes BusObject to signals from specified interface and -// method (member). -func (o *Object) AddMatchSignal(iface, member string) *Call { - return o.Call( +// CallWithContext acts like Call but takes a context +func (o *Object) CallWithContext(ctx context.Context, method string, flags Flags, args ...interface{}) *Call { + return <-o.createCall(ctx, method, flags, make(chan *Call, 1), args...).Done +} + +// AddMatchSignal subscribes BusObject to signals from specified interface, +// method (member). Additional filter rules can be added via WithMatch* option constructors. +// Note: To filter events by object path you have to specify this path via an option. +// +// Deprecated: use (*Conn) AddMatchSignal instead. +func (o *Object) AddMatchSignal(iface, member string, options ...MatchOption) *Call { + base := []MatchOption{ + withMatchType("signal"), + WithMatchInterface(iface), + WithMatchMember(member), + } + + options = append(base, options...) + return o.conn.BusObject().Call( "org.freedesktop.DBus.AddMatch", 0, - "type='signal',interface='"+iface+"',member='"+member+"'", + formatMatchOptions(options), + ) +} + +// RemoveMatchSignal unsubscribes BusObject from signals from specified interface, +// method (member). Additional filter rules can be added via WithMatch* option constructors +// +// Deprecated: use (*Conn) RemoveMatchSignal instead. +func (o *Object) RemoveMatchSignal(iface, member string, options ...MatchOption) *Call { + base := []MatchOption{ + withMatchType("signal"), + WithMatchInterface(iface), + WithMatchMember(member), + } + + options = append(base, options...) + return o.conn.BusObject().Call( + "org.freedesktop.DBus.RemoveMatch", + 0, + formatMatchOptions(options), ) } @@ -43,11 +83,24 @@ // will be allocated. Otherwise, ch has to be buffered or Go will panic. // // If the flags include FlagNoReplyExpected, ch is ignored and a Call structure -// is returned of which only the Err member is valid. +// is returned with any error in Err and a closed channel in Done containing +// the returned Call as it's one entry. // // If the method parameter contains a dot ('.'), the part before the last dot // specifies the interface on which the method is called. func (o *Object) Go(method string, flags Flags, ch chan *Call, args ...interface{}) *Call { + return o.createCall(context.Background(), method, flags, ch, args...) +} + +// GoWithContext acts like Go but takes a context +func (o *Object) GoWithContext(ctx context.Context, method string, flags Flags, ch chan *Call, args ...interface{}) *Call { + return o.createCall(ctx, method, flags, ch, args...) +} + +func (o *Object) createCall(ctx context.Context, method string, flags Flags, ch chan *Call, args ...interface{}) *Call { + if ctx == nil { + panic("nil context") + } iface := "" i := strings.LastIndex(method, ".") if i != -1 { @@ -71,40 +124,48 @@ } if msg.Flags&FlagNoReplyExpected == 0 { if ch == nil { - ch = make(chan *Call, 10) + ch = make(chan *Call, 1) } else if cap(ch) == 0 { panic("dbus: unbuffered channel passed to (*Object).Go") } + ctx, cancel := context.WithCancel(ctx) call := &Call{ Destination: o.dest, Path: o.path, Method: method, Args: args, Done: ch, + ctxCanceler: cancel, + ctx: ctx, } - o.conn.callsLck.Lock() - o.conn.calls[msg.serial] = call - o.conn.callsLck.Unlock() - o.conn.outLck.RLock() - if o.conn.closed { - call.Err = ErrClosed - call.Done <- call - } else { - o.conn.out <- msg - } - o.conn.outLck.RUnlock() + o.conn.calls.track(msg.serial, call) + o.conn.sendMessageAndIfClosed(msg, func() { + o.conn.calls.handleSendError(msg, ErrClosed) + cancel() + }) + go func() { + <-ctx.Done() + o.conn.calls.handleSendError(msg, ctx.Err()) + }() + return call } - o.conn.outLck.RLock() - defer o.conn.outLck.RUnlock() - if o.conn.closed { - return &Call{Err: ErrClosed} - } - o.conn.out <- msg - return &Call{Err: nil} + done := make(chan *Call, 1) + call := &Call{ + Err: nil, + Done: done, + } + defer func() { + call.Done <- call + close(done) + }() + o.conn.sendMessageAndIfClosed(msg, func() { + call.Err = ErrClosed + }) + return call } -// GetProperty calls org.freedesktop.DBus.Properties.GetProperty on the given +// GetProperty calls org.freedesktop.DBus.Properties.Get on the given // object. The property name must be given in interface.member notation. func (o *Object) GetProperty(p string) (Variant, error) { idx := strings.LastIndex(p, ".") @@ -125,6 +186,20 @@ return result, nil } +// SetProperty calls org.freedesktop.DBus.Properties.Set on the given +// object. The property name must be given in interface.member notation. +func (o *Object) SetProperty(p string, v interface{}) error { + idx := strings.LastIndex(p, ".") + if idx == -1 || idx+1 == len(p) { + return errors.New("dbus: invalid property " + p) + } + + iface := p[:idx] + prop := p[idx+1:] + + return o.Call("org.freedesktop.DBus.Properties.Set", 0, iface, prop, v).Err +} + // Destination returns the destination that calls on (o *Object) are sent to. func (o *Object) Destination() string { return o.dest diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/README.markdown snapd-2.42.1/vendor/github.com/godbus/dbus/README.markdown --- snapd-2.40/vendor/github.com/godbus/dbus/README.markdown 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/README.markdown 2019-10-30 12:17:43.000000000 +0000 @@ -14,7 +14,7 @@ ### Installation -This packages requires Go 1.1. If you installed it and set up your GOPATH, just run: +This packages requires Go 1.7. If you installed it and set up your GOPATH, just run: ``` go get github.com/godbus/dbus diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/server_interfaces.go snapd-2.42.1/vendor/github.com/godbus/dbus/server_interfaces.go --- snapd-2.40/vendor/github.com/godbus/dbus/server_interfaces.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/server_interfaces.go 2019-10-30 12:17:43.000000000 +0000 @@ -77,6 +77,14 @@ DeliverSignal(iface, name string, signal *Signal) } +// SignalRegistrar manages signal delivery channels. +// +// This is an optional set of methods for `SignalHandler`. +type SignalRegistrar interface { + AddSignal(ch chan<- *Signal) + RemoveSignal(ch chan<- *Signal) +} + // A DBusError is used to convert a generic object to a D-Bus error. // // Any custom error mechanism may implement this interface to provide @@ -87,3 +95,13 @@ type DBusError interface { DBusError() (string, []interface{}) } + +// SerialGenerator is responsible for serials generation. +// +// Different approaches for the serial generation can be used, +// maintaining a map guarded with a mutex (the standard way) or +// simply increment an atomic counter. +type SerialGenerator interface { + GetSerial() uint32 + RetireSerial(serial uint32) +} diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/transport_generic.go snapd-2.42.1/vendor/github.com/godbus/dbus/transport_generic.go --- snapd-2.40/vendor/github.com/godbus/dbus/transport_generic.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/transport_generic.go 2019-10-30 12:17:43.000000000 +0000 @@ -11,7 +11,7 @@ func detectEndianness() binary.ByteOrder { var x uint32 = 0x01020304 - if *(*byte)(unsafe.Pointer(&x)) == 0x01 { + if *(*byte)(unsafe.Pointer(&x)) == 0x01 { return binary.BigEndian } return binary.LittleEndian diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/transport_nonce_tcp.go snapd-2.42.1/vendor/github.com/godbus/dbus/transport_nonce_tcp.go --- snapd-2.40/vendor/github.com/godbus/dbus/transport_nonce_tcp.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/transport_nonce_tcp.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,39 @@ +//+build !windows + +package dbus + +import ( + "errors" + "io/ioutil" + "net" +) + +func init() { + transports["nonce-tcp"] = newNonceTcpTransport +} + +func newNonceTcpTransport(keys string) (transport, error) { + host := getKey(keys, "host") + port := getKey(keys, "port") + noncefile := getKey(keys, "noncefile") + if host == "" || port == "" || noncefile == "" { + return nil, errors.New("dbus: unsupported address (must set host, port and noncefile)") + } + protocol, err := tcpFamily(keys) + if err != nil { + return nil, err + } + socket, err := net.Dial(protocol, net.JoinHostPort(host, port)) + if err != nil { + return nil, err + } + b, err := ioutil.ReadFile(noncefile) + if err != nil { + return nil, err + } + _, err = socket.Write(b) + if err != nil { + return nil, err + } + return NewConn(socket) +} diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/transport_tcp.go snapd-2.42.1/vendor/github.com/godbus/dbus/transport_tcp.go --- snapd-2.40/vendor/github.com/godbus/dbus/transport_tcp.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/transport_tcp.go 2019-10-30 12:17:43.000000000 +0000 @@ -1,5 +1,3 @@ -//+build !windows - package dbus import ( diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/transport_unix.go snapd-2.42.1/vendor/github.com/godbus/dbus/transport_unix.go --- snapd-2.40/vendor/github.com/godbus/dbus/transport_unix.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/transport_unix.go 2019-10-30 12:17:43.000000000 +0000 @@ -31,6 +31,7 @@ type unixTransport struct { *net.UnixConn + rdr *oobReader hasUnixFDs bool } @@ -79,10 +80,15 @@ // To be sure that all bytes of out-of-band data are read, we use a special // reader that uses ReadUnix on the underlying connection instead of Read // and gathers the out-of-band data in a buffer. - rd := &oobReader{conn: t.UnixConn} + if t.rdr == nil { + t.rdr = &oobReader{conn: t.UnixConn} + } else { + t.rdr.oob = nil + } + // read the first 16 bytes (the part of the header that has a constant size), // from which we can figure out the length of the rest of the message - if _, err := io.ReadFull(rd, csheader[:]); err != nil { + if _, err := io.ReadFull(t.rdr, csheader[:]); err != nil { return nil, err } switch csheader[0] { @@ -104,7 +110,7 @@ // decode headers and look for unix fds headerdata := make([]byte, hlen+4) copy(headerdata, csheader[12:]) - if _, err := io.ReadFull(t, headerdata[4:]); err != nil { + if _, err := io.ReadFull(t.rdr, headerdata[4:]); err != nil { return nil, err } dec := newDecoder(bytes.NewBuffer(headerdata), order) @@ -122,7 +128,7 @@ all := make([]byte, 16+hlen+blen) copy(all, csheader[:]) copy(all[16:], headerdata[4:]) - if _, err := io.ReadFull(rd, all[16+hlen:]); err != nil { + if _, err := io.ReadFull(t.rdr, all[16+hlen:]); err != nil { return nil, err } if unixfds != 0 { @@ -130,7 +136,7 @@ return nil, errors.New("dbus: got unix fds on unsupported transport") } // read the fds from the OOB data - scms, err := syscall.ParseSocketControlMessage(rd.oob) + scms, err := syscall.ParseSocketControlMessage(t.rdr.oob) if err != nil { return nil, err } @@ -148,11 +154,23 @@ // substitute the values in the message body (which are indices for the // array receiver via OOB) with the actual values for i, v := range msg.Body { - if j, ok := v.(UnixFDIndex); ok { + switch v.(type) { + case UnixFDIndex: + j := v.(UnixFDIndex) if uint32(j) >= unixfds { return nil, InvalidMessageError("invalid index for unix fd") } msg.Body[i] = UnixFD(fds[j]) + case []UnixFDIndex: + idxArray := v.([]UnixFDIndex) + fdArray := make([]UnixFD, len(idxArray)) + for k, j := range idxArray { + if uint32(j) >= unixfds { + return nil, InvalidMessageError("invalid index for unix fd") + } + fdArray[k] = UnixFD(fds[j]) + } + msg.Body[i] = fdArray } } return msg, nil @@ -185,7 +203,7 @@ } } else { if err := msg.EncodeTo(t, nativeEndian); err != nil { - return nil + return err } } return nil diff -Nru snapd-2.40/vendor/github.com/godbus/dbus/variant_lexer.go snapd-2.42.1/vendor/github.com/godbus/dbus/variant_lexer.go --- snapd-2.40/vendor/github.com/godbus/dbus/variant_lexer.go 2017-08-23 09:04:17.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/godbus/dbus/variant_lexer.go 2019-10-30 12:17:43.000000000 +0000 @@ -51,7 +51,7 @@ } func (l *varLexer) accept(valid string) bool { - if strings.IndexRune(valid, l.next()) >= 0 { + if strings.ContainsRune(valid, l.next()) { return true } l.backup() @@ -214,17 +214,17 @@ digits = "01234567" } } - for strings.IndexRune(digits, l.next()) >= 0 { + for strings.ContainsRune(digits, l.next()) { } l.backup() if l.accept(".") { - for strings.IndexRune(digits, l.next()) >= 0 { + for strings.ContainsRune(digits, l.next()) { } l.backup() } if l.accept("eE") { l.accept("+-") - for strings.IndexRune("0123456789", l.next()) >= 0 { + for strings.ContainsRune("0123456789", l.next()) { } l.backup() } diff -Nru snapd-2.40/vendor/github.com/gorilla/context/context.go snapd-2.42.1/vendor/github.com/gorilla/context/context.go --- snapd-2.40/vendor/github.com/gorilla/context/context.go 2016-08-30 12:27:35.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/context/context.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,143 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -package context - -import ( - "net/http" - "sync" - "time" -) - -var ( - mutex sync.RWMutex - data = make(map[*http.Request]map[interface{}]interface{}) - datat = make(map[*http.Request]int64) -) - -// Set stores a value for a given key in a given request. -func Set(r *http.Request, key, val interface{}) { - mutex.Lock() - if data[r] == nil { - data[r] = make(map[interface{}]interface{}) - datat[r] = time.Now().Unix() - } - data[r][key] = val - mutex.Unlock() -} - -// Get returns a value stored for a given key in a given request. -func Get(r *http.Request, key interface{}) interface{} { - mutex.RLock() - if ctx := data[r]; ctx != nil { - value := ctx[key] - mutex.RUnlock() - return value - } - mutex.RUnlock() - return nil -} - -// GetOk returns stored value and presence state like multi-value return of map access. -func GetOk(r *http.Request, key interface{}) (interface{}, bool) { - mutex.RLock() - if _, ok := data[r]; ok { - value, ok := data[r][key] - mutex.RUnlock() - return value, ok - } - mutex.RUnlock() - return nil, false -} - -// GetAll returns all stored values for the request as a map. Nil is returned for invalid requests. -func GetAll(r *http.Request) map[interface{}]interface{} { - mutex.RLock() - if context, ok := data[r]; ok { - result := make(map[interface{}]interface{}, len(context)) - for k, v := range context { - result[k] = v - } - mutex.RUnlock() - return result - } - mutex.RUnlock() - return nil -} - -// GetAllOk returns all stored values for the request as a map and a boolean value that indicates if -// the request was registered. -func GetAllOk(r *http.Request) (map[interface{}]interface{}, bool) { - mutex.RLock() - context, ok := data[r] - result := make(map[interface{}]interface{}, len(context)) - for k, v := range context { - result[k] = v - } - mutex.RUnlock() - return result, ok -} - -// Delete removes a value stored for a given key in a given request. -func Delete(r *http.Request, key interface{}) { - mutex.Lock() - if data[r] != nil { - delete(data[r], key) - } - mutex.Unlock() -} - -// Clear removes all values stored for a given request. -// -// This is usually called by a handler wrapper to clean up request -// variables at the end of a request lifetime. See ClearHandler(). -func Clear(r *http.Request) { - mutex.Lock() - clear(r) - mutex.Unlock() -} - -// clear is Clear without the lock. -func clear(r *http.Request) { - delete(data, r) - delete(datat, r) -} - -// Purge removes request data stored for longer than maxAge, in seconds. -// It returns the amount of requests removed. -// -// If maxAge <= 0, all request data is removed. -// -// This is only used for sanity check: in case context cleaning was not -// properly set some request data can be kept forever, consuming an increasing -// amount of memory. In case this is detected, Purge() must be called -// periodically until the problem is fixed. -func Purge(maxAge int) int { - mutex.Lock() - count := 0 - if maxAge <= 0 { - count = len(data) - data = make(map[*http.Request]map[interface{}]interface{}) - datat = make(map[*http.Request]int64) - } else { - min := time.Now().Unix() - int64(maxAge) - for r := range data { - if datat[r] < min { - clear(r) - count++ - } - } - } - mutex.Unlock() - return count -} - -// ClearHandler wraps an http.Handler and clears request values at the end -// of a request lifetime. -func ClearHandler(h http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - defer Clear(r) - h.ServeHTTP(w, r) - }) -} diff -Nru snapd-2.40/vendor/github.com/gorilla/context/doc.go snapd-2.42.1/vendor/github.com/gorilla/context/doc.go --- snapd-2.40/vendor/github.com/gorilla/context/doc.go 2016-08-30 12:27:35.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/context/doc.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,82 +0,0 @@ -// Copyright 2012 The Gorilla Authors. All rights reserved. -// Use of this source code is governed by a BSD-style -// license that can be found in the LICENSE file. - -/* -Package context stores values shared during a request lifetime. - -For example, a router can set variables extracted from the URL and later -application handlers can access those values, or it can be used to store -sessions values to be saved at the end of a request. There are several -others common uses. - -The idea was posted by Brad Fitzpatrick to the go-nuts mailing list: - - http://groups.google.com/group/golang-nuts/msg/e2d679d303aa5d53 - -Here's the basic usage: first define the keys that you will need. The key -type is interface{} so a key can be of any type that supports equality. -Here we define a key using a custom int type to avoid name collisions: - - package foo - - import ( - "github.com/gorilla/context" - ) - - type key int - - const MyKey key = 0 - -Then set a variable. Variables are bound to an http.Request object, so you -need a request instance to set a value: - - context.Set(r, MyKey, "bar") - -The application can later access the variable using the same key you provided: - - func MyHandler(w http.ResponseWriter, r *http.Request) { - // val is "bar". - val := context.Get(r, foo.MyKey) - - // returns ("bar", true) - val, ok := context.GetOk(r, foo.MyKey) - // ... - } - -And that's all about the basic usage. We discuss some other ideas below. - -Any type can be stored in the context. To enforce a given type, make the key -private and wrap Get() and Set() to accept and return values of a specific -type: - - type key int - - const mykey key = 0 - - // GetMyKey returns a value for this package from the request values. - func GetMyKey(r *http.Request) SomeType { - if rv := context.Get(r, mykey); rv != nil { - return rv.(SomeType) - } - return nil - } - - // SetMyKey sets a value for this package in the request values. - func SetMyKey(r *http.Request, val SomeType) { - context.Set(r, mykey, val) - } - -Variables must be cleared at the end of a request, to remove all values -that were stored. This can be done in an http.Handler, after a request was -served. Just call Clear() passing the request: - - context.Clear(r) - -...or use ClearHandler(), which conveniently wraps an http.Handler to clear -variables at the end of a request lifetime. - -The Routers from the packages gorilla/mux and gorilla/pat call Clear() -so if you are using either of them you don't need to clear the context manually. -*/ -package context diff -Nru snapd-2.40/vendor/github.com/gorilla/context/LICENSE snapd-2.42.1/vendor/github.com/gorilla/context/LICENSE --- snapd-2.40/vendor/github.com/gorilla/context/LICENSE 2016-08-30 12:27:35.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/context/LICENSE 1970-01-01 00:00:00.000000000 +0000 @@ -1,27 +0,0 @@ -Copyright (c) 2012 Rodrigo Moraes. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are -met: - - * Redistributions of source code must retain the above copyright -notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above -copyright notice, this list of conditions and the following disclaimer -in the documentation and/or other materials provided with the -distribution. - * Neither the name of Google Inc. nor the names of its -contributors may be used to endorse or promote products derived from -this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff -Nru snapd-2.40/vendor/github.com/gorilla/context/README.md snapd-2.42.1/vendor/github.com/gorilla/context/README.md --- snapd-2.40/vendor/github.com/gorilla/context/README.md 2016-08-30 12:27:35.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/context/README.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -context -======= -[![Build Status](https://travis-ci.org/gorilla/context.png?branch=master)](https://travis-ci.org/gorilla/context) - -gorilla/context is a general purpose registry for global request variables. - -Read the full documentation here: http://www.gorillatoolkit.org/pkg/context diff -Nru snapd-2.40/vendor/github.com/gorilla/mux/AUTHORS snapd-2.42.1/vendor/github.com/gorilla/mux/AUTHORS --- snapd-2.40/vendor/github.com/gorilla/mux/AUTHORS 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/mux/AUTHORS 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,8 @@ +# This is the official list of gorilla/mux authors for copyright purposes. +# +# Please keep the list sorted. + +Google LLC (https://opensource.google.com/) +Kamil Kisielk +Matt Silverlock +Rodrigo Moraes (https://github.com/moraes) diff -Nru snapd-2.40/vendor/github.com/gorilla/mux/context.go snapd-2.42.1/vendor/github.com/gorilla/mux/context.go --- snapd-2.40/vendor/github.com/gorilla/mux/context.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/mux/context.go 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,18 @@ +package mux + +import ( + "context" + "net/http" +) + +func contextGet(r *http.Request, key interface{}) interface{} { + return r.Context().Value(key) +} + +func contextSet(r *http.Request, key, val interface{}) *http.Request { + if val == nil { + return r + } + + return r.WithContext(context.WithValue(r.Context(), key, val)) +} diff -Nru snapd-2.40/vendor/github.com/gorilla/mux/doc.go snapd-2.42.1/vendor/github.com/gorilla/mux/doc.go --- snapd-2.40/vendor/github.com/gorilla/mux/doc.go 2017-09-07 12:49:06.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/mux/doc.go 2019-10-17 12:41:14.000000000 +0000 @@ -12,8 +12,8 @@ * Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers. - * URL hosts and paths can have variables with an optional regular - expression. + * URL hosts, paths and query values can have variables with an optional + regular expression. * Registered URLs can be built, or "reversed", which helps maintaining references to resources. * Routes can be used as subrouters: nested routes are only tested if the @@ -47,12 +47,21 @@ r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler) r.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) +Groups can be used inside patterns, as long as they are non-capturing (?:re). For example: + + r.HandleFunc("/articles/{category}/{sort:(?:asc|desc|new)}", ArticlesCategoryHandler) + The names are used to create a map of route variables which can be retrieved calling mux.Vars(): vars := mux.Vars(request) category := vars["category"] +Note that if any capturing groups are present, mux will panic() during parsing. To prevent +this, convert any capturing groups to non-capturing, e.g. change "/{sort:(asc|desc)}" to +"/{sort:(?:asc|desc)}". This is a change from prior versions which behaved unpredictably +when capturing groups were present. + And this is all you need to know about the basic usage. More advanced options are explained below. @@ -136,6 +145,31 @@ // "/products/{key}/details" s.HandleFunc("/{key}/details", ProductDetailsHandler) +Note that the path provided to PathPrefix() represents a "wildcard": calling +PathPrefix("/static/").Handler(...) means that the handler will be passed any +request that matches "/static/*". This makes it easy to serve static files with mux: + + func main() { + var dir string + + flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") + flag.Parse() + r := mux.NewRouter() + + // This will serve files under http://localhost:8000/static/ + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) + + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) + } + Now let's see how to build registered URLs. Routes can be named. All routes that define a name can have their URLs built, @@ -154,18 +188,20 @@ "/articles/technology/42" -This also works for host variables: +This also works for host and query value variables: r := mux.NewRouter() r.Host("{subdomain}.domain.com"). Path("/articles/{category}/{id:[0-9]+}"). + Queries("filter", "{filter}"). HandlerFunc(ArticleHandler). Name("article") - // url.String() will be "http://news.domain.com/articles/technology/42" + // url.String() will be "http://news.domain.com/articles/technology/42?filter=gorilla" url, err := r.Get("article").URL("subdomain", "news", "category", "technology", - "id", "42") + "id", "42", + "filter", "gorilla") All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a @@ -202,5 +238,69 @@ url, err := r.Get("article").URL("subdomain", "news", "category", "technology", "id", "42") + +Mux supports the addition of middlewares to a Router, which are executed in the order they are added if a match is found, including its subrouters. Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or ResponseWriter hijacking. + + type MiddlewareFunc func(http.Handler) http.Handler + +Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc (closures can access variables from the context where they are created). + +A very basic middleware which logs the URI of the request being handled could be written as: + + func simpleMw(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println(r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) + } + +Middlewares can be added to a router using `Router.Use()`: + + r := mux.NewRouter() + r.HandleFunc("/", handler) + r.Use(simpleMw) + +A more complex authentication middleware, which maps session token to users, could be written as: + + // Define our struct + type authenticationMiddleware struct { + tokenUsers map[string]string + } + + // Initialize it somewhere + func (amw *authenticationMiddleware) Populate() { + amw.tokenUsers["00000000"] = "user0" + amw.tokenUsers["aaaaaaaa"] = "userA" + amw.tokenUsers["05f717e5"] = "randomUser" + amw.tokenUsers["deadbeef"] = "user0" + } + + // Middleware function, which will be called for each request + func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Session-Token") + + if user, found := amw.tokenUsers[token]; found { + // We found the token in our map + log.Printf("Authenticated user %s\n", user) + next.ServeHTTP(w, r) + } else { + http.Error(w, "Forbidden", http.StatusForbidden) + } + }) + } + + r := mux.NewRouter() + r.HandleFunc("/", handler) + + amw := authenticationMiddleware{tokenUsers: make(map[string]string)} + amw.Populate() + + r.Use(amw.Middleware) + +Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. + */ package mux diff -Nru snapd-2.40/vendor/github.com/gorilla/mux/go.mod snapd-2.42.1/vendor/github.com/gorilla/mux/go.mod --- snapd-2.40/vendor/github.com/gorilla/mux/go.mod 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/mux/go.mod 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1 @@ +module github.com/gorilla/mux diff -Nru snapd-2.40/vendor/github.com/gorilla/mux/LICENSE snapd-2.42.1/vendor/github.com/gorilla/mux/LICENSE --- snapd-2.40/vendor/github.com/gorilla/mux/LICENSE 2016-08-30 12:27:37.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/mux/LICENSE 2019-10-17 12:41:14.000000000 +0000 @@ -1,4 +1,4 @@ -Copyright (c) 2012 Rodrigo Moraes. All rights reserved. +Copyright (c) 2012-2018 The Gorilla Authors. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are diff -Nru snapd-2.40/vendor/github.com/gorilla/mux/middleware.go snapd-2.42.1/vendor/github.com/gorilla/mux/middleware.go --- snapd-2.40/vendor/github.com/gorilla/mux/middleware.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/mux/middleware.go 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,79 @@ +package mux + +import ( + "net/http" + "strings" +) + +// MiddlewareFunc is a function which receives an http.Handler and returns another http.Handler. +// Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed +// to it, and then calls the handler passed as parameter to the MiddlewareFunc. +type MiddlewareFunc func(http.Handler) http.Handler + +// middleware interface is anything which implements a MiddlewareFunc named Middleware. +type middleware interface { + Middleware(handler http.Handler) http.Handler +} + +// Middleware allows MiddlewareFunc to implement the middleware interface. +func (mw MiddlewareFunc) Middleware(handler http.Handler) http.Handler { + return mw(handler) +} + +// Use appends a MiddlewareFunc to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. +func (r *Router) Use(mwf ...MiddlewareFunc) { + for _, fn := range mwf { + r.middlewares = append(r.middlewares, fn) + } +} + +// useInterface appends a middleware to the chain. Middleware can be used to intercept or otherwise modify requests and/or responses, and are executed in the order that they are applied to the Router. +func (r *Router) useInterface(mw middleware) { + r.middlewares = append(r.middlewares, mw) +} + +// CORSMethodMiddleware automatically sets the Access-Control-Allow-Methods response header +// on requests for routes that have an OPTIONS method matcher to all the method matchers on +// the route. Routes that do not explicitly handle OPTIONS requests will not be processed +// by the middleware. See examples for usage. +func CORSMethodMiddleware(r *Router) MiddlewareFunc { + return func(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + allMethods, err := getAllMethodsForRoute(r, req) + if err == nil { + for _, v := range allMethods { + if v == http.MethodOptions { + w.Header().Set("Access-Control-Allow-Methods", strings.Join(allMethods, ",")) + } + } + } + + next.ServeHTTP(w, req) + }) + } +} + +// getAllMethodsForRoute returns all the methods from method matchers matching a given +// request. +func getAllMethodsForRoute(r *Router, req *http.Request) ([]string, error) { + var allMethods []string + + err := r.Walk(func(route *Route, _ *Router, _ []*Route) error { + for _, m := range route.matchers { + if _, ok := m.(*routeRegexp); ok { + if m.Match(req, &RouteMatch{}) { + methods, err := route.GetMethods() + if err != nil { + return err + } + + allMethods = append(allMethods, methods...) + } + break + } + } + return nil + }) + + return allMethods, err +} diff -Nru snapd-2.40/vendor/github.com/gorilla/mux/mux.go snapd-2.42.1/vendor/github.com/gorilla/mux/mux.go --- snapd-2.40/vendor/github.com/gorilla/mux/mux.go 2017-09-07 12:49:06.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/mux/mux.go 2019-10-17 12:41:14.000000000 +0000 @@ -10,13 +10,19 @@ "net/http" "path" "regexp" +) - "github.com/gorilla/context" +var ( + // ErrMethodMismatch is returned when the method in the request does not match + // the method defined against the route. + ErrMethodMismatch = errors.New("method is not allowed") + // ErrNotFound is returned when no route match is found. + ErrNotFound = errors.New("no matching route was found") ) // NewRouter returns a new router instance. func NewRouter() *Router { - return &Router{namedRoutes: make(map[string]*Route), KeepContext: false} + return &Router{namedRoutes: make(map[string]*Route)} } // Router registers routes to be matched and dispatches a handler. @@ -40,31 +46,125 @@ type Router struct { // Configurable Handler to be used when no route matches. NotFoundHandler http.Handler - // Parent route, if this is a subrouter. - parent parentRoute + + // Configurable Handler to be used when the request method does not match the route. + MethodNotAllowedHandler http.Handler + // Routes to be matched, in order. routes []*Route + // Routes by name for URL building. namedRoutes map[string]*Route - // See Router.StrictSlash(). This defines the flag for new routes. - strictSlash bool - // If true, do not clear the request context after handling the request + + // If true, do not clear the request context after handling the request. + // + // Deprecated: No effect when go1.7+ is used, since the context is stored + // on the request itself. KeepContext bool + + // Slice of middlewares to be called after a match is found + middlewares []middleware + + // configuration shared with `Route` + routeConf } -// Match matches registered routes against the request. +// common route configuration shared between `Router` and `Route` +type routeConf struct { + // If true, "/path/foo%2Fbar/to" will match the path "/path/{var}/to" + useEncodedPath bool + + // If true, when the path pattern is "/path/", accessing "/path" will + // redirect to the former and vice versa. + strictSlash bool + + // If true, when the path pattern is "/path//to", accessing "/path//to" + // will not redirect + skipClean bool + + // Manager for the variables from host and path. + regexp routeRegexpGroup + + // List of matchers. + matchers []matcher + + // The scheme used when building URLs. + buildScheme string + + buildVarsFunc BuildVarsFunc +} + +// returns an effective deep copy of `routeConf` +func copyRouteConf(r routeConf) routeConf { + c := r + + if r.regexp.path != nil { + c.regexp.path = copyRouteRegexp(r.regexp.path) + } + + if r.regexp.host != nil { + c.regexp.host = copyRouteRegexp(r.regexp.host) + } + + c.regexp.queries = make([]*routeRegexp, 0, len(r.regexp.queries)) + for _, q := range r.regexp.queries { + c.regexp.queries = append(c.regexp.queries, copyRouteRegexp(q)) + } + + c.matchers = make([]matcher, 0, len(r.matchers)) + for _, m := range r.matchers { + c.matchers = append(c.matchers, m) + } + + return c +} + +func copyRouteRegexp(r *routeRegexp) *routeRegexp { + c := *r + return &c +} + +// Match attempts to match the given request against the router's registered routes. +// +// If the request matches a route of this router or one of its subrouters the Route, +// Handler, and Vars fields of the the match argument are filled and this function +// returns true. +// +// If the request does not match any of this router's or its subrouters' routes +// then this function returns false. If available, a reason for the match failure +// will be filled in the match argument's MatchErr field. If the match failure type +// (eg: not found) has a registered handler, the handler is assigned to the Handler +// field of the match argument. func (r *Router) Match(req *http.Request, match *RouteMatch) bool { for _, route := range r.routes { if route.Match(req, match) { + // Build middleware chain if no error was found + if match.MatchErr == nil { + for i := len(r.middlewares) - 1; i >= 0; i-- { + match.Handler = r.middlewares[i].Middleware(match.Handler) + } + } return true } } + if match.MatchErr == ErrMethodMismatch { + if r.MethodNotAllowedHandler != nil { + match.Handler = r.MethodNotAllowedHandler + return true + } + + return false + } + // Closest match for a router (includes sub-routers) if r.NotFoundHandler != nil { match.Handler = r.NotFoundHandler + match.MatchErr = ErrNotFound return true } + + match.MatchErr = ErrNotFound return false } @@ -73,57 +173,71 @@ // When there is a match, the route variables can be retrieved calling // mux.Vars(request). func (r *Router) ServeHTTP(w http.ResponseWriter, req *http.Request) { - // Clean path to canonical form and redirect. - if p := cleanPath(req.URL.Path); p != req.URL.Path { - - // Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query. - // This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue: - // http://code.google.com/p/go/issues/detail?id=5252 - url := *req.URL - url.Path = p - p = url.String() - - w.Header().Set("Location", p) - w.WriteHeader(http.StatusMovedPermanently) - return + if !r.skipClean { + path := req.URL.Path + if r.useEncodedPath { + path = req.URL.EscapedPath() + } + // Clean path to canonical form and redirect. + if p := cleanPath(path); p != path { + + // Added 3 lines (Philip Schlump) - It was dropping the query string and #whatever from query. + // This matches with fix in go 1.2 r.c. 4 for same problem. Go Issue: + // http://code.google.com/p/go/issues/detail?id=5252 + url := *req.URL + url.Path = p + p = url.String() + + w.Header().Set("Location", p) + w.WriteHeader(http.StatusMovedPermanently) + return + } } var match RouteMatch var handler http.Handler if r.Match(req, &match) { handler = match.Handler - setVars(req, match.Vars) - setCurrentRoute(req, match.Route) + req = setVars(req, match.Vars) + req = setCurrentRoute(req, match.Route) } + + if handler == nil && match.MatchErr == ErrMethodMismatch { + handler = methodNotAllowedHandler() + } + if handler == nil { handler = http.NotFoundHandler() } - if !r.KeepContext { - defer context.Clear(req) - } + handler.ServeHTTP(w, req) } // Get returns a route registered with the given name. func (r *Router) Get(name string) *Route { - return r.getNamedRoutes()[name] + return r.namedRoutes[name] } // GetRoute returns a route registered with the given name. This method // was renamed to Get() and remains here for backwards compatibility. func (r *Router) GetRoute(name string) *Route { - return r.getNamedRoutes()[name] + return r.namedRoutes[name] } // StrictSlash defines the trailing slash behavior for new routes. The initial // value is false. // -// When true, if the route path is "/path/", accessing "/path" will redirect +// When true, if the route path is "/path/", accessing "/path" will perform a redirect // to the former and vice versa. In other words, your application will always // see the path as specified in the route. // // When false, if the route path is "/path", accessing "/path/" will not match // this route and vice versa. // +// The re-direct is a HTTP 301 (Moved Permanently). Note that when this is set for +// routes with a non-idempotent method (e.g. POST, PUT), the subsequent re-directed +// request will be made as a GET by most clients. Use middleware or client settings +// to modify this behaviour as needed. +// // Special case: when a route sets a path prefix using the PathPrefix() method, // strict slash is ignored for that route because the redirect behavior can't // be determined from a prefix alone. However, any subrouters created from that @@ -133,35 +247,28 @@ return r } -// ---------------------------------------------------------------------------- -// parentRoute -// ---------------------------------------------------------------------------- - -// getNamedRoutes returns the map where named routes are registered. -func (r *Router) getNamedRoutes() map[string]*Route { - if r.namedRoutes == nil { - if r.parent != nil { - r.namedRoutes = r.parent.getNamedRoutes() - } else { - r.namedRoutes = make(map[string]*Route) - } - } - return r.namedRoutes -} - -// getRegexpGroup returns regexp definitions from the parent route, if any. -func (r *Router) getRegexpGroup() *routeRegexpGroup { - if r.parent != nil { - return r.parent.getRegexpGroup() - } - return nil +// SkipClean defines the path cleaning behaviour for new routes. The initial +// value is false. Users should be careful about which routes are not cleaned +// +// When true, if the route path is "/path//to", it will remain with the double +// slash. This is helpful if you have a route like: /fetch/http://xkcd.com/534/ +// +// When false, the path will be cleaned, so /fetch/http://xkcd.com/534/ will +// become /fetch/http/xkcd.com/534 +func (r *Router) SkipClean(value bool) *Router { + r.skipClean = value + return r } -func (r *Router) buildVars(m map[string]string) map[string]string { - if r.parent != nil { - m = r.parent.buildVars(m) - } - return m +// UseEncodedPath tells the router to match the encoded original path +// to the routes. +// For eg. "/path/foo%2Fbar/to" will match the path "/path/{var}/to". +// +// If not called, the router will match the unencoded path to the routes. +// For eg. "/path/foo%2Fbar/to" will match the path "/path/foo/bar/to" +func (r *Router) UseEncodedPath() *Router { + r.useEncodedPath = true + return r } // ---------------------------------------------------------------------------- @@ -170,11 +277,18 @@ // NewRoute registers an empty route. func (r *Router) NewRoute() *Route { - route := &Route{parent: r, strictSlash: r.strictSlash} + // initialize a route with a copy of the parent router's configuration + route := &Route{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} r.routes = append(r.routes, route) return route } +// Name registers a new route with a name. +// See Route.Name(). +func (r *Router) Name(name string) *Route { + return r.NewRoute().Name(name) +} + // Handle registers a new route with a matcher for the URL path. // See Route.Path() and Route.Handler(). func (r *Router) Handle(path string, handler http.Handler) *Route { @@ -260,20 +374,21 @@ func (r *Router) walk(walkFn WalkFunc, ancestors []*Route) error { for _, t := range r.routes { - if t.regexp == nil || t.regexp.path == nil || t.regexp.path.template == "" { - continue - } - err := walkFn(t, r, ancestors) if err == SkipRouter { continue } + if err != nil { + return err + } for _, sr := range t.matchers { if h, ok := sr.(*Router); ok { + ancestors = append(ancestors, t) err := h.walk(walkFn, ancestors) if err != nil { return err } + ancestors = ancestors[:len(ancestors)-1] } } if h, ok := t.handler.(*Router); ok { @@ -297,6 +412,11 @@ Route *Route Handler http.Handler Vars map[string]string + + // MatchErr is set to appropriate matching error + // It is set to ErrMethodMismatch if there is a mismatch in + // the request method and route method + MatchErr error } type contextKey int @@ -308,7 +428,7 @@ // Vars returns the route variables for the current request, if any. func Vars(r *http.Request) map[string]string { - if rv := context.Get(r, varsKey); rv != nil { + if rv := contextGet(r, varsKey); rv != nil { return rv.(map[string]string) } return nil @@ -320,22 +440,18 @@ // after the handler returns, unless the KeepContext option is set on the // Router. func CurrentRoute(r *http.Request) *Route { - if rv := context.Get(r, routeKey); rv != nil { + if rv := contextGet(r, routeKey); rv != nil { return rv.(*Route) } return nil } -func setVars(r *http.Request, val interface{}) { - if val != nil { - context.Set(r, varsKey, val) - } +func setVars(r *http.Request, val interface{}) *http.Request { + return contextSet(r, varsKey, val) } -func setCurrentRoute(r *http.Request, val interface{}) { - if val != nil { - context.Set(r, routeKey, val) - } +func setCurrentRoute(r *http.Request, val interface{}) *http.Request { + return contextSet(r, routeKey, val) } // ---------------------------------------------------------------------------- @@ -357,6 +473,7 @@ if p[len(p)-1] == '/' && np != "/" { np += "/" } + return np } @@ -397,7 +514,7 @@ return m, nil } -// mapFromPairsToRegex converts variadic string paramers to a +// mapFromPairsToRegex converts variadic string parameters to a // string to regex map. func mapFromPairsToRegex(pairs ...string) (map[string]*regexp.Regexp, error) { length, err := checkPairs(pairs...) @@ -479,3 +596,12 @@ } return true } + +// methodNotAllowed replies to the request with an HTTP status code 405. +func methodNotAllowed(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusMethodNotAllowed) +} + +// methodNotAllowedHandler returns a simple request handler +// that replies to each request with a status code 405. +func methodNotAllowedHandler() http.Handler { return http.HandlerFunc(methodNotAllowed) } diff -Nru snapd-2.40/vendor/github.com/gorilla/mux/README.md snapd-2.42.1/vendor/github.com/gorilla/mux/README.md --- snapd-2.40/vendor/github.com/gorilla/mux/README.md 2017-09-07 12:49:06.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/mux/README.md 2019-10-17 12:41:14.000000000 +0000 @@ -1,29 +1,60 @@ -mux -=== +# gorilla/mux + [![GoDoc](https://godoc.org/github.com/gorilla/mux?status.svg)](https://godoc.org/github.com/gorilla/mux) [![Build Status](https://travis-ci.org/gorilla/mux.svg?branch=master)](https://travis-ci.org/gorilla/mux) +[![CircleCI](https://circleci.com/gh/gorilla/mux.svg?style=svg)](https://circleci.com/gh/gorilla/mux) +[![Sourcegraph](https://sourcegraph.com/github.com/gorilla/mux/-/badge.svg)](https://sourcegraph.com/github.com/gorilla/mux?badge) + +![Gorilla Logo](http://www.gorillatoolkit.org/static/images/gorilla-icon-64.png) -http://www.gorillatoolkit.org/pkg/mux +https://www.gorillatoolkit.org/pkg/mux -Package `gorilla/mux` implements a request router and dispatcher. +Package `gorilla/mux` implements a request router and dispatcher for matching incoming requests to +their respective handler. The name mux stands for "HTTP request multiplexer". Like the standard `http.ServeMux`, `mux.Router` matches incoming requests against a list of registered routes and calls a handler for the route that matches the URL or other conditions. The main features are: +* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`. * Requests can be matched based on URL host, path, path prefix, schemes, header and query values, HTTP methods or using custom matchers. -* URL hosts and paths can have variables with an optional regular expression. +* URL hosts, paths and query values can have variables with an optional regular expression. * Registered URLs can be built, or "reversed", which helps maintaining references to resources. * Routes can be used as subrouters: nested routes are only tested if the parent route matches. This is useful to define groups of routes that share common conditions like a host, a path prefix or other repeated attributes. As a bonus, this optimizes request matching. -* It implements the `http.Handler` interface so it is compatible with the standard `http.ServeMux`. + +--- + +* [Install](#install) +* [Examples](#examples) +* [Matching Routes](#matching-routes) +* [Static Files](#static-files) +* [Registered URLs](#registered-urls) +* [Walking Routes](#walking-routes) +* [Graceful Shutdown](#graceful-shutdown) +* [Middleware](#middleware) +* [Handling CORS Requests](#handling-cors-requests) +* [Testing Handlers](#testing-handlers) +* [Full Example](#full-example) + +--- + +## Install + +With a [correctly configured](https://golang.org/doc/install#testing) Go toolchain: + +```sh +go get -u github.com/gorilla/mux +``` + +## Examples Let's start registering a couple of URL paths and handlers: ```go func main() { - r := mux.NewRouter() - r.HandleFunc("/", HomeHandler) - r.HandleFunc("/products", ProductsHandler) - r.HandleFunc("/articles", ArticlesHandler) - http.Handle("/", r) + r := mux.NewRouter() + r.HandleFunc("/", HomeHandler) + r.HandleFunc("/products", ProductsHandler) + r.HandleFunc("/articles", ArticlesHandler) + http.Handle("/", r) } ``` @@ -41,12 +72,17 @@ The names are used to create a map of route variables which can be retrieved calling `mux.Vars()`: ```go -vars := mux.Vars(request) -category := vars["category"] +func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) { + vars := mux.Vars(r) + w.WriteHeader(http.StatusOK) + fmt.Fprintf(w, "Category: %v\n", vars["category"]) +} ``` And this is all you need to know about the basic usage. More advanced options are explained below. +### Matching Routes + Routes can also be restricted to a domain or subdomain. Just define a host pattern to be matched. They can also have variables: ```go @@ -54,7 +90,7 @@ // Only matches if domain is "www.example.com". r.Host("www.example.com") // Matches a dynamic subdomain. -r.Host("{subdomain:[a-z]+}.domain.com") +r.Host("{subdomain:[a-z]+}.example.com") ``` There are several other matchers that can be added. To match path prefixes: @@ -91,7 +127,7 @@ ```go r.MatcherFunc(func(r *http.Request, rm *RouteMatch) bool { - return r.ProtoMajor == 0 + return r.ProtoMajor == 0 }) ``` @@ -104,6 +140,14 @@ Schemes("http") ``` +Routes are tested in the order they were added to the router. If two routes match, the first one wins: + +```go +r := mux.NewRouter() +r.HandleFunc("/specific", specificHandler) +r.PathPrefix("/").Handler(catchAllHandler) +``` + Setting the same matching conditions again and again can be boring, so we have a way to group several routes that share the same requirements. We call it "subrouting". For example, let's say we have several URLs that should only match when the host is `www.example.com`. Create a route for that host and get a "subrouter" from it: @@ -118,7 +162,7 @@ ```go s.HandleFunc("/products/", ProductsHandler) s.HandleFunc("/products/{key}", ProductHandler) -s.HandleFunc("/articles/{category}/{id:[0-9]+}"), ArticleHandler) +s.HandleFunc("/articles/{category}/{id:[0-9]+}", ArticleHandler) ``` The three URL paths we registered above will only be tested if the domain is `www.example.com`, because the subrouter is tested first. This is not only convenient, but also optimizes request matching. You can create subrouters combining any attribute matchers accepted by a route. @@ -138,6 +182,38 @@ s.HandleFunc("/{key}/details", ProductDetailsHandler) ``` + +### Static Files + +Note that the path provided to `PathPrefix()` represents a "wildcard": calling +`PathPrefix("/static/").Handler(...)` means that the handler will be passed any +request that matches "/static/\*". This makes it easy to serve static files with mux: + +```go +func main() { + var dir string + + flag.StringVar(&dir, "dir", ".", "the directory to serve files from. Defaults to the current dir") + flag.Parse() + r := mux.NewRouter() + + // This will serve files under http://localhost:8000/static/ + r.PathPrefix("/static/").Handler(http.StripPrefix("/static/", http.FileServer(http.Dir(dir)))) + + srv := &http.Server{ + Handler: r, + Addr: "127.0.0.1:8000", + // Good practice: enforce timeouts for servers you create! + WriteTimeout: 15 * time.Second, + ReadTimeout: 15 * time.Second, + } + + log.Fatal(srv.ListenAndServe()) +} +``` + +### Registered URLs + Now let's see how to build registered URLs. Routes can be named. All routes that define a name can have their URLs built, or "reversed". We define a name calling `Name()` on a route. For example: @@ -160,19 +236,21 @@ "/articles/technology/42" ``` -This also works for host variables: +This also works for host and query value variables: ```go r := mux.NewRouter() -r.Host("{subdomain}.domain.com"). +r.Host("{subdomain}.example.com"). Path("/articles/{category}/{id:[0-9]+}"). + Queries("filter", "{filter}"). HandlerFunc(ArticleHandler). Name("article") -// url.String() will be "http://news.domain.com/articles/technology/42" +// url.String() will be "http://news.example.com/articles/technology/42?filter=gorilla" url, err := r.Get("article").URL("subdomain", "news", "category", "technology", - "id", "42") + "id", "42", + "filter", "gorilla") ``` All variables defined in the route are required, and their values must conform to the corresponding patterns. These requirements guarantee that a generated URL will always match a registered route -- the only exception is for explicitly defined "build-only" routes which never match. @@ -188,7 +266,7 @@ There's also a way to build only the URL host or path for a route: use the methods `URLHost()` or `URLPath()` instead. For the previous route, we would do: ```go -// "http://news.domain.com/" +// "http://news.example.com/" host, err := r.Get("article").URLHost("subdomain", "news") // "/articles/technology/42" @@ -199,41 +277,439 @@ ```go r := mux.NewRouter() -s := r.Host("{subdomain}.domain.com").Subrouter() +s := r.Host("{subdomain}.example.com").Subrouter() s.Path("/articles/{category}/{id:[0-9]+}"). HandlerFunc(ArticleHandler). Name("article") -// "http://news.domain.com/articles/technology/42" +// "http://news.example.com/articles/technology/42" url, err := r.Get("article").URL("subdomain", "news", "category", "technology", "id", "42") ``` -## Full Example +### Walking Routes -Here's a complete, runnable example of a small `mux` based server: +The `Walk` function on `mux.Router` can be used to visit all of the routes that are registered on a router. For example, +the following prints all of the registered routes: ```go package main import ( + "fmt" "net/http" + "strings" "github.com/gorilla/mux" ) -func YourHandler(w http.ResponseWriter, r *http.Request) { - w.Write([]byte("Gorilla!\n")) +func handler(w http.ResponseWriter, r *http.Request) { + return } func main() { r := mux.NewRouter() - // Routes consist of a path and a handler function. - r.HandleFunc("/", YourHandler) + r.HandleFunc("/", handler) + r.HandleFunc("/products", handler).Methods("POST") + r.HandleFunc("/articles", handler).Methods("GET") + r.HandleFunc("/articles/{id}", handler).Methods("GET", "PUT") + r.HandleFunc("/authors", handler).Queries("surname", "{surname}") + err := r.Walk(func(route *mux.Route, router *mux.Router, ancestors []*mux.Route) error { + pathTemplate, err := route.GetPathTemplate() + if err == nil { + fmt.Println("ROUTE:", pathTemplate) + } + pathRegexp, err := route.GetPathRegexp() + if err == nil { + fmt.Println("Path regexp:", pathRegexp) + } + queriesTemplates, err := route.GetQueriesTemplates() + if err == nil { + fmt.Println("Queries templates:", strings.Join(queriesTemplates, ",")) + } + queriesRegexps, err := route.GetQueriesRegexp() + if err == nil { + fmt.Println("Queries regexps:", strings.Join(queriesRegexps, ",")) + } + methods, err := route.GetMethods() + if err == nil { + fmt.Println("Methods:", strings.Join(methods, ",")) + } + fmt.Println() + return nil + }) + + if err != nil { + fmt.Println(err) + } + + http.Handle("/", r) +} +``` + +### Graceful Shutdown + +Go 1.8 introduced the ability to [gracefully shutdown](https://golang.org/doc/go1.8#http_shutdown) a `*http.Server`. Here's how to do that alongside `mux`: + +```go +package main + +import ( + "context" + "flag" + "log" + "net/http" + "os" + "os/signal" + "time" + + "github.com/gorilla/mux" +) + +func main() { + var wait time.Duration + flag.DurationVar(&wait, "graceful-timeout", time.Second * 15, "the duration for which the server gracefully wait for existing connections to finish - e.g. 15s or 1m") + flag.Parse() + + r := mux.NewRouter() + // Add your routes as needed + + srv := &http.Server{ + Addr: "0.0.0.0:8080", + // Good practice to set timeouts to avoid Slowloris attacks. + WriteTimeout: time.Second * 15, + ReadTimeout: time.Second * 15, + IdleTimeout: time.Second * 60, + Handler: r, // Pass our instance of gorilla/mux in. + } + + // Run our server in a goroutine so that it doesn't block. + go func() { + if err := srv.ListenAndServe(); err != nil { + log.Println(err) + } + }() + + c := make(chan os.Signal, 1) + // We'll accept graceful shutdowns when quit via SIGINT (Ctrl+C) + // SIGKILL, SIGQUIT or SIGTERM (Ctrl+/) will not be caught. + signal.Notify(c, os.Interrupt) + + // Block until we receive our signal. + <-c + + // Create a deadline to wait for. + ctx, cancel := context.WithTimeout(context.Background(), wait) + defer cancel() + // Doesn't block if no connections, but will otherwise wait + // until the timeout deadline. + srv.Shutdown(ctx) + // Optionally, you could run srv.Shutdown in a goroutine and block on + // <-ctx.Done() if your application should wait for other services + // to finalize based on context cancellation. + log.Println("shutting down") + os.Exit(0) +} +``` + +### Middleware + +Mux supports the addition of middlewares to a [Router](https://godoc.org/github.com/gorilla/mux#Router), which are executed in the order they are added if a match is found, including its subrouters. +Middlewares are (typically) small pieces of code which take one request, do something with it, and pass it down to another middleware or the final handler. Some common use cases for middleware are request logging, header manipulation, or `ResponseWriter` hijacking. + +Mux middlewares are defined using the de facto standard type: + +```go +type MiddlewareFunc func(http.Handler) http.Handler +``` + +Typically, the returned handler is a closure which does something with the http.ResponseWriter and http.Request passed to it, and then calls the handler passed as parameter to the MiddlewareFunc. This takes advantage of closures being able access variables from the context where they are created, while retaining the signature enforced by the receivers. + +A very basic middleware which logs the URI of the request being handled could be written as: + +```go +func loggingMiddleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // Do stuff here + log.Println(r.RequestURI) + // Call the next handler, which can be another middleware in the chain, or the final handler. + next.ServeHTTP(w, r) + }) +} +``` + +Middlewares can be added to a router using `Router.Use()`: + +```go +r := mux.NewRouter() +r.HandleFunc("/", handler) +r.Use(loggingMiddleware) +``` + +A more complex authentication middleware, which maps session token to users, could be written as: + +```go +// Define our struct +type authenticationMiddleware struct { + tokenUsers map[string]string +} + +// Initialize it somewhere +func (amw *authenticationMiddleware) Populate() { + amw.tokenUsers["00000000"] = "user0" + amw.tokenUsers["aaaaaaaa"] = "userA" + amw.tokenUsers["05f717e5"] = "randomUser" + amw.tokenUsers["deadbeef"] = "user0" +} + +// Middleware function, which will be called for each request +func (amw *authenticationMiddleware) Middleware(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + token := r.Header.Get("X-Session-Token") + + if user, found := amw.tokenUsers[token]; found { + // We found the token in our map + log.Printf("Authenticated user %s\n", user) + // Pass down the request to the next middleware (or final handler) + next.ServeHTTP(w, r) + } else { + // Write an error and stop the handler chain + http.Error(w, "Forbidden", http.StatusForbidden) + } + }) +} +``` + +```go +r := mux.NewRouter() +r.HandleFunc("/", handler) + +amw := authenticationMiddleware{} +amw.Populate() + +r.Use(amw.Middleware) +``` + +Note: The handler chain will be stopped if your middleware doesn't call `next.ServeHTTP()` with the corresponding parameters. This can be used to abort a request if the middleware writer wants to. Middlewares _should_ write to `ResponseWriter` if they _are_ going to terminate the request, and they _should not_ write to `ResponseWriter` if they _are not_ going to terminate it. + +### Handling CORS Requests + +[CORSMethodMiddleware](https://godoc.org/github.com/gorilla/mux#CORSMethodMiddleware) intends to make it easier to strictly set the `Access-Control-Allow-Methods` response header. + +* You will still need to use your own CORS handler to set the other CORS headers such as `Access-Control-Allow-Origin` +* The middleware will set the `Access-Control-Allow-Methods` header to all the method matchers (e.g. `r.Methods(http.MethodGet, http.MethodPut, http.MethodOptions)` -> `Access-Control-Allow-Methods: GET,PUT,OPTIONS`) on a route +* If you do not specify any methods, then: +> _Important_: there must be an `OPTIONS` method matcher for the middleware to set the headers. + +Here is an example of using `CORSMethodMiddleware` along with a custom `OPTIONS` handler to set all the required CORS headers: + +```go +package main + +import ( + "net/http" + "github.com/gorilla/mux" +) + +func main() { + r := mux.NewRouter() + + // IMPORTANT: you must specify an OPTIONS method matcher for the middleware to set CORS headers + r.HandleFunc("/foo", fooHandler).Methods(http.MethodGet, http.MethodPut, http.MethodPatch, http.MethodOptions) + r.Use(mux.CORSMethodMiddleware(r)) + + http.ListenAndServe(":8080", r) +} + +func fooHandler(w http.ResponseWriter, r *http.Request) { + w.Header().Set("Access-Control-Allow-Origin", "*") + if r.Method == http.MethodOptions { + return + } + + w.Write([]byte("foo")) +} +``` + +And an request to `/foo` using something like: + +```bash +curl localhost:8080/foo -v +``` + +Would look like: + +```bash +* Trying ::1... +* TCP_NODELAY set +* Connected to localhost (::1) port 8080 (#0) +> GET /foo HTTP/1.1 +> Host: localhost:8080 +> User-Agent: curl/7.59.0 +> Accept: */* +> +< HTTP/1.1 200 OK +< Access-Control-Allow-Methods: GET,PUT,PATCH,OPTIONS +< Access-Control-Allow-Origin: * +< Date: Fri, 28 Jun 2019 20:13:30 GMT +< Content-Length: 3 +< Content-Type: text/plain; charset=utf-8 +< +* Connection #0 to host localhost left intact +foo +``` + +### Testing Handlers + +Testing handlers in a Go web application is straightforward, and _mux_ doesn't complicate this any further. Given two files: `endpoints.go` and `endpoints_test.go`, here's how we'd test an application using _mux_. + +First, our simple HTTP handler: + +```go +// endpoints.go +package main + +func HealthCheckHandler(w http.ResponseWriter, r *http.Request) { + // A very simple health check. + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(http.StatusOK) + + // In the future we could report back on the status of our DB, or our cache + // (e.g. Redis) by performing a simple PING, and include them in the response. + io.WriteString(w, `{"alive": true}`) +} + +func main() { + r := mux.NewRouter() + r.HandleFunc("/health", HealthCheckHandler) + + log.Fatal(http.ListenAndServe("localhost:8080", r)) +} +``` + +Our test code: + +```go +// endpoints_test.go +package main + +import ( + "net/http" + "net/http/httptest" + "testing" +) + +func TestHealthCheckHandler(t *testing.T) { + // Create a request to pass to our handler. We don't have any query parameters for now, so we'll + // pass 'nil' as the third parameter. + req, err := http.NewRequest("GET", "/health", nil) + if err != nil { + t.Fatal(err) + } + + // We create a ResponseRecorder (which satisfies http.ResponseWriter) to record the response. + rr := httptest.NewRecorder() + handler := http.HandlerFunc(HealthCheckHandler) + + // Our handlers satisfy http.Handler, so we can call their ServeHTTP method + // directly and pass in our Request and ResponseRecorder. + handler.ServeHTTP(rr, req) + + // Check the status code is what we expect. + if status := rr.Code; status != http.StatusOK { + t.Errorf("handler returned wrong status code: got %v want %v", + status, http.StatusOK) + } + + // Check the response body is what we expect. + expected := `{"alive": true}` + if rr.Body.String() != expected { + t.Errorf("handler returned unexpected body: got %v want %v", + rr.Body.String(), expected) + } +} +``` + +In the case that our routes have [variables](#examples), we can pass those in the request. We could write +[table-driven tests](https://dave.cheney.net/2013/06/09/writing-table-driven-tests-in-go) to test multiple +possible route variables as needed. + +```go +// endpoints.go +func main() { + r := mux.NewRouter() + // A route with a route variable: + r.HandleFunc("/metrics/{type}", MetricsHandler) + + log.Fatal(http.ListenAndServe("localhost:8080", r)) +} +``` + +Our test file, with a table-driven test of `routeVariables`: + +```go +// endpoints_test.go +func TestMetricsHandler(t *testing.T) { + tt := []struct{ + routeVariable string + shouldPass bool + }{ + {"goroutines", true}, + {"heap", true}, + {"counters", true}, + {"queries", true}, + {"adhadaeqm3k", false}, + } + + for _, tc := range tt { + path := fmt.Sprintf("/metrics/%s", tc.routeVariable) + req, err := http.NewRequest("GET", path, nil) + if err != nil { + t.Fatal(err) + } + + rr := httptest.NewRecorder() + + // Need to create a router that we can pass the request through so that the vars will be added to the context + router := mux.NewRouter() + router.HandleFunc("/metrics/{type}", MetricsHandler) + router.ServeHTTP(rr, req) + + // In this case, our MetricsHandler returns a non-200 response + // for a route variable it doesn't know about. + if rr.Code == http.StatusOK && !tc.shouldPass { + t.Errorf("handler should have failed on routeVariable %s: got %v want %v", + tc.routeVariable, rr.Code, http.StatusOK) + } + } +} +``` + +## Full Example + +Here's a complete, runnable example of a small `mux` based server: + +```go +package main + +import ( + "net/http" + "log" + "github.com/gorilla/mux" +) + +func YourHandler(w http.ResponseWriter, r *http.Request) { + w.Write([]byte("Gorilla!\n")) +} + +func main() { + r := mux.NewRouter() + // Routes consist of a path and a handler function. + r.HandleFunc("/", YourHandler) - // Bind to a port and pass our router in - http.ListenAndServe(":8000", r) + // Bind to a port and pass our router in + log.Fatal(http.ListenAndServe(":8000", r)) } ``` diff -Nru snapd-2.40/vendor/github.com/gorilla/mux/regexp.go snapd-2.42.1/vendor/github.com/gorilla/mux/regexp.go --- snapd-2.40/vendor/github.com/gorilla/mux/regexp.go 2017-09-07 12:49:06.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/mux/regexp.go 2019-10-17 12:41:14.000000000 +0000 @@ -14,6 +14,20 @@ "strings" ) +type routeRegexpOptions struct { + strictSlash bool + useEncodedPath bool +} + +type regexpType int + +const ( + regexpTypePath regexpType = 0 + regexpTypeHost regexpType = 1 + regexpTypePrefix regexpType = 2 + regexpTypeQuery regexpType = 3 +) + // newRouteRegexp parses a route template and returns a routeRegexp, // used to match a host, a path or a query string. // @@ -24,7 +38,7 @@ // Previously we accepted only Python-like identifiers for variable // names ([a-zA-Z_][a-zA-Z0-9_]*), but currently the only restriction is that // name and pattern can't be empty, and names can't contain a colon. -func newRouteRegexp(tpl string, matchHost, matchPrefix, matchQuery, strictSlash bool) (*routeRegexp, error) { +func newRouteRegexp(tpl string, typ regexpType, options routeRegexpOptions) (*routeRegexp, error) { // Check if it is well-formed. idxs, errBraces := braceIndices(tpl) if errBraces != nil { @@ -34,19 +48,18 @@ template := tpl // Now let's parse it. defaultPattern := "[^/]+" - if matchQuery { - defaultPattern = "[^?&]*" - } else if matchHost { + if typ == regexpTypeQuery { + defaultPattern = ".*" + } else if typ == regexpTypeHost { defaultPattern = "[^.]+" - matchPrefix = false } // Only match strict slash if not matching - if matchPrefix || matchHost || matchQuery { - strictSlash = false + if typ != regexpTypePath { + options.strictSlash = false } // Set a flag for strictSlash. endSlash := false - if strictSlash && strings.HasSuffix(tpl, "/") { + if options.strictSlash && strings.HasSuffix(tpl, "/") { tpl = tpl[:len(tpl)-1] endSlash = true } @@ -88,18 +101,25 @@ // Add the remaining. raw := tpl[end:] pattern.WriteString(regexp.QuoteMeta(raw)) - if strictSlash { + if options.strictSlash { pattern.WriteString("[/]?") } - if matchQuery { + if typ == regexpTypeQuery { // Add the default pattern if the query value is empty if queryVal := strings.SplitN(template, "=", 2)[1]; queryVal == "" { pattern.WriteString(defaultPattern) } } - if !matchPrefix { + if typ != regexpTypePrefix { pattern.WriteByte('$') } + + var wildcardHostPort bool + if typ == regexpTypeHost { + if !strings.Contains(pattern.String(), ":") { + wildcardHostPort = true + } + } reverse.WriteString(raw) if endSlash { reverse.WriteByte('/') @@ -109,16 +129,23 @@ if errCompile != nil { return nil, errCompile } + + // Check for capturing groups which used to work in older versions + if reg.NumSubexp() != len(idxs)/2 { + panic(fmt.Sprintf("route %s contains capture groups in its regexp. ", template) + + "Only non-capturing groups are accepted: e.g. (?:pattern) instead of (pattern)") + } + // Done! return &routeRegexp{ - template: template, - matchHost: matchHost, - matchQuery: matchQuery, - strictSlash: strictSlash, - regexp: reg, - reverse: reverse.String(), - varsN: varsN, - varsR: varsR, + template: template, + regexpType: typ, + options: options, + regexp: reg, + reverse: reverse.String(), + varsN: varsN, + varsR: varsR, + wildcardHostPort: wildcardHostPort, }, nil } @@ -127,12 +154,10 @@ type routeRegexp struct { // The unmodified template. template string - // True for host match, false for path or query string match. - matchHost bool - // True for query string match, false for path and host match. - matchQuery bool - // The strictSlash value defined on the route, but disabled if PathPrefix was used. - strictSlash bool + // The type of match + regexpType regexpType + // Options for matching + options routeRegexpOptions // Expanded regexp. regexp *regexp.Regexp // Reverse template. @@ -141,19 +166,31 @@ varsN []string // Variable regexps (validators). varsR []*regexp.Regexp + // Wildcard host-port (no strict port match in hostname) + wildcardHostPort bool } // Match matches the regexp against the URL host or path. func (r *routeRegexp) Match(req *http.Request, match *RouteMatch) bool { - if !r.matchHost { - if r.matchQuery { + if r.regexpType == regexpTypeHost { + host := getHost(req) + if r.wildcardHostPort { + // Don't be strict on the port match + if i := strings.Index(host, ":"); i != -1 { + host = host[:i] + } + } + return r.regexp.MatchString(host) + } else { + if r.regexpType == regexpTypeQuery { return r.matchQueryString(req) } - - return r.regexp.MatchString(req.URL.Path) + path := req.URL.Path + if r.options.useEncodedPath { + path = req.URL.EscapedPath() + } + return r.regexp.MatchString(path) } - - return r.regexp.MatchString(getHost(req)) } // url builds a URL part using the given values. @@ -164,6 +201,9 @@ if !ok { return "", fmt.Errorf("mux: missing route variable %q", v) } + if r.regexpType == regexpTypeQuery { + value = url.QueryEscape(value) + } urlValues[k] = value } rv := fmt.Sprintf(r.reverse, urlValues...) @@ -186,7 +226,7 @@ // For a URL with foo=bar&baz=ding, we return only the relevant key // value pair for the routeRegexp. func (r *routeRegexp) getURLQuery(req *http.Request) string { - if !r.matchQuery { + if r.regexpType != regexpTypeQuery { return "" } templateKey := strings.SplitN(r.template, "=", 2)[0] @@ -244,7 +284,7 @@ } // setMatch extracts the variables from the URL once a route matches. -func (v *routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { +func (v routeRegexpGroup) setMatch(req *http.Request, m *RouteMatch, r *Route) { // Store host variables. if v.host != nil { host := getHost(req) @@ -253,14 +293,18 @@ extractVars(host, matches, v.host.varsN, m.Vars) } } + path := req.URL.Path + if r.useEncodedPath { + path = req.URL.EscapedPath() + } // Store path variables. if v.path != nil { - matches := v.path.regexp.FindStringSubmatchIndex(req.URL.Path) + matches := v.path.regexp.FindStringSubmatchIndex(path) if len(matches) > 0 { - extractVars(req.URL.Path, matches, v.path.varsN, m.Vars) + extractVars(path, matches, v.path.varsN, m.Vars) // Check if we should redirect. - if v.path.strictSlash { - p1 := strings.HasSuffix(req.URL.Path, "/") + if v.path.options.strictSlash { + p1 := strings.HasSuffix(path, "/") p2 := strings.HasSuffix(v.path.template, "/") if p1 != p2 { u, _ := url.Parse(req.URL.String()) @@ -269,7 +313,7 @@ } else { u.Path += "/" } - m.Handler = http.RedirectHandler(u.String(), 301) + m.Handler = http.RedirectHandler(u.String(), http.StatusMovedPermanently) } } } @@ -285,28 +329,17 @@ } // getHost tries its best to return the request host. +// According to section 14.23 of RFC 2616 the Host header +// can include the port number if the default value of 80 is not used. func getHost(r *http.Request) string { if r.URL.IsAbs() { return r.URL.Host } - host := r.Host - // Slice off any port information. - if i := strings.Index(host, ":"); i != -1 { - host = host[:i] - } - return host - + return r.Host } func extractVars(input string, matches []int, names []string, output map[string]string) { - matchesCount := 0 - prevEnd := -1 - for i := 2; i < len(matches) && matchesCount < len(names); i += 2 { - if prevEnd < matches[i+1] { - value := input[matches[i]:matches[i+1]] - output[names[matchesCount]] = value - prevEnd = matches[i+1] - matchesCount++ - } + for i, name := range names { + output[name] = input[matches[2*i+2]:matches[2*i+3]] } } diff -Nru snapd-2.40/vendor/github.com/gorilla/mux/route.go snapd-2.42.1/vendor/github.com/gorilla/mux/route.go --- snapd-2.40/vendor/github.com/gorilla/mux/route.go 2017-09-07 12:49:06.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/mux/route.go 2019-10-17 12:41:14.000000000 +0000 @@ -15,17 +15,8 @@ // Route stores information to match a request and build URLs. type Route struct { - // Parent where the route was registered (a Router). - parent parentRoute // Request handler for the route. handler http.Handler - // List of matchers. - matchers []matcher - // Manager for the variables from host and path. - regexp *routeRegexpGroup - // If true, when the path pattern is "/path/", accessing "/path" will - // redirect to the former and vice versa. - strictSlash bool // If true, this route never matches: it is only used to build URLs. buildOnly bool // The name used to build URLs. @@ -33,7 +24,17 @@ // Error resulted from building a route. err error - buildVarsFunc BuildVarsFunc + // "global" reference to all named routes + namedRoutes map[string]*Route + + // config possibly passed in from `Router` + routeConf +} + +// SkipClean reports whether path cleaning is enabled for this route via +// Router.SkipClean. +func (r *Route) SkipClean() bool { + return r.skipClean } // Match matches the route against the request. @@ -41,12 +42,45 @@ if r.buildOnly || r.err != nil { return false } + + var matchErr error + // Match everything. for _, m := range r.matchers { if matched := m.Match(req, match); !matched { + if _, ok := m.(methodMatcher); ok { + matchErr = ErrMethodMismatch + continue + } + + // Ignore ErrNotFound errors. These errors arise from match call + // to Subrouters. + // + // This prevents subsequent matching subrouters from failing to + // run middleware. If not ignored, the middleware would see a + // non-nil MatchErr and be skipped, even when there was a + // matching route. + if match.MatchErr == ErrNotFound { + match.MatchErr = nil + } + + matchErr = nil return false } } + + if matchErr != nil { + match.MatchErr = matchErr + return false + } + + if match.MatchErr == ErrMethodMismatch { + // We found a route which matches request method, clear MatchErr + match.MatchErr = nil + // Then override the mis-matched handler + match.Handler = r.handler + } + // Yay, we have a match. Let's collect some info about it. if match.Route == nil { match.Route = r @@ -57,10 +91,9 @@ if match.Vars == nil { match.Vars = make(map[string]string) } + // Set variables. - if r.regexp != nil { - r.regexp.setMatch(req, match, r) - } + r.regexp.setMatch(req, match, r) return true } @@ -102,7 +135,7 @@ // Name ----------------------------------------------------------------------- // Name sets the name for the route, used to build URLs. -// If the name was registered already it will be overwritten. +// It is an error to call Name more than once on a route. func (r *Route) Name(name string) *Route { if r.name != "" { r.err = fmt.Errorf("mux: route already has name %q, can't set %q", @@ -110,7 +143,7 @@ } if r.err == nil { r.name = name - r.getNamedRoutes()[name] = r + r.namedRoutes[name] = r } return r } @@ -138,20 +171,22 @@ } // addRegexpMatcher adds a host or path matcher and builder to a route. -func (r *Route) addRegexpMatcher(tpl string, matchHost, matchPrefix, matchQuery bool) error { +func (r *Route) addRegexpMatcher(tpl string, typ regexpType) error { if r.err != nil { return r.err } - r.regexp = r.getRegexpGroup() - if !matchHost && !matchQuery { - if len(tpl) == 0 || tpl[0] != '/' { + if typ == regexpTypePath || typ == regexpTypePrefix { + if len(tpl) > 0 && tpl[0] != '/' { return fmt.Errorf("mux: path must start with a slash, got %q", tpl) } if r.regexp.path != nil { tpl = strings.TrimRight(r.regexp.path.template, "/") + tpl } } - rr, err := newRouteRegexp(tpl, matchHost, matchPrefix, matchQuery, r.strictSlash) + rr, err := newRouteRegexp(tpl, typ, routeRegexpOptions{ + strictSlash: r.strictSlash, + useEncodedPath: r.useEncodedPath, + }) if err != nil { return err } @@ -160,7 +195,7 @@ return err } } - if matchHost { + if typ == regexpTypeHost { if r.regexp.path != nil { if err = uniqueVars(rr.varsN, r.regexp.path.varsN); err != nil { return err @@ -173,7 +208,7 @@ return err } } - if matchQuery { + if typ == regexpTypeQuery { r.regexp.queries = append(r.regexp.queries, rr) } else { r.regexp.path = rr @@ -225,7 +260,8 @@ // "X-Requested-With", "XMLHttpRequest") // // The above route will only match if both the request header matches both regular expressions. -// It the value is an empty string, it will match any value if the key is set. +// If the value is an empty string, it will match any value if the key is set. +// Use the start and end of string anchors (^ and $) to match an exact value. func (r *Route) HeadersRegexp(pairs ...string) *Route { if r.err == nil { var headers map[string]*regexp.Regexp @@ -255,7 +291,7 @@ // Variable names must be unique in a given route. They can be retrieved // calling mux.Vars(request). func (r *Route) Host(tpl string) *Route { - r.err = r.addRegexpMatcher(tpl, true, false, false) + r.err = r.addRegexpMatcher(tpl, regexpTypeHost) return r } @@ -315,7 +351,7 @@ // Variable names must be unique in a given route. They can be retrieved // calling mux.Vars(request). func (r *Route) Path(tpl string) *Route { - r.err = r.addRegexpMatcher(tpl, false, false, false) + r.err = r.addRegexpMatcher(tpl, regexpTypePath) return r } @@ -331,7 +367,7 @@ // Also note that the setting of Router.StrictSlash() has no effect on routes // with a PathPrefix matcher. func (r *Route) PathPrefix(tpl string) *Route { - r.err = r.addRegexpMatcher(tpl, false, true, false) + r.err = r.addRegexpMatcher(tpl, regexpTypePrefix) return r } @@ -347,7 +383,7 @@ // The above route will only match if the URL contains the defined queries // values, e.g.: ?foo=bar&id=42. // -// It the value is an empty string, it will match any value if the key is set. +// If the value is an empty string, it will match any value if the key is set. // // Variables can define an optional regexp pattern to be matched: // @@ -362,7 +398,7 @@ return nil } for i := 0; i < length; i += 2 { - if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], false, false, true); r.err != nil { + if r.err = r.addRegexpMatcher(pairs[i]+"="+pairs[i+1], regexpTypeQuery); r.err != nil { return r } } @@ -385,6 +421,9 @@ for k, v := range schemes { schemes[k] = strings.ToLower(v) } + if len(schemes) > 0 { + r.buildScheme = schemes[0] + } return r.addMatcher(schemeMatcher(schemes)) } @@ -397,7 +436,15 @@ // BuildVarsFunc adds a custom function to be used to modify build variables // before a route's URL is built. func (r *Route) BuildVarsFunc(f BuildVarsFunc) *Route { - r.buildVarsFunc = f + if r.buildVarsFunc != nil { + // compose the old and new functions + old := r.buildVarsFunc + r.buildVarsFunc = func(m map[string]string) map[string]string { + return f(old(m)) + } + } else { + r.buildVarsFunc = f + } return r } @@ -416,7 +463,8 @@ // Here, the routes registered in the subrouter won't be tested if the host // doesn't match. func (r *Route) Subrouter() *Router { - router := &Router{parent: r, strictSlash: r.strictSlash} + // initialize a subrouter with a copy of the parent route's configuration + router := &Router{routeConf: copyRouteConf(r.routeConf), namedRoutes: r.namedRoutes} r.addMatcher(router) return router } @@ -460,30 +508,38 @@ if r.err != nil { return nil, r.err } - if r.regexp == nil { - return nil, errors.New("mux: route doesn't have a host or path") - } values, err := r.prepareVars(pairs...) if err != nil { return nil, err } var scheme, host, path string + queries := make([]string, 0, len(r.regexp.queries)) if r.regexp.host != nil { - // Set a default scheme. - scheme = "http" if host, err = r.regexp.host.url(values); err != nil { return nil, err } + scheme = "http" + if r.buildScheme != "" { + scheme = r.buildScheme + } } if r.regexp.path != nil { if path, err = r.regexp.path.url(values); err != nil { return nil, err } } + for _, q := range r.regexp.queries { + var query string + if query, err = q.url(values); err != nil { + return nil, err + } + queries = append(queries, query) + } return &url.URL{ - Scheme: scheme, - Host: host, - Path: path, + Scheme: scheme, + Host: host, + Path: path, + RawQuery: strings.Join(queries, "&"), }, nil } @@ -494,7 +550,7 @@ if r.err != nil { return nil, r.err } - if r.regexp == nil || r.regexp.host == nil { + if r.regexp.host == nil { return nil, errors.New("mux: route doesn't have a host") } values, err := r.prepareVars(pairs...) @@ -505,10 +561,14 @@ if err != nil { return nil, err } - return &url.URL{ + u := &url.URL{ Scheme: "http", Host: host, - }, nil + } + if r.buildScheme != "" { + u.Scheme = r.buildScheme + } + return u, nil } // URLPath builds the path part of the URL for a route. See Route.URL(). @@ -518,7 +578,7 @@ if r.err != nil { return nil, r.err } - if r.regexp == nil || r.regexp.path == nil { + if r.regexp.path == nil { return nil, errors.New("mux: route doesn't have a path") } values, err := r.prepareVars(pairs...) @@ -543,12 +603,80 @@ if r.err != nil { return "", r.err } - if r.regexp == nil || r.regexp.path == nil { + if r.regexp.path == nil { return "", errors.New("mux: route doesn't have a path") } return r.regexp.path.template, nil } +// GetPathRegexp returns the expanded regular expression used to match route path. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define a path. +func (r *Route) GetPathRegexp() (string, error) { + if r.err != nil { + return "", r.err + } + if r.regexp.path == nil { + return "", errors.New("mux: route does not have a path") + } + return r.regexp.path.regexp.String(), nil +} + +// GetQueriesRegexp returns the expanded regular expressions used to match the +// route queries. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not have queries. +func (r *Route) GetQueriesRegexp() ([]string, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.queries == nil { + return nil, errors.New("mux: route doesn't have queries") + } + var queries []string + for _, query := range r.regexp.queries { + queries = append(queries, query.regexp.String()) + } + return queries, nil +} + +// GetQueriesTemplates returns the templates used to build the +// query matching. +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if the route does not define queries. +func (r *Route) GetQueriesTemplates() ([]string, error) { + if r.err != nil { + return nil, r.err + } + if r.regexp.queries == nil { + return nil, errors.New("mux: route doesn't have queries") + } + var queries []string + for _, query := range r.regexp.queries { + queries = append(queries, query.template) + } + return queries, nil +} + +// GetMethods returns the methods the route matches against +// This is useful for building simple REST API documentation and for instrumentation +// against third-party services. +// An error will be returned if route does not have methods. +func (r *Route) GetMethods() ([]string, error) { + if r.err != nil { + return nil, r.err + } + for _, m := range r.matchers { + if methods, ok := m.(methodMatcher); ok { + return []string(methods), nil + } + } + return nil, errors.New("mux: route doesn't have methods") +} + // GetHostTemplate returns the template used to build the // route match. // This is useful for building simple REST API documentation and for instrumentation @@ -558,7 +686,7 @@ if r.err != nil { return "", r.err } - if r.regexp == nil || r.regexp.host == nil { + if r.regexp.host == nil { return "", errors.New("mux: route doesn't have a host") } return r.regexp.host.template, nil @@ -575,53 +703,8 @@ } func (r *Route) buildVars(m map[string]string) map[string]string { - if r.parent != nil { - m = r.parent.buildVars(m) - } if r.buildVarsFunc != nil { m = r.buildVarsFunc(m) } return m } - -// ---------------------------------------------------------------------------- -// parentRoute -// ---------------------------------------------------------------------------- - -// parentRoute allows routes to know about parent host and path definitions. -type parentRoute interface { - getNamedRoutes() map[string]*Route - getRegexpGroup() *routeRegexpGroup - buildVars(map[string]string) map[string]string -} - -// getNamedRoutes returns the map where named routes are registered. -func (r *Route) getNamedRoutes() map[string]*Route { - if r.parent == nil { - // During tests router is not always set. - r.parent = NewRouter() - } - return r.parent.getNamedRoutes() -} - -// getRegexpGroup returns regexp definitions from this route. -func (r *Route) getRegexpGroup() *routeRegexpGroup { - if r.regexp == nil { - if r.parent == nil { - // During tests router is not always set. - r.parent = NewRouter() - } - regexp := r.parent.getRegexpGroup() - if regexp == nil { - r.regexp = new(routeRegexpGroup) - } else { - // Copy. - r.regexp = &routeRegexpGroup{ - host: regexp.host, - path: regexp.path, - queries: regexp.queries, - } - } - } - return r.regexp -} diff -Nru snapd-2.40/vendor/github.com/gorilla/mux/test_helpers.go snapd-2.42.1/vendor/github.com/gorilla/mux/test_helpers.go --- snapd-2.40/vendor/github.com/gorilla/mux/test_helpers.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/gorilla/mux/test_helpers.go 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,19 @@ +// Copyright 2012 The Gorilla Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package mux + +import "net/http" + +// SetURLVars sets the URL variables for the given request, to be accessed via +// mux.Vars for testing route behaviour. Arguments are not modified, a shallow +// copy is returned. +// +// This API should only be used for testing purposes; it provides a way to +// inject variables into the request context. Alternatively, URL variables +// can be set by making a route that captures the required variables, +// starting a server and sending the request to that server. +func SetURLVars(r *http.Request, val map[string]string) *http.Request { + return setVars(r, val) +} diff -Nru snapd-2.40/vendor/github.com/ojii/gettext.go/gettext.go snapd-2.42.1/vendor/github.com/ojii/gettext.go/gettext.go --- snapd-2.40/vendor/github.com/ojii/gettext.go/gettext.go 2016-11-29 08:24:15.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/ojii/gettext.go/gettext.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,77 +0,0 @@ -// Implements gettext in pure Go with Plural Forms support. - -package gettext - -import ( - "fmt" - "os" - "path" -) - -// Translations holds the translations in the different locales your app -// supports. Use NewTranslations to create an instance. -type Translations struct { - cache map[string]Catalog - root string - domain string - resolver PathResolver -} - -// PathResolver resolves a path to a mo file -type PathResolver func(root string, locale string, domain string) string - -// DefaultResolver resolves paths in the standard format of: -// //LC_MESSAGES/.mo -func DefaultResolver(root string, locale string, domain string) string { - return path.Join(root, locale, "LC_MESSAGES", fmt.Sprintf("%s.mo", domain)) -} - -// NewTranslations is the main entry point for gogettext. Use this to set up -// the locales for your app. -// root is the root of your locale folder, domain the domain you want to load -// and resolver a function that resolves mo file paths. -// If your structure is //LC_MESSAGES/.mo, you can use -// DefaultResolver. -func NewTranslations(root string, domain string, resolver PathResolver) Translations { - return Translations{ - root: root, - resolver: resolver, - domain: domain, - cache: map[string]Catalog{}, - } -} - -// Preload a list of locales (if they're available). This is useful if you want -// to limit IO to a specific time in your app, for example startup. Subsequent -// calls to Preload or Locale using a locale given here will not do any IO. -func (t Translations) Preload(locales ...string) { - for _, locale := range locales { - t.load(locale) - } -} - -func (t Translations) load(locale string) { - path := t.resolver(t.root, locale, t.domain) - f, err := os.Open(path) - if err != nil { - t.cache[locale] = nullcatalog{} - return - } - defer f.Close() - catalog, err := ParseMO(f) - if err != nil { - t.cache[locale] = nullcatalog{} - return - } - t.cache[locale] = catalog -} - -// Locale returns the catalog translations for a given Locale. If the given -// locale is not available, a NullCatalog is returned. -func (t Translations) Locale(locale string) Catalog { - _, ok := t.cache[locale] - if !ok { - t.load(locale) - } - return t.cache[locale] -} diff -Nru snapd-2.40/vendor/github.com/ojii/gettext.go/LICENSE snapd-2.42.1/vendor/github.com/ojii/gettext.go/LICENSE --- snapd-2.40/vendor/github.com/ojii/gettext.go/LICENSE 2016-11-29 08:24:15.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/ojii/gettext.go/LICENSE 1970-01-01 00:00:00.000000000 +0000 @@ -1,24 +0,0 @@ -Copyright (c) 2016, Jonas Obrist -All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright - notice, this list of conditions and the following disclaimer in the - documentation and/or other materials provided with the distribution. - * Neither the name of Jonas Obrist nor the - names of its contributors may be used to endorse or promote products - derived from this software without specific prior written permission. - -THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND -ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED -WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE -DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY -DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES -(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; -LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND -ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS -SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff -Nru snapd-2.40/vendor/github.com/ojii/gettext.go/mofile.go snapd-2.42.1/vendor/github.com/ojii/gettext.go/mofile.go --- snapd-2.40/vendor/github.com/ojii/gettext.go/mofile.go 2016-11-29 08:24:15.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/ojii/gettext.go/mofile.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,235 +0,0 @@ -package gettext - -import ( - "bytes" - "encoding/binary" - "fmt" - "github.com/ojii/gettext.go/pluralforms" - "log" - "os" - "strings" -) - -const le_magic = 0x950412de -const be_magic = 0xde120495 - -type header struct { - Version uint32 - NumStrings uint32 - MasterIndex uint32 - TranslationsIndex uint32 -} - -func (header header) get_major_version() uint32 { - return header.Version >> 16 -} - -func (header header) get_minor_version() uint32 { - return header.Version & 0xffff -} - -// Catalog of translations for a given locale. -type Catalog interface { - Gettext(msgid string) string - NGettext(msgid string, msgid_plural string, n uint32) string -} - -type mocatalog struct { - header header - language string - messages map[string][]string - pluralforms pluralforms.Expression - info map[string]string - charset string -} - -type nullcatalog struct{} - -func (catalog nullcatalog) Gettext(msgid string) string { - return msgid -} - -func (catalog nullcatalog) NGettext(msgid string, msgid_plural string, n uint32) string { - if n == 1 { - return msgid - } else { - return msgid_plural - } -} - -func (catalog mocatalog) Gettext(msgid string) string { - msgstrs, ok := catalog.messages[msgid] - if !ok { - return msgid - } - return msgstrs[0] -} - -func (catalog mocatalog) NGettext(msgid string, msgid_plural string, n uint32) string { - msgstrs, ok := catalog.messages[msgid] - if !ok { - if n == 1 { - return msgid - } else { - return msgid_plural - } - } else { - index := catalog.pluralforms.Eval(n) - if index > len(msgstrs) { - if n == 1 { - return msgid - } else { - return msgid_plural - } - } - return msgstrs[index] - } -} - -type len_offset struct { - Len uint32 - Off uint32 -} - -func read_len_off(index uint32, file *os.File, order binary.ByteOrder) (len_offset, error) { - lenoff := len_offset{} - buf := make([]byte, 8) - _, err := file.Seek(int64(index), os.SEEK_SET) - if err != nil { - return lenoff, err - } - _, err = file.Read(buf) - if err != nil { - return lenoff, err - } - buffer := bytes.NewBuffer(buf) - err = binary.Read(buffer, order, &lenoff) - if err != nil { - return lenoff, err - } - return lenoff, nil -} - -func read_message(file *os.File, lenoff len_offset) (string, error) { - _, err := file.Seek(int64(lenoff.Off), os.SEEK_SET) - if err != nil { - return "", err - } - buf := make([]byte, lenoff.Len) - _, err = file.Read(buf) - if err != nil { - return "", err - } - return string(buf), nil -} - -func (catalog *mocatalog) read_info(info string) error { - lastk := "" - for _, line := range strings.Split(info, "\n") { - item := strings.TrimSpace(line) - if len(item) == 0 { - continue - } - var k string - var v string - if strings.Contains(item, ":") { - tmp := strings.SplitN(item, ":", 2) - k = strings.ToLower(strings.TrimSpace(tmp[0])) - v = strings.TrimSpace(tmp[1]) - catalog.info[k] = v - lastk = k - } else if len(lastk) != 0 { - catalog.info[lastk] += "\n" + item - } - if k == "content-type" { - catalog.charset = strings.Split(v, "charset=")[1] - } else if k == "plural-forms" { - p := strings.Split(v, ";")[1] - s := strings.Split(p, "plural=")[1] - expr, err := pluralforms.Compile(s) - if err != nil { - return err - } - catalog.pluralforms = expr - } - } - return nil -} - -// ParseMO parses a mo file into a Catalog if possible. -func ParseMO(file *os.File) (Catalog, error) { - var order binary.ByteOrder - header := header{} - catalog := mocatalog{ - header: header, - info: make(map[string]string), - messages: make(map[string][]string), - } - magic := make([]byte, 4) - _, err := file.Read(magic) - if err != nil { - return catalog, err - } - magic_number := binary.LittleEndian.Uint32(magic) - switch magic_number { - case le_magic: - order = binary.LittleEndian - case be_magic: - order = binary.BigEndian - default: - return catalog, fmt.Errorf("Wrong magic %d", magic_number) - } - raw_headers := make([]byte, 32) - _, err = file.Read(raw_headers) - if err != nil { - return catalog, err - } - buffer := bytes.NewBuffer(raw_headers) - err = binary.Read(buffer, order, &header) - if err != nil { - return catalog, err - } - if (header.get_major_version() != 0) && (header.get_major_version() != 1) { - log.Printf("major %d minor %d", header.get_major_version(), header.get_minor_version()) - return catalog, fmt.Errorf("Unsupported version: %d.%d", header.get_major_version(), header.get_minor_version()) - } - current_master_index := header.MasterIndex - current_transl_index := header.TranslationsIndex - var index uint32 = 0 - for ; index < header.NumStrings; index++ { - mlenoff, err := read_len_off(current_master_index, file, order) - if err != nil { - return catalog, err - } - tlenoff, err := read_len_off(current_transl_index, file, order) - if err != nil { - return catalog, err - } - msgid, err := read_message(file, mlenoff) - if err != nil { - return catalog, nil - } - msgstr, err := read_message(file, tlenoff) - if err != nil { - return catalog, err - } - if mlenoff.Len == 0 { - err = catalog.read_info(msgstr) - if err != nil { - return catalog, err - } - } - if strings.Contains(msgid, "\x00") { - // Plural! - msgidsingular := strings.Split(msgid, "\x00")[0] - translations := strings.Split(msgstr, "\x00") - catalog.messages[msgidsingular] = translations - } else { - catalog.messages[msgid] = []string{msgstr} - } - - current_master_index += 8 - current_transl_index += 8 - } - return catalog, nil -} diff -Nru snapd-2.40/vendor/github.com/ojii/gettext.go/pluralforms/compiler.go snapd-2.42.1/vendor/github.com/ojii/gettext.go/pluralforms/compiler.go --- snapd-2.40/vendor/github.com/ojii/gettext.go/pluralforms/compiler.go 2016-11-29 08:24:15.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/ojii/gettext.go/pluralforms/compiler.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,427 +0,0 @@ -package pluralforms - -import ( - "errors" - "fmt" - "regexp" - "strconv" - "strings" -) - -type match struct { - open_pos int - close_pos int -} - -var pat = regexp.MustCompile(`(\?|:|\|\||&&|==|!=|>=|>|<=|<|%|\d+|n)`) - -type expr_token interface { - compile(tokens []string) (expr Expression, err error) -} - -type test_token interface { - compile(tokens []string) (test test, err error) -} - -type cmp_test_builder func(val uint32, flipped bool) test -type logic_test_build func(left test, right test) test - -var ternary_token ternary_ - -type ternary_ struct{} - -func (ternary_) compile(tokens []string) (expr Expression, err error) { - main, err := split_tokens(tokens, "?") - if err != nil { - return expr, err - } - test, err := compile_test(strings.Join(main.Left, "")) - if err != nil { - return expr, err - } - actions, err := split_tokens(main.Right, ":") - if err != nil { - return expr, err - } - true_action, err := compile_expression(strings.Join(actions.Left, "")) - if err != nil { - return expr, err - } - false_action, err := compile_expression(strings.Join(actions.Right, "")) - if err != nil { - return expr, nil - } - return ternary{ - test: test, - true_expr: true_action, - false_expr: false_action, - }, nil -} - -var const_token const_val_ - -type const_val_ struct{} - -func (const_val_) compile(tokens []string) (expr Expression, err error) { - if len(tokens) == 0 { - return expr, errors.New("Got nothing instead of constant") - } - if len(tokens) != 1 { - return expr, fmt.Errorf("Invalid constant: %s", strings.Join(tokens, "")) - } - i, err := strconv.Atoi(tokens[0]) - if err != nil { - return expr, err - } - return const_value{value: i}, nil -} - -func compile_logic_test(tokens []string, sep string, builder logic_test_build) (test test, err error) { - split, err := split_tokens(tokens, sep) - if err != nil { - return test, err - } - left, err := compile_test(strings.Join(split.Left, "")) - if err != nil { - return test, err - } - right, err := compile_test(strings.Join(split.Right, "")) - if err != nil { - return test, err - } - return builder(left, right), nil -} - -var or_token or_ - -type or_ struct{} - -func (or_) compile(tokens []string) (test test, err error) { - return compile_logic_test(tokens, "||", build_or) -} -func build_or(left test, right test) test { - return or{left: left, right: right} -} - -var and_token and_ - -type and_ struct{} - -func (and_) compile(tokens []string) (test test, err error) { - return compile_logic_test(tokens, "&&", build_and) -} -func build_and(left test, right test) test { - return and{left: left, right: right} -} - -func compile_mod(tokens []string) (math math, err error) { - split, err := split_tokens(tokens, "%") - if err != nil { - return math, err - } - if len(split.Left) != 1 || split.Left[0] != "n" { - return math, errors.New("Modulus operation requires 'n' as left operand") - } - if len(split.Right) != 1 { - return math, errors.New("Modulus operation requires simple integer as right operand") - } - i, err := parse_uint32(split.Right[0]) - if err != nil { - return math, err - } - return mod{value: uint32(i)}, nil -} - -func _pipe(mod_tokens []string, action_tokens []string, builder cmp_test_builder, flipped bool) (test test, err error) { - modifier, err := compile_mod(mod_tokens) - if err != nil { - return test, err - } - if len(action_tokens) != 1 { - return test, errors.New("Can only get modulus of integer") - } - i, err := parse_uint32(action_tokens[0]) - if err != nil { - return test, err - } - action := builder(uint32(i), flipped) - return pipe{ - modifier: modifier, - action: action, - }, nil -} - -func compile_equality(tokens []string, sep string, builder cmp_test_builder) (test test, err error) { - split, err := split_tokens(tokens, sep) - if err != nil { - return test, err - } - if len(split.Left) == 1 && split.Left[0] == "n" { - if len(split.Right) != 1 { - return test, errors.New("test can only compare n to integers") - } - i, err := parse_uint32(split.Right[0]) - if err != nil { - return test, err - } - return builder(i, false), nil - } else if len(split.Right) == 1 && split.Right[0] == "n" { - if len(split.Left) != 1 { - return test, errors.New("test can only compare n to integers") - } - i, err := parse_uint32(split.Left[0]) - if err != nil { - return test, err - } - return builder(i, true), nil - } else if contains(split.Left, "n") && contains(split.Left, "%") { - return _pipe(split.Left, split.Right, builder, false) - } else { - return test, errors.New("equality test must have 'n' as one of the two tests") - } -} - -var eq_token eq_ - -type eq_ struct{} - -func (eq_) compile(tokens []string) (test test, err error) { - return compile_equality(tokens, "==", build_eq) -} -func build_eq(val uint32, flipped bool) test { - return equal{value: val} -} - -var neq_token neq_ - -type neq_ struct{} - -func (neq_) compile(tokens []string) (test test, err error) { - return compile_equality(tokens, "!=", build_neq) -} -func build_neq(val uint32, flipped bool) test { - return notequal{value: val} -} - -var gt_token gt_ - -type gt_ struct{} - -func (gt_) compile(tokens []string) (test test, err error) { - return compile_equality(tokens, ">", build_gt) -} -func build_gt(val uint32, flipped bool) test { - return gt{value: val, flipped: flipped} -} - -var gte_token gte_ - -type gte_ struct{} - -func (gte_) compile(tokens []string) (test test, err error) { - return compile_equality(tokens, ">=", build_gte) -} -func build_gte(val uint32, flipped bool) test { - return gte{value: val, flipped: flipped} -} - -var lt_token lt_ - -type lt_ struct{} - -func (lt_) compile(tokens []string) (test test, err error) { - return compile_equality(tokens, "<", build_lt) -} -func build_lt(val uint32, flipped bool) test { - return lt{value: val, flipped: flipped} -} - -var lte_token lte_ - -type lte_ struct{} - -func (lte_) compile(tokens []string) (test test, err error) { - return compile_equality(tokens, "<=", build_lte) -} -func build_lte(val uint32, flipped bool) test { - return lte{value: val, flipped: flipped} -} - -type test_token_def struct { - op string - token test_token -} - -var precedence = []test_token_def{ - test_token_def{op: "||", token: or_token}, - test_token_def{op: "&&", token: and_token}, - test_token_def{op: "==", token: eq_token}, - test_token_def{op: "!=", token: neq_token}, - test_token_def{op: ">=", token: gte_token}, - test_token_def{op: ">", token: gt_token}, - test_token_def{op: "<=", token: lte_token}, - test_token_def{op: "<", token: lt_token}, -} - -type splitted struct { - Left []string - Right []string -} - -// Find index of token in list of tokens -func index(tokens []string, sep string) int { - for index, token := range tokens { - if token == sep { - return index - } - } - return -1 -} - -// Split a list of tokens by a token into a splitted struct holding the tokens -// before and after the token to be split by. -func split_tokens(tokens []string, sep string) (s splitted, err error) { - index := index(tokens, sep) - if index == -1 { - return s, fmt.Errorf("'%s' not found in ['%s']", sep, strings.Join(tokens, "','")) - } - return splitted{ - Left: tokens[:index], - Right: tokens[index+1:], - }, nil -} - -// Scan a string for parenthesis -func scan(s string) <-chan match { - ch := make(chan match) - go func() { - depth := 0 - opener := 0 - for index, char := range s { - switch char { - case '(': - if depth == 0 { - opener = index - } - depth++ - case ')': - depth-- - if depth == 0 { - ch <- match{ - open_pos: opener, - close_pos: index + 1, - } - } - } - - } - close(ch) - }() - return ch -} - -// Split the string into tokens -func split(s string) <- chan string { - ch := make(chan string) - go func() { - s = strings.Replace(s, " ", "", -1) - if !strings.Contains(s, "(") { - ch <- s - } else { - last := 0 - end := len(s) - for info := range scan(s) { - if last != info.open_pos { - ch <- s[last:info.open_pos] - } - ch <- s[info.open_pos:info.close_pos] - last = info.close_pos - } - if last != end { - ch <- s[last:] - } - } - close(ch) - }() - return ch -} - -// Tokenizes a string into a list of strings, tokens grouped by parenthesis are -// not split! If the string starts with ( and ends in ), those are stripped. -func tokenize(s string) []string { - /* - TODO: Properly detect if the string starts with a ( and ends with a ) - and that those two form a matching pair. - - Eg: (foo) -> true; (foo)(bar) -> false; - */ - if s[0] == '(' && s[len(s)-1] == ')' { - s = s[1 : len(s)-1] - } - ret := []string{} - for chunk := range split(s) { - if len(chunk) != 0 { - if chunk[0] == '(' && chunk[len(chunk)-1] == ')' { - ret = append(ret, chunk) - } else { - for _, token := range pat.FindAllStringSubmatch(chunk, -1) { - ret = append(ret, token[0]) - } - } - } else { - fmt.Printf("Empty chunk in string '%s'\n", s) - } - } - return ret -} - -// Compile a string containing a plural form expression to a Expression object. -func Compile(s string) (expr Expression, err error) { - if s == "0" { - return const_value{value: 0}, nil - } - if !strings.Contains(s, "?") { - s += "?1:0" - } - return compile_expression(s) -} - -// Check if a token is in a slice of strings -func contains(haystack []string, needle string) bool { - for _, s := range haystack { - if s == needle { - return true - } - } - return false -} - -// Compiles an expression (ternary or constant) -func compile_expression(s string) (expr Expression, err error) { - tokens := tokenize(s) - if contains(tokens, "?") { - return ternary_token.compile(tokens) - } else { - return const_token.compile(tokens) - } -} - -// Compiles a test (comparison) -func compile_test(s string) (test test, err error) { - tokens := tokenize(s) - for _, token_def := range precedence { - if contains(tokens, token_def.op) { - return token_def.token.compile(tokens) - } - } - return test, errors.New("Cannot compile") -} - -func parse_uint32(s string) (ui uint32, err error) { - i, err := strconv.ParseUint(s, 10, 32) - if err != nil { - return ui, err - } else { - return uint32(i), nil - } -} diff -Nru snapd-2.40/vendor/github.com/ojii/gettext.go/pluralforms/expression.go snapd-2.42.1/vendor/github.com/ojii/gettext.go/pluralforms/expression.go --- snapd-2.40/vendor/github.com/ojii/gettext.go/pluralforms/expression.go 2016-11-29 08:24:15.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/ojii/gettext.go/pluralforms/expression.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,39 +0,0 @@ -package pluralforms - -// Expression is a plurfalforms expression. Eval evaluates the expression for -// a given n value. Use pluralforms.Compile to generate Expression instances. -type Expression interface { - Eval(n uint32) int -} - -type const_value struct { - value int -} - -func (c const_value) Eval(n uint32) int { - return c.value -} - -type test interface { - test(n uint32) bool -} - -type ternary struct { - test test - true_expr Expression - false_expr Expression -} - -func (t ternary) Eval(n uint32) int { - if t.test.test(n) { - if t.true_expr == nil { - return -1 - } - return t.true_expr.Eval(n) - } else { - if t.false_expr == nil { - return -1 - } - return t.false_expr.Eval(n) - } -} diff -Nru snapd-2.40/vendor/github.com/ojii/gettext.go/pluralforms/genfixture.py snapd-2.42.1/vendor/github.com/ojii/gettext.go/pluralforms/genfixture.py --- snapd-2.40/vendor/github.com/ojii/gettext.go/pluralforms/genfixture.py 2016-11-29 08:24:15.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/ojii/gettext.go/pluralforms/genfixture.py 1970-01-01 00:00:00.000000000 +0000 @@ -1,35 +0,0 @@ -import json -from gettext import c2py - - -PLURAL_FORMS = [ - "0", - "n!=1", - "n>1", - "n%10==1&&n%100!=11?0:n!=0?1:2", - "n==1?0:n==2?1:2", - "n==1?0:(n==0||(n%100>0&&n%100<20))?1:2", - "n%10==1&&n%100!=11?0:n%10>=2&&(n%100<10||n%100>=20)?1:2", - "n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2", - "(n==1)?0:(n>=2&&n<=4)?1:2", - "n==1?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2", - "n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3", - "n==0?0:n==1?1:n==2?2:n%100>=3&&n%100<=10?3:n%100>=11?4:5", -] - -NUM = 1000 - - -def gen(): - tests = [] - for plural_form in PLURAL_FORMS: - expr = c2py(plural_form) - tests.append({ - 'pluralform': plural_form, - 'fixture': [expr(n) for n in range(NUM + 1)] - }) - return json.dumps(tests) - - -if __name__ == "__main__": - print(gen()) diff -Nru snapd-2.40/vendor/github.com/ojii/gettext.go/pluralforms/math.go snapd-2.42.1/vendor/github.com/ojii/gettext.go/pluralforms/math.go --- snapd-2.40/vendor/github.com/ojii/gettext.go/pluralforms/math.go 2016-11-29 08:24:15.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/ojii/gettext.go/pluralforms/math.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,13 +0,0 @@ -package pluralforms - -type math interface { - calc(n uint32) uint32 -} - -type mod struct { - value uint32 -} - -func (m mod) calc(n uint32) uint32 { - return n % m.value -} diff -Nru snapd-2.40/vendor/github.com/ojii/gettext.go/pluralforms/tests.go snapd-2.42.1/vendor/github.com/ojii/gettext.go/pluralforms/tests.go --- snapd-2.40/vendor/github.com/ojii/gettext.go/pluralforms/tests.go 2016-11-29 08:24:15.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/ojii/gettext.go/pluralforms/tests.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,104 +0,0 @@ -package pluralforms - -type equal struct { - value uint32 -} - -func (e equal) test(n uint32) bool { - return n == e.value -} - -type notequal struct { - value uint32 -} - -func (e notequal) test(n uint32) bool { - return n != e.value -} - -type gt struct { - value uint32 - flipped bool -} - -func (e gt) test(n uint32) bool { - if e.flipped { - return e.value > n - } else { - return n > e.value - } -} - -type lt struct { - value uint32 - flipped bool -} - -func (e lt) test(n uint32) bool { - if e.flipped { - return e.value < n - } else { - return n < e.value - } -} - -type gte struct { - value uint32 - flipped bool -} - -func (e gte) test(n uint32) bool { - if e.flipped { - return e.value >= n - } else { - return n >= e.value - } -} - -type lte struct { - value uint32 - flipped bool -} - -func (e lte) test(n uint32) bool { - if e.flipped { - return e.value <= n - } else { - return n <= e.value - } -} - -type and struct { - left test - right test -} - -func (e and) test(n uint32) bool { - if !e.left.test(n) { - return false - } else { - return e.right.test(n) - } -} - -type or struct { - left test - right test -} - -func (e or) test(n uint32) bool { - if e.left.test(n) { - return true - } else { - return e.right.test(n) - } -} - -type pipe struct { - modifier math - action test -} - -func (e pipe) test(n uint32) bool { - return e.action.test(e.modifier.calc(n)) -} diff -Nru snapd-2.40/vendor/github.com/ojii/gettext.go/README.md snapd-2.42.1/vendor/github.com/ojii/gettext.go/README.md --- snapd-2.40/vendor/github.com/ojii/gettext.go/README.md 2016-11-29 08:24:15.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/ojii/gettext.go/README.md 1970-01-01 00:00:00.000000000 +0000 @@ -1,33 +0,0 @@ -# gettext in golang - -[![Build Status](https://travis-ci.org/ojii/gettext.go.svg?branch=master)](https://travis-ci.org/ojii/gettext.go) - -## TODO - -- [x] parse mofiles -- [x] compile plural forms -- [ ] non-utf8 mo files (possible wontfix) -- [x] gettext -- [x] ngettext -- [x] managing mo files / sane API - - -## Example - - -```go - -import gettext - -translations := gettext.NewTranslations("path/to/translations/", "messages", gettext.DefaultResolver) - -locale := translations.Locale("en") - -fmt.Println(locale.Gettext("hello from gettext")) - -one := 1 -two := 2 - -fmt.Println(fmt.Sprintf(locale.NGettext("%d thing", "%d things", uint32(one)), one)) -fmt.Println(fmt.Sprintf(locale.NGettext("%d thing", "%d things", uint32(two)), two)) -``` diff -Nru snapd-2.40/vendor/github.com/ojii/gettext.go/test_utils.go snapd-2.42.1/vendor/github.com/ojii/gettext.go/test_utils.go --- snapd-2.40/vendor/github.com/ojii/gettext.go/test_utils.go 2016-11-29 08:24:15.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/ojii/gettext.go/test_utils.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,10 +0,0 @@ -package gettext - -import "testing" - -func assert_equal(t *testing.T, expected string, got string) { - if expected != got { - t.Logf("%s != %s", expected, got) - t.Fail() - } -} diff -Nru snapd-2.40/vendor/github.com/snapcore/go-gettext/gettext.go snapd-2.42.1/vendor/github.com/snapcore/go-gettext/gettext.go --- snapd-2.40/vendor/github.com/snapcore/go-gettext/gettext.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/snapcore/go-gettext/gettext.go 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,77 @@ +// Implements gettext in pure Go with Plural Forms support. + +package gettext + +import ( + "fmt" + "os" + "path" +) + +// Translations holds the translations in the different locales your app +// supports. Use NewTranslations to create an instance. +type Translations struct { + cache map[string]Catalog + root string + domain string + resolver PathResolver +} + +// PathResolver resolves a path to a mo file +type PathResolver func(root string, locale string, domain string) string + +// DefaultResolver resolves paths in the standard format of: +// //LC_MESSAGES/.mo +func DefaultResolver(root string, locale string, domain string) string { + return path.Join(root, locale, "LC_MESSAGES", fmt.Sprintf("%s.mo", domain)) +} + +// NewTranslations is the main entry point for gogettext. Use this to set up +// the locales for your app. +// root is the root of your locale folder, domain the domain you want to load +// and resolver a function that resolves mo file paths. +// If your structure is //LC_MESSAGES/.mo, you can use +// DefaultResolver. +func NewTranslations(root string, domain string, resolver PathResolver) Translations { + return Translations{ + root: root, + resolver: resolver, + domain: domain, + cache: map[string]Catalog{}, + } +} + +// Preload a list of locales (if they're available). This is useful if you want +// to limit IO to a specific time in your app, for example startup. Subsequent +// calls to Preload or Locale using a locale given here will not do any IO. +func (t Translations) Preload(locales ...string) { + for _, locale := range locales { + t.load(locale) + } +} + +func (t Translations) load(locale string) { + path := t.resolver(t.root, locale, t.domain) + f, err := os.Open(path) + if err != nil { + t.cache[locale] = nullcatalog{} + return + } + defer f.Close() + catalog, err := ParseMO(f) + if err != nil { + t.cache[locale] = nullcatalog{} + return + } + t.cache[locale] = catalog +} + +// Locale returns the catalog translations for a given Locale. If the given +// locale is not available, a NullCatalog is returned. +func (t Translations) Locale(locale string) Catalog { + _, ok := t.cache[locale] + if !ok { + t.load(locale) + } + return t.cache[locale] +} diff -Nru snapd-2.40/vendor/github.com/snapcore/go-gettext/LICENSE snapd-2.42.1/vendor/github.com/snapcore/go-gettext/LICENSE --- snapd-2.40/vendor/github.com/snapcore/go-gettext/LICENSE 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/snapcore/go-gettext/LICENSE 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,24 @@ +Copyright (c) 2016, Jonas Obrist +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + * Redistributions of source code must retain the above copyright + notice, this list of conditions and the following disclaimer. + * Redistributions in binary form must reproduce the above copyright + notice, this list of conditions and the following disclaimer in the + documentation and/or other materials provided with the distribution. + * Neither the name of Jonas Obrist nor the + names of its contributors may be used to endorse or promote products + derived from this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL JONAS OBRIST BE LIABLE FOR ANY +DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND +ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff -Nru snapd-2.40/vendor/github.com/snapcore/go-gettext/mofile.go snapd-2.42.1/vendor/github.com/snapcore/go-gettext/mofile.go --- snapd-2.40/vendor/github.com/snapcore/go-gettext/mofile.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/snapcore/go-gettext/mofile.go 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,246 @@ +package gettext + +import ( + "bytes" + "encoding/binary" + "fmt" + "log" + "os" + "strings" + + "github.com/snapcore/go-gettext/pluralforms" +) + +const le_magic = 0x950412de +const be_magic = 0xde120495 + +type header struct { + Version uint32 + NumStrings uint32 + MasterIndex uint32 + TranslationsIndex uint32 +} + +func (header header) get_major_version() uint32 { + return header.Version >> 16 +} + +func (header header) get_minor_version() uint32 { + return header.Version & 0xffff +} + +// Catalog of translations for a given locale. +type Catalog interface { + Gettext(msgid string) string + NGettext(msgid string, msgid_plural string, n uint32) string +} + +type mocatalog struct { + header header + language string + messages map[string][]string + pluralforms pluralforms.Expression + info map[string]string + charset string +} + +type nullcatalog struct{} + +func (catalog nullcatalog) Gettext(msgid string) string { + return msgid +} + +func (catalog nullcatalog) NGettext(msgid string, msgid_plural string, n uint32) string { + if n == 1 { + return msgid + } else { + return msgid_plural + } +} + +func (catalog mocatalog) Gettext(msgid string) string { + msgstrs, ok := catalog.messages[msgid] + if !ok { + return msgid + } + return msgstrs[0] +} + +func (catalog mocatalog) NGettext(msgid string, msgid_plural string, n uint32) string { + msgstrs, ok := catalog.messages[msgid] + if !ok { + if n == 1 { + return msgid + } else { + return msgid_plural + } + } else { + /* Bogus/missing pluralforms in mo */ + if catalog.pluralforms == nil { + /* Use the Germanic plural rule. */ + if n == 1 { + return msgstrs[0] + } else { + return msgstrs[1] + } + } + + index := catalog.pluralforms.Eval(n) + if index > len(msgstrs) { + if n == 1 { + return msgid + } else { + return msgid_plural + } + } + return msgstrs[index] + } +} + +type len_offset struct { + Len uint32 + Off uint32 +} + +func read_len_off(index uint32, file *os.File, order binary.ByteOrder) (len_offset, error) { + lenoff := len_offset{} + buf := make([]byte, 8) + _, err := file.Seek(int64(index), os.SEEK_SET) + if err != nil { + return lenoff, err + } + _, err = file.Read(buf) + if err != nil { + return lenoff, err + } + buffer := bytes.NewBuffer(buf) + err = binary.Read(buffer, order, &lenoff) + if err != nil { + return lenoff, err + } + return lenoff, nil +} + +func read_message(file *os.File, lenoff len_offset) (string, error) { + _, err := file.Seek(int64(lenoff.Off), os.SEEK_SET) + if err != nil { + return "", err + } + buf := make([]byte, lenoff.Len) + _, err = file.Read(buf) + if err != nil { + return "", err + } + return string(buf), nil +} + +func (catalog *mocatalog) read_info(info string) error { + lastk := "" + for _, line := range strings.Split(info, "\n") { + item := strings.TrimSpace(line) + if len(item) == 0 { + continue + } + var k string + var v string + if strings.Contains(item, ":") { + tmp := strings.SplitN(item, ":", 2) + k = strings.ToLower(strings.TrimSpace(tmp[0])) + v = strings.TrimSpace(tmp[1]) + catalog.info[k] = v + lastk = k + } else if len(lastk) != 0 { + catalog.info[lastk] += "\n" + item + } + if k == "content-type" { + catalog.charset = strings.Split(v, "charset=")[1] + } else if k == "plural-forms" { + p := strings.Split(v, ";")[1] + s := strings.Split(p, "plural=")[1] + expr, err := pluralforms.Compile(s) + if err != nil { + return err + } + catalog.pluralforms = expr + } + } + return nil +} + +// ParseMO parses a mo file into a Catalog if possible. +func ParseMO(file *os.File) (Catalog, error) { + var order binary.ByteOrder + header := header{} + catalog := mocatalog{ + header: header, + info: make(map[string]string), + messages: make(map[string][]string), + } + magic := make([]byte, 4) + _, err := file.Read(magic) + if err != nil { + return catalog, err + } + magic_number := binary.LittleEndian.Uint32(magic) + switch magic_number { + case le_magic: + order = binary.LittleEndian + case be_magic: + order = binary.BigEndian + default: + return catalog, fmt.Errorf("Wrong magic %d", magic_number) + } + raw_headers := make([]byte, 32) + _, err = file.Read(raw_headers) + if err != nil { + return catalog, err + } + buffer := bytes.NewBuffer(raw_headers) + err = binary.Read(buffer, order, &header) + if err != nil { + return catalog, err + } + if (header.get_major_version() != 0) && (header.get_major_version() != 1) { + log.Printf("major %d minor %d", header.get_major_version(), header.get_minor_version()) + return catalog, fmt.Errorf("Unsupported version: %d.%d", header.get_major_version(), header.get_minor_version()) + } + current_master_index := header.MasterIndex + current_transl_index := header.TranslationsIndex + var index uint32 = 0 + for ; index < header.NumStrings; index++ { + mlenoff, err := read_len_off(current_master_index, file, order) + if err != nil { + return catalog, err + } + tlenoff, err := read_len_off(current_transl_index, file, order) + if err != nil { + return catalog, err + } + msgid, err := read_message(file, mlenoff) + if err != nil { + return catalog, nil + } + msgstr, err := read_message(file, tlenoff) + if err != nil { + return catalog, err + } + if mlenoff.Len == 0 { + err = catalog.read_info(msgstr) + if err != nil { + return catalog, err + } + } + if strings.Contains(msgid, "\x00") { + // Plural! + msgidsingular := strings.Split(msgid, "\x00")[0] + translations := strings.Split(msgstr, "\x00") + catalog.messages[msgidsingular] = translations + } else { + catalog.messages[msgid] = []string{msgstr} + } + + current_master_index += 8 + current_transl_index += 8 + } + return catalog, nil +} diff -Nru snapd-2.40/vendor/github.com/snapcore/go-gettext/pluralforms/compiler.go snapd-2.42.1/vendor/github.com/snapcore/go-gettext/pluralforms/compiler.go --- snapd-2.40/vendor/github.com/snapcore/go-gettext/pluralforms/compiler.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/snapcore/go-gettext/pluralforms/compiler.go 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,427 @@ +package pluralforms + +import ( + "errors" + "fmt" + "regexp" + "strconv" + "strings" +) + +type match struct { + open_pos int + close_pos int +} + +var pat = regexp.MustCompile(`(\?|:|\|\||&&|==|!=|>=|>|<=|<|%|\d+|n)`) + +type expr_token interface { + compile(tokens []string) (expr Expression, err error) +} + +type test_token interface { + compile(tokens []string) (test test, err error) +} + +type cmp_test_builder func(val uint32, flipped bool) test +type logic_test_build func(left test, right test) test + +var ternary_token ternary_ + +type ternary_ struct{} + +func (ternary_) compile(tokens []string) (expr Expression, err error) { + main, err := split_tokens(tokens, "?") + if err != nil { + return expr, err + } + test, err := compile_test(strings.Join(main.Left, "")) + if err != nil { + return expr, err + } + actions, err := split_tokens(main.Right, ":") + if err != nil { + return expr, err + } + true_action, err := compile_expression(strings.Join(actions.Left, "")) + if err != nil { + return expr, err + } + false_action, err := compile_expression(strings.Join(actions.Right, "")) + if err != nil { + return expr, nil + } + return ternary{ + test: test, + true_expr: true_action, + false_expr: false_action, + }, nil +} + +var const_token const_val_ + +type const_val_ struct{} + +func (const_val_) compile(tokens []string) (expr Expression, err error) { + if len(tokens) == 0 { + return expr, errors.New("Got nothing instead of constant") + } + if len(tokens) != 1 { + return expr, fmt.Errorf("Invalid constant: %s", strings.Join(tokens, "")) + } + i, err := strconv.Atoi(tokens[0]) + if err != nil { + return expr, err + } + return const_value{value: i}, nil +} + +func compile_logic_test(tokens []string, sep string, builder logic_test_build) (test test, err error) { + split, err := split_tokens(tokens, sep) + if err != nil { + return test, err + } + left, err := compile_test(strings.Join(split.Left, "")) + if err != nil { + return test, err + } + right, err := compile_test(strings.Join(split.Right, "")) + if err != nil { + return test, err + } + return builder(left, right), nil +} + +var or_token or_ + +type or_ struct{} + +func (or_) compile(tokens []string) (test test, err error) { + return compile_logic_test(tokens, "||", build_or) +} +func build_or(left test, right test) test { + return or{left: left, right: right} +} + +var and_token and_ + +type and_ struct{} + +func (and_) compile(tokens []string) (test test, err error) { + return compile_logic_test(tokens, "&&", build_and) +} +func build_and(left test, right test) test { + return and{left: left, right: right} +} + +func compile_mod(tokens []string) (math math, err error) { + split, err := split_tokens(tokens, "%") + if err != nil { + return math, err + } + if len(split.Left) != 1 || split.Left[0] != "n" { + return math, errors.New("Modulus operation requires 'n' as left operand") + } + if len(split.Right) != 1 { + return math, errors.New("Modulus operation requires simple integer as right operand") + } + i, err := parse_uint32(split.Right[0]) + if err != nil { + return math, err + } + return mod{value: uint32(i)}, nil +} + +func _pipe(mod_tokens []string, action_tokens []string, builder cmp_test_builder, flipped bool) (test test, err error) { + modifier, err := compile_mod(mod_tokens) + if err != nil { + return test, err + } + if len(action_tokens) != 1 { + return test, errors.New("Can only get modulus of integer") + } + i, err := parse_uint32(action_tokens[0]) + if err != nil { + return test, err + } + action := builder(uint32(i), flipped) + return pipe{ + modifier: modifier, + action: action, + }, nil +} + +func compile_equality(tokens []string, sep string, builder cmp_test_builder) (test test, err error) { + split, err := split_tokens(tokens, sep) + if err != nil { + return test, err + } + if len(split.Left) == 1 && split.Left[0] == "n" { + if len(split.Right) != 1 { + return test, errors.New("test can only compare n to integers") + } + i, err := parse_uint32(split.Right[0]) + if err != nil { + return test, err + } + return builder(i, false), nil + } else if len(split.Right) == 1 && split.Right[0] == "n" { + if len(split.Left) != 1 { + return test, errors.New("test can only compare n to integers") + } + i, err := parse_uint32(split.Left[0]) + if err != nil { + return test, err + } + return builder(i, true), nil + } else if contains(split.Left, "n") && contains(split.Left, "%") { + return _pipe(split.Left, split.Right, builder, false) + } else { + return test, errors.New("equality test must have 'n' as one of the two tests") + } +} + +var eq_token eq_ + +type eq_ struct{} + +func (eq_) compile(tokens []string) (test test, err error) { + return compile_equality(tokens, "==", build_eq) +} +func build_eq(val uint32, flipped bool) test { + return equal{value: val} +} + +var neq_token neq_ + +type neq_ struct{} + +func (neq_) compile(tokens []string) (test test, err error) { + return compile_equality(tokens, "!=", build_neq) +} +func build_neq(val uint32, flipped bool) test { + return notequal{value: val} +} + +var gt_token gt_ + +type gt_ struct{} + +func (gt_) compile(tokens []string) (test test, err error) { + return compile_equality(tokens, ">", build_gt) +} +func build_gt(val uint32, flipped bool) test { + return gt{value: val, flipped: flipped} +} + +var gte_token gte_ + +type gte_ struct{} + +func (gte_) compile(tokens []string) (test test, err error) { + return compile_equality(tokens, ">=", build_gte) +} +func build_gte(val uint32, flipped bool) test { + return gte{value: val, flipped: flipped} +} + +var lt_token lt_ + +type lt_ struct{} + +func (lt_) compile(tokens []string) (test test, err error) { + return compile_equality(tokens, "<", build_lt) +} +func build_lt(val uint32, flipped bool) test { + return lt{value: val, flipped: flipped} +} + +var lte_token lte_ + +type lte_ struct{} + +func (lte_) compile(tokens []string) (test test, err error) { + return compile_equality(tokens, "<=", build_lte) +} +func build_lte(val uint32, flipped bool) test { + return lte{value: val, flipped: flipped} +} + +type test_token_def struct { + op string + token test_token +} + +var precedence = []test_token_def{ + test_token_def{op: "||", token: or_token}, + test_token_def{op: "&&", token: and_token}, + test_token_def{op: "==", token: eq_token}, + test_token_def{op: "!=", token: neq_token}, + test_token_def{op: ">=", token: gte_token}, + test_token_def{op: ">", token: gt_token}, + test_token_def{op: "<=", token: lte_token}, + test_token_def{op: "<", token: lt_token}, +} + +type splitted struct { + Left []string + Right []string +} + +// Find index of token in list of tokens +func index(tokens []string, sep string) int { + for index, token := range tokens { + if token == sep { + return index + } + } + return -1 +} + +// Split a list of tokens by a token into a splitted struct holding the tokens +// before and after the token to be split by. +func split_tokens(tokens []string, sep string) (s splitted, err error) { + index := index(tokens, sep) + if index == -1 { + return s, fmt.Errorf("'%s' not found in ['%s']", sep, strings.Join(tokens, "','")) + } + return splitted{ + Left: tokens[:index], + Right: tokens[index+1:], + }, nil +} + +// Scan a string for parenthesis +func scan(s string) <-chan match { + ch := make(chan match) + go func() { + depth := 0 + opener := 0 + for index, char := range s { + switch char { + case '(': + if depth == 0 { + opener = index + } + depth++ + case ')': + depth-- + if depth == 0 { + ch <- match{ + open_pos: opener, + close_pos: index + 1, + } + } + } + + } + close(ch) + }() + return ch +} + +// Split the string into tokens +func split(s string) <- chan string { + ch := make(chan string) + go func() { + s = strings.Replace(s, " ", "", -1) + if !strings.Contains(s, "(") { + ch <- s + } else { + last := 0 + end := len(s) + for info := range scan(s) { + if last != info.open_pos { + ch <- s[last:info.open_pos] + } + ch <- s[info.open_pos:info.close_pos] + last = info.close_pos + } + if last != end { + ch <- s[last:] + } + } + close(ch) + }() + return ch +} + +// Tokenizes a string into a list of strings, tokens grouped by parenthesis are +// not split! If the string starts with ( and ends in ), those are stripped. +func tokenize(s string) []string { + /* + TODO: Properly detect if the string starts with a ( and ends with a ) + and that those two form a matching pair. + + Eg: (foo) -> true; (foo)(bar) -> false; + */ + if s[0] == '(' && s[len(s)-1] == ')' { + s = s[1 : len(s)-1] + } + ret := []string{} + for chunk := range split(s) { + if len(chunk) != 0 { + if chunk[0] == '(' && chunk[len(chunk)-1] == ')' { + ret = append(ret, chunk) + } else { + for _, token := range pat.FindAllStringSubmatch(chunk, -1) { + ret = append(ret, token[0]) + } + } + } else { + fmt.Printf("Empty chunk in string '%s'\n", s) + } + } + return ret +} + +// Compile a string containing a plural form expression to a Expression object. +func Compile(s string) (expr Expression, err error) { + if s == "0" { + return const_value{value: 0}, nil + } + if !strings.Contains(s, "?") { + s += "?1:0" + } + return compile_expression(s) +} + +// Check if a token is in a slice of strings +func contains(haystack []string, needle string) bool { + for _, s := range haystack { + if s == needle { + return true + } + } + return false +} + +// Compiles an expression (ternary or constant) +func compile_expression(s string) (expr Expression, err error) { + tokens := tokenize(s) + if contains(tokens, "?") { + return ternary_token.compile(tokens) + } else { + return const_token.compile(tokens) + } +} + +// Compiles a test (comparison) +func compile_test(s string) (test test, err error) { + tokens := tokenize(s) + for _, token_def := range precedence { + if contains(tokens, token_def.op) { + return token_def.token.compile(tokens) + } + } + return test, errors.New("Cannot compile") +} + +func parse_uint32(s string) (ui uint32, err error) { + i, err := strconv.ParseUint(s, 10, 32) + if err != nil { + return ui, err + } else { + return uint32(i), nil + } +} diff -Nru snapd-2.40/vendor/github.com/snapcore/go-gettext/pluralforms/expression.go snapd-2.42.1/vendor/github.com/snapcore/go-gettext/pluralforms/expression.go --- snapd-2.40/vendor/github.com/snapcore/go-gettext/pluralforms/expression.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/snapcore/go-gettext/pluralforms/expression.go 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,39 @@ +package pluralforms + +// Expression is a plurfalforms expression. Eval evaluates the expression for +// a given n value. Use pluralforms.Compile to generate Expression instances. +type Expression interface { + Eval(n uint32) int +} + +type const_value struct { + value int +} + +func (c const_value) Eval(n uint32) int { + return c.value +} + +type test interface { + test(n uint32) bool +} + +type ternary struct { + test test + true_expr Expression + false_expr Expression +} + +func (t ternary) Eval(n uint32) int { + if t.test.test(n) { + if t.true_expr == nil { + return -1 + } + return t.true_expr.Eval(n) + } else { + if t.false_expr == nil { + return -1 + } + return t.false_expr.Eval(n) + } +} diff -Nru snapd-2.40/vendor/github.com/snapcore/go-gettext/pluralforms/genfixture.py snapd-2.42.1/vendor/github.com/snapcore/go-gettext/pluralforms/genfixture.py --- snapd-2.40/vendor/github.com/snapcore/go-gettext/pluralforms/genfixture.py 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/snapcore/go-gettext/pluralforms/genfixture.py 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,35 @@ +import json +from gettext import c2py + + +PLURAL_FORMS = [ + "0", + "n!=1", + "n>1", + "n%10==1&&n%100!=11?0:n!=0?1:2", + "n==1?0:n==2?1:2", + "n==1?0:(n==0||(n%100>0&&n%100<20))?1:2", + "n%10==1&&n%100!=11?0:n%10>=2&&(n%100<10||n%100>=20)?1:2", + "n%10==1&&n%100!=11?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2", + "(n==1)?0:(n>=2&&n<=4)?1:2", + "n==1?0:n%10>=2&&n%10<=4&&(n%100<10||n%100>=20)?1:2", + "n%100==1?0:n%100==2?1:n%100==3||n%100==4?2:3", + "n==0?0:n==1?1:n==2?2:n%100>=3&&n%100<=10?3:n%100>=11?4:5", +] + +NUM = 1000 + + +def gen(): + tests = [] + for plural_form in PLURAL_FORMS: + expr = c2py(plural_form) + tests.append({ + 'pluralform': plural_form, + 'fixture': [expr(n) for n in range(NUM + 1)] + }) + return json.dumps(tests) + + +if __name__ == "__main__": + print(gen()) diff -Nru snapd-2.40/vendor/github.com/snapcore/go-gettext/pluralforms/math.go snapd-2.42.1/vendor/github.com/snapcore/go-gettext/pluralforms/math.go --- snapd-2.40/vendor/github.com/snapcore/go-gettext/pluralforms/math.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/snapcore/go-gettext/pluralforms/math.go 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,13 @@ +package pluralforms + +type math interface { + calc(n uint32) uint32 +} + +type mod struct { + value uint32 +} + +func (m mod) calc(n uint32) uint32 { + return n % m.value +} diff -Nru snapd-2.40/vendor/github.com/snapcore/go-gettext/pluralforms/tests.go snapd-2.42.1/vendor/github.com/snapcore/go-gettext/pluralforms/tests.go --- snapd-2.40/vendor/github.com/snapcore/go-gettext/pluralforms/tests.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/snapcore/go-gettext/pluralforms/tests.go 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,104 @@ +package pluralforms + +type equal struct { + value uint32 +} + +func (e equal) test(n uint32) bool { + return n == e.value +} + +type notequal struct { + value uint32 +} + +func (e notequal) test(n uint32) bool { + return n != e.value +} + +type gt struct { + value uint32 + flipped bool +} + +func (e gt) test(n uint32) bool { + if e.flipped { + return e.value > n + } else { + return n > e.value + } +} + +type lt struct { + value uint32 + flipped bool +} + +func (e lt) test(n uint32) bool { + if e.flipped { + return e.value < n + } else { + return n < e.value + } +} + +type gte struct { + value uint32 + flipped bool +} + +func (e gte) test(n uint32) bool { + if e.flipped { + return e.value >= n + } else { + return n >= e.value + } +} + +type lte struct { + value uint32 + flipped bool +} + +func (e lte) test(n uint32) bool { + if e.flipped { + return e.value <= n + } else { + return n <= e.value + } +} + +type and struct { + left test + right test +} + +func (e and) test(n uint32) bool { + if !e.left.test(n) { + return false + } else { + return e.right.test(n) + } +} + +type or struct { + left test + right test +} + +func (e or) test(n uint32) bool { + if e.left.test(n) { + return true + } else { + return e.right.test(n) + } +} + +type pipe struct { + modifier math + action test +} + +func (e pipe) test(n uint32) bool { + return e.action.test(e.modifier.calc(n)) +} diff -Nru snapd-2.40/vendor/github.com/snapcore/go-gettext/README.md snapd-2.42.1/vendor/github.com/snapcore/go-gettext/README.md --- snapd-2.40/vendor/github.com/snapcore/go-gettext/README.md 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/snapcore/go-gettext/README.md 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,33 @@ +# gettext in golang + +[![Build Status](https://travis-ci.org/ojii/gettext.go.svg?branch=master)](https://travis-ci.org/ojii/gettext.go) + +## TODO + +- [x] parse mofiles +- [x] compile plural forms +- [ ] non-utf8 mo files (possible wontfix) +- [x] gettext +- [x] ngettext +- [x] managing mo files / sane API + + +## Example + + +```go + +import gettext + +translations := gettext.NewTranslations("path/to/translations/", "messages", gettext.DefaultResolver) + +locale := translations.Locale("en") + +fmt.Println(locale.Gettext("hello from gettext")) + +one := 1 +two := 2 + +fmt.Println(fmt.Sprintf(locale.NGettext("%d thing", "%d things", uint32(one)), one)) +fmt.Println(fmt.Sprintf(locale.NGettext("%d thing", "%d things", uint32(two)), two)) +``` diff -Nru snapd-2.40/vendor/github.com/snapcore/go-gettext/test_utils.go snapd-2.42.1/vendor/github.com/snapcore/go-gettext/test_utils.go --- snapd-2.40/vendor/github.com/snapcore/go-gettext/test_utils.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/vendor/github.com/snapcore/go-gettext/test_utils.go 2019-10-17 12:41:14.000000000 +0000 @@ -0,0 +1,10 @@ +package gettext + +import "testing" + +func assert_equal(t *testing.T, expected string, got string) { + if expected != got { + t.Logf("%s != %s", expected, got) + t.Fail() + } +} diff -Nru snapd-2.40/vendor/vendor.json snapd-2.42.1/vendor/vendor.json --- snapd-2.40/vendor/vendor.json 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/vendor/vendor.json 2019-10-30 12:17:43.000000000 +0000 @@ -13,10 +13,10 @@ "revisionTime": "2018-05-11T13:34:05Z" }, { - "checksumSHA1": "h77tT8kVh8x/J5ikkZReONPUjU0=", + "checksumSHA1": "qwK75TRXmR/k8CiegYaeqaCDek4=", "path": "github.com/godbus/dbus", - "revision": "97646858c46433e4afb3432ad28c12e968efa298", - "revisionTime": "2017-08-22T15:24:03Z" + "revision": "4481cbc300e2df0c0b3cecc18b6c16c6c0bb885d", + "revisionTime": "2019-07-26T02:52:47Z" }, { "checksumSHA1": "NrP46FPoALgKz3FY6puL3syMAAI=", @@ -25,16 +25,10 @@ "revisionTime": "2017-08-22T15:24:03Z" }, { - "checksumSHA1": "iIUYZyoanCQQTUaWsu8b+iOSPt4=", - "path": "github.com/gorilla/context", - "revision": "1c83b3eabd45b6d76072b66b746c20815fb2872d", - "revisionTime": "2015-08-20T05:12:45Z" - }, - { - "checksumSHA1": "xF+WqeL9eAlbUkiO2bLghMQzO4k=", + "checksumSHA1": "2Iy16w6c+4oMD8TtobbO0qsCrDo=", "path": "github.com/gorilla/mux", - "revision": "0eeaf8392f5b04950925b8a69fe70f110fa7cbfc", - "revisionTime": "2016-03-17T21:34:30Z" + "revision": "d83b6ffe499a29cc05fc977988d0392851779620", + "revisionTime": "2019-07-01T20:26:33Z" }, { "checksumSHA1": "GcYF2BhpiQkshVVXxHPS/Oq491c=", @@ -73,18 +67,6 @@ "revisionTime": "2018-03-08T15:25:21Z" }, { - "checksumSHA1": "lG6diF/yE9cGgQIKRAlsaeYAjO4=", - "path": "github.com/ojii/gettext.go", - "revision": "95289a7e0ac17c76737a5ceca3c9471c0adf70c7", - "revisionTime": "2016-07-14T06:47:45Z" - }, - { - "checksumSHA1": "j5FytTwC2nAqSrDR7V7O7E4cHKM=", - "path": "github.com/ojii/gettext.go/pluralforms", - "revision": "95289a7e0ac17c76737a5ceca3c9471c0adf70c7", - "revisionTime": "2016-07-14T06:47:45Z" - }, - { "checksumSHA1": "XEtWdIrRbRSFBR9q+SOATOuy7vM=", "comment": "fork of boltdb/bolt to fix build failure on ppc. upstream: https://github.com/boltdb/bolt/pull/740 and https://github.com/coreos/bbolt/pull/73", "path": "github.com/snapcore/bolt", @@ -92,6 +74,18 @@ "revisionTime": "2018-01-18T17:01:45Z" }, { + "checksumSHA1": "CSPV27Mm4+WBlyGFT/lKz1la/VI=", + "path": "github.com/snapcore/go-gettext", + "revision": "6598fb28bb07cb32298324b4958677c2f00cacd9", + "revisionTime": "2017-09-28T14:21:59Z" + }, + { + "checksumSHA1": "ZHYmaLRlCMJfx0+Ppr8dZ/+KtAc=", + "path": "github.com/snapcore/go-gettext/pluralforms", + "revision": "6598fb28bb07cb32298324b4958677c2f00cacd9", + "revisionTime": "2017-09-28T14:21:59Z" + }, + { "checksumSHA1": "3AmEm18mKj8XxBuru/ix4OOpRkE=", "path": "github.com/snapcore/squashfuse", "revision": "319f6d41a0419465a55d9dcb848d2408b97764f9", diff -Nru snapd-2.40/wrappers/core18.go snapd-2.42.1/wrappers/core18.go --- snapd-2.40/wrappers/core18.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/wrappers/core18.go 2019-10-30 12:17:43.000000000 +0000 @@ -204,5 +204,71 @@ return err } + // Handle the user services + if err := writeSnapdUserServicesOnCore(s, inter); err != nil { + return err + } + + return nil +} + +func writeSnapdUserServicesOnCore(s *snap.Info, inter interacter) error { + // Ensure /etc/systemd/user exists + if err := os.MkdirAll(dirs.SnapUserServicesDir, 0755); err != nil { + return err + } + + sysd := systemd.New(dirs.GlobalRootDir, systemd.GlobalUserMode, inter) + + serviceUnits, err := filepath.Glob(filepath.Join(s.MountDir(), "usr/lib/systemd/user/*.service")) + if err != nil { + return err + } + socketUnits, err := filepath.Glob(filepath.Join(s.MountDir(), "usr/lib/systemd/user/*.socket")) + if err != nil { + return err + } + units := append(serviceUnits, socketUnits...) + + snapdUnits := make(map[string]*osutil.FileState, len(units)+1) + for _, unit := range units { + st, err := os.Stat(unit) + if err != nil { + return err + } + content, err := ioutil.ReadFile(unit) + if err != nil { + return err + } + content = execStartRe.ReplaceAll(content, []byte(fmt.Sprintf(`ExecStart=%s$1`, s.MountDir()))) + + snapdUnits[filepath.Base(unit)] = &osutil.FileState{ + Content: content, + Mode: st.Mode(), + } + } + changed, removed, err := osutil.EnsureDirStateGlobs(dirs.SnapUserServicesDir, []string{"snapd.*.service", "snapd.*.socket"}, snapdUnits) + if err != nil { + // TODO: uhhhh, what do we do in this case? + return err + } + if (len(changed) + len(removed)) == 0 { + // nothing to do + return nil + } + // disable all removed units first + for _, unit := range removed { + if err := sysd.Disable(unit); err != nil { + logger.Noticef("failed to disable %q: %v", unit, err) + } + } + + // enable/start all the new services + for _, unit := range changed { + if err := sysd.Enable(unit); err != nil { + return err + } + } + return nil } diff -Nru snapd-2.40/wrappers/core18_test.go snapd-2.42.1/wrappers/core18_test.go --- snapd-2.40/wrappers/core18_test.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/wrappers/core18_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -39,6 +39,8 @@ func makeMockSnapdSnap(c *C) *snap.Info { err := os.MkdirAll(dirs.SnapServicesDir, 0755) c.Assert(err, IsNil) + err = os.MkdirAll(dirs.SnapUserServicesDir, 0755) + c.Assert(err, IsNil) info := snaptest.MockSnap(c, snapdYaml, &snap.SideInfo{Revision: snap.R(1)}) snapdDir := filepath.Join(info.MountDir(), "lib", "systemd", "system") @@ -54,6 +56,16 @@ err = ioutil.WriteFile(snapdAutoimport, []byte("[Unit]\nExecStart=/usr/bin/snap auto-import"), 0644) c.Assert(err, IsNil) + userUnitDir := filepath.Join(info.MountDir(), "usr", "lib", "systemd", "user") + err = os.MkdirAll(userUnitDir, 0755) + c.Assert(err, IsNil) + agentSrv := filepath.Join(userUnitDir, "snapd.session-agent.service") + err = ioutil.WriteFile(agentSrv, []byte("[Unit]\nExecStart=/usr/bin/snap session-agent"), 0644) + c.Assert(err, IsNil) + agentSock := filepath.Join(userUnitDir, "snapd.session-agent.socket") + err = ioutil.WriteFile(agentSock, []byte("[Unit]\n[Socket]\nListenStream=%t/snap-session.socket"), 0644) + c.Assert(err, IsNil) + return info } @@ -117,6 +129,18 @@ WantedBy=snapd.service `, dirs.GlobalRootDir)) + // check that snapd.session-agent.service is created + content, err = ioutil.ReadFile(filepath.Join(dirs.SnapUserServicesDir, "snapd.session-agent.service")) + c.Assert(err, IsNil) + // and paths get re-written + c.Check(string(content), Equals, fmt.Sprintf("[Unit]\nExecStart=%s/snapd/1/usr/bin/snap session-agent", dirs.SnapMountDir)) + + // check that snapd.session-agent.socket is created + content, err = ioutil.ReadFile(filepath.Join(dirs.SnapUserServicesDir, "snapd.session-agent.socket")) + c.Assert(err, IsNil) + // and paths get re-written + c.Check(string(content), Equals, "[Unit]\n[Socket]\nListenStream=%t/snap-session.socket") + // check the systemctl calls c.Check(s.sysdLog, DeepEquals, [][]string{ {"daemon-reload"}, @@ -135,6 +159,8 @@ {"start", "snapd.service"}, {"start", "--no-block", "snapd.seeded.service"}, {"start", "--no-block", "snapd.autoimport.service"}, + {"--user", "--global", "--root", dirs.GlobalRootDir, "enable", "snapd.session-agent.service"}, + {"--user", "--global", "--root", dirs.GlobalRootDir, "enable", "snapd.session-agent.socket"}, }) } @@ -152,6 +178,8 @@ c.Check(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, "snapd.autoimport.service")), Equals, false) c.Check(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, "snapd.system-shutdown.service")), Equals, false) c.Check(osutil.FileExists(filepath.Join(dirs.SnapServicesDir, "usr-lib-snapd.mount")), Equals, false) + c.Check(osutil.FileExists(filepath.Join(dirs.SnapUserServicesDir, "snapd.session-agent.service")), Equals, false) + c.Check(osutil.FileExists(filepath.Join(dirs.SnapUserServicesDir, "snapd.session-agent.socket")), Equals, false) // check that no systemctl calls happened c.Check(s.sysdLog, IsNil) diff -Nru snapd-2.40/wrappers/desktop.go snapd-2.42.1/wrappers/desktop.go --- snapd-2.40/wrappers/desktop.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/wrappers/desktop.go 2019-10-30 12:17:43.000000000 +0000 @@ -142,6 +142,36 @@ return "", fmt.Errorf("invalid exec command: %q", cmd) } +func rewriteIconLine(s *snap.Info, line string) (string, error) { + icon := strings.SplitN(line, "=", 2)[1] + + // If there is a path separator, assume the icon is a path name + if strings.ContainsRune(icon, filepath.Separator) { + if !strings.HasPrefix(icon, "${SNAP}/") { + return "", fmt.Errorf("icon path %q is not part of the snap", icon) + } + if filepath.Clean(icon) != icon { + return "", fmt.Errorf("icon path %q is not canonicalized, did you mean %q?", icon, filepath.Clean(icon)) + } + return line, nil + } + + // If the icon is prefixed with "snap.${SNAP_NAME}.", rewrite + // to the instance name. + snapIconPrefix := fmt.Sprintf("snap.%s.", s.SnapName()) + if strings.HasPrefix(icon, snapIconPrefix) { + return fmt.Sprintf("Icon=snap.%s.%s", s.InstanceName(), icon[len(snapIconPrefix):]), nil + } + + // If the icon has any other "snap." prefix, treat this as an error. + if strings.HasPrefix(icon, "snap.") { + return "", fmt.Errorf("invalid icon name: %q, must start with %q", icon, snapIconPrefix) + } + + // Allow other icons names through unchanged. + return line, nil +} + func sanitizeDesktopFile(s *snap.Info, desktopFile string, rawcontent []byte) []byte { var newContent bytes.Buffer mountDir := []byte(s.MountDir()) @@ -163,6 +193,16 @@ continue } bline = []byte(line) + } + + // rewrite icon line if it references an icon theme icon + if bytes.HasPrefix(bline, []byte("Icon=")) { + line, err := rewriteIconLine(s, string(bline)) + if err != nil { + logger.Debugf("ignoring icon in source desktop file %q: %s", filepath.Base(desktopFile), err) + continue + } + bline = []byte(line) } // do variable substitution diff -Nru snapd-2.40/wrappers/desktop_test.go snapd-2.42.1/wrappers/desktop_test.go --- snapd-2.40/wrappers/desktop_test.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/wrappers/desktop_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -482,6 +482,68 @@ } } +func (s *sanitizeDesktopFileSuite) TestRewriteIconLine(c *C) { + snap, err := snap.InfoFromSnapYaml([]byte(` +name: snap +version: 1.0 +`)) + c.Assert(err, IsNil) + + newl, err := wrappers.RewriteIconLine(snap, "Icon=${SNAP}/icon.png") + c.Check(err, IsNil) + c.Check(newl, Equals, "Icon=${SNAP}/icon.png") + + newl, err = wrappers.RewriteIconLine(snap, "Icon=snap.snap.icon") + c.Check(err, IsNil) + c.Check(newl, Equals, "Icon=snap.snap.icon") + + newl, err = wrappers.RewriteIconLine(snap, "Icon=other-icon") + c.Check(err, IsNil) + c.Check(newl, Equals, "Icon=other-icon") + + snap.InstanceKey = "bar" + newl, err = wrappers.RewriteIconLine(snap, "Icon=snap.snap.icon") + c.Check(err, IsNil) + c.Check(newl, Equals, "Icon=snap.snap_bar.icon") + + _, err = wrappers.RewriteIconLine(snap, "Icon=snap.othersnap.icon") + c.Check(err, ErrorMatches, `invalid icon name: "snap.othersnap.icon", must start with "snap.snap."`) + + _, err = wrappers.RewriteIconLine(snap, "Icon=/etc/passwd") + c.Check(err, ErrorMatches, `icon path "/etc/passwd" is not part of the snap`) + + _, err = wrappers.RewriteIconLine(snap, "Icon=${SNAP}/./icon.png") + c.Check(err, ErrorMatches, `icon path "\${SNAP}/./icon.png" is not canonicalized, did you mean "\${SNAP}/icon.png"\?`) + + _, err = wrappers.RewriteIconLine(snap, "Icon=${SNAP}/../outside/icon.png") + c.Check(err, ErrorMatches, `icon path "\${SNAP}/../outside/icon.png" is not canonicalized, did you mean "outside/icon.png"\?`) +} + +func (s *sanitizeDesktopFileSuite) TestSanitizeParallelInstancesIconName(c *C) { + snap, err := snap.InfoFromSnapYaml([]byte(` +name: snap +version: 1.0 +apps: + app: + command: cmd +`)) + snap.InstanceKey = "bar" + c.Assert(err, IsNil) + desktopContent := []byte(`[Desktop Entry] +Name=foo +Icon=snap.snap.icon +Exec=snap.app +`) + df := filepath.Base(snap.Apps["app"].DesktopFile()) + e := wrappers.SanitizeDesktopFile(snap, df, desktopContent) + c.Assert(string(e), Equals, fmt.Sprintf(`[Desktop Entry] +X-SnapInstanceName=snap_bar +Name=foo +Icon=snap.snap_bar.icon +Exec=env BAMF_DESKTOP_FILE_HINT=snap_bar_app.desktop %s/bin/snap_bar.app +`, dirs.SnapMountDir)) +} + func (s *desktopSuite) TestAddRemoveDesktopFiles(c *C) { var tests = []struct { instance string diff -Nru snapd-2.40/wrappers/export_test.go snapd-2.42.1/wrappers/export_test.go --- snapd-2.40/wrappers/export_test.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/wrappers/export_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -33,10 +33,14 @@ // desktop SanitizeDesktopFile = sanitizeDesktopFile RewriteExecLine = rewriteExecLine + RewriteIconLine = rewriteIconLine IsValidDesktopFileLine = isValidDesktopFileLine // timers GenerateOnCalendarSchedules = generateOnCalendarSchedules + + // icons + FindIconFiles = findIconFiles ) func MockKillWait(wait time.Duration) (restore func()) { diff -Nru snapd-2.40/wrappers/icons.go snapd-2.42.1/wrappers/icons.go --- snapd-2.40/wrappers/icons.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/wrappers/icons.go 2019-10-30 12:17:43.000000000 +0000 @@ -0,0 +1,126 @@ +// -*- 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 wrappers + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "strings" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + +func findIconFiles(snapName string, rootDir string) (icons []string, err error) { + if !osutil.IsDirectory(rootDir) { + return nil, nil + } + iconGlob := fmt.Sprintf("snap.%s.*", snapName) + forbiddenDirGlob := "snap.*" + err = filepath.Walk(rootDir, func(path string, info os.FileInfo, err error) error { + if err != nil { + return err + } + rel, err := filepath.Rel(rootDir, path) + if err != nil { + return err + } + base := filepath.Base(path) + if info.IsDir() { + // Ignore directories that could match an icon glob + if ok, err := filepath.Match(forbiddenDirGlob, base); ok || err != nil { + return filepath.SkipDir + } + } else { + if ok, err := filepath.Match(iconGlob, base); err != nil { + return err + } else if ok { + ext := filepath.Ext(base) + if ext == ".png" || ext == ".svg" { + icons = append(icons, rel) + } + } + } + return nil + }) + return icons, err +} + +func deriveIconContent(instanceName string, rootDir string, icons []string) (content map[string]map[string]*osutil.FileState, err error) { + content = make(map[string]map[string]*osutil.FileState) + snapPrefix := fmt.Sprintf("snap.%s.", snap.InstanceSnap(instanceName)) + instancePrefix := fmt.Sprintf("snap.%s.", instanceName) + + for _, iconFile := range icons { + dir := filepath.Dir(iconFile) + base := filepath.Base(iconFile) + dirContent := content[dir] + if dirContent == nil { + dirContent = make(map[string]*osutil.FileState) + content[dir] = dirContent + } + data, err := ioutil.ReadFile(filepath.Join(rootDir, iconFile)) + if err != nil { + return nil, err + } + if !strings.HasPrefix(base, snapPrefix) { + return nil, fmt.Errorf("cannot use icon file %q: must start with snap prefix %q", iconFile, snapPrefix) + } + // rename icons to match snap instance name + base = instancePrefix + base[len(snapPrefix):] + dirContent[base] = &osutil.FileState{ + Content: data, + Mode: 0644, + } + } + return content, nil +} + +func AddSnapIcons(s *snap.Info) error { + if err := os.MkdirAll(dirs.SnapDesktopIconsDir, 0755); err != nil { + return err + } + + rootDir := filepath.Join(s.MountDir(), "meta", "gui", "icons") + icons, err := findIconFiles(s.SnapName(), rootDir) + if err != nil { + return err + } + + content, err := deriveIconContent(s.InstanceName(), rootDir, icons) + if err != nil { + return err + } + iconGlob := fmt.Sprintf("snap.%s.*", s.InstanceName()) + _, _, err = osutil.EnsureTreeState(dirs.SnapDesktopIconsDir, []string{iconGlob}, content) + return err +} + +func RemoveSnapIcons(s *snap.Info) error { + if !osutil.IsDirectory(dirs.SnapDesktopIconsDir) { + return nil + } + iconGlob := fmt.Sprintf("snap.%s.*", s.InstanceName()) + _, _, err := osutil.EnsureTreeState(dirs.SnapDesktopIconsDir, []string{iconGlob}, nil) + return err +} diff -Nru snapd-2.40/wrappers/icons_test.go snapd-2.42.1/wrappers/icons_test.go --- snapd-2.40/wrappers/icons_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.42.1/wrappers/icons_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 wrappers_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "sort" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/wrappers" +) + +type iconsTestSuite struct { + testutil.BaseTest + tempdir string +} + +var _ = Suite(&iconsTestSuite{}) + +func (s *iconsTestSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) + s.tempdir = c.MkDir() + dirs.SetRootDir(s.tempdir) +} + +func (s *iconsTestSuite) TearDownTest(c *C) { + dirs.SetRootDir("") + s.BaseTest.TearDownTest(c) +} + +func (s *iconsTestSuite) TestFindIconFiles(c *C) { + info := snaptest.MockSnap(c, packageHello, &snap.SideInfo{Revision: snap.R(11)}) + + baseDir := info.MountDir() + iconsDir := filepath.Join(baseDir, "meta", "gui", "icons") + c.Assert(os.MkdirAll(filepath.Join(iconsDir, "hicolor", "256x256", "apps"), 0755), IsNil) + c.Assert(os.MkdirAll(filepath.Join(iconsDir, "hicolor", "scalable", "apps"), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(iconsDir, "hicolor", "256x256", "apps", "snap.hello-snap.foo.png"), []byte("256x256"), 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(iconsDir, "hicolor", "scalable", "apps", "snap.hello-snap.foo.svg"), []byte("scalable"), 0644), IsNil) + + // Some files that shouldn't be picked up + c.Assert(ioutil.WriteFile(filepath.Join(iconsDir, "hicolor", "scalable", "apps", "snap.hello-snap.foo.exe"), []byte("bad extension"), 0644), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(iconsDir, "hicolor", "scalable", "apps", "org.example.hello.png"), []byte("bad prefix"), 0644), IsNil) + c.Assert(os.MkdirAll(filepath.Join(iconsDir, "snap.whatever"), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(iconsDir, "snap.whatever", "snap.hello-snap.foo.png"), []byte("bad dir"), 0644), IsNil) + + icons, err := wrappers.FindIconFiles(info.SnapName(), iconsDir) + sort.Strings(icons) + c.Assert(err, IsNil) + c.Check(icons, DeepEquals, []string{ + "hicolor/256x256/apps/snap.hello-snap.foo.png", + "hicolor/scalable/apps/snap.hello-snap.foo.svg", + }) +} + +func (s *iconsTestSuite) TestAddSnapIcons(c *C) { + info := snaptest.MockSnap(c, packageHello, &snap.SideInfo{Revision: snap.R(11)}) + + baseDir := info.MountDir() + iconsDir := filepath.Join(baseDir, "meta", "gui", "icons") + c.Assert(os.MkdirAll(filepath.Join(iconsDir, "hicolor", "scalable", "apps"), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(iconsDir, "hicolor", "scalable", "apps", "snap.hello-snap.foo.svg"), []byte("scalable"), 0644), IsNil) + + c.Assert(wrappers.AddSnapIcons(info), IsNil) + iconFile := filepath.Join(dirs.SnapDesktopIconsDir, "hicolor", "scalable", "apps", "snap.hello-snap.foo.svg") + c.Check(iconFile, testutil.FileEquals, "scalable") +} + +func (s *iconsTestSuite) TestRemoveSnapIcons(c *C) { + iconDir := filepath.Join(dirs.SnapDesktopIconsDir, "hicolor", "scalable", "apps") + iconFile := filepath.Join(iconDir, "snap.hello-snap.foo.svg") + c.Assert(os.MkdirAll(iconDir, 0755), IsNil) + c.Assert(ioutil.WriteFile(iconFile, []byte("contents"), 0644), IsNil) + + info := snaptest.MockSnap(c, packageHello, &snap.SideInfo{Revision: snap.R(11)}) + c.Assert(wrappers.RemoveSnapIcons(info), IsNil) + c.Check(iconFile, testutil.FileAbsent) +} + +func (s *iconsTestSuite) TestParallelInstanceAddIcons(c *C) { + info := snaptest.MockSnap(c, packageHello, &snap.SideInfo{Revision: snap.R(11)}) + info.InstanceKey = "instance" + + baseDir := info.MountDir() + iconsDir := filepath.Join(baseDir, "meta", "gui", "icons") + c.Assert(os.MkdirAll(filepath.Join(iconsDir, "hicolor", "scalable", "apps"), 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(iconsDir, "hicolor", "scalable", "apps", "snap.hello-snap.foo.svg"), []byte("scalable"), 0644), IsNil) + + c.Assert(wrappers.AddSnapIcons(info), IsNil) + iconFile := filepath.Join(dirs.SnapDesktopIconsDir, "hicolor", "scalable", "apps", "snap.hello-snap_instance.foo.svg") + c.Check(iconFile, testutil.FileEquals, "scalable") + + // No file installed without the instance qualifier + iconFile = filepath.Join(dirs.SnapDesktopIconsDir, "hicolor", "scalable", "apps", "snap.hello-snap.foo.svg") + c.Check(iconFile, testutil.FileAbsent) +} + +func (s *iconsTestSuite) TestParallelInstanceRemoveIcons(c *C) { + iconDir := filepath.Join(dirs.SnapDesktopIconsDir, "hicolor", "scalable", "apps") + c.Assert(os.MkdirAll(iconDir, 0755), IsNil) + snapNameFile := filepath.Join(iconDir, "snap.hello-snap.foo.svg") + c.Assert(ioutil.WriteFile(snapNameFile, []byte("contents"), 0644), IsNil) + instanceNameFile := filepath.Join(iconDir, "snap.hello-snap_instance.foo.svg") + c.Assert(ioutil.WriteFile(instanceNameFile, []byte("contents"), 0644), IsNil) + + info := snaptest.MockSnap(c, packageHello, &snap.SideInfo{Revision: snap.R(11)}) + info.InstanceKey = "instance" + c.Assert(wrappers.RemoveSnapIcons(info), IsNil) + c.Check(instanceNameFile, testutil.FileAbsent) + // The non-instance qualified icon remains + c.Check(snapNameFile, testutil.FilePresent) +} diff -Nru snapd-2.40/wrappers/services.go snapd-2.42.1/wrappers/services.go --- snapd-2.40/wrappers/services.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/wrappers/services.go 2019-10-30 12:17:43.000000000 +0000 @@ -200,7 +200,7 @@ // AddSnapServices adds service units for the applications from the snap which are services. func AddSnapServices(s *snap.Info, inter interacter) (err error) { - if s.SnapName() == "snapd" { + if s.GetType() == snap.TypeSnapd { return writeSnapdServicesOnCore(s, inter) } @@ -335,7 +335,27 @@ } return nil +} + +// ServicesEnableState returns a map of service names from the given snap, +// together with their enable/disable status. +func ServicesEnableState(s *snap.Info, inter interacter) (map[string]bool, error) { + sysd := systemd.New(dirs.GlobalRootDir, systemd.SystemMode, inter) + // loop over all services in the snap, querying systemd for the current + // systemd state of the snaps + snapSvcsState := make(map[string]bool, len(s.Apps)) + for name, app := range s.Apps { + if !app.IsService() { + continue + } + state, err := sysd.IsEnabled(app.ServiceName()) + if err != nil { + return nil, err + } + snapSvcsState[name] = state + } + return snapSvcsState, nil } // RemoveSnapServices disables and removes service units for the applications from the snap which are services. diff -Nru snapd-2.40/wrappers/services_test.go snapd-2.42.1/wrappers/services_test.go --- snapd-2.40/wrappers/services_test.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/wrappers/services_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -115,6 +115,7 @@ var snapdYaml = `name: snapd version: 1.0 +type: snapd ` func (s *servicesTestSuite) TestRemoveSnapWithSocketsRemovesSocketsService(c *C) { @@ -188,6 +189,111 @@ }) } +func (s *servicesTestSuite) TestServicesEnableState(c *C) { + info := snaptest.MockSnap(c, packageHello+` + svc2: + command: bin/hello + daemon: forking +`, &snap.SideInfo{Revision: snap.R(12)}) + svc1File := "snap.hello-snap.svc1.service" + svc2File := "snap.hello-snap.svc2.service" + + s.systemctlRestorer() + r := testutil.MockCommand(c, "systemctl", `#!/bin/sh + if [ "$1" = "--root" ]; then + # shifting by 2 also drops the temp dir arg to --root + shift 2 + fi + + case "$1" in + is-enabled) + case "$2" in + "snap.hello-snap.svc1.service") + echo "disabled" + exit 1 + ;; + "snap.hello-snap.svc2.service") + echo "enabled" + exit 0 + ;; + *) + echo "unexpected service $*" + exit 2 + ;; + esac + ;; + *) + echo "unexpected op $*" + exit 2 + esac + + exit 1 + `) + defer r.Restore() + + states, err := wrappers.ServicesEnableState(info, progress.Null) + c.Assert(err, IsNil) + + c.Assert(states, DeepEquals, map[string]bool{ + "svc1": false, + "svc2": true, + }) + + // the calls could be out of order in the list, since iterating over a map + // is non-deterministic, so manually check each call + c.Assert(r.Calls(), HasLen, 2) + for _, call := range r.Calls() { + c.Assert(call, HasLen, 5) + c.Assert(call[:4], DeepEquals, []string{"systemctl", "--root", s.tempdir, "is-enabled"}) + switch call[4] { + case svc1File, svc2File: + default: + c.Errorf("unknown service for systemctl call: %s", call[4]) + } + } +} + +func (s *servicesTestSuite) TestServicesEnableStateFail(c *C) { + info := snaptest.MockSnap(c, packageHello, &snap.SideInfo{Revision: snap.R(12)}) + svc1File := "snap.hello-snap.svc1.service" + + s.systemctlRestorer() + r := testutil.MockCommand(c, "systemctl", `#!/bin/sh + if [ "$1" = "--root" ]; then + # shifting by 2 also drops the temp dir arg to --root + shift 2 + fi + + case "$1" in + is-enabled) + case "$2" in + "snap.hello-snap.svc1.service") + echo "whoops" + exit 1 + ;; + *) + echo "unexpected service $*" + exit 2 + ;; + esac + ;; + *) + echo "unexpected op $*" + exit 2 + esac + + exit 1 + `) + defer r.Restore() + + _, err := wrappers.ServicesEnableState(info, progress.Null) + c.Assert(err, ErrorMatches, ".*is-enabled snap.hello-snap.svc1.service\\] failed with exit status 1: whoops\n.*") + + c.Assert(r.Calls(), DeepEquals, [][]string{ + {"systemctl", "--root", s.tempdir, "is-enabled", svc1File}, + }) +} + func (s *servicesTestSuite) TestStopServicesWithSockets(c *C) { var sysdLog []string r := systemd.MockSystemctl(func(cmd ...string) ([]byte, error) { diff -Nru snapd-2.40/xdgopenproxy/xdgopenproxy_test.go snapd-2.42.1/xdgopenproxy/xdgopenproxy_test.go --- snapd-2.40/xdgopenproxy/xdgopenproxy_test.go 2019-07-12 08:40:08.000000000 +0000 +++ snapd-2.42.1/xdgopenproxy/xdgopenproxy_test.go 2019-10-30 12:17:43.000000000 +0000 @@ -20,6 +20,7 @@ package xdgopenproxy_test import ( + "context" "fmt" "io/ioutil" "net/url" @@ -142,14 +143,35 @@ return &dbus.Call{Err: err} } +func (f fakeBusObject) CallWithContext(ctx context.Context, method string, flags dbus.Flags, args ...interface{}) *dbus.Call { + err := f(method, args...) + return &dbus.Call{Err: err} +} + func (f fakeBusObject) Go(method string, flags dbus.Flags, ch chan *dbus.Call, args ...interface{}) *dbus.Call { return nil } +func (f fakeBusObject) GoWithContext(ctx context.Context, method string, flags dbus.Flags, ch chan *dbus.Call, args ...interface{}) *dbus.Call { + return nil +} + +func (f fakeBusObject) AddMatchSignal(iface, member string, options ...dbus.MatchOption) *dbus.Call { + return nil +} + +func (f fakeBusObject) RemoveMatchSignal(iface, member string, options ...dbus.MatchOption) *dbus.Call { + return nil +} + func (f fakeBusObject) GetProperty(prop string) (dbus.Variant, error) { return dbus.Variant{}, nil } +func (f fakeBusObject) SetProperty(p string, v interface{}) error { + return nil +} + func (f fakeBusObject) Destination() string { return "" }