diff -Nru snapd-2.37.4ubuntu0.1/advisor/backend.go snapd-2.45.1ubuntu0.2/advisor/backend.go --- snapd-2.37.4ubuntu0.1/advisor/backend.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/advisor/backend.go 2020-06-05 13:13:49.000000000 +0000 @@ -29,7 +29,7 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/strutil" + "github.com/snapcore/snapd/randutil" ) var ( @@ -65,7 +65,7 @@ func Create() (CommandDB, error) { var err error t := &writer{ - fn: dirs.SnapCommandsDB + "." + strutil.MakeRandomString(12) + "~", + fn: dirs.SnapCommandsDB + "." + randutil.RandomString(12) + "~", } t.db, err = bolt.Open(t.fn, 0644, &bolt.Options{Timeout: 1 * time.Second}) diff -Nru snapd-2.37.4ubuntu0.1/arch/arch.go snapd-2.45.1ubuntu0.2/arch/arch.go --- snapd-2.37.4ubuntu0.1/arch/arch.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/arch/arch.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/arch/arch_test.go snapd-2.45.1ubuntu0.2/arch/arch_test.go --- snapd-2.37.4ubuntu0.1/arch/arch_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/arch/arch_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/asserts/asserts.go snapd-2.45.1ubuntu0.2/asserts/asserts.go --- snapd-2.37.4ubuntu0.1/asserts/asserts.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/asserts.go 2020-06-05 13:13:49.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. @@ -125,7 +136,8 @@ // 1: plugs and slots // 2: support for $SLOT()/$PLUG()/$MISSING // 3: support for on-store/on-brand/on-model device scope constraints - maxSupportedFormat[SnapDeclarationType.Name] = 3 + // 4: support for plug-names/slot-names constraints + maxSupportedFormat[SnapDeclarationType.Name] = 4 } func MockMaxSupportedFormat(assertType *AssertionType, maxFormat int) (restore func()) { diff -Nru snapd-2.37.4ubuntu0.1/asserts/assertstest/assertstest.go snapd-2.45.1ubuntu0.2/asserts/assertstest/assertstest.go --- snapd-2.37.4ubuntu0.1/asserts/assertstest/assertstest.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/assertstest/assertstest.go 2020-06-05 13:13:49.000000000 +0000 @@ -35,7 +35,7 @@ "golang.org/x/crypto/openpgp/packet" "github.com/snapcore/snapd/asserts" - "github.com/snapcore/snapd/strutil" + "github.com/snapcore/snapd/randutil" ) // GenerateKey generates a private/public key pair of the given bits. It panics on error. @@ -167,7 +167,7 @@ } otherHeaders["username"] = username if otherHeaders["account-id"] == nil { - otherHeaders["account-id"] = strutil.MakeRandomString(32) + otherHeaders["account-id"] = randutil.RandomString(32) } if otherHeaders["display-name"] == nil { otherHeaders["display-name"] = strings.ToTitle(username[:1]) + username[1:] @@ -444,3 +444,167 @@ } } } + +// FakeAssertionWithBody builds a fake assertion with the given body +// and layered headers. A fake assertion cannot be verified or added +// to a database or properly encoded. It can still be useful for unit +// tests but shouldn't be used in integration tests. +func FakeAssertionWithBody(body []byte, headerLayers ...map[string]interface{}) asserts.Assertion { + headers := map[string]interface{}{ + "sign-key-sha3-384": "Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij", + } + for _, h := range headerLayers { + for k, v := range h { + headers[k] = v + } + } + + _, hasTimestamp := headers["timestamp"] + _, hasSince := headers["since"] + if !(hasTimestamp || hasSince) { + headers["timestamp"] = time.Now().Format(time.RFC3339) + } + + a, err := asserts.Assemble(headers, body, nil, []byte("AXNpZw==")) + if err != nil { + panic(fmt.Sprintf("cannot build fake assertion: %v", err)) + } + return a +} + +// FakeAssertion builds a fake assertion with given layered headers +// and an empty body. A fake assertion cannot be verified or added to +// a database or properly encoded. It can still be useful for unit +// tests but shouldn't be used in integration tests. +func FakeAssertion(headerLayers ...map[string]interface{}) asserts.Assertion { + return FakeAssertionWithBody(nil, headerLayers...) +} + +type accuDB interface { + Add(asserts.Assertion) error +} + +// AddMany conveniently adds the given assertions to the db. +// It is idempotent but otherwise panics on error. +func AddMany(db accuDB, assertions ...asserts.Assertion) { + for _, a := range assertions { + err := db.Add(a) + if _, ok := err.(*asserts.RevisionError); !ok { + if err != nil { + panic(fmt.Sprintf("cannot add test assertions: %v", err)) + } + } + } +} + +// SigningAccounts manages a set of brand or user accounts, +// with their keys that can sign models etc. +type SigningAccounts struct { + store *StoreStack + + signing map[string]*SigningDB + + accts map[string]*asserts.Account + acctKeys map[string]*asserts.AccountKey +} + +// NewSigningAccounts creates a new SigningAccounts instance. +func NewSigningAccounts(store *StoreStack) *SigningAccounts { + return &SigningAccounts{ + store: store, + signing: make(map[string]*SigningDB), + accts: make(map[string]*asserts.Account), + acctKeys: make(map[string]*asserts.AccountKey), + } +} + +func (sa *SigningAccounts) Register(accountID string, brandPrivKey asserts.PrivateKey, extra map[string]interface{}) *SigningDB { + brandSigning := NewSigningDB(accountID, brandPrivKey) + sa.signing[accountID] = brandSigning + + acctHeaders := map[string]interface{}{ + "account-id": accountID, + } + for k, v := range extra { + acctHeaders[k] = v + } + + brandAcct := NewAccount(sa.store, accountID, acctHeaders, "") + sa.accts[accountID] = brandAcct + + brandPubKey, err := brandSigning.PublicKey("") + if err != nil { + panic(err) + } + brandAcctKey := NewAccountKey(sa.store, brandAcct, nil, brandPubKey, "") + sa.acctKeys[accountID] = brandAcctKey + + return brandSigning +} + +func (sa *SigningAccounts) Account(accountID string) *asserts.Account { + if acct := sa.accts[accountID]; acct != nil { + return acct + } + panic(fmt.Sprintf("unknown test account-id: %s", accountID)) +} + +func (sa *SigningAccounts) AccountKey(accountID string) *asserts.AccountKey { + if acctKey := sa.acctKeys[accountID]; acctKey != nil { + return acctKey + } + panic(fmt.Sprintf("unknown test account-id: %s", accountID)) +} + +func (sa *SigningAccounts) PublicKey(accountID string) asserts.PublicKey { + pubKey, err := sa.Signing(accountID).PublicKey("") + if err != nil { + panic(err) + } + return pubKey +} + +func (sa *SigningAccounts) Signing(accountID string) *SigningDB { + // convenience + if accountID == sa.store.RootSigning.AuthorityID { + return sa.store.RootSigning + } + if signer := sa.signing[accountID]; signer != nil { + return signer + } + panic(fmt.Sprintf("unknown test account-id: %s", accountID)) +} + +// Model creates a new model for accountID. accountID can also be the account-id of the underlying store stack. +func (sa *SigningAccounts) Model(accountID, model string, extras ...map[string]interface{}) *asserts.Model { + headers := map[string]interface{}{ + "series": "16", + "brand-id": accountID, + "model": model, + "timestamp": time.Now().Format(time.RFC3339), + } + for _, extra := range extras { + for k, v := range extra { + headers[k] = v + } + } + + signer := sa.Signing(accountID) + + modelAs, err := signer.Sign(asserts.ModelType, headers, nil, "") + if err != nil { + panic(err) + } + return modelAs.(*asserts.Model) +} + +// AccountsAndKeys returns the account and account-key for each given +// accountID in that order. +func (sa *SigningAccounts) AccountsAndKeys(accountIDs ...string) []asserts.Assertion { + res := make([]asserts.Assertion, 0, 2*len(accountIDs)) + for _, accountID := range accountIDs { + res = append(res, sa.Account(accountID)) + res = append(res, sa.AccountKey(accountID)) + } + return res +} diff -Nru snapd-2.37.4ubuntu0.1/asserts/assertstest/assertstest_test.go snapd-2.45.1ubuntu0.2/asserts/assertstest/assertstest_test.go --- snapd-2.37.4ubuntu0.1/asserts/assertstest/assertstest_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/assertstest/assertstest_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -161,3 +161,65 @@ err = db.Add(store.GenericKey) c.Assert(err, IsNil) } + +func (s *helperSuite) TestSigningAccounts(c *C) { + brandKey, _ := assertstest.GenerateKey(752) + + store := assertstest.NewStoreStack("super", nil) + + sa := assertstest.NewSigningAccounts(store) + sa.Register("my-brand", brandKey, map[string]interface{}{ + "validation": "verified", + }) + + acct := sa.Account("my-brand") + c.Check(acct.Username(), Equals, "my-brand") + c.Check(acct.Validation(), Equals, "verified") + + c.Check(sa.AccountKey("my-brand").PublicKeyID(), Equals, brandKey.PublicKey().ID()) + + c.Check(sa.PublicKey("my-brand").ID(), Equals, brandKey.PublicKey().ID()) + + model := sa.Model("my-brand", "my-model", map[string]interface{}{ + "classic": "true", + }) + c.Check(model.BrandID(), Equals, "my-brand") + c.Check(model.Model(), Equals, "my-model") + c.Check(model.Classic(), Equals, true) + + // can also sign models for store account-id + model = sa.Model("super", "pc", map[string]interface{}{ + "classic": "true", + }) + c.Check(model.BrandID(), Equals, "super") + c.Check(model.Model(), Equals, "pc") +} + +func (s *helperSuite) TestSigningAccountsAccountsAndKeysPlusAddMany(c *C) { + brandKey, _ := assertstest.GenerateKey(752) + + store := assertstest.NewStoreStack("super", nil) + + sa := assertstest.NewSigningAccounts(store) + sa.Register("my-brand", brandKey, map[string]interface{}{ + "validation": "verified", + }) + + db, err := asserts.OpenDatabase(&asserts.DatabaseConfig{ + Backstore: asserts.NewMemoryBackstore(), + Trusted: store.Trusted, + }) + c.Assert(err, IsNil) + err = db.Add(store.StoreAccountKey("")) + c.Assert(err, IsNil) + + assertstest.AddMany(db, sa.AccountsAndKeys("my-brand")...) + as, err := db.FindMany(asserts.AccountKeyType, map[string]string{ + "account-id": "my-brand", + }) + c.Check(err, IsNil) + c.Check(as, HasLen, 1) + + // idempotent + assertstest.AddMany(db, sa.AccountsAndKeys("my-brand")...) +} diff -Nru snapd-2.37.4ubuntu0.1/asserts/batch.go snapd-2.45.1ubuntu0.2/asserts/batch.go --- snapd-2.37.4ubuntu0.1/asserts/batch.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/batch.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/asserts/batch_test.go snapd-2.45.1ubuntu0.2/asserts/batch_test.go --- snapd-2.37.4ubuntu0.1/asserts/batch_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/batch_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/asserts/database.go snapd-2.45.1ubuntu0.2/asserts/database.go --- snapd-2.37.4ubuntu0.1/asserts/database.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/database.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/asserts/database_test.go snapd-2.45.1ubuntu0.2/asserts/database_test.go --- snapd-2.37.4ubuntu0.1/asserts/database_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/database_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/asserts/device_asserts.go snapd-2.45.1ubuntu0.2/asserts/device_asserts.go --- snapd-2.37.4ubuntu0.1/asserts/device_asserts.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/device_asserts.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,557 +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/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"} -) - -var almostValidName = regexp.MustCompile("^[a-z0-9-]*[a-z][a-z0-9-]*$") - -// validateSnapName checks whether the name can be used as a snap name -// -// This function should be synchronized with the reference implementation -// snap.ValidateName() in snap/validate.go -func validateSnapName(name string, headerName string) error { - isValidName := func() bool { - if !almostValidName.MatchString(name) { - return false - } - if name[0] == '-' || name[len(name)-1] == '-' || strings.Contains(name, "--") { - return false - } - return true - } - - if len(name) > 40 || !isValidName() { - 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.37.4ubuntu0.1/asserts/device_asserts_test.go snapd-2.45.1ubuntu0.2/asserts/device_asserts_test.go --- snapd-2.37.4ubuntu0.1/asserts/device_asserts_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/device_asserts_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,712 +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{ - "a", "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 - "", - // 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.37.4ubuntu0.1/asserts/export_test.go snapd-2.45.1ubuntu0.2/asserts/export_test.go --- snapd-2.37.4ubuntu0.1/asserts/export_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/export_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -180,6 +180,7 @@ // ifacedecls tests var ( CompileAttributeConstraints = compileAttributeConstraints + CompileNameConstraints = compileNameConstraints CompilePlugRule = compilePlugRule CompileSlotRule = compileSlotRule ) @@ -191,3 +192,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.37.4ubuntu0.1/asserts/header_checks.go snapd-2.45.1ubuntu0.2/asserts/header_checks.go --- snapd-2.37.4ubuntu0.1/asserts/header_checks.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/header_checks.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/asserts/ifacedecls.go snapd-2.45.1ubuntu0.2/asserts/ifacedecls.go --- snapd-2.37.4ubuntu0.1/asserts/ifacedecls.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/ifacedecls.go 2020-06-05 13:13:49.000000000 +0000 @@ -26,6 +26,9 @@ "regexp" "strconv" "strings" + "unicode" + + "github.com/snapcore/snapd/snap/naming" ) // AttrMatchContext has contextual helpers for evaluating attribute constraints. @@ -39,6 +42,8 @@ dollarAttrConstraintsFeature = "dollar-attr-constraints" // feature label for on-store/on-brand/on-model deviceScopeConstraintsFeature = "device-scope-constraints" + // feature label for plug-names/slot-names constraints + nameConstraintsFeature = "name-constraints" ) type attrMatcher interface { @@ -360,6 +365,81 @@ return c.matcher.match("", attrer, ctx) } +// SideArityConstraint specifies a constraint for the overall arity of +// the set of connected slots for a given plug or the set of +// connected plugs for a given slot. +// It is used to express parsed slots-per-plug and plugs-per-slot +// constraints. +// See https://forum.snapcraft.io/t/plug-slot-declaration-rules-greedy-plugs/12438 +type SideArityConstraint struct { + // N can be: + // =>1 + // 0 means default and is used only internally during rule + // compilation or on deny- rules where these constraints are + // not applicable + // -1 represents *, that means any (number of) + N int +} + +// Any returns whether this represents the * (any number of) constraint. +func (ac SideArityConstraint) Any() bool { + return ac.N == -1 +} + +func compileSideArityConstraint(context *subruleContext, which string, v interface{}) (SideArityConstraint, error) { + var a SideArityConstraint + if context.installation() || !context.allow() { + return a, fmt.Errorf("%s cannot specify a %s constraint, they apply only to allow-*connection", context, which) + } + x, ok := v.(string) + if !ok || len(x) == 0 { + return a, fmt.Errorf("%s in %s must be an integer >=1 or *", which, context) + } + if x == "*" { + return SideArityConstraint{N: -1}, nil + } + n, err := strconv.Atoi(x) + if err != nil || n < 1 { + return a, fmt.Errorf("%s in %s must be an integer >=1 or *", which, context) + } + return SideArityConstraint{N: n}, nil +} + +type sideArityConstraintsHolder interface { + setSlotsPerPlug(SideArityConstraint) + setPlugsPerSlot(SideArityConstraint) + + slotsPerPlug() SideArityConstraint + plugsPerSlot() SideArityConstraint +} + +func normalizeSideArityConstraints(context *subruleContext, c sideArityConstraintsHolder) { + if !context.allow() { + return + } + any := SideArityConstraint{N: -1} + // normalized plugs-per-slot is always * + c.setPlugsPerSlot(any) + slotsPerPlug := c.slotsPerPlug() + if context.autoConnection() { + // auto-connection slots-per-plug can be any or 1 + if !slotsPerPlug.Any() { + c.setSlotsPerPlug(SideArityConstraint{N: 1}) + } + } else { + // connection slots-per-plug can be only any + c.setSlotsPerPlug(any) + } +} + +var ( + sideArityConstraints = []string{"slots-per-plug", "plugs-per-slot"} + sideArityConstraintsSetters = map[string]func(sideArityConstraintsHolder, SideArityConstraint){ + "slots-per-plug": sideArityConstraintsHolder.setSlotsPerPlug, + "plugs-per-slot": sideArityConstraintsHolder.setPlugsPerSlot, + } +) + // OnClassicConstraint specifies a constraint based whether the system is classic and optional specific distros' sets. type OnClassicConstraint struct { Classic bool @@ -426,25 +506,109 @@ }, nil } +type nameMatcher interface { + match(name string, special map[string]string) error +} + +var ( + // validates special name constraints like $INTERFACE + validSpecialNameConstraint = regexp.MustCompile("^\\$[A-Z][A-Z0-9_]*$") +) + +func compileNameMatcher(whichName string, v interface{}) (nameMatcher, error) { + s, ok := v.(string) + if !ok { + return nil, fmt.Errorf("%s constraint entry must be a regexp or special $ value", whichName) + } + if strings.HasPrefix(s, "$") { + if !validSpecialNameConstraint.MatchString(s) { + return nil, fmt.Errorf("%s constraint entry special value %q is invalid", whichName, s) + } + return specialNameMatcher{special: s}, nil + } + if strings.IndexFunc(s, unicode.IsSpace) != -1 { + return nil, fmt.Errorf("%s constraint entry regexp contains unexpected spaces", whichName) + } + rx, err := regexp.Compile("^(" + s + ")$") + if err != nil { + return nil, fmt.Errorf("cannot compile %s constraint entry %q: %v", whichName, s, err) + } + return regexpNameMatcher{rx}, nil +} + +type regexpNameMatcher struct { + *regexp.Regexp +} + +func (matcher regexpNameMatcher) match(name string, special map[string]string) error { + if !matcher.Regexp.MatchString(name) { + return fmt.Errorf("%q does not match %v", name, matcher.Regexp) + } + return nil +} + +type specialNameMatcher struct { + special string +} + +func (matcher specialNameMatcher) match(name string, special map[string]string) error { + expected := special[matcher.special] + if expected == "" || expected != name { + return fmt.Errorf("%q does not match %v", name, matcher.special) + } + return nil +} + +// NameConstraints implements a set of constraints on the names of slots or plugs. +// See https://forum.snapcraft.io/t/plug-slot-rules-plug-names-slot-names-constraints/12439 +type NameConstraints struct { + matchers []nameMatcher +} + +func compileNameConstraints(whichName string, constraints interface{}) (*NameConstraints, error) { + l, ok := constraints.([]interface{}) + if !ok { + return nil, fmt.Errorf("%s constraints must be a list of regexps and special $ values", whichName) + } + matchers := make([]nameMatcher, 0, len(l)) + for _, nm := range l { + matcher, err := compileNameMatcher(whichName, nm) + if err != nil { + return nil, err + } + matchers = append(matchers, matcher) + } + return &NameConstraints{matchers: matchers}, nil +} + +// Check checks whether name doesn't match the constraints. +func (nc *NameConstraints) Check(whichName, name string, special map[string]string) error { + for _, m := range nc.matchers { + if err := m.match(name, special); err == nil { + return nil + } + } + return fmt.Errorf("%s %q does not match constraints", whichName, name) +} + // rules var ( validSnapType = regexp.MustCompile("^(?:core|kernel|gadget|app)$") validDistro = regexp.MustCompile("^[-0-9a-z._]+$") - validSnapID = regexp.MustCompile("^[a-z0-9A-Z]{32}$") // snap-ids look like this validPublisher = regexp.MustCompile("^(?:[a-z0-9A-Z]{32}|[-a-z0-9]{2,28}|\\$[A-Z][A-Z0-9_]*)$") // account ids look like snap-ids or are nice identifiers, support our own special markers $MARKER validIDConstraints = map[string]*regexp.Regexp{ "slot-snap-type": validSnapType, - "slot-snap-id": validSnapID, + "slot-snap-id": naming.ValidSnapID, "slot-publisher-id": validPublisher, "plug-snap-type": validSnapType, - "plug-snap-id": validSnapID, + "plug-snap-id": naming.ValidSnapID, "plug-publisher-id": validPublisher, } ) -func checkMapOrShortcut(context string, v interface{}) (m map[string]interface{}, invert bool, err error) { +func checkMapOrShortcut(v interface{}) (m map[string]interface{}, invert bool, err error) { switch x := v.(type) { case map[string]interface{}: return x, false, nil @@ -460,13 +624,14 @@ } type constraintsHolder interface { + setNameConstraints(field string, cstrs *NameConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) setIDConstraints(field string, cstrs []string) setOnClassicConstraint(onClassic *OnClassicConstraint) setDeviceScopeConstraint(deviceScope *DeviceScopeConstraint) } -func baseCompileConstraints(context string, cDef constraintsDef, target constraintsHolder, attrConstraints, idConstraints []string) error { +func baseCompileConstraints(context *subruleContext, cDef constraintsDef, target constraintsHolder, nameConstraints, attrConstraints, idConstraints []string) error { cMap := cDef.cMap if cMap == nil { fixed := AlwaysMatchAttributes // "true" @@ -479,6 +644,32 @@ return nil } defaultUsed := 0 + for _, field := range nameConstraints { + v := cMap[field] + if v != nil { + nc, err := compileNameConstraints(field, v) + if err != nil { + return err + } + target.setNameConstraints(field, nc) + } else { + defaultUsed++ + } + } + for _, field := range attrConstraints { + cstrs := AlwaysMatchAttributes + v := cMap[field] + if v != nil { + var err error + cstrs, err = compileAttributeConstraints(cMap[field]) + if err != nil { + return fmt.Errorf("cannot compile %s in %s: %v", field, context, err) + } + } else { + defaultUsed++ + } + target.setAttributeConstraints(field, cstrs) + } for _, field := range idConstraints { lst, err := checkStringListInMap(cMap, field, fmt.Sprintf("%s in %s", field, context), validIDConstraints[field]) if err != nil { @@ -489,19 +680,21 @@ } target.setIDConstraints(field, lst) } - for _, field := range attrConstraints { - cstrs := AlwaysMatchAttributes + for _, field := range sideArityConstraints { v := cMap[field] if v != nil { - var err error - cstrs, err = compileAttributeConstraints(cMap[field]) + c, err := compileSideArityConstraint(context, field, v) if err != nil { - return fmt.Errorf("cannot compile %s in %s: %v", field, context, err) + return err } + h, ok := target.(sideArityConstraintsHolder) + if !ok { + return fmt.Errorf("internal error: side arity constraint compiled for unexpected subrule %T", target) + } + sideArityConstraintsSetters[field](h, c) } else { defaultUsed++ } - target.setAttributeConstraints(field, cstrs) } onClassic := cMap["on-classic"] if onClassic == nil { @@ -531,7 +724,7 @@ if !detectDeviceScopeConstraint(cMap) { defaultUsed++ } else { - c, err := compileDeviceScopeConstraint(cMap, context) + c, err := compileDeviceScopeConstraint(cMap, context.String()) if err != nil { return err } @@ -541,8 +734,8 @@ // well-formed // +1+1 accounts for defaults for missing on-classic plus missing // on-store/on-brand/on-model - if defaultUsed == len(attributeConstraints)+len(idConstraints)+1+1 { - return fmt.Errorf("%s must specify at least one of %s, %s, on-classic, on-store, on-brand, on-model", context, strings.Join(attrConstraints, ", "), strings.Join(idConstraints, ", ")) + if defaultUsed == len(nameConstraints)+len(attributeConstraints)+len(idConstraints)+len(sideArityConstraints)+1+1 { + return fmt.Errorf("%s must specify at least one of %s, %s, %s, %s, on-classic, on-store, on-brand, on-model", context, strings.Join(nameConstraints, ", "), strings.Join(attrConstraints, ", "), strings.Join(idConstraints, ", "), strings.Join(sideArityConstraints, ", ")) } return nil } @@ -556,10 +749,50 @@ invert bool } -type subruleCompiler func(context string, def constraintsDef) (constraintsHolder, error) +// subruleContext carries queryable context information about one the +// {allow,deny}-* subrules that end up compiled as +// Plug|Slot*Constraints. The information includes the parent rule, +// the introductory subrule key ({allow,deny}-*) and which alternative +// it corresponds to if any. +// The information is useful for constraints compilation now that we +// have constraints with different behavior depending on the kind of +// subrule that hosts them (e.g. slots-per-plug, plugs-per-slot). +type subruleContext struct { + // rule is the parent rule context description + rule string + // subrule is the subrule key + subrule string + // alt is which alternative this is (if > 0) + alt int +} + +func (c *subruleContext) String() string { + subctxt := fmt.Sprintf("%s in %s", c.subrule, c.rule) + if c.alt != 0 { + subctxt = fmt.Sprintf("alternative %d of %s", c.alt, subctxt) + } + return subctxt +} + +// allow returns whether the subrule is an allow-* subrule. +func (c *subruleContext) allow() bool { + return strings.HasPrefix(c.subrule, "allow-") +} + +// installation returns whether the subrule is an *-installation subrule. +func (c *subruleContext) installation() bool { + return strings.HasSuffix(c.subrule, "-installation") +} + +// autoConnection returns whether the subrule is an *-auto-connection subrule. +func (c *subruleContext) autoConnection() bool { + return strings.HasSuffix(c.subrule, "-auto-connection") +} + +type subruleCompiler func(context *subruleContext, def constraintsDef) (constraintsHolder, error) func baseCompileRule(context string, rule interface{}, target rule, subrules []string, compilers map[string]subruleCompiler, defaultOutcome, invertedOutcome map[string]interface{}) error { - rMap, invert, err := checkMapOrShortcut(context, rule) + rMap, invert, err := checkMapOrShortcut(rule) if err != nil { return fmt.Errorf("%s must be a map or one of the shortcuts 'true' or 'false'", context) } @@ -592,11 +825,14 @@ } alts := make([]constraintsHolder, len(lst)) for i, alt := range lst { - subctxt := fmt.Sprintf("%s in %s", subrule, context) + subctxt := &subruleContext{ + rule: context, + subrule: subrule, + } if alternatives { - subctxt = fmt.Sprintf("alternative %d of %s", i+1, subctxt) + subctxt.alt = i + 1 } - cMap, invert, err := checkMapOrShortcut(subctxt, alt) + cMap, invert, err := checkMapOrShortcut(alt) if err != nil || (cMap == nil && alternatives) { efmt := "%s must be a map" if !alternatives { @@ -710,6 +946,8 @@ type PlugInstallationConstraints struct { PlugSnapTypes []string + PlugNames *NameConstraints + PlugAttributes *AttributeConstraints OnClassic *OnClassicConstraint @@ -721,9 +959,21 @@ if flabel == deviceScopeConstraintsFeature { return c.DeviceScope != nil } + if flabel == nameConstraintsFeature { + return c.PlugNames != nil + } return c.PlugAttributes.feature(flabel) } +func (c *PlugInstallationConstraints) setNameConstraints(field string, cstrs *NameConstraints) { + switch field { + case "plug-names": + c.PlugNames = cstrs + default: + panic("unknown PlugInstallationConstraints field " + field) + } +} + func (c *PlugInstallationConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { switch field { case "plug-attributes": @@ -750,9 +1000,9 @@ c.DeviceScope = deviceScope } -func compilePlugInstallationConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { +func compilePlugInstallationConstraints(context *subruleContext, cDef constraintsDef) (constraintsHolder, error) { plugInstCstrs := &PlugInstallationConstraints{} - err := baseCompileConstraints(context, cDef, plugInstCstrs, []string{"plug-attributes"}, []string{"plug-snap-type"}) + err := baseCompileConstraints(context, cDef, plugInstCstrs, []string{"plug-names"}, []string{"plug-attributes"}, []string{"plug-snap-type"}) if err != nil { return nil, err } @@ -767,9 +1017,17 @@ SlotSnapIDs []string SlotPublisherIDs []string + PlugNames *NameConstraints + SlotNames *NameConstraints + PlugAttributes *AttributeConstraints SlotAttributes *AttributeConstraints + // SlotsPerPlug defaults to 1 for auto-connection, can be * (any) + SlotsPerPlug SideArityConstraint + // PlugsPerSlot is always * (any) (for now) + PlugsPerSlot SideArityConstraint + OnClassic *OnClassicConstraint DeviceScope *DeviceScopeConstraint @@ -779,9 +1037,23 @@ if flabel == deviceScopeConstraintsFeature { return c.DeviceScope != nil } + if flabel == nameConstraintsFeature { + return c.PlugNames != nil || c.SlotNames != nil + } return c.PlugAttributes.feature(flabel) || c.SlotAttributes.feature(flabel) } +func (c *PlugConnectionConstraints) setNameConstraints(field string, cstrs *NameConstraints) { + switch field { + case "plug-names": + c.PlugNames = cstrs + case "slot-names": + c.SlotNames = cstrs + default: + panic("unknown PlugConnectionConstraints field " + field) + } +} + func (c *PlugConnectionConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { switch field { case "plug-attributes": @@ -806,6 +1078,22 @@ } } +func (c *PlugConnectionConstraints) setSlotsPerPlug(a SideArityConstraint) { + c.SlotsPerPlug = a +} + +func (c *PlugConnectionConstraints) setPlugsPerSlot(a SideArityConstraint) { + c.PlugsPerSlot = a +} + +func (c *PlugConnectionConstraints) slotsPerPlug() SideArityConstraint { + return c.SlotsPerPlug +} + +func (c *PlugConnectionConstraints) plugsPerSlot() SideArityConstraint { + return c.PlugsPerSlot +} + func (c *PlugConnectionConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) { c.OnClassic = onClassic } @@ -815,16 +1103,18 @@ } var ( + nameConstraints = []string{"plug-names", "slot-names"} attributeConstraints = []string{"plug-attributes", "slot-attributes"} plugIDConstraints = []string{"slot-snap-type", "slot-publisher-id", "slot-snap-id"} ) -func compilePlugConnectionConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { +func compilePlugConnectionConstraints(context *subruleContext, cDef constraintsDef) (constraintsHolder, error) { plugConnCstrs := &PlugConnectionConstraints{} - err := baseCompileConstraints(context, cDef, plugConnCstrs, attributeConstraints, plugIDConstraints) + err := baseCompileConstraints(context, cDef, plugConnCstrs, nameConstraints, attributeConstraints, plugIDConstraints) if err != nil { return nil, err } + normalizeSideArityConstraints(context, plugConnCstrs) return plugConnCstrs, nil } @@ -960,6 +1250,8 @@ type SlotInstallationConstraints struct { SlotSnapTypes []string + SlotNames *NameConstraints + SlotAttributes *AttributeConstraints OnClassic *OnClassicConstraint @@ -971,9 +1263,21 @@ if flabel == deviceScopeConstraintsFeature { return c.DeviceScope != nil } + if flabel == nameConstraintsFeature { + return c.SlotNames != nil + } return c.SlotAttributes.feature(flabel) } +func (c *SlotInstallationConstraints) setNameConstraints(field string, cstrs *NameConstraints) { + switch field { + case "slot-names": + c.SlotNames = cstrs + default: + panic("unknown SlotInstallationConstraints field " + field) + } +} + func (c *SlotInstallationConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { switch field { case "slot-attributes": @@ -1000,9 +1304,9 @@ c.DeviceScope = deviceScope } -func compileSlotInstallationConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { +func compileSlotInstallationConstraints(context *subruleContext, cDef constraintsDef) (constraintsHolder, error) { slotInstCstrs := &SlotInstallationConstraints{} - err := baseCompileConstraints(context, cDef, slotInstCstrs, []string{"slot-attributes"}, []string{"slot-snap-type"}) + err := baseCompileConstraints(context, cDef, slotInstCstrs, []string{"slot-names"}, []string{"slot-attributes"}, []string{"slot-snap-type"}) if err != nil { return nil, err } @@ -1017,9 +1321,17 @@ PlugSnapIDs []string PlugPublisherIDs []string + SlotNames *NameConstraints + PlugNames *NameConstraints + SlotAttributes *AttributeConstraints PlugAttributes *AttributeConstraints + // SlotsPerPlug defaults to 1 for auto-connection, can be * (any) + SlotsPerPlug SideArityConstraint + // PlugsPerSlot is always * (any) (for now) + PlugsPerSlot SideArityConstraint + OnClassic *OnClassicConstraint DeviceScope *DeviceScopeConstraint @@ -1029,9 +1341,23 @@ if flabel == deviceScopeConstraintsFeature { return c.DeviceScope != nil } + if flabel == nameConstraintsFeature { + return c.PlugNames != nil || c.SlotNames != nil + } return c.PlugAttributes.feature(flabel) || c.SlotAttributes.feature(flabel) } +func (c *SlotConnectionConstraints) setNameConstraints(field string, cstrs *NameConstraints) { + switch field { + case "plug-names": + c.PlugNames = cstrs + case "slot-names": + c.SlotNames = cstrs + default: + panic("unknown SlotConnectionConstraints field " + field) + } +} + func (c *SlotConnectionConstraints) setAttributeConstraints(field string, cstrs *AttributeConstraints) { switch field { case "plug-attributes": @@ -1060,6 +1386,22 @@ slotIDConstraints = []string{"plug-snap-type", "plug-publisher-id", "plug-snap-id"} ) +func (c *SlotConnectionConstraints) setSlotsPerPlug(a SideArityConstraint) { + c.SlotsPerPlug = a +} + +func (c *SlotConnectionConstraints) setPlugsPerSlot(a SideArityConstraint) { + c.PlugsPerSlot = a +} + +func (c *SlotConnectionConstraints) slotsPerPlug() SideArityConstraint { + return c.SlotsPerPlug +} + +func (c *SlotConnectionConstraints) plugsPerSlot() SideArityConstraint { + return c.PlugsPerSlot +} + func (c *SlotConnectionConstraints) setOnClassicConstraint(onClassic *OnClassicConstraint) { c.OnClassic = onClassic } @@ -1068,12 +1410,13 @@ c.DeviceScope = deviceScope } -func compileSlotConnectionConstraints(context string, cDef constraintsDef) (constraintsHolder, error) { +func compileSlotConnectionConstraints(context *subruleContext, cDef constraintsDef) (constraintsHolder, error) { slotConnCstrs := &SlotConnectionConstraints{} - err := baseCompileConstraints(context, cDef, slotConnCstrs, attributeConstraints, slotIDConstraints) + err := baseCompileConstraints(context, cDef, slotConnCstrs, nameConstraints, attributeConstraints, slotIDConstraints) if err != nil { return nil, err } + normalizeSideArityConstraints(context, slotConnCstrs) return slotConnCstrs, nil } diff -Nru snapd-2.37.4ubuntu0.1/asserts/ifacedecls_test.go snapd-2.45.1ubuntu0.2/asserts/ifacedecls_test.go --- snapd-2.37.4ubuntu0.1/asserts/ifacedecls_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/ifacedecls_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -22,6 +22,7 @@ import ( "fmt" "regexp" + "strings" . "gopkg.in/check.v1" "gopkg.in/yaml.v2" @@ -34,6 +35,7 @@ var ( _ = Suite(&attrConstraintsSuite{}) + _ = Suite(&nameConstraintsSuite{}) _ = Suite(&plugSlotRulesSuite{}) ) @@ -483,6 +485,51 @@ c.Check(asserts.NeverMatchAttributes.Check(nil, nil), NotNil) } +type nameConstraintsSuite struct{} + +func (s *nameConstraintsSuite) TestCompileErrors(c *C) { + _, err := asserts.CompileNameConstraints("slot-names", "true") + c.Check(err, ErrorMatches, `slot-names constraints must be a list of regexps and special \$ values`) + + _, err = asserts.CompileNameConstraints("slot-names", []interface{}{map[string]interface{}{"foo": "bar"}}) + c.Check(err, ErrorMatches, `slot-names constraint entry must be a regexp or special \$ value`) + + _, err = asserts.CompileNameConstraints("plug-names", []interface{}{"["}) + c.Check(err, ErrorMatches, `cannot compile plug-names constraint entry "\[":.*`) + + _, err = asserts.CompileNameConstraints("plug-names", []interface{}{"$"}) + c.Check(err, ErrorMatches, `plug-names constraint entry special value "\$" is invalid`) + + _, err = asserts.CompileNameConstraints("slot-names", []interface{}{"$12"}) + c.Check(err, ErrorMatches, `slot-names constraint entry special value "\$12" is invalid`) + + _, err = asserts.CompileNameConstraints("plug-names", []interface{}{"a b"}) + c.Check(err, ErrorMatches, `plug-names constraint entry regexp contains unexpected spaces`) +} + +func (s *nameConstraintsSuite) TestCheck(c *C) { + nc, err := asserts.CompileNameConstraints("slot-names", []interface{}{"foo[0-9]", "bar"}) + c.Assert(err, IsNil) + + for _, matching := range []string{"foo0", "foo1", "bar"} { + c.Check(nc.Check("slot name", matching, nil), IsNil) + } + + for _, notMatching := range []string{"baz", "fooo", "foo12"} { + c.Check(nc.Check("slot name", notMatching, nil), ErrorMatches, fmt.Sprintf(`slot name %q does not match constraints`, notMatching)) + } + +} + +func (s *nameConstraintsSuite) TestCheckSpecial(c *C) { + nc, err := asserts.CompileNameConstraints("slot-names", []interface{}{"$INTERFACE"}) + c.Assert(err, IsNil) + + c.Check(nc.Check("slot name", "foo", nil), ErrorMatches, `slot name "foo" does not match constraints`) + c.Check(nc.Check("slot name", "foo", map[string]string{"$INTERFACE": "foo"}), IsNil) + c.Check(nc.Check("slot name", "bar", map[string]string{"$INTERFACE": "foo"}), ErrorMatches, `slot name "bar" does not match constraints`) +} + type plugSlotRulesSuite struct{} func checkAttrs(c *C, attrs *asserts.AttributeConstraints, witness, expected string) { @@ -496,7 +543,12 @@ c.Check(attrs.Check(plug, nil), IsNil) } -func checkBoolPlugConnConstraints(c *C, cstrs []*asserts.PlugConnectionConstraints, always bool) { +var ( + sideArityAny = asserts.SideArityConstraint{N: -1} + sideArityOne = asserts.SideArityConstraint{N: 1} +) + +func checkBoolPlugConnConstraints(c *C, subrule string, cstrs []*asserts.PlugConnectionConstraints, always bool) { expected := asserts.NeverMatchAttributes if always { expected = asserts.AlwaysMatchAttributes @@ -505,12 +557,24 @@ cstrs1 := cstrs[0] c.Check(cstrs1.PlugAttributes, Equals, expected) c.Check(cstrs1.SlotAttributes, Equals, expected) + if strings.HasPrefix(subrule, "deny-") { + undef := asserts.SideArityConstraint{} + c.Check(cstrs1.SlotsPerPlug, Equals, undef) + c.Check(cstrs1.PlugsPerSlot, Equals, undef) + } else { + c.Check(cstrs1.PlugsPerSlot, Equals, sideArityAny) + if strings.HasSuffix(subrule, "-auto-connection") { + c.Check(cstrs1.SlotsPerPlug, Equals, sideArityOne) + } else { + c.Check(cstrs1.SlotsPerPlug, Equals, sideArityAny) + } + } c.Check(cstrs1.SlotSnapIDs, HasLen, 0) c.Check(cstrs1.SlotPublisherIDs, HasLen, 0) c.Check(cstrs1.SlotSnapTypes, HasLen, 0) } -func checkBoolSlotConnConstraints(c *C, cstrs []*asserts.SlotConnectionConstraints, always bool) { +func checkBoolSlotConnConstraints(c *C, subrule string, cstrs []*asserts.SlotConnectionConstraints, always bool) { expected := asserts.NeverMatchAttributes if always { expected = asserts.AlwaysMatchAttributes @@ -519,6 +583,18 @@ cstrs1 := cstrs[0] c.Check(cstrs1.PlugAttributes, Equals, expected) c.Check(cstrs1.SlotAttributes, Equals, expected) + if strings.HasPrefix(subrule, "deny-") { + undef := asserts.SideArityConstraint{} + c.Check(cstrs1.SlotsPerPlug, Equals, undef) + c.Check(cstrs1.PlugsPerSlot, Equals, undef) + } else { + c.Check(cstrs1.PlugsPerSlot, Equals, sideArityAny) + if strings.HasSuffix(subrule, "-auto-connection") { + c.Check(cstrs1.SlotsPerPlug, Equals, sideArityOne) + } else { + c.Check(cstrs1.SlotsPerPlug, Equals, sideArityAny) + } + } c.Check(cstrs1.PlugSnapIDs, HasLen, 0) c.Check(cstrs1.PlugPublisherIDs, HasLen, 0) c.Check(cstrs1.PlugSnapTypes, HasLen, 0) @@ -675,11 +751,11 @@ c.Assert(rule.DenyInstallation, HasLen, 1) c.Check(rule.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) // connection subrules - checkBoolPlugConnConstraints(c, rule.AllowConnection, true) - checkBoolPlugConnConstraints(c, rule.DenyConnection, false) + checkBoolPlugConnConstraints(c, "allow-connection", rule.AllowConnection, true) + checkBoolPlugConnConstraints(c, "deny-connection", rule.DenyConnection, false) // auto-connection subrules - checkBoolPlugConnConstraints(c, rule.AllowAutoConnection, true) - checkBoolPlugConnConstraints(c, rule.DenyAutoConnection, false) + checkBoolPlugConnConstraints(c, "allow-auto-connection", rule.AllowAutoConnection, true) + checkBoolPlugConnConstraints(c, "deny-auto-connection", rule.DenyAutoConnection, false) } func (s *plugSlotRulesSuite) TestCompilePlugRuleShortcutFalse(c *C) { @@ -692,11 +768,11 @@ c.Assert(rule.DenyInstallation, HasLen, 1) c.Check(rule.DenyInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) // connection subrules - checkBoolPlugConnConstraints(c, rule.AllowConnection, false) - checkBoolPlugConnConstraints(c, rule.DenyConnection, true) + checkBoolPlugConnConstraints(c, "allow-connection", rule.AllowConnection, false) + checkBoolPlugConnConstraints(c, "deny-connection", rule.DenyConnection, true) // auto-connection subrules - checkBoolPlugConnConstraints(c, rule.AllowAutoConnection, false) - checkBoolPlugConnConstraints(c, rule.DenyAutoConnection, true) + checkBoolPlugConnConstraints(c, "allow-auto-connection", rule.AllowAutoConnection, false) + checkBoolPlugConnConstraints(c, "deny-auto-connection", rule.DenyAutoConnection, true) } func (s *plugSlotRulesSuite) TestCompilePlugRuleDefaults(c *C) { @@ -713,12 +789,12 @@ c.Assert(rule.DenyInstallation, HasLen, 1) c.Check(rule.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) // connection subrules - checkBoolPlugConnConstraints(c, rule.AllowConnection, true) - checkBoolPlugConnConstraints(c, rule.DenyConnection, false) + checkBoolPlugConnConstraints(c, "allow-connection", rule.AllowConnection, true) + checkBoolPlugConnConstraints(c, "deny-connection", rule.DenyConnection, false) // auto-connection subrules - checkBoolPlugConnConstraints(c, rule.AllowAutoConnection, true) + checkBoolPlugConnConstraints(c, "allow-auto-connection", rule.AllowAutoConnection, true) // ... but deny-auto-connection is on - checkBoolPlugConnConstraints(c, rule.DenyAutoConnection, true) + checkBoolPlugConnConstraints(c, "deny-auto-connection", rule.DenyAutoConnection, true) } func (s *plugSlotRulesSuite) TestCompilePlugRuleInstalationConstraintsIDConstraints(c *C) { @@ -836,6 +912,52 @@ } } +func (s *plugSlotRulesSuite) TestCompilePlugRuleInstallationConstraintsPlugNames(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].PlugNames, IsNil) + + tests := []struct { + rule string + matching []string + notMatching []string + }{ + {`iface: + allow-installation: + plug-names: + - foo`, []string{"foo"}, []string{"bar"}}, + {`iface: + allow-installation: + plug-names: + - foo + - bar`, []string{"foo", "bar"}, []string{"baz"}}, + {`iface: + allow-installation: + plug-names: + - foo[0-9] + - bar`, []string{"foo0", "foo1", "bar"}, []string{"baz", "fooo", "foo12"}}, + } + for _, t := range tests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + for _, matching := range t.matching { + c.Check(rule.AllowInstallation[0].PlugNames.Check("plug name", matching, nil), IsNil) + } + for _, notMatching := range t.notMatching { + c.Check(rule.AllowInstallation[0].PlugNames.Check("plug name", notMatching, nil), NotNil) + } + } +} + func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsIDConstraints(c *C) { rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{ "allow-connection": map[string]interface{}{ @@ -956,6 +1078,144 @@ } } +func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsPlugNamesSlotNames(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-connection: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].PlugNames, IsNil) + c.Check(rule.AllowConnection[0].SlotNames, IsNil) + + tests := []struct { + rule string + matching []string + notMatching []string + }{ + {`iface: + allow-connection: + plug-names: + - Pfoo + slot-names: + - Sfoo`, []string{"foo"}, []string{"bar"}}, + {`iface: + allow-connection: + plug-names: + - Pfoo + - Pbar + slot-names: + - Sfoo + - Sbar`, []string{"foo", "bar"}, []string{"baz"}}, + {`iface: + allow-connection: + plug-names: + - Pfoo[0-9] + - Pbar + slot-names: + - Sfoo[0-9] + - Sbar`, []string{"foo0", "foo1", "bar"}, []string{"baz", "fooo", "foo12"}}, + } + for _, t := range tests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + for _, matching := range t.matching { + c.Check(rule.AllowConnection[0].PlugNames.Check("plug name", "P"+matching, nil), IsNil) + + c.Check(rule.AllowConnection[0].SlotNames.Check("slot name", "S"+matching, nil), IsNil) + } + + for _, notMatching := range t.notMatching { + c.Check(rule.AllowConnection[0].SlotNames.Check("plug name", "P"+notMatching, nil), NotNil) + + c.Check(rule.AllowConnection[0].SlotNames.Check("slot name", "S"+notMatching, nil), NotNil) + } + } +} + +func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsSideArityConstraints(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-auto-connection: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + // defaults + c.Check(rule.AllowAutoConnection[0].SlotsPerPlug, Equals, asserts.SideArityConstraint{N: 1}) + c.Check(rule.AllowAutoConnection[0].PlugsPerSlot.Any(), Equals, true) + + c.Check(rule.AllowConnection[0].SlotsPerPlug.Any(), Equals, true) + c.Check(rule.AllowConnection[0].PlugsPerSlot.Any(), Equals, true) + + // test that the arity constraints get normalized away to any + // under allow-connection + // see https://forum.snapcraft.io/t/plug-slot-declaration-rules-greedy-plugs/12438 + allowConnTests := []string{ + `iface: + allow-connection: + slots-per-plug: 1 + plugs-per-slot: 2`, + `iface: + allow-connection: + slots-per-plug: * + plugs-per-slot: 1`, + `iface: + allow-connection: + slots-per-plug: 2 + plugs-per-slot: *`, + } + + for _, t := range allowConnTests { + m, err = asserts.ParseHeaders([]byte(t)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].SlotsPerPlug.Any(), Equals, true) + c.Check(rule.AllowConnection[0].PlugsPerSlot.Any(), Equals, true) + } + + // test that under allow-auto-connection: + // slots-per-plug can be * (any) or otherwise gets normalized to 1 + // plugs-per-slot gets normalized to any (*) + // see https://forum.snapcraft.io/t/plug-slot-declaration-rules-greedy-plugs/12438 + allowAutoConnTests := []struct { + rule string + slotsPerPlug asserts.SideArityConstraint + }{ + {`iface: + allow-auto-connection: + slots-per-plug: 1 + plugs-per-slot: 2`, sideArityOne}, + {`iface: + allow-auto-connection: + slots-per-plug: * + plugs-per-slot: 1`, sideArityAny}, + {`iface: + allow-auto-connection: + slots-per-plug: 2 + plugs-per-slot: *`, sideArityOne}, + } + + for _, t := range allowAutoConnTests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowAutoConnection[0].SlotsPerPlug, Equals, t.slotsPerPlug) + c.Check(rule.AllowAutoConnection[0].PlugsPerSlot.Any(), Equals, true) + } +} + func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsAttributesDefault(c *C) { rule, err := asserts.CompilePlugRule("iface", map[string]interface{}{ "allow-connection": map[string]interface{}{ @@ -1018,19 +1278,19 @@ {`iface: allow-connection: slot-snap-ids: - - foo`, `allow-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic, on-store, on-brand, on-model`}, + - foo`, `allow-connection in plug rule for interface "iface" must specify at least one of plug-names, slot-names, plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, slots-per-plug, plugs-per-slot, on-classic, on-store, on-brand, on-model`}, {`iface: deny-connection: slot-snap-ids: - - foo`, `deny-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic, on-store, on-brand, on-model`}, + - foo`, `deny-connection in plug rule for interface "iface" must specify at least one of plug-names, slot-names, plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, slots-per-plug, plugs-per-slot, on-classic, on-store, on-brand, on-model`}, {`iface: allow-auto-connection: slot-snap-ids: - - foo`, `allow-auto-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic, on-store, on-brand, on-model`}, + - foo`, `allow-auto-connection in plug rule for interface "iface" must specify at least one of plug-names, slot-names, plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, slots-per-plug, plugs-per-slot, on-classic, on-store, on-brand, on-model`}, {`iface: deny-auto-connection: slot-snap-ids: - - foo`, `deny-auto-connection in plug rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, on-classic, on-store, on-brand, on-model`}, + - foo`, `deny-auto-connection in plug rule for interface "iface" must specify at least one of plug-names, slot-names, plug-attributes, slot-attributes, slot-snap-type, slot-publisher-id, slot-snap-id, slots-per-plug, plugs-per-slot, on-classic, on-store, on-brand, on-model`}, {`iface: allow-connect: true`, `plug rule for interface "iface" must specify at least one of allow-installation, deny-installation, allow-connection, deny-connection, allow-auto-connection, deny-auto-connection`}, {`iface: @@ -1063,6 +1323,28 @@ allow-auto-connection: on-model: - foo/!qz`, `on-model in allow-auto-connection in plug rule for interface \"iface\" contains an invalid element: \"foo/!qz"`}, + {`iface: + allow-installation: + slots-per-plug: 1`, `allow-installation in plug rule for interface "iface" cannot specify a slots-per-plug constraint, they apply only to allow-\*connection`}, + {`iface: + deny-connection: + slots-per-plug: 1`, `deny-connection in plug rule for interface "iface" cannot specify a slots-per-plug constraint, they apply only to allow-\*connection`}, + {`iface: + allow-auto-connection: + plugs-per-slot: any`, `plugs-per-slot in allow-auto-connection in plug rule for interface "iface" must be an integer >=1 or \*`}, + {`iface: + allow-auto-connection: + slots-per-plug: 0`, `slots-per-plug in allow-auto-connection in plug rule for interface "iface" must be an integer >=1 or \*`}, + {`iface: + allow-auto-connection: + slots-per-plug: + what: 1`, `slots-per-plug in allow-auto-connection in plug rule for interface "iface" must be an integer >=1 or \*`}, + {`iface: + allow-auto-connection: + plug-names: true`, `plug-names constraints must be a list of regexps and special \$ values`}, + {`iface: + allow-auto-connection: + slot-names: true`, `slot-names constraints must be a list of regexps and special \$ values`}, } for _, t := range tests { @@ -1084,26 +1366,26 @@ func (s *plugSlotRulesSuite) TestPlugRuleFeatures(c *C) { combos := []struct { - subrule string - attrConstraints []string + subrule string + constraintsPrefixes []string }{ - {"allow-installation", []string{"plug-attributes"}}, - {"deny-installation", []string{"plug-attributes"}}, - {"allow-connection", []string{"plug-attributes", "slot-attributes"}}, - {"deny-connection", []string{"plug-attributes", "slot-attributes"}}, - {"allow-auto-connection", []string{"plug-attributes", "slot-attributes"}}, - {"deny-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + {"allow-installation", []string{"plug-"}}, + {"deny-installation", []string{"plug-"}}, + {"allow-connection", []string{"plug-", "slot-"}}, + {"deny-connection", []string{"plug-", "slot-"}}, + {"allow-auto-connection", []string{"plug-", "slot-"}}, + {"deny-auto-connection", []string{"plug-", "slot-"}}, } for _, combo := range combos { - for _, attrConstr := range combo.attrConstraints { + for _, attrConstrPrefix := range combo.constraintsPrefixes { attrConstraintMap := map[string]interface{}{ "a": "ATTR", "other": []interface{}{"x", "y"}, } ruleMap := map[string]interface{}{ combo.subrule: map[string]interface{}{ - attrConstr: attrConstraintMap, + attrConstrPrefix + "attributes": attrConstraintMap, }, } @@ -1112,6 +1394,7 @@ c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, false, Commentf("%v", ruleMap)) c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, false, Commentf("%v", ruleMap)) + c.Check(asserts.RuleFeature(rule, "name-constraints"), Equals, false, Commentf("%v", ruleMap)) attrConstraintMap["a"] = "$MISSING" rule, err = asserts.CompilePlugRule("iface", ruleMap) @@ -1125,6 +1408,7 @@ c.Check(asserts.RuleFeature(rule, "dollar-attr-constraints"), Equals, true, Commentf("%v", ruleMap)) c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, false, Commentf("%v", ruleMap)) + c.Check(asserts.RuleFeature(rule, "name-constraints"), Equals, false, Commentf("%v", ruleMap)) } @@ -1139,6 +1423,18 @@ c.Assert(err, IsNil) c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, true, Commentf("%v", ruleMap)) } + + for _, nameConstrPrefix := range combo.constraintsPrefixes { + ruleMap := map[string]interface{}{ + combo.subrule: map[string]interface{}{ + nameConstrPrefix + "names": []interface{}{"foo"}, + }, + } + + rule, err := asserts.CompilePlugRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "name-constraints"), Equals, true, Commentf("%v", ruleMap)) + } } } @@ -1293,11 +1589,11 @@ c.Assert(rule.DenyInstallation, HasLen, 1) c.Check(rule.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) // connection subrules - checkBoolSlotConnConstraints(c, rule.AllowConnection, true) - checkBoolSlotConnConstraints(c, rule.DenyConnection, false) + checkBoolSlotConnConstraints(c, "allow-connection", rule.AllowConnection, true) + checkBoolSlotConnConstraints(c, "deny-connection", rule.DenyConnection, false) // auto-connection subrules - checkBoolSlotConnConstraints(c, rule.AllowAutoConnection, true) - checkBoolSlotConnConstraints(c, rule.DenyAutoConnection, false) + checkBoolSlotConnConstraints(c, "allow-auto-connection", rule.AllowAutoConnection, true) + checkBoolSlotConnConstraints(c, "deny-auto-connection", rule.DenyAutoConnection, false) } func (s *plugSlotRulesSuite) TestCompileSlotRuleShortcutFalse(c *C) { @@ -1310,11 +1606,11 @@ c.Assert(rule.DenyInstallation, HasLen, 1) c.Check(rule.DenyInstallation[0].SlotAttributes, Equals, asserts.AlwaysMatchAttributes) // connection subrules - checkBoolSlotConnConstraints(c, rule.AllowConnection, false) - checkBoolSlotConnConstraints(c, rule.DenyConnection, true) + checkBoolSlotConnConstraints(c, "allwo-connection", rule.AllowConnection, false) + checkBoolSlotConnConstraints(c, "deny-connection", rule.DenyConnection, true) // auto-connection subrules - checkBoolSlotConnConstraints(c, rule.AllowAutoConnection, false) - checkBoolSlotConnConstraints(c, rule.DenyAutoConnection, true) + checkBoolSlotConnConstraints(c, "allow-auto-connection", rule.AllowAutoConnection, false) + checkBoolSlotConnConstraints(c, "deny-auto-connection", rule.DenyAutoConnection, true) } func (s *plugSlotRulesSuite) TestCompileSlotRuleDefaults(c *C) { @@ -1331,12 +1627,12 @@ c.Assert(rule.DenyInstallation, HasLen, 1) c.Check(rule.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) // connection subrules - checkBoolSlotConnConstraints(c, rule.AllowConnection, true) - checkBoolSlotConnConstraints(c, rule.DenyConnection, false) + checkBoolSlotConnConstraints(c, "allow-connection", rule.AllowConnection, true) + checkBoolSlotConnConstraints(c, "deny-connection", rule.DenyConnection, false) // auto-connection subrules - checkBoolSlotConnConstraints(c, rule.AllowAutoConnection, true) + checkBoolSlotConnConstraints(c, "allow-auto-connection", rule.AllowAutoConnection, true) // ... but deny-auto-connection is on - checkBoolSlotConnConstraints(c, rule.DenyAutoConnection, true) + checkBoolSlotConnConstraints(c, "deny-auto-connection", rule.DenyAutoConnection, true) } func (s *plugSlotRulesSuite) TestCompileSlotRuleInstallationConstraintsIDConstraints(c *C) { @@ -1454,6 +1750,52 @@ } } +func (s *plugSlotRulesSuite) TestCompileSlotRuleInstallationConstraintsSlotNames(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-installation: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowInstallation[0].SlotNames, IsNil) + + tests := []struct { + rule string + matching []string + notMatching []string + }{ + {`iface: + allow-installation: + slot-names: + - foo`, []string{"foo"}, []string{"bar"}}, + {`iface: + allow-installation: + slot-names: + - foo + - bar`, []string{"foo", "bar"}, []string{"baz"}}, + {`iface: + allow-installation: + slot-names: + - foo[0-9] + - bar`, []string{"foo0", "foo1", "bar"}, []string{"baz", "fooo", "foo12"}}, + } + for _, t := range tests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + for _, matching := range t.matching { + c.Check(rule.AllowInstallation[0].SlotNames.Check("slot name", matching, nil), IsNil) + } + for _, notMatching := range t.notMatching { + c.Check(rule.AllowInstallation[0].SlotNames.Check("slot name", notMatching, nil), NotNil) + } + } +} + func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsIDConstraints(c *C) { rule, err := asserts.CompileSlotRule("iface", map[string]interface{}{ "allow-connection": map[string]interface{}{ @@ -1573,6 +1915,144 @@ } } +func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsPlugNamesSlotNames(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-connection: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].PlugNames, IsNil) + c.Check(rule.AllowConnection[0].SlotNames, IsNil) + + tests := []struct { + rule string + matching []string + notMatching []string + }{ + {`iface: + allow-connection: + plug-names: + - Pfoo + slot-names: + - Sfoo`, []string{"foo"}, []string{"bar"}}, + {`iface: + allow-connection: + plug-names: + - Pfoo + - Pbar + slot-names: + - Sfoo + - Sbar`, []string{"foo", "bar"}, []string{"baz"}}, + {`iface: + allow-connection: + plug-names: + - Pfoo[0-9] + - Pbar + slot-names: + - Sfoo[0-9] + - Sbar`, []string{"foo0", "foo1", "bar"}, []string{"baz", "fooo", "foo12"}}, + } + for _, t := range tests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + for _, matching := range t.matching { + c.Check(rule.AllowConnection[0].PlugNames.Check("plug name", "P"+matching, nil), IsNil) + + c.Check(rule.AllowConnection[0].SlotNames.Check("slot name", "S"+matching, nil), IsNil) + } + + for _, notMatching := range t.notMatching { + c.Check(rule.AllowConnection[0].SlotNames.Check("plug name", "P"+notMatching, nil), NotNil) + + c.Check(rule.AllowConnection[0].SlotNames.Check("slot name", "S"+notMatching, nil), NotNil) + } + } +} + +func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsSideArityConstraints(c *C) { + m, err := asserts.ParseHeaders([]byte(`iface: + allow-auto-connection: true`)) + c.Assert(err, IsNil) + + rule, err := asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + // defaults + c.Check(rule.AllowAutoConnection[0].SlotsPerPlug, Equals, asserts.SideArityConstraint{N: 1}) + c.Check(rule.AllowAutoConnection[0].PlugsPerSlot.Any(), Equals, true) + + c.Check(rule.AllowConnection[0].SlotsPerPlug.Any(), Equals, true) + c.Check(rule.AllowConnection[0].PlugsPerSlot.Any(), Equals, true) + + // test that the arity constraints get normalized away to any + // under allow-connection + // see https://forum.snapcraft.io/t/plug-slot-declaration-rules-greedy-plugs/12438 + allowConnTests := []string{ + `iface: + allow-connection: + slots-per-plug: 1 + plugs-per-slot: 2`, + `iface: + allow-connection: + slots-per-plug: * + plugs-per-slot: 1`, + `iface: + allow-connection: + slots-per-plug: 2 + plugs-per-slot: *`, + } + + for _, t := range allowConnTests { + m, err = asserts.ParseHeaders([]byte(t)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowConnection[0].SlotsPerPlug.Any(), Equals, true) + c.Check(rule.AllowConnection[0].PlugsPerSlot.Any(), Equals, true) + } + + // test that under allow-auto-connection: + // slots-per-plug can be * (any) or otherwise gets normalized to 1 + // plugs-per-slot gets normalized to any (*) + // see https://forum.snapcraft.io/t/plug-slot-declaration-rules-greedy-plugs/12438 + allowAutoConnTests := []struct { + rule string + slotsPerPlug asserts.SideArityConstraint + }{ + {`iface: + allow-auto-connection: + slots-per-plug: 1 + plugs-per-slot: 2`, sideArityOne}, + {`iface: + allow-auto-connection: + slots-per-plug: * + plugs-per-slot: 1`, sideArityAny}, + {`iface: + allow-auto-connection: + slots-per-plug: 2 + plugs-per-slot: *`, sideArityOne}, + } + + for _, t := range allowAutoConnTests { + m, err = asserts.ParseHeaders([]byte(t.rule)) + c.Assert(err, IsNil) + + rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{})) + c.Assert(err, IsNil) + + c.Check(rule.AllowAutoConnection[0].SlotsPerPlug, Equals, t.slotsPerPlug) + c.Check(rule.AllowAutoConnection[0].PlugsPerSlot.Any(), Equals, true) + } +} + func (s *plugSlotRulesSuite) TestCompileSlotRuleErrors(c *C) { tests := []struct { stanza string @@ -1629,19 +2109,19 @@ {`iface: allow-connection: plug-snap-ids: - - foo`, `allow-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic, on-store, on-brand, on-model`}, + - foo`, `allow-connection in slot rule for interface "iface" must specify at least one of plug-names, slot-names, plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, slots-per-plug, plugs-per-slot, on-classic, on-store, on-brand, on-model`}, {`iface: deny-connection: plug-snap-ids: - - foo`, `deny-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic, on-store, on-brand, on-model`}, + - foo`, `deny-connection in slot rule for interface "iface" must specify at least one of plug-names, slot-names, plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, slots-per-plug, plugs-per-slot, on-classic, on-store, on-brand, on-model`}, {`iface: allow-auto-connection: plug-snap-ids: - - foo`, `allow-auto-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic, on-store, on-brand, on-model`}, + - foo`, `allow-auto-connection in slot rule for interface "iface" must specify at least one of plug-names, slot-names, plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, slots-per-plug, plugs-per-slot, on-classic, on-store, on-brand, on-model`}, {`iface: deny-auto-connection: plug-snap-ids: - - foo`, `deny-auto-connection in slot rule for interface "iface" must specify at least one of plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, on-classic, on-store, on-brand, on-model`}, + - foo`, `deny-auto-connection in slot rule for interface "iface" must specify at least one of plug-names, slot-names, plug-attributes, slot-attributes, plug-snap-type, plug-publisher-id, plug-snap-id, slots-per-plug, plugs-per-slot, on-classic, on-store, on-brand, on-model`}, {`iface: allow-connect: true`, `slot rule for interface "iface" must specify at least one of allow-installation, deny-installation, allow-connection, deny-connection, allow-auto-connection, deny-auto-connection`}, {`iface: @@ -1674,6 +2154,28 @@ allow-auto-connection: on-model: - foo//bar`, `on-model in allow-auto-connection in slot rule for interface \"iface\" contains an invalid element: \"foo//bar"`}, + {`iface: + allow-installation: + slots-per-plug: 1`, `allow-installation in slot rule for interface "iface" cannot specify a slots-per-plug constraint, they apply only to allow-\*connection`}, + {`iface: + deny-auto-connection: + slots-per-plug: 1`, `deny-auto-connection in slot rule for interface "iface" cannot specify a slots-per-plug constraint, they apply only to allow-\*connection`}, + {`iface: + allow-auto-connection: + plugs-per-slot: any`, `plugs-per-slot in allow-auto-connection in slot rule for interface "iface" must be an integer >=1 or \*`}, + {`iface: + allow-auto-connection: + slots-per-plug: 0`, `slots-per-plug in allow-auto-connection in slot rule for interface "iface" must be an integer >=1 or \*`}, + {`iface: + allow-auto-connection: + slots-per-plug: + what: 1`, `slots-per-plug in allow-auto-connection in slot rule for interface "iface" must be an integer >=1 or \*`}, + {`iface: + allow-auto-connection: + plug-names: true`, `plug-names constraints must be a list of regexps and special \$ values`}, + {`iface: + allow-auto-connection: + slot-names: true`, `slot-names constraints must be a list of regexps and special \$ values`}, } for _, t := range tests { @@ -1686,25 +2188,25 @@ func (s *plugSlotRulesSuite) TestSlotRuleFeatures(c *C) { combos := []struct { - subrule string - attrConstraints []string + subrule string + constraintsPrefixes []string }{ - {"allow-installation", []string{"slot-attributes"}}, - {"deny-installation", []string{"slot-attributes"}}, - {"allow-connection", []string{"plug-attributes", "slot-attributes"}}, - {"deny-connection", []string{"plug-attributes", "slot-attributes"}}, - {"allow-auto-connection", []string{"plug-attributes", "slot-attributes"}}, - {"deny-auto-connection", []string{"plug-attributes", "slot-attributes"}}, + {"allow-installation", []string{"slot-"}}, + {"deny-installation", []string{"slot-"}}, + {"allow-connection", []string{"plug-", "slot-"}}, + {"deny-connection", []string{"plug-", "slot-"}}, + {"allow-auto-connection", []string{"plug-", "slot-"}}, + {"deny-auto-connection", []string{"plug-", "slot-"}}, } for _, combo := range combos { - for _, attrConstr := range combo.attrConstraints { + for _, attrConstrPrefix := range combo.constraintsPrefixes { attrConstraintMap := map[string]interface{}{ "a": "ATTR", } ruleMap := map[string]interface{}{ combo.subrule: map[string]interface{}{ - attrConstr: attrConstraintMap, + attrConstrPrefix + "attributes": attrConstraintMap, }, } @@ -1733,6 +2235,19 @@ c.Assert(err, IsNil) c.Check(asserts.RuleFeature(rule, "device-scope-constraints"), Equals, true, Commentf("%v", ruleMap)) } + + for _, nameConstrPrefix := range combo.constraintsPrefixes { + ruleMap := map[string]interface{}{ + combo.subrule: map[string]interface{}{ + nameConstrPrefix + "names": []interface{}{"foo"}, + }, + } + + rule, err := asserts.CompileSlotRule("iface", ruleMap) + c.Assert(err, IsNil) + c.Check(asserts.RuleFeature(rule, "name-constraints"), Equals, true, Commentf("%v", ruleMap)) + } + } } diff -Nru snapd-2.37.4ubuntu0.1/asserts/model.go snapd-2.45.1ubuntu0.2/asserts/model.go --- snapd-2.37.4ubuntu0.1/asserts/model.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/model.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,794 @@ +// -*- 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 latest/stable in an extended model + DefaultChannel string + // PinnedTrack is a pinned track for the snap, if set DefaultChannel + // cannot be set at the same time (Core 18 models feature) + PinnedTrack 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 { + snapd *ModelSnap + 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.snapd, 1) + addSnap(ms.kernel, 1) + addSnap(ms.base, 1) + addSnap(ms.gadget, 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, grade ModelGrade) (*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, grade) + if err != nil { + return nil, err + } + + if seen[modelSnap.Name] { + return nil, fmt.Errorf("cannot list the same snap %q multiple times", modelSnap.Name) + } + seen[modelSnap.Name] = true + // at this time we do not support parallel installing + // from model/seed + if snapID := modelSnap.SnapID; snapID != "" { + if underName := seenIDs[snapID]; underName != "" { + return nil, fmt.Errorf("cannot specify the same snap id %q multiple times, specified for snaps %q and %q", snapID, underName, modelSnap.Name) + } + seenIDs[snapID] = modelSnap.Name + } + + essential := false + switch { + case modelSnap.SnapType == "snapd": + // TODO: allow to be explicit only in grade: dangerous? + essential = true + if modelSnaps.snapd != nil { + return nil, fmt.Errorf("cannot specify multiple snapd snaps: %q and %q", modelSnaps.snapd.Name, modelSnap.Name) + } + modelSnaps.snapd = modelSnap + 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", "snapd"} + validSnapMode = regexp.MustCompile("^[a-z][-a-z]+$") + validSnapPresences = []string{"required", "optional"} +) + +func checkModelSnap(snap map[string]interface{}, grade ModelGrade) (*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) + + var snapID string + _, ok := snap["id"] + if ok { + var err error + snapID, err = checkStringMatchesWhat(snap, "id", what, naming.ValidSnapID) + if err != nil { + return nil, err + } + } else { + // snap ids are optional with grade dangerous to allow working + // with local/not pushed yet to the store snaps + if grade != ModelDangerous { + return nil, fmt.Errorf(`"id" %s is mandatory for %s grade model`, what, grade) + } + } + + 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 %s", name, strings.Join(validSnapTypes, "|")) + } + + 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 + } + if defaultChannel == "" { + defaultChannel = "latest/stable" + } + defCh, err := channel.ParseVerbatim(defaultChannel, "-") + if err != nil { + return nil, fmt.Errorf("invalid default channel for snap %q: %v", name, err) + } + if defCh.Track == "" { + return nil, fmt.Errorf("default channel for snap %q must specify a track", name) + } + + 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, + 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) + } + } + + return &ModelSnap{ + Name: name, + SnapType: which, + Modes: defaultModes, + PinnedTrack: 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, + Presence: "required", + }, nil +} + +// ModelGrade characterizes the security of the model which then +// controls related policy. +type ModelGrade string + +const ( + ModelGradeUnset ModelGrade = "unset" + // ModelSecured implies mandatory full disk encryption and secure boot. + ModelSecured ModelGrade = "secured" + // ModelSigned implies all seed snaps are signed and mentioned + // in the model, i.e. no unasserted or extra snaps. + ModelSigned ModelGrade = "signed" + // ModelDangerous allows unasserted snaps and extra snaps. + ModelDangerous ModelGrade = "dangerous" +) + +var validModelGrades = []string{string(ModelSecured), string(ModelSigned), string(ModelDangerous)} + +// gradeToCode encodes grades into 32 bits, trying to be slightly future-proof: +// * lower 16 bits are reserved +// * in the higher bits use the sequence 1, 8, 16 to have some space +// to possibly add new grades in between +var gradeToCode = map[ModelGrade]uint32{ + ModelGradeUnset: 0, + ModelDangerous: 0x10000, + ModelSigned: 0x80000, + ModelSecured: 0x100000, +} + +// Code returns a bit representation of the grade, for example for +// measuring it in a full disk encryption implementation. +func (mg ModelGrade) Code() uint32 { + code, ok := gradeToCode[mg] + if !ok { + panic(fmt.Sprintf("unknown model grade: %s", mg)) + } + return code +} + +// 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 + + grade ModelGrade + + allSnaps []*ModelSnap + // consumers of this info should care only about snap identity => + // snapRef + requiredWithEssentialSnaps []naming.SnapRef + numEssentialSnaps int + + serialAuthority []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") +} + +// Grade returns the stability grade of the model. Will be ModeGradeUnset +// for Core 16/18 models. +func (mod *Model) Grade() ModelGrade { + return mod.grade +} + +// 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.PinnedTrack +} + +// 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.PinnedTrack +} + +// 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 +} + +// SerialAuthority returns the authority ids that are accepted as +// signers for serial assertions for this model. It always includes the +// brand of the model. +func (mod *Model) SerialAuthority() []string { + return mod.serialAuthority +} + +// SystemUserAuthority returns the authority ids that are accepted as +// signers of system-user assertions for this model. Empty list means +// any, otherwise it always includes the brand of the model. +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 checkOptionalSerialAuthority(headers map[string]interface{}, brandID string) ([]string, error) { + ids := []string{brandID} + const name = "serial-authority" + if _, ok := headers[name]; !ok { + return ids, nil + } + if lst, err := checkStringListMatches(headers, name, validAccountID); err == nil { + if !strutil.ListContains(lst, brandID) { + lst = append(ids, lst...) + } + return lst, nil + } + return nil, fmt.Errorf("%q header must be a list of account ids", name) +} + +func checkOptionalSystemUserAuthority(headers map[string]interface{}, brandID string) ([]string, error) { + ids := []string{brandID} + const name = "system-user-authority" + v, ok := headers[name] + if !ok { + return ids, nil + } + switch x := v.(type) { + case string: + if x == "*" { + return nil, nil + } + case []interface{}: + lst, err := checkStringListMatches(headers, name, validAccountID) + if err == nil { + if !strutil.ListContains(lst, brandID) { + lst = append(ids, lst...) + } + 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 _, ok := assert.headers["grade"]; ok { + return nil, fmt.Errorf("cannot specify a grade for model without the extended snaps header") + } + } + + 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 + grade := ModelGradeUnset + if extended { + gradeStr, err := checkOptionalString(assert.headers, "grade") + if err != nil { + return nil, err + } + if gradeStr != "" && !strutil.ListContains(validModelGrades, gradeStr) { + return nil, fmt.Errorf("grade for model must be %s, not %q", strings.Join(validModelGrades, "|"), gradeStr) + } + grade = ModelSigned + if gradeStr != "" { + grade = ModelGrade(gradeStr) + } + + modSnaps, err = checkExtendedSnaps(extendedSnaps, base, grade) + 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 + snapID := naming.WellKnownSnapID(modSnaps.base.Name) + if snapID == "" && grade != ModelDangerous { + return nil, fmt.Errorf(`cannot specify not well-known base %q without a corresponding "snaps" header entry`, modSnaps.base.Name) + } + modSnaps.base.SnapID = snapID + modSnaps.base.Modes = essentialSnapModes + modSnaps.base.DefaultChannel = "latest/stable" + } + } 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) + } + } + + brandID := assert.HeaderString("brand-id") + + serialAuthority, err := checkOptionalSerialAuthority(assert.headers, brandID) + if err != nil { + return nil, err + } + + sysUserAuthority, err := checkOptionalSystemUserAuthority(assert.headers, brandID) + 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, + grade: grade, + allSnaps: allSnaps, + requiredWithEssentialSnaps: requiredWithEssentialSnaps, + numEssentialSnaps: numEssentialSnaps, + serialAuthority: serialAuthority, + sysUserAuthority: sysUserAuthority, + timestamp: timestamp, + }, nil +} diff -Nru snapd-2.37.4ubuntu0.1/asserts/model_test.go snapd-2.45.1ubuntu0.2/asserts/model_test.go --- snapd-2.37.4ubuntu0.1/asserts/model_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/model_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,850 @@ +// -*- 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" + "github.com/snapcore/snapd/snap/naming" +) + +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" + serialAuths = "serial-authority:\n - generic\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" + + serialAuths + + 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 + default-channel: 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: secured +` + "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"}, + 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"}, + 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"}, + Presence: "required", + }) + c.Check(model.Store(), Equals, "brand-store") + c.Check(model.Grade(), Equals, asserts.ModelGradeUnset) + allSnaps := model.AllSnaps() + c.Check(allSnaps, DeepEquals, []*asserts.ModelSnap{ + model.KernelSnap(), + model.BaseSnap(), + model.GadgetSnap(), + { + Name: "foo", + Modes: []string{"run"}, + Presence: "required", + }, + { + Name: "bar", + Modes: []string{"run"}, + 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) + c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1", "generic"}) +} + +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) TestDecodeSerialAuthorityIsOptional(c *C) { + withTimestamp := strings.Replace(modelExample, "TSLINE", mods.tsLine, 1) + encoded := strings.Replace(withTimestamp, serialAuths, "", 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.SerialAuthority(), DeepEquals, []string{"brand-id1"}) + + encoded = strings.Replace(withTimestamp, serialAuths, "serial-authority:\n - foo\n - bar\n", 1) + a, err = asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + model = a.(*asserts.Model) + // the brand is always added implicitly + c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1", "foo", "bar"}) +} + +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) + // the brand is always added implicitly, it can always sign + // a new revision of the model anyway + c.Check(model.SystemUserAuthority(), DeepEquals, []string{"brand-id1", "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"}, + PinnedTrack: "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"}, + PinnedTrack: "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`}, + {serialAuths, "serial-authority:\n a: 1\n", `"serial-authority" header must be a list of account ids`}, + {serialAuths, "serial-authority:\n - 5_6\n", `"serial-authority" header must be a list of account ids`}, + {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`}, + {reqSnaps, "grade: dangerous\n", `cannot specify a grade for model without the extended snaps header`}, + } + + 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"}, + 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"}, + Presence: "required", + }, + { + Name: "bar", + Modes: []string{"run"}, + 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: "latest/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"}, + DefaultChannel: "20", + Presence: "required", + }) + c.Check(model.Kernel(), Equals, "baz-linux") + c.Check(model.KernelTrack(), Equals, "") + c.Check(model.Base(), Equals, "core20") + c.Check(model.BaseSnap(), DeepEquals, &asserts.ModelSnap{ + Name: "core20", + SnapID: naming.WellKnownSnapID("core20"), + SnapType: "base", + Modes: []string{"run", "ephemeral"}, + DefaultChannel: "latest/stable", + Presence: "required", + }) + c.Check(model.Store(), Equals, "brand-store") + c.Check(model.Grade(), Equals, asserts.ModelSecured) + allSnaps := model.AllSnaps() + c.Check(allSnaps, DeepEquals, []*asserts.ModelSnap{ + model.KernelSnap(), + model.BaseSnap(), + model.GadgetSnap(), + { + Name: "other-base", + SnapID: "otherbasedididididididididididid", + SnapType: "base", + Modes: []string{"run"}, + DefaultChannel: "latest/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: "latest/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) + c.Check(model.SerialAuthority(), DeepEquals, []string{"brand-id1"}) +} + +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: latest/candidate +`, 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.BaseSnap(), DeepEquals, &asserts.ModelSnap{ + Name: "core20", + SnapID: "core20ididididididididididididid", + SnapType: "base", + Modes: []string{"run", "ephemeral"}, + DefaultChannel: "latest/candidate", + Presence: "required", + }) +} + +func (mods *modelSuite) TestCore20ExplictSnapd(c *C) { + encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1) + encoded = strings.Replace(encoded, "OTHER", ` - + name: snapd + id: snapdidididididididididididididd + type: snapd + default-channel: latest/edge +`, 1) + a, err := asserts.Decode([]byte(encoded)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + model := a.(*asserts.Model) + snapdSnap := model.AllSnaps()[0] + c.Check(snapdSnap, DeepEquals, &asserts.ModelSnap{ + Name: "snapd", + SnapID: "snapdidididididididididididididd", + SnapType: "snapd", + Modes: []string{"run", "ephemeral"}, + DefaultChannel: "latest/edge", + Presence: "required", + }) +} + +func (mods *modelSuite) TestCore20GradeOptionalDefaultSigned(c *C) { + encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1) + encoded = strings.Replace(encoded, "OTHER", "", 1) + encoded = strings.Replace(encoded, "grade: secured\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.Grade(), Equals, asserts.ModelSigned) +} + +func (mods *modelSuite) TestCore20ValidGrades(c *C) { + encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1) + encoded = strings.Replace(encoded, "OTHER", "", 1) + + for _, grade := range []string{"signed", "secured", "dangerous"} { + ex := strings.Replace(encoded, "grade: secured\n", fmt.Sprintf("grade: %s\n", grade), 1) + a, err := asserts.Decode([]byte(ex)) + c.Assert(err, IsNil) + c.Check(a.Type(), Equals, asserts.ModelType) + model := a.(*asserts.Model) + c.Check(string(model.Grade()), Equals, grade) + } +} + +func (mods *modelSuite) TestModelGradeCode(c *C) { + for i, grade := range []asserts.ModelGrade{asserts.ModelGradeUnset, asserts.ModelDangerous, asserts.ModelSigned, asserts.ModelSecured} { + // unset is represented as zero + code := 0 + if i > 0 { + // have some space between grades to add new ones + n := (i - 1) * 8 + if n == 0 { + n = 1 // dangerous + } + // lower 16 bits are reserved + code = n << 16 + } + c.Check(grade.Code(), Equals, uint32(code)) + } +} + +func (mods *modelSuite) TestCore20GradeDangerous(c *C) { + encoded := strings.Replace(core20ModelExample, "TSLINE", mods.tsLine, 1) + encoded = strings.Replace(encoded, "OTHER", "", 1) + encoded = strings.Replace(encoded, "grade: secured\n", "grade: dangerous\n", 1) + // snap ids are optional with grade dangerous to allow working + // with local/not pushed yet to the store snaps + encoded = strings.Replace(encoded, " id: myappdididididididididididididid\n", "", 1) + encoded = strings.Replace(encoded, " id: brandgadgetdidididididididididid\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.Grade(), Equals, asserts.ModelDangerous) + allSnaps := model.AllSnaps() + c.Check(allSnaps[len(allSnaps)-2], DeepEquals, &asserts.ModelSnap{ + Name: "myapp", + SnapType: "app", + Modes: []string{"run"}, + DefaultChannel: "2.0", + Presence: "required", + }) +} + +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`}, + {"base: core20\n", "base: alt-base\n", `cannot specify not well-known base "alt-base" without a corresponding "snaps" header entry`}, + {"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"`}, + {" id: myappdididididididididididididid\n", "", `"id" of snap "myapp" is mandatory for secured grade model`}, + {"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|snapd`}, + {"modes:\n - run\n", "modes: run\n", `"modes" of snap "other-base" must be a list of strings`}, + {"default-channel: 20\n", "default-channel: edge\n", `default channel for snap "baz-linux" must specify a track`}, + {"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`}, + {"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`}, + {"grade: secured\n", "grade: foo\n", `grade for model must be secured|signed|dangerous`}, + } + 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.37.4ubuntu0.1/asserts/serial_asserts.go snapd-2.45.1ubuntu0.2/asserts/serial_asserts.go --- snapd-2.37.4ubuntu0.1/asserts/serial_asserts.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/serial_asserts.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,250 @@ +// -*- 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" + + "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/strutil" +) + +// 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 +} + +func (ser *Serial) checkConsistency(db RODatabase, acck *AccountKey) error { + if ser.AuthorityID() != ser.BrandID() { + // serial authority and brand do not match, check the model + a, err := db.Find(ModelType, map[string]string{ + "series": release.Series, + "brand-id": ser.BrandID(), + "model": ser.Model(), + }) + if err != nil && !IsNotFound(err) { + return err + } + if IsNotFound(err) || !strutil.ListContains(a.(*Model).SerialAuthority(), ser.AuthorityID()) { + return fmt.Errorf("serial with authority %q different from brand %q without model assertion with serial-authority set to to allow for them", ser.AuthorityID(), ser.BrandID()) + } + } + return nil +} + +func assembleSerial(assert assertionBase) (Assertion, error) { + // brand-id and authority-id can diverge if the model allows + // for it via serial-authority, check for brand-id well-formedness + _, err := checkStringMatches(assert.headers, "brand-id", validAccountID) + 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.37.4ubuntu0.1/asserts/serial_asserts_test.go snapd-2.45.1ubuntu0.2/asserts/serial_asserts_test.go --- snapd-2.37.4ubuntu0.1/asserts/serial_asserts_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/serial_asserts_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,365 @@ +// -*- 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`}, + {"brand-id: brand-id1\n", "brand-id: ,1\n", `"brand-id" header contains invalid characters: ",1\"`}, + {"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) + + const serialMismatchErr = `serial with authority "generic" different from brand "brand1" without model assertion with serial-authority set to to allow for them` + brandID := brandDB.AuthorityID + brandKeyID := brandDB.KeyID + genericKeyID := storeDB.GenericKey.PublicKeyID() + modelNA := []interface{}(nil) + brandOnly := []interface{}{} + tests := []struct { + // serial-authority setting in model + // nil == model not available at check (modelNA) + // empty == just brand (brandOnly) + serialAuth []interface{} + signDB assertstest.SignerDB + authID string + keyID string + expectedErr string + }{ + {modelNA, brandDB, "", brandKeyID, ""}, + {brandOnly, brandDB, "", brandKeyID, ""}, + {[]interface{}{"generic"}, brandDB, "", brandKeyID, ""}, + {[]interface{}{"generic", brandID}, brandDB, "", brandKeyID, ""}, + {[]interface{}{"generic"}, storeDB, "generic", genericKeyID, ""}, + {brandOnly, storeDB, "generic", genericKeyID, serialMismatchErr}, + {modelNA, storeDB, "generic", genericKeyID, serialMismatchErr}, + {[]interface{}{"other"}, storeDB, "generic", genericKeyID, serialMismatchErr}, + } + + for _, test := range tests { + checkDB := db.WithStackedBackstore(asserts.NewMemoryBackstore()) + + if test.serialAuth != nil { + modHeaders := map[string]interface{}{ + "series": "16", + "brand-id": brandID, + "architecture": "amd64", + "model": "baz-3000", + "gadget": "gadget", + "kernel": "kernel", + "timestamp": time.Now().Format(time.RFC3339), + } + if len(test.serialAuth) != 0 { + modHeaders["serial-authority"] = test.serialAuth + } + model, err := brandDB.Sign(asserts.ModelType, modHeaders, nil, "") + c.Assert(err, IsNil) + err = checkDB.Add(model) + c.Assert(err, IsNil) + } + + headers := ex.Headers() + headers["brand-id"] = brandID + if test.authID != "" { + headers["authority-id"] = test.authID + } else { + headers["authority-id"] = brandID + } + headers["timestamp"] = time.Now().Format(time.RFC3339) + serial, err := test.signDB.Sign(asserts.SerialType, headers, nil, test.keyID) + c.Check(err, IsNil) + if err != nil { + continue + } + + err = checkDB.Check(serial) + if test.expectedErr == "" { + c.Check(err, IsNil) + } else { + c.Check(err, ErrorMatches, test.expectedErr) + } + } +} + +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.37.4ubuntu0.1/asserts/snapasserts/snapasserts.go snapd-2.45.1ubuntu0.2/asserts/snapasserts/snapasserts.go --- snapd-2.37.4ubuntu0.1/asserts/snapasserts/snapasserts.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/snapasserts/snapasserts.go 2020-06-05 13:13:49.000000000 +0000 @@ -114,13 +114,16 @@ return nil, err } - name := snapDecl.SnapName() + return SideInfoFromSnapAssertions(snapDecl, snapRev), nil +} +// SideInfoFromSnapAssertions returns a *snap.SideInfo reflecting the given snap assertions. +func SideInfoFromSnapAssertions(snapDecl *asserts.SnapDeclaration, snapRev *asserts.SnapRevision) *snap.SideInfo { return &snap.SideInfo{ - RealName: name, - SnapID: snapID, + RealName: snapDecl.SnapName(), + SnapID: snapDecl.SnapID(), Revision: snap.R(snapRev.SnapRevision()), - }, nil + } } // FetchSnapAssertions fetches the assertions matching the snap file digest using the given fetcher. diff -Nru snapd-2.37.4ubuntu0.1/asserts/snap_asserts.go snapd-2.45.1ubuntu0.2/asserts/snap_asserts.go --- snapd-2.37.4ubuntu0.1/asserts/snap_asserts.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/snap_asserts.go 2020-06-05 13:13:49.000000000 +0000 @@ -23,13 +23,13 @@ "bytes" "crypto" "fmt" - "regexp" "time" _ "golang.org/x/crypto/sha3" // expected for digests "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snap/naming" ) // SnapDeclaration holds a snap-declaration assertion, declaring a @@ -171,6 +171,9 @@ if rule.feature(deviceScopeConstraintsFeature) { setFormatNum(3) } + if rule.feature(nameConstraintsFeature) { + setFormatNum(4) + } }) if err != nil { return 0, err @@ -187,6 +190,9 @@ if rule.feature(deviceScopeConstraintsFeature) { setFormatNum(3) } + if rule.feature(nameConstraintsFeature) { + setFormatNum(4) + } }) if err != nil { return 0, err @@ -195,11 +201,6 @@ return formatnum, nil } -var ( - validAlias = regexp.MustCompile("^[a-zA-Z0-9][-_.a-zA-Z0-9]*$") - validAppName = regexp.MustCompile("^[a-zA-Z0-9](?:-?[a-zA-Z0-9])*$") -) - func checkAliases(headers map[string]interface{}) (map[string]string, error) { value, ok := headers["aliases"] if !ok { @@ -221,13 +222,13 @@ } what := fmt.Sprintf(`in "aliases" item %d`, i+1) - name, err := checkStringMatchesWhat(aliasItem, "name", what, validAlias) + name, err := checkStringMatchesWhat(aliasItem, "name", what, naming.ValidAlias) if err != nil { return nil, err } what = fmt.Sprintf(`for alias %q`, name) - target, err := checkStringMatchesWhat(aliasItem, "target", what, validAppName) + target, err := checkStringMatchesWhat(aliasItem, "target", what, naming.ValidApp) if err != nil { return nil, err } @@ -296,7 +297,7 @@ } // XXX: depracated, will go away later - autoAliases, err := checkStringListMatches(assert.headers, "auto-aliases", validAlias) + autoAliases, err := checkStringListMatches(assert.headers, "auto-aliases", naming.ValidAlias) if err != nil { return nil, err } diff -Nru snapd-2.37.4ubuntu0.1/asserts/snap_asserts_test.go snapd-2.45.1ubuntu0.2/asserts/snap_asserts_test.go --- snapd-2.37.4ubuntu0.1/asserts/snap_asserts_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/snap_asserts_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -499,6 +499,53 @@ } _, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) c.Assert(err, ErrorMatches, `assertion snap-declaration: "slots" header must be a map`) + + // plug-names/slot-names => format 4 + for _, sidePrefix := range []string{"plug", "slot"} { + side := sidePrefix + "s" + headers := map[string]interface{}{ + side: map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-installation": map[string]interface{}{ + sidePrefix + "-names": []interface{}{"foo"}, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 4) + + for _, conn := range []string{"connection", "auto-connection"} { + + headers = map[string]interface{}{ + side: map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-" + conn: map[string]interface{}{ + sidePrefix + "-names": []interface{}{"foo"}, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 4) + + headers = map[string]interface{}{ + side: map[string]interface{}{ + "interface3": map[string]interface{}{ + "allow-" + conn: map[string]interface{}{ + "plug-names": []interface{}{"Pfoo"}, + "slot-names": []interface{}{"Sfoo"}, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 4) + } + } } func prereqDevAccount(c *C, storeDB assertstest.SignerDB, db *asserts.Database) { diff -Nru snapd-2.37.4ubuntu0.1/asserts/sysdb/generic.go snapd-2.45.1ubuntu0.2/asserts/sysdb/generic.go --- snapd-2.37.4ubuntu0.1/asserts/sysdb/generic.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/sysdb/generic.go 2020-06-05 13:13:49.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2017 Canonical Ltd + * Copyright (C) 2017-2020 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -23,7 +23,7 @@ "fmt" "github.com/snapcore/snapd/asserts" - "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snapdenv" ) const ( @@ -152,7 +152,7 @@ // Generic returns a copy of the current set of predefined assertions for the 'generic' authority as used by Open. func Generic() []asserts.Assertion { generic := []asserts.Assertion(nil) - if !osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + if !snapdenv.UseStagingStore() { generic = append(generic, genericAssertions...) } else { generic = append(generic, genericStagingAssertions...) @@ -179,7 +179,7 @@ if genericClassicModelOverride != nil { return genericClassicModelOverride } - if !osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + if !snapdenv.UseStagingStore() { return genericClassicModel } else { return genericStagingClassicModel diff -Nru snapd-2.37.4ubuntu0.1/asserts/sysdb/trusted.go snapd-2.45.1ubuntu0.2/asserts/sysdb/trusted.go --- snapd-2.37.4ubuntu0.1/asserts/sysdb/trusted.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/sysdb/trusted.go 2020-06-05 13:13:49.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2020 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -23,7 +23,7 @@ "fmt" "github.com/snapcore/snapd/asserts" - "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snapdenv" ) const ( @@ -130,7 +130,7 @@ // Trusted returns a copy of the current set of trusted assertions as used by Open. func Trusted() []asserts.Assertion { trusted := []asserts.Assertion(nil) - if !osutil.GetenvBool("SNAPPY_USE_STAGING_STORE") { + if !snapdenv.UseStagingStore() { trusted = append(trusted, trustedAssertions...) } else { if len(trustedStagingAssertions) == 0 { diff -Nru snapd-2.37.4ubuntu0.1/asserts/system_user.go snapd-2.45.1ubuntu0.2/asserts/system_user.go --- snapd-2.37.4ubuntu0.1/asserts/system_user.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/system_user.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/asserts/system_user_test.go snapd-2.45.1ubuntu0.2/asserts/system_user_test.go --- snapd-2.37.4ubuntu0.1/asserts/system_user_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/asserts/system_user_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/asserts/user.go snapd-2.45.1ubuntu0.2/asserts/user.go --- snapd-2.37.4ubuntu0.1/asserts/user.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/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.37.4ubuntu0.1/asserts/user_test.go snapd-2.45.1ubuntu0.2/asserts/user_test.go --- snapd-2.37.4ubuntu0.1/asserts/user_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/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.37.4ubuntu0.1/boot/booted_kernel_partition_linux.go snapd-2.45.1ubuntu0.2/boot/booted_kernel_partition_linux.go --- snapd-2.37.4ubuntu0.1/boot/booted_kernel_partition_linux.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/booted_kernel_partition_linux.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,53 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package boot + +import ( + "fmt" + "strings" + + "github.com/snapcore/snapd/bootloader/efi" +) + +const ( + // note the vendor ID 4a67b082-0a4c-41cf-b6c7-440b29bb8c4f is systemd, this + // variable is populated by shim + loaderDevicePartUUID = "LoaderDevicePartUUID-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f" +) + +// FindPartitionUUIDForBootedKernelDisk returns the partition uuid for the +// partition that the booted kernel is located on. +func FindPartitionUUIDForBootedKernelDisk() (string, error) { + // try efi variables first + partuuid, _, err := efi.ReadVarString(loaderDevicePartUUID) + if err == nil { + // the LoaderDevicePartUUID is in all caps, but lsblk, + // etc. use lower case so for consistency just make it + // lower case here too + return strings.ToLower(partuuid), nil + } + if err == efi.ErrNoEFISystem { + return "", err + } + + // TODO:UC20: add more fallbacks here, even on amd64, when we don't have efi + // i.e. on bios? + return "", fmt.Errorf("could not find partition uuid for booted kernel: %v", err) +} diff -Nru snapd-2.37.4ubuntu0.1/boot/booted_kernel_partition_test.go snapd-2.45.1ubuntu0.2/boot/booted_kernel_partition_test.go --- snapd-2.37.4ubuntu0.1/boot/booted_kernel_partition_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/booted_kernel_partition_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,60 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package boot_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/bootloader/bootloadertest" + "github.com/snapcore/snapd/bootloader/efi" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/testutil" +) + +var _ = Suite(&bootedKernelPartitionSuite{}) + +type bootedKernelPartitionSuite struct { + testutil.BaseTest +} + +func (s *bootedKernelPartitionSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) + s.AddCleanup(func() { dirs.SetRootDir("") }) +} + +func (s *bootedKernelPartitionSuite) TestFindPartitionUUIDForBootedKernelDisk(c *C) { + restore := efi.MockVars(map[string][]byte{ + "LoaderDevicePartUUID-4a67b082-0a4c-41cf-b6c7-440b29bb8c4f": bootloadertest.UTF16Bytes("A9F5C949-AB89-5B47-A7BF-56DD28F96E65"), + }, nil) + defer restore() + + partuuid, err := boot.FindPartitionUUIDForBootedKernelDisk() + c.Assert(err, IsNil) + c.Assert(partuuid, Equals, "a9f5c949-ab89-5b47-a7bf-56dd28f96e65") +} + +func (s *bootedKernelPartitionSuite) TestFindPartitionUUIDForBootedKernelDiskNoEFISystem(c *C) { + restore := efi.MockVars(nil, nil) + defer restore() + + _, err := boot.FindPartitionUUIDForBootedKernelDisk() + c.Check(err, Equals, efi.ErrNoEFISystem) +} diff -Nru snapd-2.37.4ubuntu0.1/boot/boot.go snapd-2.45.1ubuntu0.2/boot/boot.go --- snapd-2.37.4ubuntu0.1/boot/boot.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/boot.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,365 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019-2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package boot + +import ( + "errors" + "fmt" + + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/snap" +) + +const ( + // DefaultStatus is the value of a status boot variable when nothing is + // being tried + DefaultStatus = "" + // TryStatus is the value of a status boot variable when something is about + // to be tried + TryStatus = "try" + // TryingStatus is the value of a status boot variable after we have + // attempted a boot with a try snap - this status is only set in the early + // boot sequence (bootloader, initramfs, etc.) + TryingStatus = "trying" +) + +// 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). It is a noop for not relevant snaps. + // Otherwise it returns whether a reboot is required. + SetNextBoot() (rebootRequired bool, err error) + + // 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() (bool, error) { return false, nil } +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{} + +// Device carries information about the device model and mode that is +// relevant to boot. Note snapstate.DeviceContext implements this, and that's +// the expected use case. +type Device interface { + RunMode() bool + Classic() bool + + Kernel() string + Base() string + + HasModeenv() 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, dev Device) BootParticipant { + if applicable(s, t, dev) { + bs, err := bootStateFor(t, dev) + if err != nil { + // all internal errors at this point + panic(err) + } + return &coreBootParticipant{s: s, bs: bs} + } + return trivial{} +} + +// bootloaderOptionsForDeviceKernel returns a set of bootloader options that +// enable correct kernel extraction and removal for given device +func bootloaderOptionsForDeviceKernel(dev Device) *bootloader.Options { + return &bootloader.Options{ + // unified extractable kernel if in uc20 mode + ExtractedRunKernelImage: dev.HasModeenv(), + } +} + +// 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, dev Device) BootKernel { + if t == snap.TypeKernel && applicable(s, t, dev) { + return &coreKernel{s: s, bopts: bootloaderOptionsForDeviceKernel(dev)} + } + return trivial{} +} + +func applicable(s snap.PlaceInfo, t snap.Type, dev Device) bool { + if dev.Classic() { + return false + } + // In ephemeral modes we never need to care about updating the boot + // config. This will be done via boot.MakeBootable(). + if !dev.RunMode() { + 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 + } + + switch t { + case snap.TypeKernel: + if s.InstanceName() != dev.Kernel() { + // a remodel might leave you in this state + return false + } + case snap.TypeBase, snap.TypeOS: + base := dev.Base() + if base == "" { + base = "core" + } + if s.InstanceName() != base { + return false + } + } + + return true +} + +// bootState exposes the boot state for a type of boot snap. +type bootState interface { + // revisions retrieves the revisions of the current snap and + // the try snap (only the latter might not be set), and + // the status of the trying snap. + revisions() (curSnap, trySnap snap.PlaceInfo, tryingStatus string, err error) + + // setNext lazily implements setting the next boot target for + // the type's boot snap. actually committing the update + // is done via the returned bootStateUpdate's commit. + setNext(s snap.PlaceInfo) (rebootRequired bool, u bootStateUpdate, err error) + + // markSuccessful lazily implements marking the boot + // successful for the type's boot snap. The actual committing + // of the update is done via bootStateUpdate's commit, that + // way different markSuccessful can be folded together. + markSuccessful(bootStateUpdate) (bootStateUpdate, error) +} + +// bootStateFor finds the right bootState implementation of the given +// snap type and Device, if applicable. +func bootStateFor(typ snap.Type, dev Device) (s bootState, err error) { + if !dev.RunMode() { + return nil, fmt.Errorf("internal error: no boot state handling for ephemeral modes") + } + newBootState := newBootState16 + if dev.HasModeenv() { + newBootState = newBootState20 + } + switch typ { + case snap.TypeOS, snap.TypeBase: + return newBootState(snap.TypeBase), nil + case snap.TypeKernel: + return newBootState(snap.TypeKernel), nil + default: + return nil, fmt.Errorf("internal error: no boot state handling for snap type %q", typ) + } +} + +// InUseFunc is a function to check if the snap is in use or not. +type InUseFunc func(name string, rev snap.Revision) bool + +func fixedInUse(inUse bool) InUseFunc { + return func(string, snap.Revision) bool { + return inUse + } +} + +// InUse returns a checker for whether a given name/revision is used in the +// boot environment for snaps of the relevant snap type. +func InUse(typ snap.Type, dev Device) (InUseFunc, error) { + if dev.Classic() { + // no boot state on classic + return fixedInUse(false), nil + } + if !dev.RunMode() { + // ephemeral mode, block manipulations for now + return fixedInUse(true), nil + } + switch typ { + case snap.TypeKernel, snap.TypeBase, snap.TypeOS: + break + default: + return fixedInUse(false), nil + } + cands := make([]snap.PlaceInfo, 0, 2) + s, err := bootStateFor(typ, dev) + if err != nil { + return nil, err + } + cand, tryCand, _, err := s.revisions() + if err != nil { + return nil, err + } + cands = append(cands, cand) + if tryCand != nil { + cands = append(cands, tryCand) + } + + return func(name string, rev snap.Revision) bool { + for _, cand := range cands { + if cand.SnapName() == name && cand.SnapRevision() == rev { + return true + } + } + return false + }, nil +} + +var ( + // ErrBootNameAndRevisionNotReady is returned when the boot revision is not + // established yet. + ErrBootNameAndRevisionNotReady = errors.New("boot revision not yet established") +) + +// 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, dev Device) (snap.PlaceInfo, error) { + s, err := bootStateFor(t, dev) + if err != nil { + return nil, err + } + + snap, _, status, err := s.revisions() + if err != nil { + return nil, err + } + + if status == TryingStatus { + return nil, ErrBootNameAndRevisionNotReady + } + + return snap, nil +} + +// bootStateUpdate carries the state for an on-going boot state update. +// At the end it can be used to commit it. +type bootStateUpdate interface { + commit() error +} + +// 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 for UC16/18 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(dev Device) error { + const errPrefix = "cannot mark boot successful: %s" + + var u bootStateUpdate + for _, t := range []snap.Type{snap.TypeBase, snap.TypeKernel} { + s, err := bootStateFor(t, dev) + if err != nil { + return err + } + u, err = s.markSuccessful(u) + if err != nil { + return fmt.Errorf(errPrefix, err) + } + } + + if u != nil { + if err := u.commit(); err != nil { + return fmt.Errorf(errPrefix, err) + } + } + return nil +} + +var ErrUnsupportedSystemMode = errors.New("system mode is unsupported") + +// SetRecoveryBootSystemAndMode configures the recovery bootloader to boot into +// the given recovery system in a particular mode. Returns +// ErrUnsupportedSystemMode when booting into a recovery system is not supported +// by the device. +func SetRecoveryBootSystemAndMode(dev Device, systemLabel, mode string) error { + if !dev.HasModeenv() { + // only UC20 devices are supported + return ErrUnsupportedSystemMode + } + if systemLabel == "" { + return fmt.Errorf("internal error: system label is unset") + } + if mode == "" { + return fmt.Errorf("internal error: system mode is unset") + } + + opts := &bootloader.Options{ + // setup the recovery bootloader + Recovery: true, + } + // TODO:UC20: should the recovery partition stay around as RW during run + // mode all the time? + bl, err := bootloader.Find(InitramfsUbuntuSeedDir, opts) + if err != nil { + return err + } + + m := map[string]string{ + "snapd_recovery_system": systemLabel, + "snapd_recovery_mode": mode, + } + return bl.SetBootVars(m) +} diff -Nru snapd-2.37.4ubuntu0.1/boot/boot_robustness_test.go snapd-2.45.1ubuntu0.2/boot/boot_robustness_test.go --- snapd-2.37.4ubuntu0.1/boot/boot_robustness_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/boot_robustness_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,324 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ +package boot_test + +import ( + "fmt" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/boot/boottest" + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/snap" +) + +// TODO:UC20: move this to bootloadertest package and use from i.e. managers_test.go ? +func runBootloaderLogic(c *C, bl bootloader.Bootloader) (snap.PlaceInfo, error) { + // switch on which kind of bootloader we have + ebl, ok := bl.(bootloader.ExtractedRunKernelImageBootloader) + if ok { + return extractedRunKernelImageBootloaderLogic(c, ebl) + } + + return pureenvBootloaderLogic(c, "kernel_status", bl) +} + +// runBootloaderLogic implements the logic from the gadget snap bootloader, +// namely that we transition kernel_status "try" -> "trying" and "trying" -> "" +// and use try-kernel.efi when kernel_status is "try" and kernel.efi in all +// other situations +func extractedRunKernelImageBootloaderLogic(c *C, ebl bootloader.ExtractedRunKernelImageBootloader) (snap.PlaceInfo, error) { + m, err := ebl.GetBootVars("kernel_status") + c.Assert(err, IsNil) + kernStatus := m["kernel_status"] + + kern, err := ebl.Kernel() + c.Assert(err, IsNil) + c.Assert(kern, Not(IsNil)) + + switch kernStatus { + case boot.DefaultStatus: + case boot.TryStatus: + // move to trying, use the try-kernel + m["kernel_status"] = boot.TryingStatus + + // ensure that the try-kernel exists + tryKern, err := ebl.TryKernel() + c.Assert(err, IsNil) + c.Assert(tryKern, Not(IsNil)) + kern = tryKern + + case boot.TryingStatus: + // boot failed, move back to default + m["kernel_status"] = boot.DefaultStatus + } + + err = ebl.SetBootVars(m) + c.Assert(err, IsNil) + + return kern, nil +} + +func pureenvBootloaderLogic(c *C, modeVar string, bl bootloader.Bootloader) (snap.PlaceInfo, error) { + m, err := bl.GetBootVars(modeVar, "snap_kernel", "snap_try_kernel") + c.Assert(err, IsNil) + var kern snap.PlaceInfo + + kernStatus := m[modeVar] + + kern, err = snap.ParsePlaceInfoFromSnapFileName(m["snap_kernel"]) + c.Assert(err, IsNil) + c.Assert(kern, Not(IsNil)) + + switch kernStatus { + case boot.DefaultStatus: + // nothing to do, use normal kernel + + case boot.TryStatus: + // move to trying, use the try-kernel + m[modeVar] = boot.TryingStatus + + tryKern, err := snap.ParsePlaceInfoFromSnapFileName(m["snap_try_kernel"]) + c.Assert(err, IsNil) + c.Assert(tryKern, Not(IsNil)) + kern = tryKern + + case boot.TryingStatus: + // boot failed, move back to default status + m[modeVar] = boot.DefaultStatus + + } + + err = bl.SetBootVars(m) + c.Assert(err, IsNil) + + return kern, nil +} + +// note: this could be implemented just as a function which takes a bootloader +// as an argument and then inspect the type of MockBootloader that was passed +// in, but the gains are little, since we don't need to use this function for +// the non-ExtractedRunKernelImageBootloader implementations, as those +// implementations just have one critical function to run which is just +// SetBootVars +func (s *bootenv20Suite) checkBootStateAfterUnexpectedRebootAndCleanup( + c *C, + dev boot.Device, + bootFunc func(boot.Device) error, + panicFunc string, + expectedBootedKernel snap.PlaceInfo, + expectedModeenvCurrentKernels []snap.PlaceInfo, + blKernelAfterReboot snap.PlaceInfo, + comment string, +) { + if panicFunc != "" { + // setup a panic during the given bootloader function + restoreBootloaderPanic := s.bootloader.SetRunKernelImagePanic(panicFunc) + + // run the boot function that will now panic + c.Assert( + func() { bootFunc(dev) }, + PanicMatches, + fmt.Sprintf("mocked reboot panic in %s", panicFunc), + Commentf(comment), + ) + + // don't panic anymore + restoreBootloaderPanic() + } else { + // just run the function directly + err := bootFunc(dev) + c.Assert(err, IsNil, Commentf(comment)) + } + + // do the bootloader kernel failover logic handling + nextBootingKernel, err := runBootloaderLogic(c, s.bootloader) + c.Assert(err, IsNil, Commentf(comment)) + + // check that the kernel we booted now is expected + c.Assert(nextBootingKernel, Equals, expectedBootedKernel, Commentf(comment)) + + // also check that the normal kernel on the bootloader is what we expect + kern, err := s.bootloader.Kernel() + c.Assert(err, IsNil, Commentf(comment)) + c.Assert(kern, Equals, blKernelAfterReboot, Commentf(comment)) + + // mark the boot successful like we were rebooted + err = boot.MarkBootSuccessful(dev) + c.Assert(err, IsNil, Commentf(comment)) + + // the boot vars should be empty now too + afterVars, err := s.bootloader.GetBootVars("kernel_status") + c.Assert(err, IsNil, Commentf(comment)) + c.Assert(afterVars["kernel_status"], DeepEquals, boot.DefaultStatus, Commentf(comment)) + + // the modeenv's setting for CurrentKernels also matches + m, err := boot.ReadModeenv("") + c.Assert(err, IsNil, Commentf(comment)) + // it's nicer to pass in just the snap.PlaceInfo's, but to compare we need + // the string filenames + currentKernels := make([]string, len(expectedModeenvCurrentKernels)) + for i, sn := range expectedModeenvCurrentKernels { + currentKernels[i] = sn.Filename() + } + c.Assert(m.CurrentKernels, DeepEquals, currentKernels, Commentf(comment)) + + // the final kernel on the bootloader should always match what we booted - + // after MarkSuccessful runs that is + afterKernel, err := s.bootloader.Kernel() + c.Assert(err, IsNil, Commentf(comment)) + c.Assert(afterKernel, DeepEquals, expectedBootedKernel, Commentf(comment)) + + // we should never have a leftover try kernel + _, err = s.bootloader.TryKernel() + c.Assert(err, Equals, bootloader.ErrNoTryKernelRef, Commentf(comment)) +} + +func (s *bootenv20Suite) TestHappyMarkBootSuccessful20KernelUpgradeUnexpectedReboots(c *C) { + coreDev := boottest.MockUC20Device("some-snap") + c.Assert(coreDev.HasModeenv(), Equals, true) + + tt := []struct { + rebootBeforeFunc string + expBootKernel snap.PlaceInfo + expModeenvKernels []snap.PlaceInfo + expBlKernel snap.PlaceInfo + comment string + }{ + { + "", // don't do any reboots for the happy path + s.kern2, // we should boot the new kernel + []snap.PlaceInfo{s.kern2}, // expected modeenv kernel is new one + s.kern2, // after reboot, current kernel on bl is new one + "happy path", + }, + { + "SetBootVars", // reboot right before SetBootVars + s.kern1, // we should boot the old kernel + []snap.PlaceInfo{s.kern1}, // expected modeenv kernel is old one + s.kern1, // after reboot, current kernel on bl is old one + "reboot before SetBootVars results in old kernel", + }, + { + "EnableKernel", // reboot right before EnableKernel + s.kern1, // we should boot the old kernel + []snap.PlaceInfo{s.kern1}, // expected modeenv kernel is old one + s.kern1, // after reboot, current kernel on bl is old one + "reboot before EnableKernel results in old kernel", + }, + { + "DisableTryKernel", // reboot right before DisableTryKernel + s.kern2, // we should boot the new kernel + []snap.PlaceInfo{s.kern2}, // expected modeenv kernel is new one + s.kern2, // after reboot, current kernel on bl is new one + "reboot before DisableTryKernel results in new kernel", + }, + } + + for _, t := range tt { + // setup the bootloader per test + restore := setupUC20Bootenv( + c, + s.bootloader, + s.normalTryingKernelState, + ) + + s.checkBootStateAfterUnexpectedRebootAndCleanup( + c, + coreDev, + boot.MarkBootSuccessful, + t.rebootBeforeFunc, + t.expBlKernel, + t.expModeenvKernels, + t.expBlKernel, + t.comment, + ) + + restore() + } +} + +func (s *bootenv20Suite) TestHappySetNextBoot20KernelUpgradeUnexpectedReboots(c *C) { + coreDev := boottest.MockUC20Device("pc-kernel") + c.Assert(coreDev.HasModeenv(), Equals, true) + + tt := []struct { + rebootBeforeFunc string + expBootKernel snap.PlaceInfo + expModeenvKernels []snap.PlaceInfo + expBlKernel snap.PlaceInfo + comment string + }{ + { + "", // don't do any reboots for the happy path + s.kern2, // we should boot the new kernel + []snap.PlaceInfo{s.kern2}, // final expected modeenv kernel is new one + s.kern1, // after reboot, current kernel on bl is old one + "happy path", + }, + { + "EnableTryKernel", // reboot right before EnableTryKernel + s.kern1, // we should boot the old kernel + []snap.PlaceInfo{s.kern1}, // final expected modeenv kernel is old one + s.kern1, // after reboot, current kernel on bl is old one + "reboot before EnableTryKernel results in old kernel", + }, + { + "SetBootVars", // reboot right before SetBootVars + s.kern1, // we should boot the old kernel + []snap.PlaceInfo{s.kern1}, // final expected modeenv kernel is old one + s.kern1, // after reboot, current kernel on bl is old one + "reboot before SetBootVars results in old kernel", + }, + } + + for _, t := range tt { + // setup the bootloader per test + restore := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + + // get the boot kernel participant from our new kernel snap + bootKern := boot.Participant(s.kern2, snap.TypeKernel, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootKern.IsTrivial(), Equals, false) + + setNextFunc := func(boot.Device) error { + // we don't care about the reboot required logic here + _, err := bootKern.SetNextBoot() + return err + } + + s.checkBootStateAfterUnexpectedRebootAndCleanup( + c, + coreDev, + setNextFunc, + t.rebootBeforeFunc, + t.expBootKernel, + t.expModeenvKernels, + t.expBlKernel, + t.comment, + ) + + restore() + } +} diff -Nru snapd-2.37.4ubuntu0.1/boot/bootstate16.go snapd-2.45.1ubuntu0.2/boot/bootstate16.go --- snapd-2.37.4ubuntu0.1/boot/bootstate16.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/bootstate16.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,192 @@ +// -*- 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 ( + "fmt" + + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/snap" +) + +type bootState16 struct { + varSuffix string + errName string +} + +func newBootState16(typ snap.Type) bootState { + var varSuffix, errName string + switch typ { + case snap.TypeKernel: + varSuffix = "kernel" + errName = "kernel" + case snap.TypeBase: + varSuffix = "core" + errName = "boot base" + default: + panic(fmt.Sprintf("cannot make a bootState16 for snap type %q", typ)) + } + return &bootState16{varSuffix: varSuffix, errName: errName} +} + +func (s16 *bootState16) revisions() (s, tryS snap.PlaceInfo, status string, err error) { + bloader, err := bootloader.Find("", nil) + if err != nil { + return nil, nil, "", fmt.Errorf("cannot get boot settings: %s", err) + } + + snapVar := "snap_" + s16.varSuffix + trySnapVar := "snap_try_" + s16.varSuffix + vars := []string{"snap_mode", snapVar, trySnapVar} + snaps := make(map[string]snap.PlaceInfo, 2) + + m, err := bloader.GetBootVars(vars...) + if err != nil { + return nil, nil, "", fmt.Errorf("cannot get boot variables: %s", err) + } + + for _, vName := range vars { + v := m[vName] + if v == "" && vName != snapVar { + // snap_mode & snap_try_ can be empty + // snap_ cannot be! and will fail parsing + // below + continue + } + + if vName == "snap_mode" { + status = v + } else { + if v == "" { + return nil, nil, "", fmt.Errorf("cannot get name and revision of %s (%s): boot variable unset", s16.errName, vName) + } + snap, err := snap.ParsePlaceInfoFromSnapFileName(v) + if err != nil { + return nil, nil, "", fmt.Errorf("cannot get name and revision of %s (%s): %v", s16.errName, vName, err) + } + snaps[vName] = snap + } + } + + return snaps[snapVar], snaps[trySnapVar], status, nil +} + +type bootStateUpdate16 struct { + bl bootloader.Bootloader + env map[string]string + toCommit map[string]string +} + +func newBootStateUpdate16(u bootStateUpdate, names ...string) (*bootStateUpdate16, error) { + if u != nil { + u16, ok := u.(*bootStateUpdate16) + if !ok { + return nil, fmt.Errorf("internal error: threading unexpected boot state update on UC16/18: %T", u) + } + return u16, nil + } + bl, err := bootloader.Find("", nil) + if err != nil { + return nil, err + } + m, err := bl.GetBootVars(names...) + if err != nil { + return nil, err + } + return &bootStateUpdate16{bl: bl, env: m, toCommit: make(map[string]string)}, nil +} + +func (u16 *bootStateUpdate16) commit() error { + if len(u16.toCommit) == 0 { + // nothing to do + return nil + } + env := u16.env + // TODO: we could just SetBootVars(toCommit) but it's not + // fully backward compatible with the preexisting behavior + for k, v := range u16.toCommit { + env[k] = v + } + return u16.bl.SetBootVars(env) +} + +func (s16 *bootState16) markSuccessful(update bootStateUpdate) (bootStateUpdate, error) { + u16, err := newBootStateUpdate16(update, "snap_mode", "snap_try_core", "snap_try_kernel") + if err != nil { + return nil, err + } + + env := u16.env + toCommit := u16.toCommit + + // snap_mode goes from "" -> "try" -> "trying" -> "" + // so if we are not in "trying" mode, nothing to do here + if env["snap_mode"] != TryingStatus { + return u16, nil + } + + tryBootVar := fmt.Sprintf("snap_try_%s", s16.varSuffix) + bootVar := fmt.Sprintf("snap_%s", s16.varSuffix) + // update the boot vars + if env[tryBootVar] != "" { + toCommit[bootVar] = env[tryBootVar] + toCommit[tryBootVar] = "" + } + toCommit["snap_mode"] = DefaultStatus + + return u16, nil +} + +func (s16 *bootState16) setNext(s snap.PlaceInfo) (rebootRequired bool, u bootStateUpdate, err error) { + nextBoot := s.Filename() + + nextBootVar := fmt.Sprintf("snap_try_%s", s16.varSuffix) + goodBootVar := fmt.Sprintf("snap_%s", s16.varSuffix) + + u16, err := newBootStateUpdate16(nil, "snap_mode", goodBootVar) + if err != nil { + return false, nil, err + } + + env := u16.env + toCommit := u16.toCommit + + snapMode := TryStatus + rebootRequired = true + if env[goodBootVar] == nextBoot { + // If we were in anything but default ("") mode before + // and switched to the good core/kernel again, make + // sure to clean the snap_mode here. This also + // mitigates https://forum.snapcraft.io/t/5253 + if env["snap_mode"] == DefaultStatus { + // already clean + return false, nil, nil + } + // clean + snapMode = DefaultStatus + nextBoot = "" + rebootRequired = false + } + + toCommit["snap_mode"] = snapMode + toCommit[nextBootVar] = nextBoot + + return rebootRequired, u16, nil +} diff -Nru snapd-2.37.4ubuntu0.1/boot/bootstate20.go snapd-2.45.1ubuntu0.2/boot/bootstate20.go --- snapd-2.37.4ubuntu0.1/boot/bootstate20.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/bootstate20.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,812 @@ +// -*- 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 ( + "fmt" + + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/snap" +) + +func newBootState20(typ snap.Type) bootState { + switch typ { + case snap.TypeBase: + return &bootState20Base{} + case snap.TypeKernel: + return &bootState20Kernel{} + default: + panic(fmt.Sprintf("cannot make a bootState20 for snap type %q", typ)) + } +} + +// +// modeenv methods +// + +type bootState20Modeenv struct { + modeenv *Modeenv +} + +func (bsm *bootState20Modeenv) loadModeenv() error { + // don't read modeenv multiple times + if bsm.modeenv != nil { + return nil + } + modeenv, err := ReadModeenv("") + if err != nil { + return fmt.Errorf("cannot get snap revision: unable to read modeenv: %v", err) + } + bsm.modeenv = modeenv + + return nil +} + +// +// bootloaderKernelState20 methods +// + +type bootloaderKernelState20 interface { + // load will setup any state / actors needed to use other methods + load() error + // kernelStatus returns the current status of the kernel, i.e. the + // kernel_status bootenv + kernelStatus() string + // kernel returns the current non-try kernel + kernel() snap.PlaceInfo + // kernel returns the current try kernel if it exists on the bootloader + tryKernel() (snap.PlaceInfo, error) + + // setNextKernel marks the kernel as the next, if it's not the currently + // booted kernel, then the specified kernel is setup as a try-kernel + setNextKernel(sn snap.PlaceInfo, status string) error + // markSuccessfulKernel marks the specified kernel as having booted + // successfully, whether that kernel is the current kernel or the try-kernel + markSuccessfulKernel(sn snap.PlaceInfo) error +} + +// extractedRunKernelImageBootloaderKernelState implements bootloaderKernelState20 for +// bootloaders that implement ExtractedRunKernelImageBootloader +type extractedRunKernelImageBootloaderKernelState struct { + // the bootloader + ebl bootloader.ExtractedRunKernelImageBootloader + // the current kernel status as read by the bootloader's bootenv + currentKernelStatus string + // the current kernel on the bootloader (not the try-kernel) + currentKernel snap.PlaceInfo +} + +func (bks *extractedRunKernelImageBootloaderKernelState) load() error { + // get the kernel_status + m, err := bks.ebl.GetBootVars("kernel_status") + if err != nil { + return err + } + + bks.currentKernelStatus = m["kernel_status"] + + // get the current kernel for this bootloader to compare during commit() for + // markSuccessful() if we booted the current kernel or not + kernel, err := bks.ebl.Kernel() + if err != nil { + return fmt.Errorf("cannot identify kernel snap with bootloader %s: %v", bks.ebl.Name(), err) + } + + bks.currentKernel = kernel + + return nil +} + +func (bks *extractedRunKernelImageBootloaderKernelState) kernel() snap.PlaceInfo { + return bks.currentKernel +} + +func (bks *extractedRunKernelImageBootloaderKernelState) tryKernel() (snap.PlaceInfo, error) { + return bks.ebl.TryKernel() +} + +func (bks *extractedRunKernelImageBootloaderKernelState) kernelStatus() string { + return bks.currentKernelStatus +} + +func (bks *extractedRunKernelImageBootloaderKernelState) markSuccessfulKernel(sn snap.PlaceInfo) error { + // set the boot vars first, then enable the successful kernel, then disable + // the old try-kernel, see the comment in bootState20MarkSuccessful.commit() + // for details + + // the ordering here is very important for boot reliability! + + // If we have successfully just booted from a try-kernel and are + // marking it successful (this implies that snap_kernel=="trying" as set + // by the boot script), we need to do the following in order (since we + // have the added complexity of moving the kernel symlink): + // 1. Update kernel_status to "" + // 2. Move kernel symlink to point to the new try kernel + // 3. Remove try-kernel symlink + // 4. Remove old kernel from modeenv (this happens one level up from this + // function) + // + // If we got rebooted after step 1, then the bootloader is booting the wrong + // kernel, but is at least booting a known good kernel and snapd in + // user-space would be able to figure out the inconsistency. + // If we got rebooted after step 2, the bootloader would boot from the new + // try-kernel which is okay because we were in the middle of committing + // that new kernel as good and all that's left is for snapd to cleanup + // the left-over try-kernel symlink. + // + // If instead we had moved the kernel symlink first to point to the new try + // kernel, and got rebooted before the kernel_status was updated, we would + // have kernel_status="trying" which would cause the bootloader to think + // the boot failed, and revert to booting using the kernel symlink, but that + // now points to the new kernel we were trying and we did not successfully + // boot from that kernel to know we should trust it. + // + // Removing the old kernel from the modeenv needs to happen after it is + // impossible for the bootloader to boot from that kernel, otherwise we + // could end up in a state where the bootloader doesn't want to boot the + // new kernel, but the initramfs doesn't trust the old kernel and we are + // stuck. As such, do this last, after the symlink no longer exists. + // + // The try-kernel symlink removal should happen last because it will not + // affect anything, except that if it was removed before updating + // kernel_status to "", the bootloader will think that the try kernel failed + // to boot and fall back to booting the old kernel which is safe. + + // always set the boot vars first before mutating any of the kernel symlinks + // etc. + // for markSuccessful, we will always set the status to Default, even if + // technically this boot wasn't "successful" - it was successful in the + // sense that we booted some combination of boot snaps and made it all the + // way to snapd in user space + if bks.currentKernelStatus != DefaultStatus { + m := map[string]string{ + "kernel_status": DefaultStatus, + } + + // set the boot variables + err := bks.ebl.SetBootVars(m) + if err != nil { + return err + } + } + + // if the kernel we booted is not the current one, we must have tried + // a new kernel, so enable that one as the current one now + if bks.currentKernel.Filename() != sn.Filename() { + err := bks.ebl.EnableKernel(sn) + if err != nil { + return err + } + } + + // always disable the try kernel snap to cleanup in case we have upgrade + // failures which leave behind try-kernel.efi + err := bks.ebl.DisableTryKernel() + if err != nil { + return err + } + + return nil +} + +func (bks *extractedRunKernelImageBootloaderKernelState) setNextKernel(sn snap.PlaceInfo, status string) error { + // always enable the try-kernel first, if we did the reverse and got + // rebooted after setting the boot vars but before enabling the try-kernel + // we could get stuck where the bootloader can't find the try-kernel and + // gets stuck waiting for a user to reboot, at which point we would fallback + // see i.e. https://github.com/snapcore/pc-amd64-gadget/issues/36 + if sn.Filename() != bks.currentKernel.Filename() { + err := bks.ebl.EnableTryKernel(sn) + if err != nil { + return err + } + } + + // only if the new kernel status is different from what we read should we + // run SetBootVars() to minimize wear/corruption possibility on the bootenv + if status != bks.currentKernelStatus { + m := map[string]string{ + "kernel_status": status, + } + + // set the boot variables + return bks.ebl.SetBootVars(m) + } + + return nil +} + +// envRefExtractedKernelBootloaderKernelState implements bootloaderKernelState20 for +// bootloaders that only support using bootloader env and i.e. don't support +// ExtractedRunKernelImageBootloader +type envRefExtractedKernelBootloaderKernelState struct { + // the bootloader + bl bootloader.Bootloader + + // the current state of env + env map[string]string + + // the state of env to commit + toCommit map[string]string + + // the current kernel + kern snap.PlaceInfo +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) load() error { + // for uc20, we only care about kernel_status, snap_kernel, and + // snap_try_kernel + m, err := envbks.bl.GetBootVars("kernel_status", "snap_kernel", "snap_try_kernel") + if err != nil { + return err + } + + // the default commit env is the same state as the current env + envbks.env = m + envbks.toCommit = make(map[string]string, len(m)) + for k, v := range m { + envbks.toCommit[k] = v + } + + // snap_kernel is the current kernel snap + // parse the filename here because the kernel() method doesn't return an err + sn, err := snap.ParsePlaceInfoFromSnapFileName(envbks.env["snap_kernel"]) + if err != nil { + return err + } + + envbks.kern = sn + + return nil +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) kernel() snap.PlaceInfo { + return envbks.kern +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) tryKernel() (snap.PlaceInfo, error) { + // empty snap_try_kernel is special case + if envbks.env["snap_try_kernel"] == "" { + return nil, bootloader.ErrNoTryKernelRef + } + sn, err := snap.ParsePlaceInfoFromSnapFileName(envbks.env["snap_try_kernel"]) + if err != nil { + return nil, err + } + + return sn, nil +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) kernelStatus() string { + return envbks.env["kernel_status"] +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) commonStateCommitUpdate(sn snap.PlaceInfo, bootvar string) bool { + envChanged := false + + // check kernel_status + if envbks.env["kernel_status"] != envbks.toCommit["kernel_status"] { + envChanged = true + } + + // if the specified snap is not the current snap, update the bootvar + if sn.Filename() != envbks.kern.Filename() { + envbks.toCommit[bootvar] = sn.Filename() + envChanged = true + } + + return envChanged +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) markSuccessfulKernel(sn snap.PlaceInfo) error { + // the ordering here doesn't matter, as the only actual state we mutate is + // writing the bootloader env vars, so just do that once at the end after + // processing all the changes + + // always set kernel_status to DefaultStatus + envbks.toCommit["kernel_status"] = DefaultStatus + envChanged := envbks.commonStateCommitUpdate(sn, "snap_kernel") + + // if the snap_try_kernel is set, we should unset that to both cleanup after + // a successful trying -> "" transition, but also to cleanup if we got + // rebooted during the process and have it leftover + if envbks.env["snap_try_kernel"] != "" { + envChanged = true + envbks.toCommit["snap_try_kernel"] = "" + } + + if envChanged { + return envbks.bl.SetBootVars(envbks.toCommit) + } + + return nil +} + +func (envbks *envRefExtractedKernelBootloaderKernelState) setNextKernel(sn snap.PlaceInfo, status string) error { + envbks.toCommit["kernel_status"] = status + bootenvChanged := envbks.commonStateCommitUpdate(sn, "snap_try_kernel") + + if bootenvChanged { + return envbks.bl.SetBootVars(envbks.toCommit) + } + + return nil +} + +// +// kernel snap methods +// + +// bootState20Kernel implements the bootState and bootStateUpdate interfaces for +// kernel snaps on UC20. It is used for setNext() and markSuccessful() - though +// note that for markSuccessful() a different bootStateUpdate implementation is +// returned, see bootState20MarkSuccessful +type bootState20Kernel struct { + bks bootloaderKernelState20 + + // the kernel snap that was booted for markSuccessful() + bootedKernelSnap snap.PlaceInfo + + // the kernel snap to try for setNext() + nextKernelSnap snap.PlaceInfo + + // the kernel_status to commit during commit() + commitKernelStatus string + + // don't embed this struct - it will conflict with embedding + // bootState20Modeenv in bootState20Base when both bootState20Base and + // bootState20Kernel are embedded in bootState20MarkSuccessful + // also we only need to use it with setNext() + kModeenv bootState20Modeenv +} + +func (ks20 *bootState20Kernel) loadBootenv() error { + // don't setup multiple times + if ks20.bks != nil { + return nil + } + + // find the bootloader and ensure it's an extracted run kernel image + // bootloader + opts := &bootloader.Options{ + // we want extracted run kernel images for uc20 + // TODO:UC20: the name of this flag is now confusing, as it is being + // slightly abused to tell the uboot bootloader to just look + // in a different directory, even when we don't have an + // actual extracted kernel image for that impl + ExtractedRunKernelImage: true, + } + bl, err := bootloader.Find("", opts) + if err != nil { + return err + } + ebl, ok := bl.(bootloader.ExtractedRunKernelImageBootloader) + if ok { + // use the new 20-style ExtractedRunKernelImage implementation + ks20.bks = &extractedRunKernelImageBootloaderKernelState{ebl: ebl} + } else { + // use fallback pure bootloader env implementation + ks20.bks = &envRefExtractedKernelBootloaderKernelState{bl: bl} + } + + // setup the bootloaderKernelState20 + if err := ks20.bks.load(); err != nil { + return err + } + + return nil +} + +func (ks20 *bootState20Kernel) revisions() (curSnap, trySnap snap.PlaceInfo, tryingStatus string, err error) { + var tryBootSn snap.PlaceInfo + err = ks20.loadBootenv() + if err != nil { + return nil, nil, "", err + } + + status := ks20.bks.kernelStatus() + kern := ks20.bks.kernel() + + tryKernel, err := ks20.bks.tryKernel() + if err != nil && err != bootloader.ErrNoTryKernelRef { + return nil, nil, "", err + } + + if err == nil { + tryBootSn = tryKernel + } + + return kern, tryBootSn, status, nil +} + +func (ks20 *bootState20Kernel) markSuccessful(update bootStateUpdate) (bootStateUpdate, error) { + // call the generic method with this object to do most of the legwork + u, sn, err := selectSuccessfulBootSnap(ks20, update) + if err != nil { + return nil, err + } + + // save this object inside the update to share bootenv / modeenv between + // multiple calls to markSuccessful for different snap types, but the same + // bootStateUpdate object + u.bootState20Kernel = *ks20 + + // u should always be non-nil if err is nil + u.bootedKernelSnap = sn + return u, nil +} + +func (ks20 *bootState20Kernel) setNext(next snap.PlaceInfo) (rebootRequired bool, u bootStateUpdate, err error) { + // commit() for setNext() also needs to add to the kernels in modeenv + err = ks20.kModeenv.loadModeenv() + if err != nil { + return false, nil, err + } + + nextStatus, err := genericSetNext(ks20, next) + if err != nil { + return false, nil, err + } + + // if we are setting a snap as a try snap, then we need to reboot + rebootRequired = false + ks20.nextKernelSnap = next + if nextStatus == TryStatus { + rebootRequired = true + } + ks20.commitKernelStatus = nextStatus + + // any state changes done so far are consumed in commit() + + return rebootRequired, ks20, nil +} + +// commit for bootState20Kernel is meant only to be used with setNext(). +// For markSuccessful(), use bootState20MarkSuccessful. +func (ks20 *bootState20Kernel) commit() error { + // The ordering of this is very important for boot safety/reliability!!! + + // If we are about to try an update, and need to add the try-kernel symlink, + // we need to do things in this order: + // 1. Add the kernel snap to the modeenv + // 2. Create try-kernel symlink + // 3. Update kernel_status to "try" + // + // This is because if we get rebooted in before 3, kernel_status is still + // unset and boot scripts proceeds to boot with the old kernel, effectively + // ignoring the try-kernel symlink. + // If we did it in the opposite order however, we would set kernel_status to + // "try" and then get rebooted before we could create the try-kernel + // symlink, so the bootloader would try to boot from the non-existent + // try-kernel symlink and become broken. + // + // Adding the kernel snap to the modeenv's list of trusted kernel snaps can + // effectively happen any time before we update the kernel_status to "try" + // for the same reasoning as for creating the try-kernel symlink. Putting it + // first is currently a purely aesthetic choice. + + // add the kernel to the modeenv if it is not the current kernel (if it is + // the current kernel then it must already be in the modeenv) + currentKernel := ks20.bks.kernel() + if ks20.nextKernelSnap.Filename() != currentKernel.Filename() { + // add the kernel to the modeenv + ks20.kModeenv.modeenv.CurrentKernels = append( + ks20.kModeenv.modeenv.CurrentKernels, + ks20.nextKernelSnap.Filename(), + ) + err := ks20.kModeenv.modeenv.Write() + if err != nil { + return err + } + } + + err := ks20.bks.setNextKernel(ks20.nextKernelSnap, ks20.commitKernelStatus) + if err != nil { + return err + } + + return nil +} + +// +// base snap methods +// + +// bootState20Kernel implements the bootState and bootStateUpdate interfaces for +// base snaps on UC20. It is used for setNext() and markSuccessful() - though +// note that for markSuccessful() a different bootStateUpdate implementation is +// returned, see bootState20MarkSuccessful +type bootState20Base struct { + bootState20Modeenv + + // the base_status to be written to the modeenv, stored separately to + // eliminate unnecessary writes to the modeenv when it's already in the + // state we want it in + commitBaseStatus string + + // the base snap that was booted for markSuccessful() + bootedBaseSnap snap.PlaceInfo + + // the base snap to try for setNext() + tryBaseSnap snap.PlaceInfo +} + +func (bs20 *bootState20Base) loadModeenv() error { + // don't read modeenv multiple times + if bs20.modeenv != nil { + return nil + } + modeenv, err := ReadModeenv("") + if err != nil { + return fmt.Errorf("cannot get snap revision: unable to read modeenv: %v", err) + } + bs20.modeenv = modeenv + + // default commit status is the current status + bs20.commitBaseStatus = bs20.modeenv.BaseStatus + + return nil +} + +// revisions returns the current boot snap and optional try boot snap for the +// type specified in bsgeneric. +func (bs20 *bootState20Base) revisions() (curSnap, trySnap snap.PlaceInfo, tryingStatus string, err error) { + var bootSn, tryBootSn snap.PlaceInfo + err = bs20.loadModeenv() + if err != nil { + return nil, nil, "", err + } + + if bs20.modeenv.Base == "" { + return nil, nil, "", fmt.Errorf("cannot get snap revision: modeenv base boot variable is empty") + } + + bootSn, err = snap.ParsePlaceInfoFromSnapFileName(bs20.modeenv.Base) + if err != nil { + return nil, nil, "", fmt.Errorf("cannot get snap revision: modeenv base boot variable is invalid: %v", err) + } + + if bs20.modeenv.BaseStatus == TryingStatus && bs20.modeenv.TryBase != "" { + tryBootSn, err = snap.ParsePlaceInfoFromSnapFileName(bs20.modeenv.TryBase) + if err != nil { + return nil, nil, "", fmt.Errorf("cannot get snap revision: modeenv try base boot variable is invalid: %v", err) + } + } + + return bootSn, tryBootSn, bs20.modeenv.BaseStatus, nil +} + +func (bs20 *bootState20Base) markSuccessful(update bootStateUpdate) (bootStateUpdate, error) { + // call the generic method with this object to do most of the legwork + u, sn, err := selectSuccessfulBootSnap(bs20, update) + if err != nil { + return nil, err + } + + // save this object inside the update to share bootenv / modeenv between + // multiple calls to markSuccessful for different snap types, but the same + // bootStateUpdate object + u.bootState20Base = *bs20 + + // u should always be non-nil if err is nil + u.bootedBaseSnap = sn + return u, nil +} + +func (bs20 *bootState20Base) setNext(next snap.PlaceInfo) (rebootRequired bool, u bootStateUpdate, err error) { + nextStatus, err := genericSetNext(bs20, next) + if err != nil { + return false, nil, err + } + // if we are setting a snap as a try snap, then we need to reboot + rebootRequired = false + if nextStatus == TryStatus { + bs20.tryBaseSnap = next + rebootRequired = true + } + bs20.commitBaseStatus = nextStatus + + // any state changes done so far are consumed in to commit() + + return rebootRequired, bs20, nil +} + +// commit for bootState20Base is meant only to be used with setNext(), for +// markSuccessful(), use bootState20MarkSuccessful. +func (bs20 *bootState20Base) commit() error { + // the ordering here is less important than the kernel commit(), since the + // only operation that has side-effects is writing the modeenv at the end, + // and that uses an atomic file writing operation, so it's not a concern if + // we get rebooted during this snippet like it is with the kernel snap above + + // the TryBase is the snap we are trying - note this could be nil if we + // are calling setNext on the same snap that is current + changed := false + if bs20.tryBaseSnap != nil { + tryBase := bs20.tryBaseSnap.Filename() + if tryBase != bs20.modeenv.TryBase { + bs20.modeenv.TryBase = tryBase + changed = true + } + } + + if bs20.commitBaseStatus != bs20.modeenv.BaseStatus { + bs20.modeenv.BaseStatus = bs20.commitBaseStatus + changed = true + } + + // only write the modeenv if we actually changed it + if changed { + return bs20.modeenv.Write() + } + return nil +} + +// +// generic methods +// + +// genericSetNext implements the generic logic for setting up a snap to be tried +// for boot and works for both kernel and base snaps (though not +// simultaneously). +func genericSetNext(b bootState, next snap.PlaceInfo) (setStatus string, err error) { + // get the current snap + current, _, _, err := b.revisions() + if err != nil { + return "", err + } + + // check if the next snap is really the same as the current snap, in which + // case we either do nothing or just clear the status (and not reboot) + if current.SnapName() == next.SnapName() && next.SnapRevision() == current.SnapRevision() { + // if we are setting the next snap as the current snap, don't need to + // change any snaps, just reset the status to default + return DefaultStatus, nil + } + + // by default we will set the status as "try" to prepare for an update, + // which also by default will require a reboot + return TryStatus, nil +} + +// bootState20MarkSuccessful is like bootState20Base and +// bootState20Kernel, but is the combination of both of those things so we can +// mark both snaps successful in one go +type bootState20MarkSuccessful struct { + // base snap + bootState20Base + // kernel snap + bootState20Kernel +} + +// selectSuccessfulBootSnap inspects the specified boot state to pick what +// boot snap should be marked as successful and use as a valid rollback target. +// If the first return value is non-nil, the second return value will be the +// snap that was booted and should be marked as successful. +// It also loads boot environment state into b. +func selectSuccessfulBootSnap(b bootState, update bootStateUpdate) ( + bsmark *bootState20MarkSuccessful, + bootedSnap snap.PlaceInfo, + err error, +) { + // get the try snap and the current status + sn, trySnap, status, err := b.revisions() + if err != nil { + return nil, nil, err + } + + // try to extract bsmark out of update + var ok bool + if update != nil { + if bsmark, ok = update.(*bootState20MarkSuccessful); !ok { + return nil, nil, fmt.Errorf("internal error, cannot thread %T with update for UC20", update) + } + } + + if bsmark == nil { + bsmark = &bootState20MarkSuccessful{} + } + + // kernel_status and base_status go from "" -> "try" (set by snapd), to + // "try" -> "trying" (set by the boot script) + // so if we are in "trying" mode, then we should choose the try snap + if status == TryingStatus && trySnap != nil { + return bsmark, trySnap, nil + } + + // if we are not in trying then choose the normal snap + return bsmark, sn, nil +} + +// commit will persistently write out the boot variables, etc. needed to mark +// the snaps saved in bsmark as successful boot targets/combinations. +// note that this makes the assumption that markSuccessful() has been called for +// both the base and kernel snaps here, if that assumption is not true anymore, +// this could end up auto-cleaning status variables for something it shouldn't +// be. +func (bsmark *bootState20MarkSuccessful) commit() error { + // the base and kernel snap updates will modify the modeenv, so we only + // issue a single write at the end if something changed + modeenvChanged := false + + // for full explanation of the robustness and ordering, see the comments + // on the implementations of bks.markSuccessfulKernel + + // kernel snap first, slightly higher priority + + // bootedKernelSnap will only ever be non-nil if we aren't marking a kernel + // snap successful, i.e. we are only marking a base snap successful + // this shouldn't happen except in tests, but let's be robust against it + // just in case + if bsmark.bootedKernelSnap != nil { + // always mark the kernel snap successful _before_ any other state + // mutating that may happen in bks.markSuccessful, because what we don't + // want to happen is to remove the old kernel and only trust the new + // try kernel before we actually set it up to boot from the new try + // kernel - that would brick us because we wouldn't trust the new kernel + // but the bootloader still thinks it should boot from the old kernel + err := bsmark.bks.markSuccessfulKernel(bsmark.bootedKernelSnap) + if err != nil { + return err + } + + // also always set current_kernels to be just the kernel we booted, for + // same reason we always disable the try-kernel + bsmark.modeenv.CurrentKernels = []string{bsmark.bootedKernelSnap.Filename()} + modeenvChanged = true + } + + // base snap next + // the ordering here is less important, since the only operation that + // has side-effects is writing the modeenv at the end, and that uses an + // atomic file writing operation, so it's not a concern if we get + // rebooted during this snippet like it is with the kernel snap above + + // always clear the base_status and try_base when marking successful, this + // has the useful side-effect of cleaning up if we have base_status=trying + // but no try_base set, or if we had an issue with try_base being invalid + if bsmark.modeenv.BaseStatus != DefaultStatus { + modeenvChanged = true + bsmark.modeenv.TryBase = "" + bsmark.modeenv.BaseStatus = DefaultStatus + } + + if bsmark.bootedBaseSnap != nil { + // set the new base as the tried base snap + tryBase := bsmark.bootedBaseSnap.Filename() + if bsmark.modeenv.Base != tryBase { + bsmark.modeenv.Base = tryBase + modeenvChanged = true + } + + // clear the TryBase + if bsmark.modeenv.TryBase != "" { + bsmark.modeenv.TryBase = "" + modeenvChanged = true + } + } + + // write the modeenv + if modeenvChanged { + return bsmark.modeenv.Write() + } + + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/boot/boottest/bootenv.go snapd-2.45.1ubuntu0.2/boot/boottest/bootenv.go --- snapd-2.37.4ubuntu0.1/boot/boottest/bootenv.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/boottest/bootenv.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,174 @@ +// -*- 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 boottest + +import ( + "fmt" + + "github.com/snapcore/snapd/bootloader/bootloadertest" + "github.com/snapcore/snapd/snap" +) + +// Bootenv16 implements manipulating a UC16/18 boot env for testing. +type Bootenv16 struct { + *bootloadertest.MockBootloader + statusVar string +} + +// MockUC16Bootenv wraps a mock bootloader for UC16/18 boot env +// manipulation. +func MockUC16Bootenv(b *bootloadertest.MockBootloader) *Bootenv16 { + return &Bootenv16{ + MockBootloader: b, + statusVar: "snap_mode", + } +} + +// SetBootKernel sets the current boot kernel string. Should be +// something like "pc-kernel_1234.snap". +func (b16 Bootenv16) SetBootKernel(kernel string) { + b16.SetBootVars(map[string]string{"snap_kernel": kernel}) +} + +// SetBootTryKernel sets the try boot kernel string. Should be +// something like "pc-kernel_1235.snap". +func (b16 Bootenv16) SetBootTryKernel(kernel string) { + b16.SetBootVars(map[string]string{"snap_try_kernel": kernel}) +} + +// SetBootBase sets the current boot base string. Should be something +// like "core_1234.snap". +func (b16 Bootenv16) SetBootBase(base string) { + b16.SetBootVars(map[string]string{"snap_core": base}) +} + +// SetTryingDuringReboot indicates that new kernel or base are being tried +// same as done by bootloader config. +func (b16 Bootenv16) SetTryingDuringReboot(which []snap.Type) error { + if b16.BootVars[b16.statusVar] != "try" { + return fmt.Errorf("bootloader must be in 'try' mode") + } + b16.BootVars[b16.statusVar] = "trying" + return nil +} + +func includesType(which []snap.Type, t snap.Type) bool { + for _, t1 := range which { + if t1 == t { + return true + } + } + return true +} + +func exactlyType(which []snap.Type, t snap.Type) bool { + if len(which) != 1 { + return false + } + if which[0] != t { + return false + } + return true +} + +// SetRollbackAcrossReboot will simulate a rollback across reboots. This +// means that the bootloader had "snap_try_{core,kernel}" set but this +// boot failed. In this case the bootloader will clear +// "snap_try_{core,kernel}" and "snap_mode" which means the "old" kernel,core +// in "snap_{core,kernel}" will be used. which indicates whether rollback +// applies to kernel, base or both. +func (b16 Bootenv16) SetRollbackAcrossReboot(which []snap.Type) error { + if b16.BootVars[b16.statusVar] != "try" { + return fmt.Errorf("rollback can only be simulated in 'try' mode") + } + rollbackBase := includesType(which, snap.TypeBase) + rollbackKernel := includesType(which, snap.TypeKernel) + if !rollbackBase && !rollbackKernel { + return fmt.Errorf("rollback of either base or kernel must be requested") + } + if rollbackBase && b16.BootVars["snap_core"] == "" && b16.BootVars["snap_kernel"] == "" { + return fmt.Errorf("base rollback can only be simulated if snap_core is set") + } + if rollbackKernel && b16.BootVars["snap_kernel"] == "" { + return fmt.Errorf("kernel rollback can only be simulated if snap_kernel is set") + } + // clean try bootvars and statusVar + b16.BootVars[b16.statusVar] = "" + if rollbackBase { + b16.BootVars["snap_try_core"] = "" + } + if rollbackKernel { + b16.BootVars["snap_try_kernel"] = "" + } + return nil +} + +// RunBootenv20 implements manipulating a UC20 run-mode boot env for +// testing. +type RunBootenv20 struct { + *bootloadertest.MockExtractedRunKernelImageBootloader +} + +// MockUC20EnvRefExtractedKernelRunBootenv wraps a mock bootloader for UC20 run-mode boot +// env manipulation. +func MockUC20EnvRefExtractedKernelRunBootenv(b *bootloadertest.MockBootloader) *Bootenv16 { + // TODO:UC20: implement this w/o returning Bootenv16 because that doesn't + // make a lot of sense to the caller + return &Bootenv16{ + MockBootloader: b, + statusVar: "kernel_status", + } +} + +// MockUC20RunBootenv wraps a mock bootloader for UC20 run-mode boot +// env manipulation. +func MockUC20RunBootenv(b *bootloadertest.MockBootloader) *RunBootenv20 { + return &RunBootenv20{b.WithExtractedRunKernelImage()} +} + +// TODO:UC20: expose actual snap-boostrap logic for testing + +// SetTryingDuringReboot indicates that new kernel or base are being tried +// same as done by bootloader config. +func (b20 RunBootenv20) SetTryingDuringReboot(which []snap.Type) error { + if !exactlyType(which, snap.TypeKernel) { + return fmt.Errorf("for now only kernel related simulation is supported") + } + if b20.BootVars["kernel_status"] != "try" { + return fmt.Errorf("bootloader must be in 'try' mode") + } + b20.BootVars["kernel_status"] = "trying" + return nil +} + +// SetRollbackAcrossReboot will simulate a rollback across reboots for either +// a new base or kernel or both, as indicated by which. +// TODO: only kernel is supported for now. +func (b20 RunBootenv20) SetRollbackAcrossReboot(which []snap.Type) error { + if !exactlyType(which, snap.TypeKernel) { + return fmt.Errorf("for now only kernel related simulation is supported") + } + if b20.BootVars["kernel_status"] != "try" { + return fmt.Errorf("rollback can only be simulated in 'try' mode") + } + // clean try bootvars and snap_mode + b20.BootVars["kernel_status"] = "" + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/boot/boottest/device.go snapd-2.45.1ubuntu0.2/boot/boottest/device.go --- snapd-2.37.4ubuntu0.1/boot/boottest/device.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/boottest/device.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,65 @@ +// -*- 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 boottest + +import ( + "strings" + + "github.com/snapcore/snapd/boot" +) + +type mockDevice struct { + bootSnap string + mode string + uc20 bool +} + +// MockDevice implements boot.Device. It wraps a string like +// [@], no means classic, no +// defaults to "run". It returns for both +// Base and Kernel, for more control mock a DeviceContext. +func MockDevice(s string) boot.Device { + bootsnap, mode := snapAndMode(s) + return &mockDevice{ + bootSnap: bootsnap, + mode: mode, + } +} + +// MockUC20Device implements boot.Device and returns true for HasModeenv. +func MockUC20Device(s string) boot.Device { + m := MockDevice(s).(*mockDevice) + m.uc20 = true + return m +} + +func snapAndMode(str string) (snap, mode string) { + parts := strings.SplitN(string(str), "@", 2) + if len(parts) == 1 || parts[1] == "" { + return parts[0], "run" + } + return parts[0], parts[1] +} + +func (d *mockDevice) Kernel() string { return d.bootSnap } +func (d *mockDevice) Base() string { return d.bootSnap } +func (d *mockDevice) Classic() bool { return d.bootSnap == "" } +func (d *mockDevice) RunMode() bool { return d.mode == "run" } +func (d *mockDevice) HasModeenv() bool { return d.uc20 } diff -Nru snapd-2.37.4ubuntu0.1/boot/boottest/device_test.go snapd-2.45.1ubuntu0.2/boot/boottest/device_test.go --- snapd-2.37.4ubuntu0.1/boot/boottest/device_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/boottest/device_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,100 @@ +// -*- 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 boottest_test + +import ( + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot/boottest" +) + +func TestBoottest(t *testing.T) { TestingT(t) } + +type boottestSuite struct{} + +var _ = Suite(&boottestSuite{}) + +func (s *boottestSuite) TestMockDeviceClassic(c *C) { + dev := boottest.MockDevice("") + c.Check(dev.Classic(), Equals, true) + c.Check(dev.Kernel(), Equals, "") + c.Check(dev.Base(), Equals, "") + c.Check(dev.RunMode(), Equals, true) + c.Check(dev.HasModeenv(), Equals, false) + + dev = boottest.MockDevice("@run") + c.Check(dev.Classic(), Equals, true) + c.Check(dev.Kernel(), Equals, "") + c.Check(dev.Base(), Equals, "") + c.Check(dev.RunMode(), Equals, true) + c.Check(dev.HasModeenv(), Equals, false) + + dev = boottest.MockDevice("@recover") + c.Check(dev.Classic(), Equals, true) + c.Check(dev.Kernel(), Equals, "") + c.Check(dev.Base(), Equals, "") + c.Check(dev.RunMode(), Equals, false) + c.Check(dev.HasModeenv(), Equals, false) +} + +func (s *boottestSuite) TestMockDeviceBaseOrKernel(c *C) { + dev := boottest.MockDevice("boot-snap") + c.Check(dev.Classic(), Equals, false) + c.Check(dev.Kernel(), Equals, "boot-snap") + c.Check(dev.Base(), Equals, "boot-snap") + c.Check(dev.RunMode(), Equals, true) + c.Check(dev.HasModeenv(), Equals, false) + + dev = boottest.MockDevice("boot-snap@run") + c.Check(dev.Classic(), Equals, false) + c.Check(dev.Kernel(), Equals, "boot-snap") + c.Check(dev.Base(), Equals, "boot-snap") + c.Check(dev.RunMode(), Equals, true) + c.Check(dev.HasModeenv(), Equals, false) + + dev = boottest.MockDevice("boot-snap@recover") + c.Check(dev.Classic(), Equals, false) + c.Check(dev.Kernel(), Equals, "boot-snap") + c.Check(dev.Base(), Equals, "boot-snap") + c.Check(dev.RunMode(), Equals, false) + c.Check(dev.HasModeenv(), Equals, false) +} + +func (s *boottestSuite) TestMockUC20Device(c *C) { + dev := boottest.MockUC20Device("boot-snap") + c.Check(dev.HasModeenv(), Equals, true) + + dev = boottest.MockUC20Device("boot-snap@run") + c.Check(dev.HasModeenv(), Equals, true) + + dev = boottest.MockUC20Device("boot-snap@recover") + c.Check(dev.HasModeenv(), Equals, true) + + dev = boottest.MockUC20Device("") + c.Check(dev.HasModeenv(), Equals, true) + + dev = boottest.MockUC20Device("@run") + c.Check(dev.HasModeenv(), Equals, true) + + dev = boottest.MockUC20Device("@recover") + c.Check(dev.HasModeenv(), Equals, true) +} diff -Nru snapd-2.37.4ubuntu0.1/boot/boottest/mockbootloader.go snapd-2.45.1ubuntu0.2/boot/boottest/mockbootloader.go --- snapd-2.37.4ubuntu0.1/boot/boottest/mockbootloader.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/boottest/mockbootloader.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,72 +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 ( - "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 -} - -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") -} diff -Nru snapd-2.37.4ubuntu0.1/boot/boot_test.go snapd-2.45.1ubuntu0.2/boot/boot_test.go --- snapd-2.37.4ubuntu0.1/boot/boot_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/boot_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,1392 @@ +// -*- 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/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/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" +) + +func TestBoot(t *testing.T) { TestingT(t) } + +type baseBootenvSuite struct { + testutil.BaseTest + + bootdir string +} + +func (s *baseBootenvSuite) 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") +} + +func (s *baseBootenvSuite) forceBootloader(bloader bootloader.Bootloader) { + bootloader.Force(bloader) + s.AddCleanup(func() { bootloader.Force(nil) }) +} + +type bootenvSuite struct { + baseBootenvSuite + + bootloader *bootloadertest.MockBootloader +} + +var _ = Suite(&bootenvSuite{}) + +func (s *bootenvSuite) SetUpTest(c *C) { + s.baseBootenvSuite.SetUpTest(c) + + s.bootloader = bootloadertest.Mock("mock", c.MkDir()) + s.forceBootloader(s.bootloader) +} + +type baseBootenv20Suite struct { + baseBootenvSuite + + kern1 snap.PlaceInfo + kern2 snap.PlaceInfo + base1 snap.PlaceInfo + base2 snap.PlaceInfo + + normalDefaultState *bootenv20Setup + normalTryingKernelState *bootenv20Setup +} + +func (s *baseBootenv20Suite) SetUpTest(c *C) { + s.baseBootenvSuite.SetUpTest(c) + + var err error + s.kern1, err = snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") + c.Assert(err, IsNil) + s.kern2, err = snap.ParsePlaceInfoFromSnapFileName("pc-kernel_2.snap") + c.Assert(err, IsNil) + + s.base1, err = snap.ParsePlaceInfoFromSnapFileName("core20_1.snap") + c.Assert(err, IsNil) + s.base2, err = snap.ParsePlaceInfoFromSnapFileName("core20_2.snap") + c.Assert(err, IsNil) + + // default boot state for robustness tests, etc. + s.normalDefaultState = &bootenv20Setup{ + modeenv: &boot.Modeenv{ + // base is base1 + Base: s.base1.Filename(), + // no try base + TryBase: "", + // base status is default + BaseStatus: boot.DefaultStatus, + // current kernels is just kern1 + CurrentKernels: []string{s.kern1.Filename()}, + // operating mode is run + Mode: "run", + // RecoverySystem is unset, as it should be during run mode + RecoverySystem: "", + }, + // enabled kernel is kern1 + kern: s.kern1, + // no try kernel enabled + tryKern: nil, + // kernel status is default + kernStatus: boot.DefaultStatus, + } + + // state for after trying a new kernel for robustness tests, etc. + s.normalTryingKernelState = &bootenv20Setup{ + modeenv: &boot.Modeenv{ + // base is base1 + Base: s.base1.Filename(), + // no try base + TryBase: "", + // base status is default + BaseStatus: boot.DefaultStatus, + // current kernels is kern1 + kern2 + CurrentKernels: []string{s.kern1.Filename(), s.kern2.Filename()}, + }, + // enabled kernel is kern1 + kern: s.kern1, + // try kernel is kern2 + tryKern: s.kern2, + // kernel status is trying + kernStatus: boot.TryingStatus, + } +} + +type bootenv20Suite struct { + baseBootenv20Suite + + bootloader *bootloadertest.MockExtractedRunKernelImageBootloader +} + +type bootenv20EnvRefKernelSuite struct { + baseBootenv20Suite + + bootloader *bootloadertest.MockBootloader +} + +var defaultUC20BootEnv = map[string]string{"kernel_status": boot.DefaultStatus} + +var _ = Suite(&bootenv20Suite{}) +var _ = Suite(&bootenv20EnvRefKernelSuite{}) + +func (s *bootenv20Suite) SetUpTest(c *C) { + s.baseBootenv20Suite.SetUpTest(c) + + s.bootloader = bootloadertest.Mock("mock", c.MkDir()).WithExtractedRunKernelImage() + s.forceBootloader(s.bootloader) +} + +func (s *bootenv20EnvRefKernelSuite) SetUpTest(c *C) { + s.baseBootenv20Suite.SetUpTest(c) + + s.bootloader = bootloadertest.Mock("mock", c.MkDir()) + s.forceBootloader(s.bootloader) +} + +type bootenv20Setup struct { + modeenv *boot.Modeenv + kern snap.PlaceInfo + tryKern snap.PlaceInfo + kernStatus string +} + +func setupUC20Bootenv(c *C, bl bootloader.Bootloader, opts *bootenv20Setup) (restore func()) { + var cleanups []func() + + // write the modeenv + if opts.modeenv != nil { + c.Assert(opts.modeenv.WriteTo(""), IsNil) + // this isn't strictly necessary since the modeenv will be written to + // the test's private dir anyways, but it's nice to have so we can write + // multiple modeenvs from a single test and just call the restore + // function in between the parts of the test that use different modeenvs + r := func() { + emptyModeenv := &boot.Modeenv{} + c.Assert(emptyModeenv.WriteTo(""), IsNil) + } + cleanups = append(cleanups, r) + } + + // set the status + origEnv, err := bl.GetBootVars("kernel_status") + c.Assert(err, IsNil) + + err = bl.SetBootVars(map[string]string{"kernel_status": opts.kernStatus}) + c.Assert(err, IsNil) + cleanups = append(cleanups, func() { + err := bl.SetBootVars(origEnv) + c.Assert(err, IsNil) + }) + + // check what kind of real mock bootloader we have to use different methods + // to set the kernel snaps are if they're non-nil + switch vbl := bl.(type) { + case *bootloadertest.MockExtractedRunKernelImageBootloader: + // then we can use the advanced methods on it + if opts.kern != nil { + r := vbl.SetRunKernelImageEnabledKernel(opts.kern) + cleanups = append(cleanups, r) + } + + if opts.tryKern != nil { + r := vbl.SetRunKernelImageEnabledTryKernel(opts.tryKern) + cleanups = append(cleanups, r) + } + + // don't count any calls to SetBootVars made thus far + vbl.SetBootVarsCalls = 0 + + case *bootloadertest.MockBootloader: + // then we need to use the bootenv to set the current kernels + origEnv, err := vbl.GetBootVars("snap_kernel", "snap_try_kernel") + c.Assert(err, IsNil) + m := make(map[string]string, 2) + if opts.kern != nil { + m["snap_kernel"] = opts.kern.Filename() + } else { + m["snap_kernel"] = "" + } + + if opts.tryKern != nil { + m["snap_try_kernel"] = opts.tryKern.Filename() + } else { + m["snap_try_kernel"] = "" + } + + err = vbl.SetBootVars(m) + c.Assert(err, IsNil) + + // don't count any calls to SetBootVars made thus far + vbl.SetBootVarsCalls = 0 + + cleanups = append(cleanups, func() { + err := bl.SetBootVars(origEnv) + c.Assert(err, IsNil) + }) + } + + return func() { + for _, r := range cleanups { + r() + } + } +} + +func (s *bootenvSuite) TestInUseClassic(c *C) { + classicDev := boottest.MockDevice("") + + // make bootloader.Find fail but shouldn't matter + bootloader.ForceError(errors.New("broken bootloader")) + + inUse, err := boot.InUse(snap.TypeBase, classicDev) + c.Assert(err, IsNil) + c.Check(inUse("core18", snap.R(41)), Equals, false) +} + +func (s *bootenvSuite) TestInUseIrrelevantTypes(c *C) { + coreDev := boottest.MockDevice("some-snap") + + // make bootloader.Find fail but shouldn't matter + bootloader.ForceError(errors.New("broken bootloader")) + + inUse, err := boot.InUse(snap.TypeGadget, coreDev) + c.Assert(err, IsNil) + c.Check(inUse("gadget", snap.R(41)), Equals, false) +} + +func (s *bootenvSuite) TestInUse(c *C) { + coreDev := boottest.MockDevice("some-snap") + + 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}, + } { + typ := snap.TypeBase + if t.snapName == "kernel" { + typ = snap.TypeKernel + } + s.bootloader.BootVars[t.bootVarKey] = t.bootVarValue + inUse, err := boot.InUse(typ, coreDev) + c.Assert(err, IsNil) + c.Assert(inUse(t.snapName, t.snapRev), Equals, t.inUse, Commentf("unexpected result: %s %s %v", t.snapName, t.snapRev, t.inUse)) + } +} + +func (s *bootenvSuite) TestInUseEphemeral(c *C) { + coreDev := boottest.MockDevice("some-snap@install") + + // make bootloader.Find fail but shouldn't matter + bootloader.ForceError(errors.New("broken bootloader")) + + inUse, err := boot.InUse(snap.TypeBase, coreDev) + c.Assert(err, IsNil) + c.Check(inUse("whatever", snap.R(0)), Equals, true) +} + +func (s *bootenvSuite) TestInUseUnhappy(c *C) { + coreDev := boottest.MockDevice("some-snap") + + // make GetVars fail + s.bootloader.GetErr = errors.New("zap") + _, err := boot.InUse(snap.TypeKernel, coreDev) + c.Check(err, ErrorMatches, `cannot get boot variables: zap`) + + // make bootloader.Find fail + bootloader.ForceError(errors.New("broken bootloader")) + _, err = boot.InUse(snap.TypeKernel, coreDev) + c.Check(err, ErrorMatches, `cannot get boot settings: broken bootloader`) +} + +func (s *bootenvSuite) TestCurrentBootNameAndRevision(c *C) { + coreDev := boottest.MockDevice("some-snap") + + s.bootloader.BootVars["snap_core"] = "core_2.snap" + s.bootloader.BootVars["snap_kernel"] = "canonical-pc-linux_2.snap" + + current, err := boot.GetCurrentBoot(snap.TypeOS, coreDev) + c.Check(err, IsNil) + c.Check(current.SnapName(), Equals, "core") + c.Check(current.SnapRevision(), Equals, snap.R(2)) + + current, err = boot.GetCurrentBoot(snap.TypeKernel, coreDev) + c.Check(err, IsNil) + c.Check(current.SnapName(), Equals, "canonical-pc-linux") + c.Check(current.SnapRevision(), Equals, snap.R(2)) + + s.bootloader.BootVars["snap_mode"] = boot.TryingStatus + _, err = boot.GetCurrentBoot(snap.TypeKernel, coreDev) + c.Check(err, Equals, boot.ErrBootNameAndRevisionNotReady) +} + +func (s *bootenv20Suite) TestCurrentBoot20NameAndRevision(c *C) { + coreDev := boottest.MockUC20Device("some-snap") + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + + current, err := boot.GetCurrentBoot(snap.TypeBase, coreDev) + c.Check(err, IsNil) + c.Check(current.SnapName(), Equals, s.base1.SnapName()) + c.Check(current.SnapRevision(), Equals, snap.R(1)) + + current, err = boot.GetCurrentBoot(snap.TypeKernel, coreDev) + c.Check(err, IsNil) + c.Check(current.SnapName(), Equals, s.kern1.SnapName()) + c.Check(current.SnapRevision(), Equals, snap.R(1)) + + s.bootloader.BootVars["kernel_status"] = boot.TryingStatus + _, err = boot.GetCurrentBoot(snap.TypeKernel, coreDev) + c.Check(err, Equals, boot.ErrBootNameAndRevisionNotReady) +} + +// only difference between this test and TestCurrentBoot20NameAndRevision is the +// base bootloader which doesn't support ExtractedRunKernelImageBootloader. +func (s *bootenv20EnvRefKernelSuite) TestCurrentBoot20NameAndRevision(c *C) { + coreDev := boottest.MockUC20Device("some-snap") + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + + current, err := boot.GetCurrentBoot(snap.TypeKernel, coreDev) + c.Assert(err, IsNil) + c.Assert(current.SnapName(), Equals, s.kern1.SnapName()) + c.Assert(current.SnapRevision(), Equals, snap.R(1)) +} + +func (s *bootenvSuite) TestCurrentBootNameAndRevisionUnhappy(c *C) { + coreDev := boottest.MockDevice("some-snap") + + _, err := boot.GetCurrentBoot(snap.TypeKernel, coreDev) + c.Check(err, ErrorMatches, `cannot get name and revision of kernel \(snap_kernel\): boot variable unset`) + + _, err = boot.GetCurrentBoot(snap.TypeOS, coreDev) + c.Check(err, ErrorMatches, `cannot get name and revision of boot base \(snap_core\): boot variable unset`) + + _, err = boot.GetCurrentBoot(snap.TypeBase, coreDev) + c.Check(err, ErrorMatches, `cannot get name and revision of boot base \(snap_core\): boot variable unset`) + + _, err = boot.GetCurrentBoot(snap.TypeApp, coreDev) + c.Check(err, ErrorMatches, `internal error: no boot state handling for snap type "app"`) + + // sanity check + s.bootloader.BootVars["snap_kernel"] = "kernel_41.snap" + current, err := boot.GetCurrentBoot(snap.TypeKernel, coreDev) + c.Check(err, IsNil) + c.Check(current.SnapName(), Equals, "kernel") + c.Check(current.SnapRevision(), Equals, snap.R(41)) + + // make GetVars fail + s.bootloader.GetErr = errors.New("zap") + _, err = boot.GetCurrentBoot(snap.TypeKernel, coreDev) + 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, coreDev) + c.Check(err, ErrorMatches, "cannot get boot settings: broken bootloader") +} + +func (s *bootenvSuite) TestParticipant(c *C) { + info := &snap.Info{} + info.RealName = "some-snap" + + coreDev := boottest.MockDevice("some-snap") + classicDev := boottest.MockDevice("") + + bp := boot.Participant(info, snap.TypeApp, coreDev) + c.Check(bp.IsTrivial(), Equals, true) + + for _, typ := range []snap.Type{ + snap.TypeKernel, + snap.TypeOS, + snap.TypeBase, + } { + bp = boot.Participant(info, typ, classicDev) + c.Check(bp.IsTrivial(), Equals, true) + + bp = boot.Participant(info, typ, coreDev) + c.Check(bp.IsTrivial(), Equals, false) + + c.Check(bp, DeepEquals, boot.NewCoreBootParticipant(info, typ, coreDev)) + } +} + +func (s *bootenvSuite) 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 string + nop bool + } + + table := []tableT{ + { + with: core, + model: "", + nop: true, + }, { + 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, + }, + { + with: core18, + model: "core18@install", + nop: true, + }, + { + with: core, + model: "core@install", + nop: true, + }, + } + + for i, t := range table { + dev := boottest.MockDevice(t.model) + bp := boot.Participant(t.with, t.with.GetType(), dev) + c.Check(bp.IsTrivial(), Equals, t.nop, Commentf("%d", i)) + if !t.nop { + c.Check(bp, DeepEquals, boot.NewCoreBootParticipant(t.with, t.with.GetType(), dev)) + } + } +} + +func (s *bootenvSuite) TestKernelWithModel(c *C) { + info := &snap.Info{} + info.RealName = "kernel" + + type tableT struct { + model string + nop bool + krn boot.BootKernel + } + + table := []tableT{ + { + model: "other-kernel", + nop: true, + krn: boot.Trivial{}, + }, { + model: "kernel", + nop: false, + krn: boot.NewCoreKernel(info, boottest.MockDevice("kernel")), + }, { + model: "", + nop: true, + krn: boot.Trivial{}, + }, { + model: "kernel@install", + nop: true, + krn: boot.Trivial{}, + }, + } + + for _, t := range table { + dev := boottest.MockDevice(t.model) + krn := boot.Kernel(info, snap.TypeKernel, dev) + c.Check(krn.IsTrivial(), Equals, t.nop) + c.Check(krn, DeepEquals, t.krn) + } +} + +func (s *bootenv20Suite) TestCoreKernel20(c *C) { + coreDev := boottest.MockUC20Device("pc-kernel") + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + + // get the boot kernel from our kernel snap + bootKern := boot.Kernel(s.kern1, snap.TypeKernel, coreDev) + // can't use FitsTypeOf with coreKernel here, cause that causes an import + // loop as boottest imports boot and coreKernel is unexported + c.Assert(bootKern.IsTrivial(), Equals, false) + + // extract the kernel assets from the coreKernel + // the container here doesn't really matter since it's just being passed + // to the mock bootloader method anyways + kernelContainer := snaptest.MockContainer(c, nil) + err := bootKern.ExtractKernelAssets(kernelContainer) + c.Assert(err, IsNil) + + // make sure that the bootloader was told to extract some assets + c.Assert(s.bootloader.ExtractKernelAssetsCalls, DeepEquals, []snap.PlaceInfo{s.kern1}) + + // now remove the kernel assets and ensure that we get those calls + err = bootKern.RemoveKernelAssets() + c.Assert(err, IsNil) + + // make sure that the bootloader was told to remove assets + c.Assert(s.bootloader.RemoveKernelAssetsCalls, DeepEquals, []snap.PlaceInfo{s.kern1}) +} + +func (s *bootenv20Suite) TestCoreParticipant20SetNextSameKernelSnap(c *C) { + coreDev := boottest.MockUC20Device("pc-kernel") + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + + // get the boot kernel participant from our kernel snap + bootKern := boot.Participant(s.kern1, snap.TypeKernel, coreDev) + + // make sure it's not a trivial boot participant + c.Assert(bootKern.IsTrivial(), Equals, false) + + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot() + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, false) + + // make sure that the bootloader was asked for the current kernel + _, nKernelCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("Kernel") + c.Assert(nKernelCalls, Equals, 1) + + // ensure that kernel_status is still empty + c.Assert(s.bootloader.BootVars["kernel_status"], Equals, boot.DefaultStatus) + + // there was no attempt to enable a kernel + _, enableKernelCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableTryKernel") + c.Assert(enableKernelCalls, Equals, 0) + + // the modeenv is still the same as well + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) + + // finally we didn't call SetBootVars on the bootloader because nothing + // changed + c.Assert(s.bootloader.SetBootVarsCalls, Equals, 0) +} + +func (s *bootenv20EnvRefKernelSuite) TestCoreParticipant20SetNextSameKernelSnap(c *C) { + coreDev := boottest.MockUC20Device("pc-kernel") + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + + // get the boot kernel participant from our kernel snap + bootKern := boot.Participant(s.kern1, snap.TypeKernel, coreDev) + + // make sure it's not a trivial boot participant + c.Assert(bootKern.IsTrivial(), Equals, false) + + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot() + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, false) + + // ensure that bootenv is unchanged + m, err := s.bootloader.GetBootVars("kernel_status", "snap_kernel", "snap_try_kernel") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, map[string]string{ + "kernel_status": boot.DefaultStatus, + "snap_kernel": s.kern1.Filename(), + "snap_try_kernel": "", + }) + + // the modeenv is still the same as well + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) + + // finally we didn't call SetBootVars on the bootloader because nothing + // changed + c.Assert(s.bootloader.SetBootVarsCalls, Equals, 0) +} + +func (s *bootenv20Suite) TestCoreParticipant20SetNextNewKernelSnap(c *C) { + coreDev := boottest.MockUC20Device("pc-kernel") + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + + // get the boot kernel participant from our new kernel snap + bootKern := boot.Participant(s.kern2, snap.TypeKernel, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootKern.IsTrivial(), Equals, false) + + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot() + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, true) + + // make sure that the bootloader was asked for the current kernel + _, nKernelCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("Kernel") + c.Assert(nKernelCalls, Equals, 1) + + // ensure that kernel_status is now try + c.Assert(s.bootloader.BootVars["kernel_status"], Equals, boot.TryStatus) + + // and we were asked to enable kernel2 as the try kernel + actual, _ := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableTryKernel") + c.Assert(actual, DeepEquals, []snap.PlaceInfo{s.kern2}) + + // and that the modeenv now has this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename(), s.kern2.Filename()}) +} + +func (s *bootenv20EnvRefKernelSuite) TestCoreParticipant20SetNextNewKernelSnap(c *C) { + coreDev := boottest.MockUC20Device("pc-kernel") + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + + // get the boot kernel participant from our new kernel snap + bootKern := boot.Participant(s.kern2, snap.TypeKernel, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootKern.IsTrivial(), Equals, false) + + // make the kernel used on next boot + rebootRequired, err := bootKern.SetNextBoot() + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, true) + + // make sure the env was updated + m := s.bootloader.BootVars + c.Assert(m, DeepEquals, map[string]string{ + "kernel_status": boot.TryStatus, + "snap_kernel": s.kern1.Filename(), + "snap_try_kernel": s.kern2.Filename(), + }) + + // and that the modeenv now has this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename(), s.kern2.Filename()}) +} + +func (s *bootenv20Suite) TestMarkBootSuccessful20KernelStatusTryingNoKernelSnapCleansUp(c *C) { + coreDev := boottest.MockUC20Device("some-snap") + c.Assert(coreDev.HasModeenv(), Equals, true) + + // set all the same vars as if we were doing trying, except don't set a try + // kernel + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename(), s.kern2.Filename()}, + }, + kern: s.kern1, + // no try-kernel + kernStatus: boot.TryingStatus, + }, + ) + defer r() + + // mark successful + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check that the bootloader variable was cleaned + expected := map[string]string{"kernel_status": boot.DefaultStatus} + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // check that MarkBootSuccessful didn't enable a kernel (since there was no + // try kernel) + _, nEnableCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") + c.Assert(nEnableCalls, Equals, 0) + + // we will always end up disabling a try-kernel though as cleanup + _, nDisableTryCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("DisableTryKernel") + c.Assert(nDisableTryCalls, Equals, 1) + + // do it again, verify it's still okay + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // no new enabled kernels + _, nEnableCalls = s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") + c.Assert(nEnableCalls, Equals, 0) + + // again we will try to cleanup any leftover try-kernels + _, nDisableTryCalls = s.bootloader.GetRunKernelImageFunctionSnapCalls("DisableTryKernel") + c.Assert(nDisableTryCalls, Equals, 2) + + // check that the modeenv re-wrote the CurrentKernels + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) +} + +func (s *bootenv20EnvRefKernelSuite) TestMarkBootSuccessful20KernelStatusTryingNoKernelSnapCleansUp(c *C) { + coreDev := boottest.MockUC20Device("some-snap") + c.Assert(coreDev.HasModeenv(), Equals, true) + + // set all the same vars as if we were doing trying, except don't set a try + // kernel + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename(), s.kern2.Filename()}, + }, + kern: s.kern1, + // no try-kernel + kernStatus: boot.TryingStatus, + }, + ) + defer r() + + // mark successful + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // make sure the env was updated + expected := map[string]string{ + "kernel_status": boot.DefaultStatus, + "snap_kernel": s.kern1.Filename(), + "snap_try_kernel": "", + } + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // do it again, verify it's still okay + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // check that the modeenv re-wrote the CurrentKernels + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) +} + +func (s *bootenv20Suite) TestMarkBootSuccessful20BaseStatusTryingNoTryBaseSnapCleansUp(c *C) { + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + // no TryBase set + BaseStatus: boot.TryingStatus, + } + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: m, + // no kernel setup necessary + }, + ) + defer r() + + coreDev := boottest.MockUC20Device("core20") + c.Assert(coreDev.HasModeenv(), Equals, true) + + // mark successful + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check that the modeenv base_status was re-written to default + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.BaseStatus, Equals, boot.DefaultStatus) + c.Assert(m2.Base, Equals, m.Base) + c.Assert(m2.TryBase, Equals, m.TryBase) + + // do it again, verify it's still okay + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + m3, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m3.BaseStatus, Equals, boot.DefaultStatus) + c.Assert(m3.Base, Equals, m.Base) + c.Assert(m3.TryBase, Equals, m.TryBase) +} + +func (s *bootenv20Suite) TestCoreParticipant20SetNextSameBaseSnap(c *C) { + coreDev := boottest.MockUC20Device("core20") + c.Assert(coreDev.HasModeenv(), Equals, true) + + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + } + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: m, + // no kernel setup necessary + }, + ) + defer r() + + // get the boot base participant from our base snap + bootBase := boot.Participant(s.base1, snap.TypeBase, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootBase.IsTrivial(), Equals, false) + + // make the base used on next boot + rebootRequired, err := bootBase.SetNextBoot() + c.Assert(err, IsNil) + + // we don't need to reboot because it's the same base snap + c.Assert(rebootRequired, Equals, false) + + // make sure the modeenv wasn't changed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.Base, Equals, m.Base) + c.Assert(m2.BaseStatus, Equals, m.BaseStatus) + c.Assert(m2.TryBase, Equals, m.TryBase) +} + +func (s *bootenv20Suite) TestCoreParticipant20SetNextNewBaseSnap(c *C) { + coreDev := boottest.MockUC20Device("core20") + c.Assert(coreDev.HasModeenv(), Equals, true) + + // default state + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + } + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: m, + // no kernel setup necessary + }, + ) + defer r() + + // get the boot base participant from our new base snap + bootBase := boot.Participant(s.base2, snap.TypeBase, coreDev) + // make sure it's not a trivial boot participant + c.Assert(bootBase.IsTrivial(), Equals, false) + + // make the base used on next boot + rebootRequired, err := bootBase.SetNextBoot() + c.Assert(err, IsNil) + c.Assert(rebootRequired, Equals, true) + + // make sure the modeenv was updated + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.Base, Equals, m.Base) + c.Assert(m2.BaseStatus, Equals, boot.TryStatus) + c.Assert(m2.TryBase, Equals, s.base2.Filename()) +} + +func (s *bootenvSuite) TestMarkBootSuccessfulAllSnap(c *C) { + coreDev := boottest.MockDevice("some-snap") + + s.bootloader.BootVars["snap_mode"] = boot.TryingStatus + s.bootloader.BootVars["snap_try_core"] = "os1" + s.bootloader.BootVars["snap_try_kernel"] = "k1" + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + expected := map[string]string{ + // cleared + "snap_mode": boot.DefaultStatus, + "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(coreDev) + c.Assert(err, IsNil) + c.Assert(s.bootloader.BootVars, DeepEquals, expected) +} + +func (s *bootenv20Suite) TestMarkBootSuccessful20AllSnap(c *C) { + coreDev := boottest.MockUC20Device("some-snap") + c.Assert(coreDev.HasModeenv(), Equals, true) + + // bonus points: we were trying both a base snap and a kernel snap + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + TryBase: s.base2.Filename(), + BaseStatus: boot.TryingStatus, + CurrentKernels: []string{s.kern1.Filename(), s.kern2.Filename()}, + } + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.kern1, + tryKern: s.kern2, + kernStatus: boot.TryingStatus, + }, + ) + defer r() + + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check the bootloader variables + expected := map[string]string{ + // cleared + "kernel_status": boot.DefaultStatus, + } + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // check that we called EnableKernel() on the try-kernel + actual, _ := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") + c.Assert(actual, DeepEquals, []snap.PlaceInfo{s.kern2}) + + // and that we disabled a try kernel + _, nDisableTryCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("DisableTryKernel") + c.Assert(nDisableTryCalls, Equals, 1) + + // also check that the modeenv was updated + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.Base, Equals, s.base2.Filename()) + c.Assert(m2.TryBase, Equals, "") + c.Assert(m2.BaseStatus, Equals, boot.DefaultStatus) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern2.Filename()}) + + // do it again, verify its still valid + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // no new enabled kernels + actual, _ = s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") + c.Assert(actual, DeepEquals, []snap.PlaceInfo{s.kern2}) + // we always disable the try kernel as a cleanup operation, so there's one + // more call here + _, nDisableTryCalls = s.bootloader.GetRunKernelImageFunctionSnapCalls("DisableTryKernel") + c.Assert(nDisableTryCalls, Equals, 2) +} + +func (s *bootenv20EnvRefKernelSuite) TestMarkBootSuccessful20AllSnap(c *C) { + coreDev := boottest.MockUC20Device("some-snap") + c.Assert(coreDev.HasModeenv(), Equals, true) + + // bonus points: we were trying both a base snap and a kernel snap + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + TryBase: s.base2.Filename(), + BaseStatus: boot.TryingStatus, + CurrentKernels: []string{s.kern1.Filename(), s.kern2.Filename()}, + } + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.kern1, + tryKern: s.kern2, + kernStatus: boot.TryingStatus, + }, + ) + defer r() + + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check the bootloader variables + expected := map[string]string{ + // cleared + "kernel_status": boot.DefaultStatus, + "snap_try_kernel": "", + // enabled new kernel + "snap_kernel": s.kern2.Filename(), + } + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // also check that the modeenv was updated + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.Base, Equals, s.base2.Filename()) + c.Assert(m2.TryBase, Equals, "") + c.Assert(m2.BaseStatus, Equals, boot.DefaultStatus) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern2.Filename()}) + + // do it again, verify its still valid + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + c.Assert(s.bootloader.BootVars, DeepEquals, expected) +} + +func (s *bootenvSuite) TestMarkBootSuccessfulKernelUpdate(c *C) { + coreDev := boottest.MockDevice("some-snap") + + s.bootloader.BootVars["snap_mode"] = boot.TryingStatus + 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(coreDev) + c.Assert(err, IsNil) + c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + // cleared + "snap_mode": boot.DefaultStatus, + "snap_try_kernel": "", + "snap_try_core": "", + // unchanged + "snap_core": "os1", + // updated + "snap_kernel": "k2", + }) +} + +func (s *bootenvSuite) TestMarkBootSuccessfulBaseUpdate(c *C) { + coreDev := boottest.MockDevice("some-snap") + + s.bootloader.BootVars["snap_mode"] = boot.TryingStatus + s.bootloader.BootVars["snap_core"] = "os1" + s.bootloader.BootVars["snap_kernel"] = "k1" + s.bootloader.BootVars["snap_try_core"] = "os2" + s.bootloader.BootVars["snap_try_kernel"] = "" + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ + // cleared + "snap_mode": boot.DefaultStatus, + "snap_try_core": "", + // unchanged + "snap_kernel": "k1", + "snap_try_kernel": "", + // updated + "snap_core": "os2", + }) +} + +func (s *bootenv20Suite) TestMarkBootSuccessful20KernelUpdate(c *C) { + // trying a kernel snap + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename(), s.kern2.Filename()}, + } + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.kern1, + tryKern: s.kern2, + kernStatus: boot.TryingStatus, + }, + ) + defer r() + + coreDev := boottest.MockUC20Device("some-snap") + c.Assert(coreDev.HasModeenv(), Equals, true) + + // mark successful + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check the bootloader variables + expected := map[string]string{"kernel_status": boot.DefaultStatus} + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // check that MarkBootSuccessful enabled the try kernel + actual, _ := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") + c.Assert(actual, DeepEquals, []snap.PlaceInfo{s.kern2}) + + // and that we disabled a try kernel + _, nDisableTryCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("DisableTryKernel") + c.Assert(nDisableTryCalls, Equals, 1) + + // check that the new kernel is the only one in modeenv + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern2.Filename()}) + + // do it again, verify its still valid + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // no new bootloader calls + actual, _ = s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") + c.Assert(actual, DeepEquals, []snap.PlaceInfo{s.kern2}) + + // we did disable the kernel again because we always do this to cleanup in + // case there were leftovers + _, nDisableTryCalls = s.bootloader.GetRunKernelImageFunctionSnapCalls("DisableTryKernel") + c.Assert(nDisableTryCalls, Equals, 2) +} + +func (s *bootenv20EnvRefKernelSuite) TestMarkBootSuccessful20KernelUpdate(c *C) { + // trying a kernel snap + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename(), s.kern2.Filename()}, + } + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.kern1, + tryKern: s.kern2, + kernStatus: boot.TryingStatus, + }, + ) + defer r() + + coreDev := boottest.MockUC20Device("some-snap") + c.Assert(coreDev.HasModeenv(), Equals, true) + + // mark successful + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check the bootloader variables + expected := map[string]string{ + "kernel_status": boot.DefaultStatus, + "snap_kernel": s.kern2.Filename(), + "snap_try_kernel": "", + } + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + + // check that the new kernel is the only one in modeenv + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern2.Filename()}) + + // do it again, verify its still valid + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + c.Assert(s.bootloader.BootVars, DeepEquals, expected) + c.Assert(s.bootloader.BootVars, DeepEquals, expected) +} + +func (s *bootenv20Suite) TestMarkBootSuccessful20BaseUpdate(c *C) { + // we were trying a base snap + m := &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + TryBase: s.base2.Filename(), + BaseStatus: boot.TryingStatus, + CurrentKernels: []string{s.kern1.Filename()}, + } + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: m, + kern: s.kern1, + kernStatus: boot.DefaultStatus, + }, + ) + defer r() + + coreDev := boottest.MockUC20Device("some-snap") + c.Assert(coreDev.HasModeenv(), Equals, true) + + // mark successful + err := boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check the modeenv + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.Base, Equals, s.base2.Filename()) + c.Assert(m2.TryBase, Equals, "") + c.Assert(m2.BaseStatus, Equals, "") + + // do it again, verify its still valid + err = boot.MarkBootSuccessful(coreDev) + c.Assert(err, IsNil) + + // check the modeenv again + m3, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m3.Base, Equals, s.base2.Filename()) + c.Assert(m3.TryBase, Equals, "") + c.Assert(m3.BaseStatus, Equals, "") +} + +type recoveryBootenv20Suite struct { + baseBootenvSuite + + bootloader *bootloadertest.MockBootloader + + dev boot.Device +} + +var _ = Suite(&recoveryBootenv20Suite{}) + +func (s *recoveryBootenv20Suite) SetUpTest(c *C) { + s.baseBootenvSuite.SetUpTest(c) + + s.bootloader = bootloadertest.Mock("mock", c.MkDir()) + s.forceBootloader(s.bootloader) + + s.dev = boottest.MockUC20Device("some-snap") +} + +func (s *recoveryBootenv20Suite) TestSetRecoveryBootSystemAndModeHappy(c *C) { + err := boot.SetRecoveryBootSystemAndMode(s.dev, "1234", "install") + c.Assert(err, IsNil) + c.Check(s.bootloader.BootVars, DeepEquals, map[string]string{ + "snapd_recovery_system": "1234", + "snapd_recovery_mode": "install", + }) +} + +func (s *recoveryBootenv20Suite) TestSetRecoveryBootSystemAndModeSetErr(c *C) { + s.bootloader.SetErr = errors.New("no can do") + err := boot.SetRecoveryBootSystemAndMode(s.dev, "1234", "install") + c.Assert(err, ErrorMatches, `no can do`) +} + +func (s *recoveryBootenv20Suite) TestSetRecoveryBootSystemAndModeNonUC20(c *C) { + non20Dev := boottest.MockDevice("some-snap") + err := boot.SetRecoveryBootSystemAndMode(non20Dev, "1234", "install") + c.Assert(err, Equals, boot.ErrUnsupportedSystemMode) +} + +func (s *recoveryBootenv20Suite) TestSetRecoveryBootSystemAndModeErrClumsy(c *C) { + err := boot.SetRecoveryBootSystemAndMode(s.dev, "", "install") + c.Assert(err, ErrorMatches, "internal error: system label is unset") + err = boot.SetRecoveryBootSystemAndMode(s.dev, "1234", "") + c.Assert(err, ErrorMatches, "internal error: system mode is unset") +} + +func (s *recoveryBootenv20Suite) TestSetRecoveryBootSystemAndModeRealHappy(c *C) { + bootloader.Force(nil) + + mockSeedGrubDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI", "ubuntu") + err := os.MkdirAll(mockSeedGrubDir, 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(mockSeedGrubDir, "grub.cfg"), nil, 0644) + c.Assert(err, IsNil) + + err = boot.SetRecoveryBootSystemAndMode(s.dev, "1234", "install") + c.Assert(err, IsNil) + + bl, err := bootloader.Find(boot.InitramfsUbuntuSeedDir, &bootloader.Options{Recovery: true}) + c.Assert(err, IsNil) + + blvars, err := bl.GetBootVars("snapd_recovery_mode", "snapd_recovery_system") + c.Assert(err, IsNil) + c.Check(blvars, DeepEquals, map[string]string{ + "snapd_recovery_system": "1234", + "snapd_recovery_mode": "install", + }) +} diff -Nru snapd-2.37.4ubuntu0.1/boot/cmdline.go snapd-2.45.1ubuntu0.2/boot/cmdline.go --- snapd-2.37.4ubuntu0.1/boot/cmdline.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/cmdline.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,110 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package boot + +import ( + "bufio" + "bytes" + "fmt" + "io/ioutil" + "strings" + + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/strutil" +) + +const ( + // ModeRun indicates the regular operating system mode of the device. + ModeRun = "run" + // ModeInstall is a mode in which a new system is installed on the + // device. + ModeInstall = "install" + // ModeRecover is a mode in which the device boots into the recovery + // system. + ModeRecover = "recover" +) + +var ( + // the kernel commandline - can be overridden in tests + procCmdline = "/proc/cmdline" + + validModes = []string{ModeInstall, ModeRecover, ModeRun} +) + +func whichModeAndRecoverySystem(cmdline []byte) (mode string, sysLabel string, err error) { + scanner := bufio.NewScanner(bytes.NewBuffer(cmdline)) + scanner.Split(bufio.ScanWords) + + for scanner.Scan() { + w := scanner.Text() + if strings.HasPrefix(w, "snapd_recovery_mode=") { + if mode != "" { + return "", "", fmt.Errorf("cannot specify mode more than once") + } + mode = strings.SplitN(w, "=", 2)[1] + if mode == "" { + mode = ModeInstall + } + if !strutil.ListContains(validModes, mode) { + return "", "", fmt.Errorf("cannot use unknown mode %q", mode) + } + } + if strings.HasPrefix(w, "snapd_recovery_system=") { + if sysLabel != "" { + return "", "", fmt.Errorf("cannot specify recovery system label more than once") + } + sysLabel = strings.SplitN(w, "=", 2)[1] + } + } + if err := scanner.Err(); err != nil { + return "", "", err + } + switch { + case mode == "" && sysLabel == "": + return "", "", fmt.Errorf("cannot detect mode nor recovery system to use") + case mode == ModeInstall && sysLabel == "": + return "", "", fmt.Errorf("cannot specify install mode without system label") + case mode == ModeRun && sysLabel != "": + // XXX: should we silently ignore the label? at least log for now + logger.Noticef(`ignoring recovery system label %q in "run" mode`, sysLabel) + sysLabel = "" + } + return mode, sysLabel, nil +} + +// ModeAndRecoverySystemFromKernelCommandLine returns the current system mode +// and the recovery system label as passed in the kernel command line by the +// bootloader. +func ModeAndRecoverySystemFromKernelCommandLine() (mode, sysLabel string, err error) { + cmdline, err := ioutil.ReadFile(procCmdline) + if err != nil { + return "", "", err + } + return whichModeAndRecoverySystem(cmdline) +} + +// MockProcCmdline overrides the path to /proc/cmdline. For use in tests. +func MockProcCmdline(newPath string) (restore func()) { + oldProcCmdline := procCmdline + procCmdline = newPath + return func() { + procCmdline = oldProcCmdline + } +} diff -Nru snapd-2.37.4ubuntu0.1/boot/cmdline_test.go snapd-2.45.1ubuntu0.2/boot/cmdline_test.go --- snapd-2.37.4ubuntu0.1/boot/cmdline_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/cmdline_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,105 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package boot_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/testutil" +) + +var _ = Suite(&kernelCommandLineSuite{}) + +// baseBootSuite is used to setup the common test environment +type kernelCommandLineSuite struct { + testutil.BaseTest + rootDir string +} + +func (s *kernelCommandLineSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + s.rootDir = c.MkDir() + + err := os.MkdirAll(filepath.Join(s.rootDir, "proc"), 0755) + c.Assert(err, IsNil) + restore := boot.MockProcCmdline(filepath.Join(s.rootDir, "proc/cmdline")) + s.AddCleanup(restore) +} + +func (s *kernelCommandLineSuite) mockProcCmdlineContent(c *C, newContent string) { + mockProcCmdline := filepath.Join(s.rootDir, "proc/cmdline") + err := ioutil.WriteFile(mockProcCmdline, []byte(newContent), 0644) + c.Assert(err, IsNil) +} + +func (s *kernelCommandLineSuite) TestModeAndLabel(c *C) { + for _, tc := range []struct { + cmd string + mode string + label string + err string + }{{ + cmd: "snapd_recovery_mode= snapd_recovery_system=this-is-a-label other-option=foo", + mode: boot.ModeInstall, + label: "this-is-a-label", + }, { + cmd: "snapd_recovery_system=label foo=bar foobaz=\\0\\0123 snapd_recovery_mode=install", + label: "label", + mode: boot.ModeInstall, + }, { + cmd: "snapd_recovery_mode=run snapd_recovery_system=1234", + mode: boot.ModeRun, + }, { + cmd: "option=1 other-option=\0123 none", + err: "cannot detect mode nor recovery system to use", + }, { + cmd: "snapd_recovery_mode=install-foo", + err: `cannot use unknown mode "install-foo"`, + }, { + // no recovery system label + cmd: "snapd_recovery_mode=install foo=bar", + err: `cannot specify install mode without system label`, + }, { + // boot scripts couldn't decide on mode + cmd: "snapd_recovery_mode=install snapd_recovery_system=1234 snapd_recovery_mode=run", + err: "cannot specify mode more than once", + }, { + // boot scripts couldn't decide which system to use + cmd: "snapd_recovery_system=not-this-one snapd_recovery_mode=install snapd_recovery_system=1234", + err: "cannot specify recovery system label more than once", + }} { + c.Logf("tc: %q", tc) + s.mockProcCmdlineContent(c, tc.cmd) + + mode, label, err := boot.ModeAndRecoverySystemFromKernelCommandLine() + if tc.err == "" { + c.Assert(err, IsNil) + c.Check(mode, Equals, tc.mode) + c.Check(label, Equals, tc.label) + } else { + c.Assert(err, ErrorMatches, tc.err) + } + } +} diff -Nru snapd-2.37.4ubuntu0.1/boot/debug.go snapd-2.45.1ubuntu0.2/boot/debug.go --- snapd-2.37.4ubuntu0.1/boot/debug.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/debug.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,58 @@ +// -*- 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 ( + "fmt" + "io" + + "github.com/snapcore/snapd/bootloader" +) + +// DumpBootVars writes a dump of the snapd bootvars to the given writer +func DumpBootVars(w io.Writer, dir string, uc20 bool) error { + bloader, err := bootloader.Find(dir, nil) + if err != nil { + return err + } + var allKeys []string + if uc20 { + // TODO:UC20: what about snapd_recovery_kernel, snapd_recovery_mode, and + // snapd_recovery_system? + allKeys = []string{"kernel_status"} + } else { + allKeys = []string{ + "snap_mode", + "snap_core", + "snap_try_core", + "snap_kernel", + "snap_try_kernel", + } + } + + bootVars, err := bloader.GetBootVars(allKeys...) + if err != nil { + return err + } + for _, k := range allKeys { + fmt.Fprintf(w, "%s=%s\n", k, bootVars[k]) + } + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/boot/export_test.go snapd-2.45.1ubuntu0.2/boot/export_test.go --- snapd-2.37.4ubuntu0.1/boot/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/export_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,42 @@ +// -*- 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" +) + +func NewCoreBootParticipant(s snap.PlaceInfo, t snap.Type, dev Device) *coreBootParticipant { + bs, err := bootStateFor(t, dev) + if err != nil { + panic(err) + } + return &coreBootParticipant{s: s, bs: bs} +} + +func NewCoreKernel(s snap.PlaceInfo, d Device) *coreKernel { + return &coreKernel{s, bootloaderOptionsForDeviceKernel(d)} +} + +type Trivial = trivial + +func (m *Modeenv) WasRead() bool { + return m.read +} diff -Nru snapd-2.37.4ubuntu0.1/boot/initramfs20dirs.go snapd-2.45.1ubuntu0.2/boot/initramfs20dirs.go --- snapd-2.37.4ubuntu0.1/boot/initramfs20dirs.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/initramfs20dirs.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,80 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package boot + +import ( + "path/filepath" + + "github.com/snapcore/snapd/dirs" +) + +var ( + // InitramfsRunMntDir is the directory where ubuntu partitions are mounted + // during the initramfs. + InitramfsRunMntDir string + + // InitramfsDataDir is the location of system-data role partition + // (typically a partition labeled "ubuntu-data") during the initramfs. + InitramfsDataDir string + + // InitramfsHostUbuntuDataDir is the location of the host ubuntu-data + // during the initramfs, typically used in recover mode. + InitramfsHostUbuntuDataDir string + + // InitramfsUbuntuBootDir is the location of ubuntu-boot during the + // initramfs. + InitramfsUbuntuBootDir string + + // InitramfsUbuntuSeedDir is the location of ubuntu-seed during the + // initramfs. + InitramfsUbuntuSeedDir string + + // InitramfsWritableDir is the location of the writable partition during the + // initramfs. Note that this may refer to a temporary filesystem or a + // physical partition depending on what system mode the system is in. + InitramfsWritableDir string + + // InstallHostWritableDir is the location of the writable partition of the + // installed host during install mode. This should always be on a physical + // partition. + InstallHostWritableDir string + + // InitramfsEncryptionKeyDir is the location of the encrypted partition keys + // during the initramfs. + InitramfsEncryptionKeyDir string +) + +func setInitramfsDirVars(rootdir string) { + InitramfsRunMntDir = filepath.Join(rootdir, "run/mnt") + InitramfsDataDir = filepath.Join(InitramfsRunMntDir, "data") + InitramfsHostUbuntuDataDir = filepath.Join(InitramfsRunMntDir, "host", "ubuntu-data") + InitramfsUbuntuBootDir = filepath.Join(InitramfsRunMntDir, "ubuntu-boot") + InitramfsUbuntuSeedDir = filepath.Join(InitramfsRunMntDir, "ubuntu-seed") + InstallHostWritableDir = filepath.Join(InitramfsRunMntDir, "ubuntu-data", "system-data") + InitramfsWritableDir = filepath.Join(InitramfsDataDir, "system-data") + InitramfsEncryptionKeyDir = filepath.Join(InitramfsUbuntuSeedDir, "device/fde") +} + +func init() { + setInitramfsDirVars(dirs.GlobalRootDir) + // register to change the values of Initramfs* dir values when the global + // root dir changes + dirs.AddRootDirCallback(setInitramfsDirVars) +} diff -Nru snapd-2.37.4ubuntu0.1/boot/kernel_os.go snapd-2.45.1ubuntu0.2/boot/kernel_os.go --- snapd-2.37.4ubuntu0.1/boot/kernel_os.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/kernel_os.go 2020-06-05 13:13:49.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 @@ -21,190 +21,63 @@ import ( "fmt" - "os" - "path/filepath" - "github.com/snapcore/snapd/logger" - "github.com/snapcore/snapd/partition" - "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/bootloader" "github.com/snapcore/snapd/snap" ) -// RemoveKernelAssets removes the unpacked kernel/initrd for the given -// kernel snap. -func RemoveKernelAssets(s snap.PlaceInfo) error { - bootloader, err := partition.FindBootloader() - if err != nil { - return fmt.Errorf("no not remove kernel assets: %s", err) - } - - // remove the kernel blob - blobName := filepath.Base(s.MountFile()) - dstDir := filepath.Join(bootloader.Dir(), blobName) - if err := os.RemoveAll(dstDir); err != nil { - return err - } - - return nil +type coreBootParticipant struct { + s snap.PlaceInfo + bs bootState } -// 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.Type != snap.TypeKernel { - return fmt.Errorf("cannot extract kernel assets from snap type %q", s.Type) - } +// ensure coreBootParticipant is a BootParticipant +var _ BootParticipant = (*coreBootParticipant)(nil) - bootloader, err := partition.FindBootloader() - if err != nil { - return fmt.Errorf("cannot extract kernel assets: %s", err) - } +func (*coreBootParticipant) IsTrivial() bool { return false } - if bootloader.Name() == "grub" { - return nil - } +func (bp *coreBootParticipant) SetNextBoot() (rebootRequired bool, err error) { + const errPrefix = "cannot set next boot: %s" - // now do the kernel specific bits - blobName := filepath.Base(s.MountFile()) - dstDir := filepath.Join(bootloader.Dir(), blobName) - if err := os.MkdirAll(dstDir, 0755); err != nil { - return err - } - dir, err := os.Open(dstDir) + rebootRequired, u, err := bp.bs.setNext(bp.s) if err != nil { - return err + return false, fmt.Errorf(errPrefix, err) } - defer dir.Close() - for _, src := range []string{"kernel.img", "initrd.img"} { - if err := snapf.Unpack(src, dstDir); err != nil { - return err + if u != nil { + if err := u.commit(); err != nil { + return false, fmt.Errorf(errPrefix, err) } - if err := dir.Sync(); err != nil { - return err - } - } - if err := snapf.Unpack("dtbs/*", dstDir); err != nil { - return err } - - return dir.Sync() + return rebootRequired, nil } -// 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") - } - - if s.Type != snap.TypeOS && s.Type != snap.TypeKernel && s.Type != snap.TypeBase { - return fmt.Errorf("cannot set next boot to snap %q with type %q", s.SnapName(), s.Type) - } - - bootloader, err := partition.FindBootloader() - if err != nil { - return fmt.Errorf("cannot set next boot: %s", err) - } - - var nextBoot, goodBoot string - switch s.Type { - case snap.TypeOS, snap.TypeBase: - nextBoot = "snap_try_core" - goodBoot = "snap_core" - case snap.TypeKernel: - nextBoot = "snap_try_kernel" - goodBoot = "snap_kernel" - } - blobName := filepath.Base(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 - } - if m[goodBoot] == blobName { - // If we were in anything but default ("") mode before - // and now switch to the good core/kernel again, make - // sure to clean the snap_mode here. This also - // mitigates https://forum.snapcraft.io/t/5253 - if m["snap_mode"] != "" { - return bootloader.SetBootVars(map[string]string{ - "snap_mode": "", - nextBoot: "", - }) - } - return nil - } - - return bootloader.SetBootVars(map[string]string{ - nextBoot: blobName, - "snap_mode": "try", - }) +type coreKernel struct { + s snap.PlaceInfo + bopts *bootloader.Options } -// 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.Type != snap.TypeKernel && s.Type != snap.TypeOS && s.Type != snap.TypeBase { - return false - } +// ensure coreKernel is a Kernel +var _ BootKernel = (*coreKernel)(nil) - bootloader, err := partition.FindBootloader() - if err != nil { - logger.Noticef("cannot get boot settings: %s", err) - return false - } - - var nextBoot, goodBoot string - switch s.Type { - case snap.TypeKernel: - nextBoot = "snap_try_kernel" - goodBoot = "snap_kernel" - case snap.TypeOS, snap.TypeBase: - nextBoot = "snap_try_core" - goodBoot = "snap_core" - } +func (*coreKernel) IsTrivial() bool { return false } - m, err := bootloader.GetBootVars(nextBoot, goodBoot) +func (k *coreKernel) RemoveKernelAssets() error { + // XXX: shouldn't we check the snap type? + bootloader, err := bootloader.Find("", k.bopts) if err != nil { - logger.Noticef("cannot get boot variables: %s", err) - return false + return fmt.Errorf("cannot remove kernel assets: %s", err) } - squashfsName := filepath.Base(s.MountFile()) - if m[nextBoot] == squashfsName && m[goodBoot] != m[nextBoot] { - return true - } - - return false + // ask bootloader to remove the kernel assets if needed + return bootloader.RemoveKernelAssets(k.s) } -// InUse checks if the given name/revision is used in the -// boot environment -func InUse(name string, rev snap.Revision) bool { - bootloader, err := partition.FindBootloader() - 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") +func (k *coreKernel) ExtractKernelAssets(snapf snap.Container) error { + bootloader, err := bootloader.Find("", k.bopts) 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 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.37.4ubuntu0.1/boot/kernel_os_test.go snapd-2.45.1ubuntu0.2/boot/kernel_os_test.go --- snapd-2.37.4ubuntu0.1/boot/kernel_os_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/kernel_os_test.go 2020-06-05 13:13:49.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,277 +20,673 @@ package boot_test import ( + "errors" + "io/ioutil" + "os" "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/dirs" "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/partition" - "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapfile" "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 +type: kernel +vendor: Someone +` -type kernelOSSuite struct { - testutil.BaseTest - bootloader *boottest.MockBootloader +func (s *bootenvSuite) TestExtractKernelAssetsError(c *C) { + bootloader.ForceError(errors.New("brkn")) + err := boot.NewCoreKernel(&snap.Info{}, boottest.MockDevice("")).ExtractKernelAssets(nil) + c.Check(err, ErrorMatches, `cannot extract kernel assets: brkn`) } -var _ = Suite(&kernelOSSuite{}) - -func (s *kernelOSSuite) SetUpTest(c *C) { - s.BaseTest.SetUpTest(c) - s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) - dirs.SetRootDir(c.MkDir()) - s.bootloader = boottest.NewMockBootloader("mock", c.MkDir()) - partition.ForceBootloader(s.bootloader) +func (s *bootenvSuite) TestRemoveKernelAssetsError(c *C) { + bootloader.ForceError(errors.New("brkn")) + err := boot.NewCoreKernel(&snap.Info{}, boottest.MockDevice("")).RemoveKernelAssets() + c.Check(err, ErrorMatches, `cannot remove kernel assets: brkn`) } -func (s *kernelOSSuite) TearDownTest(c *C) { - s.BaseTest.TearDownTest(c) - dirs.SetRootDir("") - partition.ForceBootloader(nil) +func (s *bootenvSuite) TestSetNextBootError(c *C) { + coreDev := boottest.MockDevice("some-snap") + + s.bootloader.GetErr = errors.New("zap") + _, err := boot.NewCoreBootParticipant(&snap.Info{}, snap.TypeKernel, coreDev).SetNextBoot() + c.Check(err, ErrorMatches, `cannot set next boot: zap`) + + bootloader.ForceError(errors.New("brkn")) + _, err = boot.NewCoreBootParticipant(&snap.Info{}, snap.TypeKernel, coreDev).SetNextBoot() + c.Check(err, ErrorMatches, `cannot set next boot: brkn`) } -const packageKernel = ` -name: ubuntu-kernel -version: 4.0-1 -type: kernel -vendor: Someone -` +func (s *bootenvSuite) TestSetNextBootForCore(c *C) { + coreDev := boottest.MockDevice("core") -func (s *kernelOSSuite) TestExtractKernelAssetsAndRemove(c *C) { - files := [][]string{ - {"kernel.img", "I'm a kernel"}, - {"initrd.img", "...and I'm an initrd"}, - {"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"}, - } + info := &snap.Info{} + info.SnapType = snap.TypeOS + info.RealName = "core" + info.Revision = snap.R(100) - si := &snap.SideInfo{ - RealName: "ubuntu-kernel", - Revision: snap.R(42), - } - fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) - snapf, err := snap.Open(fn) + bs := boot.NewCoreBootParticipant(info, info.GetType(), coreDev) + reboot, err := bs.SetNextBoot() c.Assert(err, IsNil) - info, err := snap.ReadInfoFromSnapFile(snapf, si) + 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": boot.TryStatus, + }) + + c.Check(reboot, Equals, true) +} + +func (s *bootenvSuite) TestSetNextBootWithBaseForCore(c *C) { + coreDev := boottest.MockDevice("core18") + + info := &snap.Info{} + info.SnapType = snap.TypeBase + info.RealName = "core18" + info.Revision = snap.R(1818) - err = boot.ExtractKernelAssets(info, snapf) + bs := boot.NewCoreBootParticipant(info, info.GetType(), coreDev) + reboot, err := bs.SetNextBoot() c.Assert(err, IsNil) - // this is where the kernel/initrd is unpacked - bootdir := s.bootloader.Dir() + 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": boot.TryStatus, + }) - kernelAssetsDir := filepath.Join(bootdir, "ubuntu-kernel_42.snap") + c.Check(reboot, Equals, true) +} - for _, def := range files { - if def[0] == "meta/kernel.yaml" { - break - } +func (s *bootenvSuite) TestSetNextBootForKernel(c *C) { + coreDev := boottest.MockDevice("krnl") - fullFn := filepath.Join(kernelAssetsDir, def[0]) - c.Check(fullFn, testutil.FileEquals, def[1]) - } + info := &snap.Info{} + info.SnapType = snap.TypeKernel + info.RealName = "krnl" + info.Revision = snap.R(42) - // remove - err = boot.RemoveKernelAssets(info) + bp := boot.NewCoreBootParticipant(info, snap.TypeKernel, coreDev) + reboot, err := bp.SetNextBoot() c.Assert(err, IsNil) - c.Check(osutil.FileExists(kernelAssetsDir), Equals, false) -} + 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", + "snap_mode": boot.TryStatus, + }) -func (s *kernelOSSuite) TestExtractKernelAssetsNoUnpacksKernelForGrub(c *C) { - // pretend to be a grub system - mockGrub := boottest.NewMockBootloader("grub", c.MkDir()) - partition.ForceBootloader(mockGrub) + bootVars := map[string]string{ + "snap_kernel": "krnl_40.snap", + "snap_try_kernel": "krnl_42.snap"} + s.bootloader.SetBootVars(bootVars) + c.Check(reboot, Equals, true) - files := [][]string{ - {"kernel.img", "I'm a kernel"}, - {"initrd.img", "...and I'm an initrd"}, - {"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) + // simulate good boot + bootVars = map[string]string{"snap_kernel": "krnl_42.snap"} + s.bootloader.SetBootVars(bootVars) + + reboot, err = bp.SetNextBoot() c.Assert(err, IsNil) + c.Check(reboot, Equals, false) +} - info, err := snap.ReadInfoFromSnapFile(snapf, si) +func (s *bootenv20Suite) TestSetNextBoot20ForKernel(c *C) { + coreDev := boottest.MockUC20Device("pc-kernel") + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + + bs := boot.NewCoreBootParticipant(s.kern2, snap.TypeKernel, coreDev) + c.Assert(bs.IsTrivial(), Equals, false) + reboot, err := bs.SetNextBoot() c.Assert(err, IsNil) - err = boot.ExtractKernelAssets(info, snapf) + // check that kernel_status is now try + v, err := s.bootloader.GetBootVars("kernel_status") c.Assert(err, IsNil) + c.Assert(v, DeepEquals, map[string]string{ + "kernel_status": boot.TryStatus, + }) - // kernel is *not* here - kernimg := filepath.Join(mockGrub.Dir(), "ubuntu-kernel_42.snap", "kernel.img") - c.Assert(osutil.FileExists(kernimg), Equals, false) -} + c.Check(reboot, Equals, true) -func (s *kernelOSSuite) TestExtractKernelAssetsError(c *C) { - info := &snap.Info{} - info.Type = snap.TypeApp + // check that SetNextBoot enabled kernel2 as a TryKernel + actual, _ := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableTryKernel") + c.Assert(actual, DeepEquals, []snap.PlaceInfo{s.kern2}) + + // also didn't move any try kernels to trusted kernels + actual, _ = s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") + c.Assert(actual, DeepEquals, []snap.PlaceInfo(nil)) + + // check that SetNextBoot asked the bootloader for a kernel + _, nKernelCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("Kernel") + c.Assert(nKernelCalls, Equals, 1) - err := boot.ExtractKernelAssets(info, nil) - c.Assert(err, ErrorMatches, `cannot extract kernel assets from snap type "app"`) + // and that the modeenv now has this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename(), s.kern2.Filename()}) } -// SetNextBoot should do nothing on classic LP: #1580403 -func (s *kernelOSSuite) TestSetNextBootOnClassic(c *C) { - restore := release.MockOnClassic(true) - defer restore() +func (s *bootenv20EnvRefKernelSuite) TestSetNextBoot20ForKernel(c *C) { + coreDev := boottest.MockUC20Device("pc-kernel") + c.Assert(coreDev.HasModeenv(), Equals, true) + + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() + + bs := boot.NewCoreBootParticipant(s.kern2, snap.TypeKernel, coreDev) + c.Assert(bs.IsTrivial(), Equals, false) + reboot, err := bs.SetNextBoot() + c.Assert(err, IsNil) + + m := s.bootloader.BootVars + c.Assert(m, DeepEquals, map[string]string{ + "kernel_status": boot.TryStatus, + "snap_try_kernel": s.kern2.Filename(), + "snap_kernel": s.kern1.Filename(), + }) - // 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") + c.Check(reboot, Equals, true) - c.Assert(s.bootloader.BootVars, HasLen, 0) + // and that the modeenv now has this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename(), s.kern2.Filename()}) } -func (s *kernelOSSuite) TestSetNextBootForCore(c *C) { - restore := release.MockOnClassic(false) - defer restore() +func (s *bootenvSuite) TestSetNextBootForKernelForTheSameKernel(c *C) { + coreDev := boottest.MockDevice("krnl") info := &snap.Info{} - info.Type = snap.TypeOS - info.RealName = "core" - info.Revision = snap.R(100) + info.SnapType = snap.TypeKernel + info.RealName = "krnl" + info.Revision = snap.R(40) + + bootVars := map[string]string{"snap_kernel": "krnl_40.snap"} + s.bootloader.SetBootVars(bootVars) - err := boot.SetNextBoot(info) + reboot, err := boot.NewCoreBootParticipant(info, snap.TypeKernel, coreDev).SetNextBoot() c.Assert(err, IsNil) - c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ - "snap_try_core": "core_100.snap", - "snap_mode": "try", + v, err := s.bootloader.GetBootVars("snap_kernel") + c.Assert(err, IsNil) + c.Assert(v, DeepEquals, map[string]string{ + "snap_kernel": "krnl_40.snap", }) - c.Check(boot.ChangeRequiresReboot(info), Equals, true) + c.Check(reboot, Equals, false) } -func (s *kernelOSSuite) TestSetNextBootWithBaseForCore(c *C) { - restore := release.MockOnClassic(false) - defer restore() +func (s *bootenv20Suite) TestSetNextBoot20ForKernelForTheSameKernel(c *C) { + coreDev := boottest.MockUC20Device("pc-kernel") + c.Assert(coreDev.HasModeenv(), Equals, true) - info := &snap.Info{} - info.Type = snap.TypeBase - info.RealName = "core18" - info.Revision = snap.R(1818) + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() - err := boot.SetNextBoot(info) + bs := boot.NewCoreBootParticipant(s.kern1, snap.TypeKernel, coreDev) + c.Assert(bs.IsTrivial(), Equals, false) + reboot, err := bs.SetNextBoot() c.Assert(err, IsNil) - c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ - "snap_try_core": "core18_1818.snap", - "snap_mode": "try", + // check that kernel_status is cleared + v, err := s.bootloader.GetBootVars("kernel_status") + c.Assert(err, IsNil) + c.Assert(v, DeepEquals, map[string]string{ + "kernel_status": boot.DefaultStatus, }) - c.Check(boot.ChangeRequiresReboot(info), Equals, true) + c.Check(reboot, Equals, false) + + // check that SetNextBoot didn't try to enable any try kernels + actual, _ := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableTryKernel") + c.Assert(actual, HasLen, 0) + + // also didn't move any try kernels to trusted kernels + actual, _ = s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") + c.Assert(actual, HasLen, 0) + + // check that SetNextBoot asked the bootloader for a kernel + _, nKernelCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("Kernel") + c.Assert(nKernelCalls, Equals, 1) + + // and that the modeenv now has this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) } -func (s *kernelOSSuite) TestSetNextBootForKernel(c *C) { - restore := release.MockOnClassic(false) - defer restore() +func (s *bootenv20EnvRefKernelSuite) TestSetNextBoot20ForKernelForTheSameKernel(c *C) { + coreDev := boottest.MockUC20Device("pc-kernel") + c.Assert(coreDev.HasModeenv(), Equals, true) - info := &snap.Info{} - info.Type = snap.TypeKernel - info.RealName = "krnl" - info.Revision = snap.R(42) + r := setupUC20Bootenv( + c, + s.bootloader, + s.normalDefaultState, + ) + defer r() - err := boot.SetNextBoot(info) + bs := boot.NewCoreBootParticipant(s.kern1, snap.TypeKernel, coreDev) + c.Assert(bs.IsTrivial(), Equals, false) + reboot, err := bs.SetNextBoot() c.Assert(err, IsNil) - c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ - "snap_try_kernel": "krnl_42.snap", - "snap_mode": "try", + // check that kernel_status is cleared + m := s.bootloader.BootVars + c.Assert(m, DeepEquals, map[string]string{ + "kernel_status": boot.DefaultStatus, + "snap_kernel": s.kern1.Filename(), + "snap_try_kernel": "", }) - s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap" - s.bootloader.BootVars["snap_try_kernel"] = "krnl_42.snap" - c.Check(boot.ChangeRequiresReboot(info), Equals, true) + c.Check(reboot, Equals, false) - // simulate good boot - s.bootloader.BootVars["snap_kernel"] = "krnl_42.snap" - c.Check(boot.ChangeRequiresReboot(info), Equals, false) + // and that the modeenv now has this kernel listed + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) } -func (s *kernelOSSuite) TestSetNextBootForKernelForTheSameKernel(c *C) { - restore := release.MockOnClassic(false) - defer restore() +func (s *bootenvSuite) TestSetNextBootForKernelForTheSameKernelTryMode(c *C) { + coreDev := boottest.MockDevice("krnl") info := &snap.Info{} - info.Type = snap.TypeKernel + info.SnapType = snap.TypeKernel info.RealName = "krnl" info.Revision = snap.R(40) - s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap" + bootVars := map[string]string{ + "snap_kernel": "krnl_40.snap", + "snap_try_kernel": "krnl_99.snap", + "snap_mode": boot.TryStatus} + s.bootloader.SetBootVars(bootVars) - err := boot.SetNextBoot(info) + reboot, err := boot.NewCoreBootParticipant(info, snap.TypeKernel, coreDev).SetNextBoot() c.Assert(err, IsNil) - c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ - "snap_kernel": "krnl_40.snap", + 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", + "snap_try_kernel": "", + "snap_mode": boot.DefaultStatus, }) + + c.Check(reboot, Equals, false) } -func (s *kernelOSSuite) TestSetNextBootForKernelForTheSameKernelTryMode(c *C) { - restore := release.MockOnClassic(false) - defer restore() +func (s *bootenv20Suite) TestSetNextBoot20ForKernelForTheSameKernelTryMode(c *C) { + coreDev := boottest.MockUC20Device("pc-kernel") + c.Assert(coreDev.HasModeenv(), Equals, true) + + // set all the same vars as if we were doing trying, except don't set a try + // kernel + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename()}, + }, + kern: s.kern1, + // no try-kernel + kernStatus: boot.TryStatus, + }, + ) + defer r() + + bs := boot.NewCoreBootParticipant(s.kern1, snap.TypeKernel, coreDev) + c.Assert(bs.IsTrivial(), Equals, false) + reboot, err := bs.SetNextBoot() + c.Assert(err, IsNil) - info := &snap.Info{} - info.Type = snap.TypeKernel - info.RealName = "krnl" - info.Revision = snap.R(40) + // check that kernel_status is cleared + v, err := s.bootloader.GetBootVars("kernel_status") + c.Assert(err, IsNil) + c.Assert(v, DeepEquals, map[string]string{ + "kernel_status": boot.DefaultStatus, + }) + + c.Check(reboot, Equals, false) - s.bootloader.BootVars["snap_kernel"] = "krnl_40.snap" - s.bootloader.BootVars["snap_try_kernel"] = "krnl_99.snap" - s.bootloader.BootVars["snap_mode"] = "try" + // check that SetNextBoot didn't try to enable any try kernels + actual, _ := s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableTryKernel") + c.Assert(actual, HasLen, 0) + + // also didn't move any try kernels to trusted kernels + actual, _ = s.bootloader.GetRunKernelImageFunctionSnapCalls("EnableKernel") + c.Assert(actual, HasLen, 0) + + // check that SetNextBoot asked the bootloader for a kernel + _, nKernelCalls := s.bootloader.GetRunKernelImageFunctionSnapCalls("Kernel") + c.Assert(nKernelCalls, Equals, 1) + + // and that the modeenv didn't change + m2, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) +} + +func (s *bootenv20EnvRefKernelSuite) TestSetNextBoot20ForKernelForTheSameKernelTryMode(c *C) { + coreDev := boottest.MockUC20Device("pc-kernel") + c.Assert(coreDev.HasModeenv(), Equals, true) + + // set all the same vars as if we were doing trying, except don't set a try + // kernel + r := setupUC20Bootenv( + c, + s.bootloader, + &bootenv20Setup{ + modeenv: &boot.Modeenv{ + Mode: "run", + Base: s.base1.Filename(), + CurrentKernels: []string{s.kern1.Filename()}, + }, + kern: s.kern1, + // no try-kernel + kernStatus: boot.TryStatus, + }, + ) + defer r() + + bs := boot.NewCoreBootParticipant(s.kern1, snap.TypeKernel, coreDev) + c.Assert(bs.IsTrivial(), Equals, false) + reboot, err := bs.SetNextBoot() + c.Assert(err, IsNil) + + // check that kernel_status is cleared + m := s.bootloader.BootVars + c.Assert(m, DeepEquals, map[string]string{ + "kernel_status": boot.DefaultStatus, + "snap_kernel": s.kern1.Filename(), + "snap_try_kernel": "", + }) - err := boot.SetNextBoot(info) + c.Check(reboot, Equals, false) + + // and that the modeenv didn't change + m2, err := boot.ReadModeenv("") c.Assert(err, IsNil) + c.Assert(m2.CurrentKernels, DeepEquals, []string{s.kern1.Filename()}) +} - c.Assert(s.bootloader.BootVars, DeepEquals, map[string]string{ - "snap_kernel": "krnl_40.snap", - "snap_try_kernel": "", - "snap_mode": "", +type ubootSuite struct { + baseBootenvSuite +} + +var _ = Suite(&ubootSuite{}) + +// forceUbootBootloader sets up a uboot bootloader, in the uc16/uc18 style +// where all env is stored in a single uboot.env +func (s *ubootSuite) forceUbootBootloader(c *C) { + bootloader.Force(nil) + + mockGadgetDir := c.MkDir() + // this is testing the uc16/uc18 style uboot bootloader layout, the file + // must be non-empty for uc16/uc18 gadget config install behavior + err := ioutil.WriteFile(filepath.Join(mockGadgetDir, "uboot.conf"), []byte{1}, 0644) + c.Assert(err, IsNil) + err = bootloader.InstallBootConfig(mockGadgetDir, dirs.GlobalRootDir, nil) + c.Assert(err, IsNil) + + bloader, err := bootloader.Find("", nil) + c.Assert(err, IsNil) + c.Check(bloader, NotNil) + s.forceBootloader(bloader) + + fn := filepath.Join(s.bootdir, "/uboot/uboot.env") + c.Assert(osutil.FileExists(fn), Equals, true) +} + +// forceUbootBootloader sets up a uboot bootloader, in the uc20 style where we +// have a separate boot.sel file for snapd specific bootloader env +func (s *ubootSuite) forceUC20UbootBootloader(c *C) { + bootloader.Force(nil) + + // to find the uboot bootloader we need to pass in NoSlashBoot because + // that's where the gadget assets get installed to + installOpts := &bootloader.Options{ + NoSlashBoot: true, + } + + mockGadgetDir := c.MkDir() + // this must be empty for uc20 behavior + // TODO:UC20: update this test for the new behavior when that is implemented + err := ioutil.WriteFile(filepath.Join(mockGadgetDir, "uboot.conf"), nil, 0644) + c.Assert(err, IsNil) + err = bootloader.InstallBootConfig(mockGadgetDir, dirs.GlobalRootDir, installOpts) + c.Assert(err, IsNil) + + // in reality for uc20, we will bind mount /uboot/ubuntu/ onto + // /boot/uboot, so to emulate this at runtime for the tests, just put files + // into "/uboot" under bootdir for the test to see things that on disk are + // at "/uboot/ubuntu" as "/boot/uboot/" + + fn := filepath.Join(dirs.GlobalRootDir, "/uboot/ubuntu/boot.sel") + c.Assert(osutil.FileExists(fn), Equals, true) + + targetFile := filepath.Join(s.bootdir, "uboot", "boot.sel") + err = os.MkdirAll(filepath.Dir(targetFile), 0755) + c.Assert(err, IsNil) + err = os.Rename(fn, targetFile) + c.Assert(err, IsNil) + + // however when finding the bootloader, since we want it to show up as the + // "runtime" bootloader, just use ExtractedRunKernelImage + runtimeOpts := &bootloader.Options{ + ExtractedRunKernelImage: true, + } + + bloader, err := bootloader.Find("", runtimeOpts) + c.Assert(err, IsNil) + c.Check(bloader, NotNil) + s.forceBootloader(bloader) + c.Assert(bloader.Name(), Equals, "uboot") +} + +func (s *ubootSuite) TestExtractKernelAssetsAndRemoveOnUboot(c *C) { + + // test for both uc16/uc18 style uboot bootloader and for uc20 style bootloader + bloaderSetups := []func(){ + func() { s.forceUbootBootloader(c) }, + func() { s.forceUC20UbootBootloader(c) }, + } + + for _, setup := range bloaderSetups { + setup() + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"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 := snapfile.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + bp := boot.NewCoreKernel(info, boottest.MockDevice("")) + err = bp.ExtractKernelAssets(snapf) + c.Assert(err, IsNil) + + // this is where the kernel/initrd is unpacked + kernelAssetsDir := filepath.Join(s.bootdir, "/uboot/ubuntu-kernel_42.snap") + for _, def := range files { + if def[0] == "meta/kernel.yaml" { + break + } + + fullFn := filepath.Join(kernelAssetsDir, def[0]) + c.Check(fullFn, testutil.FileEquals, def[1]) + } + + // it's idempotent + err = bp.ExtractKernelAssets(snapf) + c.Assert(err, IsNil) + + // remove + err = bp.RemoveKernelAssets() + c.Assert(err, IsNil) + c.Check(osutil.FileExists(kernelAssetsDir), Equals, false) + + // it's idempotent + err = bp.RemoveKernelAssets() + c.Assert(err, IsNil) + + } + +} + +type grubSuite struct { + baseBootenvSuite +} + +var _ = Suite(&grubSuite{}) + +func (s *grubSuite) SetUpTest(c *C) { + s.baseBootenvSuite.SetUpTest(c) + s.forceGrubBootloader(c) +} + +func (s *grubSuite) forceGrubBootloader(c *C) bootloader.Bootloader { + bootloader.Force(nil) + + // 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, dirs.GlobalRootDir, nil) + c.Assert(err, IsNil) + + bloader, err := bootloader.Find("", nil) + c.Assert(err, IsNil) + c.Check(bloader, NotNil) + bloader.SetBootVars(map[string]string{ + "snap_kernel": "kernel_41.snap", + "snap_core": "core_21.snap", }) + s.forceBootloader(bloader) + + fn := filepath.Join(s.bootdir, "/grub/grub.cfg") + c.Assert(osutil.FileExists(fn), Equals, true) + return bloader +} + +func (s *grubSuite) TestExtractKernelAssetsNoUnpacksKernelForGrub(c *C) { + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snapfile.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + bp := boot.NewCoreKernel(info, boottest.MockDevice("")) + err = bp.ExtractKernelAssets(snapf) + c.Assert(err, IsNil) + + // kernel is *not* here + kernimg := filepath.Join(s.bootdir, "grub", "ubuntu-kernel_42.snap", "kernel.img") + c.Assert(osutil.FileExists(kernimg), Equals, false) + + // it's idempotent + err = bp.ExtractKernelAssets(snapf) + c.Assert(err, IsNil) } -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.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 *grubSuite) TestExtractKernelForceWorks(c *C) { + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"meta/force-kernel-extraction", ""}, + {"meta/kernel.yaml", "version: 4.2"}, } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snapfile.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + bp := boot.NewCoreKernel(info, boottest.MockDevice("")) + err = bp.ExtractKernelAssets(snapf) + c.Assert(err, IsNil) + + // kernel is extracted + kernimg := filepath.Join(s.bootdir, "/grub/ubuntu-kernel_42.snap/kernel.img") + c.Assert(osutil.FileExists(kernimg), Equals, true) + // initrd + initrdimg := filepath.Join(s.bootdir, "/grub/ubuntu-kernel_42.snap/initrd.img") + c.Assert(osutil.FileExists(initrdimg), Equals, true) + + // it's idempotent + err = bp.ExtractKernelAssets(snapf) + c.Assert(err, IsNil) + + // ensure that removal of assets also works + 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 = bp.RemoveKernelAssets() + c.Assert(err, IsNil) } diff -Nru snapd-2.37.4ubuntu0.1/boot/makebootable.go snapd-2.45.1ubuntu0.2/boot/makebootable.go --- snapd-2.37.4ubuntu0.1/boot/makebootable.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/makebootable.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,351 @@ +// -*- 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 ( + "fmt" + "os" + "path/filepath" + + "github.com/snapcore/snapd/asserts" + "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/snapfile" +) + +// BootableSet represents the boot snaps of a system to be made bootable. +type BootableSet struct { + Base *snap.Info + BasePath string + Kernel *snap.Info + KernelPath string + + RecoverySystemLabel string + RecoverySystemDir string + + UnpackedGadgetDir string + + // Recover is set when making the recovery partition bootable. + Recovery bool +} + +// MakeBootable sets up the given bootable set and target filesystem +// such that the system can be booted. +// +// rootdir points to an image filesystem (UC 16/18), image recovery +// filesystem (UC20 at prepare-image time) or ephemeral system (UC20 +// install mode). +func MakeBootable(model *asserts.Model, rootdir string, bootWith *BootableSet) error { + if model.Grade() == asserts.ModelGradeUnset { + return makeBootable16(model, rootdir, bootWith) + } + + if !bootWith.Recovery { + return makeBootable20RunMode(model, rootdir, bootWith) + } + return makeBootable20(model, rootdir, bootWith) +} + +// makeBootable16 setups the image filesystem for boot with UC16 +// and UC18 models. 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 makeBootable16(model *asserts.Model, rootdir string, bootWith *BootableSet) error { + opts := &bootloader.Options{ + PrepareImageTime: true, + } + + // install the bootloader configuration from the gadget + if err := bootloader.InstallBootConfig(bootWith.UnpackedGadgetDir, rootdir, opts); 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 []string{bootWith.BasePath, bootWith.KernelPath} { + 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, opts) + 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() + } + + setBoot := func(name, fn string) { + m[name] = filepath.Base(fn) + } + // base + setBoot("snap_core", bootWith.BasePath) + + // kernel + kernelf, err := snapfile.Open(bootWith.KernelPath) + if err != nil { + return err + } + if err := bl.ExtractKernelAssets(bootWith.Kernel, kernelf); err != nil { + return err + } + setBoot("snap_kernel", bootWith.KernelPath) + + if err := bl.SetBootVars(m); err != nil { + return err + } + + return nil +} + +func makeBootable20(model *asserts.Model, rootdir string, bootWith *BootableSet) error { + // we can only make a single recovery system bootable right now + recoverySystems, err := filepath.Glob(filepath.Join(rootdir, "systems/*")) + if err != nil { + return fmt.Errorf("cannot validate recovery systems: %v", err) + } + if len(recoverySystems) > 1 { + return fmt.Errorf("cannot make multiple recovery systems bootable yet") + } + + if bootWith.RecoverySystemLabel == "" { + return fmt.Errorf("internal error: recovery system label unset") + } + + opts := &bootloader.Options{ + PrepareImageTime: true, + // setup the recovery bootloader + Recovery: true, + } + + // install the bootloader configuration from the gadget + if err := bootloader.InstallBootConfig(bootWith.UnpackedGadgetDir, rootdir, opts); err != nil { + return err + } + + // now install the recovery system specific boot config + bl, err := bootloader.Find(rootdir, opts) + if err != nil { + return fmt.Errorf("internal error: cannot find bootloader: %v", err) + } + + // record which recovery system is to be used on the bootloader, note + // that this goes on the main bootloader environment, and not on the + // recovery system bootloader environment, for example for grub + // bootloader, this env var is set on the ubuntu-seed root grubenv, and + // not on the recovery system grubenv in the systems/20200314/ subdir on + // ubuntu-seed + blVars := map[string]string{ + "snapd_recovery_system": bootWith.RecoverySystemLabel, + } + if err := bl.SetBootVars(blVars); err != nil { + return fmt.Errorf("cannot set recovery environment: %v", err) + } + + // on e.g. ARM we need to extract the kernel assets on the recovery + // system as well, but the bootloader does not load any environment from + // the recovery system + erkbl, ok := bl.(bootloader.ExtractedRecoveryKernelImageBootloader) + if ok { + kernelf, err := snapfile.Open(bootWith.KernelPath) + if err != nil { + return err + } + + err = erkbl.ExtractRecoveryKernelAssets( + bootWith.RecoverySystemDir, + bootWith.Kernel, + kernelf, + ) + if err != nil { + return fmt.Errorf("cannot extract recovery system kernel assets: %v", err) + } + + return nil + } + + rbl, ok := bl.(bootloader.RecoveryAwareBootloader) + if !ok { + return fmt.Errorf("cannot use %s bootloader: does not support recovery systems", bl.Name()) + } + kernelPath, err := filepath.Rel(rootdir, bootWith.KernelPath) + if err != nil { + return fmt.Errorf("cannot construct kernel boot path: %v", err) + } + recoveryBlVars := map[string]string{ + "snapd_recovery_kernel": filepath.Join("/", kernelPath), + } + if err := rbl.SetRecoverySystemEnv(bootWith.RecoverySystemDir, recoveryBlVars); err != nil { + return fmt.Errorf("cannot set recovery system environment: %v", err) + } + return nil +} + +func makeBootable20RunMode(model *asserts.Model, rootdir string, bootWith *BootableSet) error { + // TODO:UC20: + // - create grub.cfg instead of using the gadget one + // - figure out what to do for uboot gadgets, currently we require them to + // install the boot.sel onto ubuntu-boot directly, but the file should be + // managed by snapd instead + + // copy kernel/base into the ubuntu-data partition + snapBlobDir := dirs.SnapBlobDirUnder(InstallHostWritableDir) + if err := os.MkdirAll(snapBlobDir, 0755); err != nil { + return err + } + for _, fn := range []string{bootWith.BasePath, bootWith.KernelPath} { + dst := filepath.Join(snapBlobDir, filepath.Base(fn)) + // if the source filename is a symlink, don't copy the symlink, copy the + // target file instead of copying the symlink, as the initramfs won't + // follow the symlink when it goes to mount the base and kernel snaps by + // design as the initramfs should only be using trusted things from + // ubuntu-data to boot in run mode + if osutil.IsSymlink(fn) { + link, err := os.Readlink(fn) + if err != nil { + return err + } + fn = link + } + if err := osutil.CopyFile(fn, dst, osutil.CopyFlagPreserveAll|osutil.CopyFlagSync); err != nil { + return err + } + } + + // write modeenv on the ubuntu-data partition + modeenv := &Modeenv{ + Mode: "run", + RecoverySystem: filepath.Base(bootWith.RecoverySystemDir), + Base: filepath.Base(bootWith.BasePath), + CurrentKernels: []string{bootWith.Kernel.Filename()}, + BrandID: model.BrandID(), + Model: model.Model(), + Grade: string(model.Grade()), + } + if err := modeenv.WriteTo(InstallHostWritableDir); err != nil { + return fmt.Errorf("cannot write modeenv: %v", err) + } + + // get the ubuntu-boot bootloader and extract the kernel there + opts := &bootloader.Options{ + // At this point the run mode bootloader is under the native + // layout, no /boot mount. + NoSlashBoot: true, + // Bootloader that supports kernel asset extraction + ExtractedRunKernelImage: true, + } + bl, err := bootloader.Find(InitramfsUbuntuBootDir, opts) + if err != nil { + return fmt.Errorf("internal error: cannot find run system bootloader: %v", err) + } + + // extract the kernel first and mark kernel_status ready + kernelf, err := snapfile.Open(bootWith.KernelPath) + if err != nil { + return err + } + + err = bl.ExtractKernelAssets(bootWith.Kernel, kernelf) + if err != nil { + return err + } + + blVars := map[string]string{ + "kernel_status": "", + } + + ebl, ok := bl.(bootloader.ExtractedRunKernelImageBootloader) + if ok { + // the bootloader supports additional extracted kernel handling + + // enable the kernel on the bootloader and finally transition to + // run-mode last in case we get rebooted in between anywhere here + + // it's okay to enable the kernel before writing the boot vars, because + // we haven't written snapd_recovery_mode=run, which is the critical + // thing that will inform the bootloader to try booting from ubuntu-boot + if err := ebl.EnableKernel(bootWith.Kernel); err != nil { + return err + } + } else { + // TODO:UC20: should we make this more explicit with a new + // bootloader interface that is checked for first before + // ExtractedRunKernelImageBootloader the same way we do with + // ExtractedRecoveryKernelImageBootloader? + + // the bootloader does not support additional handling of + // extracted kernel images, we must name the kernel to be used + // explicitly in bootloader variables + blVars["snap_kernel"] = bootWith.Kernel.Filename() + } + + // set the ubuntu-boot bootloader variables before triggering transition to + // try and boot from ubuntu-boot (that transition happens when we write + // snapd_recovery_mode below) + if err := bl.SetBootVars(blVars); err != nil { + return fmt.Errorf("cannot set run system environment: %v", err) + } + + // LAST step: update recovery bootloader environment to indicate that we + // transition to run mode now + opts = &bootloader.Options{ + // let the bootloader know we will be touching the recovery + // partition + Recovery: true, + } + bl, err = bootloader.Find(InitramfsUbuntuSeedDir, opts) + if err != nil { + return fmt.Errorf("internal error: cannot find recovery system bootloader: %v", err) + } + blVars = map[string]string{ + "snapd_recovery_mode": "run", + } + if err := bl.SetBootVars(blVars); err != nil { + return fmt.Errorf("cannot set recovery environment: %v", err) + } + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/boot/makebootable_test.go snapd-2.45.1ubuntu0.2/boot/makebootable_test.go --- snapd-2.37.4ubuntu0.1/boot/makebootable_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/makebootable_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,652 @@ +// -*- 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 ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + . "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/bootloader/ubootenv" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapfile" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" +) + +type makeBootableSuite struct { + baseBootenvSuite + + bootloader *bootloadertest.MockBootloader +} + +var _ = Suite(&makeBootableSuite{}) + +func (s *makeBootableSuite) SetUpTest(c *C) { + s.baseBootenvSuite.SetUpTest(c) + + s.bootloader = bootloadertest.Mock("mock", c.MkDir()) + s.forceBootloader(s.bootloader) +} + +func makeSnap(c *C, name, yaml string, revno snap.Revision) (fn string, info *snap.Info) { + return makeSnapWithFiles(c, name, yaml, revno, nil) +} + +func makeSnapWithFiles(c *C, name, yaml string, revno snap.Revision, files [][]string) (fn string, info *snap.Info) { + si := &snap.SideInfo{ + RealName: name, + Revision: revno, + } + fn = snaptest.MakeTestSnapWithFiles(c, yaml, files) + snapf, err := snapfile.Open(fn) + c.Assert(err, IsNil) + info, err = snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + return fn, info +} + +func (s *makeBootableSuite) 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 := makeSnap(c, "core18", `name: core18 +type: base +version: 4.0 +`, snap.R(3)) + baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename()) + err = os.Rename(baseFn, baseInSeed) + c.Assert(err, IsNil) + kernelFn, kernelInfo := makeSnap(c, "pc-kernel", `name: pc-kernel +type: kernel +version: 4.0 +`, snap.R(5)) + kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename()) + err = os.Rename(kernelFn, kernelInSeed) + c.Assert(err, IsNil) + + bootWith := &boot.BootableSet{ + Base: baseInfo, + BasePath: baseInSeed, + Kernel: kernelInfo, + KernelPath: kernelInSeed, + UnpackedGadgetDir: unpackedGadgetDir, + } + + err = boot.MakeBootable(model, rootdir, bootWith) + 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), kernelInfo.Filename()) + dst, err := os.Readlink(filepath.Join(dirs.SnapBlobDirUnder(rootdir), kernelInfo.Filename())) + 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), baseInfo.Filename()) + dst, err = os.Readlink(filepath.Join(dirs.SnapBlobDirUnder(rootdir), baseInfo.Filename())) + 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) +} + +type makeBootable20Suite struct { + baseBootenvSuite + + bootloader *bootloadertest.MockRecoveryAwareBootloader +} + +type makeBootable20UbootSuite struct { + baseBootenvSuite + + bootloader *bootloadertest.MockExtractedRecoveryKernelImageBootloader +} + +var _ = Suite(&makeBootable20Suite{}) +var _ = Suite(&makeBootable20UbootSuite{}) + +func (s *makeBootable20Suite) SetUpTest(c *C) { + s.baseBootenvSuite.SetUpTest(c) + + s.bootloader = bootloadertest.Mock("mock", c.MkDir()).RecoveryAware() + s.forceBootloader(s.bootloader) +} + +func (s *makeBootable20UbootSuite) SetUpTest(c *C) { + s.baseBootenvSuite.SetUpTest(c) + + s.bootloader = bootloadertest.Mock("mock", c.MkDir()).ExtractedRecoveryKernelImage() + s.forceBootloader(s.bootloader) +} + +func makeMockUC20Model() *asserts.Model { + headers := map[string]interface{}{ + "type": "model", + "authority-id": "my-brand", + "series": "16", + "brand-id": "my-brand", + "model": "my-model-uc20", + "display-name": "My Model", + "architecture": "amd64", + "base": "core20", + "grade": "dangerous", + "timestamp": "2019-11-01T08:00:00+00:00", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-linux", + "id": "pclinuxdidididididididididididid", + "type": "kernel", + }, + map[string]interface{}{ + "name": "pc", + "id": "pcididididididididididididididid", + "type": "gadget", + }, + }, + } + return assertstest.FakeAssertion(headers).(*asserts.Model) +} + +func (s *makeBootable20Suite) TestMakeBootable20(c *C) { + dirs.SetRootDir("") + + model := makeMockUC20Model() + + unpackedGadgetDir := c.MkDir() + grubRecoveryCfg := []byte("#grub-recovery cfg") + err := ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub-recovery.conf"), grubRecoveryCfg, 0644) + c.Assert(err, IsNil) + grubCfg := []byte("#grub cfg") + err = ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub.conf"), grubCfg, 0644) + c.Assert(err, IsNil) + + rootdir := c.MkDir() + // on uc20 the seed layout if different + seedSnapsDirs := filepath.Join(rootdir, "/snaps") + err = os.MkdirAll(seedSnapsDirs, 0755) + c.Assert(err, IsNil) + + baseFn, baseInfo := makeSnap(c, "core20", `name: core20 +type: base +version: 5.0 +`, snap.R(3)) + baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename()) + err = os.Rename(baseFn, baseInSeed) + c.Assert(err, IsNil) + kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel +type: kernel +version: 5.0 +`, snap.R(5), [][]string{ + {"kernel.efi", "I'm a kernel.efi"}, + }) + kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename()) + err = os.Rename(kernelFn, kernelInSeed) + c.Assert(err, IsNil) + + label := "20191209" + recoverySystemDir := filepath.Join("/systems", label) + bootWith := &boot.BootableSet{ + Base: baseInfo, + BasePath: baseInSeed, + Kernel: kernelInfo, + KernelPath: kernelInSeed, + RecoverySystemDir: recoverySystemDir, + RecoverySystemLabel: label, + UnpackedGadgetDir: unpackedGadgetDir, + Recovery: true, + } + + err = boot.MakeBootable(model, rootdir, bootWith) + c.Assert(err, IsNil) + + // ensure only a single file got copied (the grub.cfg) + files, err := filepath.Glob(filepath.Join(rootdir, "EFI/ubuntu/*")) + c.Assert(err, IsNil) + c.Check(files, HasLen, 1) + // check that the recovery bootloader configuration was copied with + // the correct content + c.Check(filepath.Join(rootdir, "EFI/ubuntu/grub.cfg"), testutil.FileEquals, grubRecoveryCfg) + + // ensure no /boot was setup + c.Check(filepath.Join(rootdir, "boot"), testutil.FileAbsent) + + // ensure the correct recovery system configuration was set + c.Check(s.bootloader.RecoverySystemDir, Equals, recoverySystemDir) + c.Check(s.bootloader.RecoverySystemBootVars, DeepEquals, map[string]string{ + "snapd_recovery_kernel": "/snaps/pc-kernel_5.snap", + }) + c.Check(s.bootloader.BootVars, DeepEquals, map[string]string{ + "snapd_recovery_system": label, + }) +} + +func (s *makeBootable20Suite) TestMakeBootable20UnsetRecoverySystemLabelError(c *C) { + dirs.SetRootDir("") + + model := makeMockUC20Model() + + unpackedGadgetDir := c.MkDir() + grubRecoveryCfg := []byte("#grub-recovery cfg") + err := ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub-recovery.conf"), grubRecoveryCfg, 0644) + c.Assert(err, IsNil) + grubCfg := []byte("#grub cfg") + err = ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "grub.conf"), grubCfg, 0644) + c.Assert(err, IsNil) + + rootdir := c.MkDir() + + label := "20191209" + recoverySystemDir := filepath.Join("/systems", label) + bootWith := &boot.BootableSet{ + RecoverySystemDir: recoverySystemDir, + UnpackedGadgetDir: unpackedGadgetDir, + Recovery: true, + } + + err = boot.MakeBootable(model, rootdir, bootWith) + c.Assert(err, ErrorMatches, "internal error: recovery system label unset") +} + +func (s *makeBootable20Suite) TestMakeBootable20MultipleRecoverySystemsError(c *C) { + dirs.SetRootDir("") + + model := makeMockUC20Model() + + bootWith := &boot.BootableSet{Recovery: true} + rootdir := c.MkDir() + err := os.MkdirAll(filepath.Join(rootdir, "systems/20191204"), 0755) + c.Assert(err, IsNil) + err = os.MkdirAll(filepath.Join(rootdir, "systems/20191205"), 0755) + c.Assert(err, IsNil) + + err = boot.MakeBootable(model, rootdir, bootWith) + c.Assert(err, ErrorMatches, "cannot make multiple recovery systems bootable yet") +} + +func (s *makeBootable20Suite) TestMakeBootable20RunMode(c *C) { + dirs.SetRootDir("") + bootloader.Force(nil) + + model := makeMockUC20Model() + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + seedSnapsDirs := filepath.Join(rootdir, "/snaps") + err := os.MkdirAll(seedSnapsDirs, 0755) + c.Assert(err, IsNil) + + // grub on ubuntu-seed + mockSeedGrubDir := filepath.Join(boot.InitramfsUbuntuSeedDir, "EFI", "ubuntu") + mockSeedGrubCfg := filepath.Join(mockSeedGrubDir, "grub.cfg") + err = os.MkdirAll(filepath.Dir(mockSeedGrubCfg), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(mockSeedGrubCfg, nil, 0644) + c.Assert(err, IsNil) + + // grub on ubuntu-boot + mockBootGrubDir := filepath.Join(boot.InitramfsUbuntuBootDir, "EFI", "ubuntu") + mockBootGrubCfg := filepath.Join(mockBootGrubDir, "grub.cfg") + err = os.MkdirAll(filepath.Dir(mockBootGrubCfg), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(mockBootGrubCfg, nil, 0644) + c.Assert(err, IsNil) + + // make the snaps symlinks so that we can ensure that makebootable follows + // the symlinks and copies the files and not the symlinks + baseFn, baseInfo := makeSnap(c, "core20", `name: core20 +type: base +version: 5.0 +`, snap.R(3)) + baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename()) + err = os.Symlink(baseFn, baseInSeed) + c.Assert(err, IsNil) + kernelFn, kernelInfo := makeSnapWithFiles(c, "pc-kernel", `name: pc-kernel +type: kernel +version: 5.0 +`, snap.R(5), + [][]string{ + {"kernel.efi", "I'm a kernel.efi"}, + }, + ) + kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename()) + err = os.Symlink(kernelFn, kernelInSeed) + c.Assert(err, IsNil) + + bootWith := &boot.BootableSet{ + RecoverySystemDir: "20191216", + BasePath: baseInSeed, + Base: baseInfo, + KernelPath: kernelInSeed, + Kernel: kernelInfo, + Recovery: false, + } + + err = boot.MakeBootable(model, rootdir, bootWith) + c.Assert(err, IsNil) + + // ensure base/kernel got copied to /var/lib/snapd/snaps + core20Snap := filepath.Join(dirs.SnapBlobDirUnder(boot.InstallHostWritableDir), "core20_3.snap") + pcKernelSnap := filepath.Join(dirs.SnapBlobDirUnder(boot.InstallHostWritableDir), "pc-kernel_5.snap") + c.Check(core20Snap, testutil.FilePresent) + c.Check(pcKernelSnap, testutil.FilePresent) + c.Check(osutil.IsSymlink(core20Snap), Equals, false) + c.Check(osutil.IsSymlink(pcKernelSnap), Equals, false) + + // ensure the bootvars got updated the right way + mockSeedGrubenv := filepath.Join(mockSeedGrubDir, "grubenv") + c.Check(mockSeedGrubenv, testutil.FilePresent) + c.Check(mockSeedGrubenv, testutil.FileContains, "snapd_recovery_mode=run") + mockBootGrubenv := filepath.Join(mockBootGrubDir, "grubenv") + c.Check(mockBootGrubenv, testutil.FilePresent) + + // ensure that kernel_status is empty, we specifically want this to be set + // to the empty string + // use (?m) to match multi-line file in the regex here, because the file is + // a grubenv with padding #### blocks + c.Check(mockBootGrubenv, testutil.FileMatches, `(?m)^kernel_status=$`) + + // check that we have the extracted kernel in the right places, both in the + // old uc16/uc18 location and the new ubuntu-boot partition grub dir + extractedKernel := filepath.Join(mockBootGrubDir, "pc-kernel_5.snap", "kernel.efi") + c.Check(extractedKernel, testutil.FilePresent) + + // the new uc20 location + extractedKernelSymlink := filepath.Join(mockBootGrubDir, "kernel.efi") + c.Check(extractedKernelSymlink, testutil.FilePresent) + + // ensure modeenv looks correct + ubuntuDataModeEnvPath := filepath.Join(rootdir, "/run/mnt/ubuntu-data/system-data/var/lib/snapd/modeenv") + c.Check(ubuntuDataModeEnvPath, testutil.FileEquals, `mode=run +recovery_system=20191216 +base=core20_3.snap +current_kernels=pc-kernel_5.snap +model=my-brand/my-model-uc20 +grade=dangerous +`) +} + +func (s *makeBootable20UbootSuite) TestUbootMakeBootable20TraditionalUbootenvFails(c *C) { + dirs.SetRootDir("") + + model := makeMockUC20Model() + + unpackedGadgetDir := c.MkDir() + ubootEnv := []byte("#uboot env") + err := ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "uboot.conf"), ubootEnv, 0644) + c.Assert(err, IsNil) + + rootdir := c.MkDir() + // on uc20 the seed layout if different + seedSnapsDirs := filepath.Join(rootdir, "/snaps") + err = os.MkdirAll(seedSnapsDirs, 0755) + c.Assert(err, IsNil) + + baseFn, baseInfo := makeSnap(c, "core20", `name: core20 +type: base +version: 5.0 +`, snap.R(3)) + baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename()) + err = os.Rename(baseFn, baseInSeed) + c.Assert(err, IsNil) + kernelFn, kernelInfo := makeSnapWithFiles(c, "arm-kernel", `name: arm-kernel +type: kernel +version: 5.0 +`, snap.R(5), [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"dtbs/foo.dtb", "foo dtb"}, + {"dtbs/bar.dto", "bar dtbo"}, + }) + kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename()) + err = os.Rename(kernelFn, kernelInSeed) + c.Assert(err, IsNil) + + label := "20191209" + recoverySystemDir := filepath.Join("/systems", label) + bootWith := &boot.BootableSet{ + Base: baseInfo, + BasePath: baseInSeed, + Kernel: kernelInfo, + KernelPath: kernelInSeed, + RecoverySystemDir: recoverySystemDir, + RecoverySystemLabel: label, + UnpackedGadgetDir: unpackedGadgetDir, + Recovery: true, + } + + // TODO:UC20: enable this use case + err = boot.MakeBootable(model, rootdir, bootWith) + c.Assert(err, ErrorMatches, fmt.Sprintf("cannot find boot config in %q", unpackedGadgetDir)) +} + +func (s *makeBootable20UbootSuite) TestUbootMakeBootable20BootScr(c *C) { + dirs.SetRootDir("") + + model := makeMockUC20Model() + + unpackedGadgetDir := c.MkDir() + // the uboot.conf must be empty for this to work/do the right thing + err := ioutil.WriteFile(filepath.Join(unpackedGadgetDir, "uboot.conf"), nil, 0644) + c.Assert(err, IsNil) + + rootdir := c.MkDir() + // on uc20 the seed layout if different + seedSnapsDirs := filepath.Join(rootdir, "/snaps") + err = os.MkdirAll(seedSnapsDirs, 0755) + c.Assert(err, IsNil) + + baseFn, baseInfo := makeSnap(c, "core20", `name: core20 +type: base +version: 5.0 +`, snap.R(3)) + baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename()) + err = os.Rename(baseFn, baseInSeed) + c.Assert(err, IsNil) + kernelFn, kernelInfo := makeSnapWithFiles(c, "arm-kernel", `name: arm-kernel +type: kernel +version: 5.0 +`, snap.R(5), [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"dtbs/foo.dtb", "foo dtb"}, + {"dtbs/bar.dto", "bar dtbo"}, + }) + kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename()) + err = os.Rename(kernelFn, kernelInSeed) + c.Assert(err, IsNil) + + label := "20191209" + recoverySystemDir := filepath.Join("/systems", label) + bootWith := &boot.BootableSet{ + Base: baseInfo, + BasePath: baseInSeed, + Kernel: kernelInfo, + KernelPath: kernelInSeed, + RecoverySystemDir: recoverySystemDir, + RecoverySystemLabel: label, + UnpackedGadgetDir: unpackedGadgetDir, + Recovery: true, + } + + err = boot.MakeBootable(model, rootdir, bootWith) + c.Assert(err, IsNil) + + // since uboot.conf was absent, we won't have installed the uboot.env, as + // it is expected that the gadget assets would have installed boot.scr + // instead + c.Check(filepath.Join(rootdir, "uboot.env"), testutil.FileAbsent) + + c.Check(s.bootloader.BootVars, DeepEquals, map[string]string{ + "snapd_recovery_system": label, + }) + + // ensure the correct recovery system configuration was set + c.Check( + s.bootloader.ExtractRecoveryKernelAssetsCalls, + DeepEquals, + []bootloadertest.ExtractedRecoveryKernelCall{{ + RecoverySystemDir: recoverySystemDir, + S: kernelInfo, + }}, + ) +} + +func (s *makeBootable20UbootSuite) TestUbootMakeBootable20RunModeBootScr(c *C) { + dirs.SetRootDir("") + bootloader.Force(nil) + + model := makeMockUC20Model() + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + seedSnapsDirs := filepath.Join(rootdir, "/snaps") + err := os.MkdirAll(seedSnapsDirs, 0755) + c.Assert(err, IsNil) + + // uboot on ubuntu-seed + mockSeedUbootBootSel := filepath.Join(boot.InitramfsUbuntuSeedDir, "uboot/ubuntu/boot.sel") + err = os.MkdirAll(filepath.Dir(mockSeedUbootBootSel), 0755) + c.Assert(err, IsNil) + env, err := ubootenv.Create(mockSeedUbootBootSel, 4096) + c.Assert(err, IsNil) + c.Assert(env.Save(), IsNil) + + // uboot on ubuntu-boot + mockBootUbootBootSel := filepath.Join(boot.InitramfsUbuntuBootDir, "uboot/ubuntu/boot.sel") + err = os.MkdirAll(filepath.Dir(mockBootUbootBootSel), 0755) + c.Assert(err, IsNil) + env, err = ubootenv.Create(mockBootUbootBootSel, 4096) + c.Assert(err, IsNil) + c.Assert(env.Save(), IsNil) + + baseFn, baseInfo := makeSnap(c, "core20", `name: core20 +type: base +version: 5.0 +`, snap.R(3)) + baseInSeed := filepath.Join(seedSnapsDirs, baseInfo.Filename()) + err = os.Rename(baseFn, baseInSeed) + c.Assert(err, IsNil) + kernelSnapFiles := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"dtbs/foo.dtb", "foo dtb"}, + {"dtbs/bar.dto", "bar dtbo"}, + } + kernelFn, kernelInfo := makeSnapWithFiles(c, "arm-kernel", `name: arm-kernel +type: kernel +version: 5.0 +`, snap.R(5), kernelSnapFiles) + kernelInSeed := filepath.Join(seedSnapsDirs, kernelInfo.Filename()) + err = os.Rename(kernelFn, kernelInSeed) + c.Assert(err, IsNil) + + bootWith := &boot.BootableSet{ + RecoverySystemDir: "20191216", + BasePath: baseInSeed, + Base: baseInfo, + KernelPath: kernelInSeed, + Kernel: kernelInfo, + Recovery: false, + } + + err = boot.MakeBootable(model, rootdir, bootWith) + c.Assert(err, IsNil) + + // ensure base/kernel got copied to /var/lib/snapd/snaps + c.Check(filepath.Join(dirs.SnapBlobDirUnder(boot.InstallHostWritableDir), "core20_3.snap"), testutil.FilePresent) + c.Check(filepath.Join(dirs.SnapBlobDirUnder(boot.InstallHostWritableDir), "arm-kernel_5.snap"), testutil.FilePresent) + + // ensure the bootvars on ubuntu-seed got updated the right way + mockSeedUbootenv := filepath.Join(boot.InitramfsUbuntuSeedDir, "uboot/ubuntu/boot.sel") + uenvSeed, err := ubootenv.Open(mockSeedUbootenv) + c.Assert(err, IsNil) + c.Assert(uenvSeed.Get("snapd_recovery_mode"), Equals, "run") + + // now check ubuntu-boot boot.sel + mockBootUbootenv := filepath.Join(boot.InitramfsUbuntuBootDir, "uboot/ubuntu/boot.sel") + uenvBoot, err := ubootenv.Open(mockBootUbootenv) + c.Assert(err, IsNil) + c.Assert(uenvBoot.Get("snap_try_kernel"), Equals, "") + c.Assert(uenvBoot.Get("snap_kernel"), Equals, "arm-kernel_5.snap") + c.Assert(uenvBoot.Get("kernel_status"), Equals, boot.DefaultStatus) + + // check that we have the extracted kernel in the right places, in the + // old uc16/uc18 location + for _, file := range kernelSnapFiles { + fName := file[0] + c.Check(filepath.Join(boot.InitramfsUbuntuBootDir, "uboot/ubuntu/arm-kernel_5.snap", fName), testutil.FilePresent) + } + + // ensure modeenv looks correct + ubuntuDataModeEnvPath := filepath.Join(rootdir, "/run/mnt/ubuntu-data/system-data/var/lib/snapd/modeenv") + c.Check(ubuntuDataModeEnvPath, testutil.FileEquals, `mode=run +recovery_system=20191216 +base=core20_3.snap +current_kernels=arm-kernel_5.snap +model=my-brand/my-model-uc20 +grade=dangerous +`) +} diff -Nru snapd-2.37.4ubuntu0.1/boot/modeenv.go snapd-2.45.1ubuntu0.2/boot/modeenv.go --- snapd-2.37.4ubuntu0.1/boot/modeenv.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/modeenv.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,172 @@ +// -*- 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 ( + "bytes" + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/mvo5/goconfigparser" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +// Modeenv is a file on UC20 that provides additional information +// about the current mode (run,recover,install) +type Modeenv struct { + Mode string + RecoverySystem string + Base string + TryBase string + BaseStatus string + CurrentKernels []string + Model string + BrandID string + Grade string + + // read is set to true when a modenv was read successfully + read bool + + // originRootdir is set to the root whence the modeenv was + // read from, and where it will be written back to + originRootdir string +} + +func modeenvFile(rootdir string) string { + if rootdir == "" { + rootdir = dirs.GlobalRootDir + } + return dirs.SnapModeenvFileUnder(rootdir) +} + +// ReadModeenv attempts to read the modeenv file at +// /var/iib/snapd/modeenv. +func ReadModeenv(rootdir string) (*Modeenv, error) { + modeenvPath := modeenvFile(rootdir) + cfg := goconfigparser.New() + cfg.AllowNoSectionHeader = true + if err := cfg.ReadFile(modeenvPath); err != nil { + return nil, err + } + // TODO:UC20: should we check these errors and try to do something? + recoverySystem, _ := cfg.Get("", "recovery_system") + mode, _ := cfg.Get("", "mode") + base, _ := cfg.Get("", "base") + baseStatus, _ := cfg.Get("", "base_status") + tryBase, _ := cfg.Get("", "try_base") + + // current_kernels is a comma-delimited list in a string + kernelsString, _ := cfg.Get("", "current_kernels") + var kernels []string + if kernelsString != "" { + kernels = strings.Split(kernelsString, ",") + // drop empty strings + nonEmptyKernels := make([]string, 0, len(kernels)) + for _, kernel := range kernels { + if kernel != "" { + nonEmptyKernels = append(nonEmptyKernels, kernel) + } + } + kernels = nonEmptyKernels + } + brand := "" + model := "" + brandSlashModel, _ := cfg.Get("", "model") + if bsmSplit := strings.SplitN(brandSlashModel, "/", 2); len(bsmSplit) == 2 { + if bsmSplit[0] != "" && bsmSplit[1] != "" { + brand = bsmSplit[0] + model = bsmSplit[1] + } + } + // expect the caller to validate the grade + grade, _ := cfg.Get("", "grade") + + return &Modeenv{ + Mode: mode, + RecoverySystem: recoverySystem, + Base: base, + TryBase: tryBase, + BaseStatus: baseStatus, + CurrentKernels: kernels, + BrandID: brand, + Grade: grade, + Model: model, + read: true, + originRootdir: rootdir, + }, nil +} + +// Write outputs the modeenv to the file where it was read, only valid on +// modeenv that has been read. +func (m *Modeenv) Write() error { + if m.read { + return m.WriteTo(m.originRootdir) + } + return fmt.Errorf("internal error: must use WriteTo with modeenv not read from disk") +} + +// WriteTo outputs the modeenv to the file at /var/lib/snapd/modeenv. +func (m *Modeenv) WriteTo(rootdir string) error { + modeenvPath := modeenvFile(rootdir) + + if err := os.MkdirAll(filepath.Dir(modeenvPath), 0755); err != nil { + return err + } + buf := bytes.NewBuffer(nil) + if m.Mode != "" { + fmt.Fprintf(buf, "mode=%s\n", m.Mode) + } + if m.RecoverySystem != "" { + fmt.Fprintf(buf, "recovery_system=%s\n", m.RecoverySystem) + } + if m.Base != "" { + fmt.Fprintf(buf, "base=%s\n", m.Base) + } + if m.TryBase != "" { + fmt.Fprintf(buf, "try_base=%s\n", m.TryBase) + } + if m.BaseStatus != "" { + fmt.Fprintf(buf, "base_status=%s\n", m.BaseStatus) + } + if len(m.CurrentKernels) != 0 { + fmt.Fprintf(buf, "current_kernels=%s\n", strings.Join(m.CurrentKernels, ",")) + } + if m.Model != "" || m.Grade != "" { + if m.Model == "" { + return fmt.Errorf("internal error: model is unset") + } + if m.BrandID == "" { + return fmt.Errorf("internal error: brand is unset") + } + fmt.Fprintf(buf, "model=%s/%s\n", m.BrandID, m.Model) + } + if m.Grade != "" { + fmt.Fprintf(buf, "grade=%s\n", m.Grade) + } + + if err := osutil.AtomicWriteFile(modeenvPath, buf.Bytes(), 0644, 0); err != nil { + return err + } + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/boot/modeenv_test.go snapd-2.45.1ubuntu0.2/boot/modeenv_test.go --- snapd-2.37.4ubuntu0.1/boot/modeenv_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/boot/modeenv_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,274 @@ +// -*- 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_test + +import ( + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/testutil" +) + +// baseBootSuite is used to setup the common test environment +type modeenvSuite struct { + testutil.BaseTest + + tmpdir string + mockModeenvPath string +} + +var _ = Suite(&modeenvSuite{}) + +func (s *modeenvSuite) SetUpTest(c *C) { + s.tmpdir = c.MkDir() + s.mockModeenvPath = filepath.Join(s.tmpdir, dirs.SnapModeenvFile) +} + +func (s *modeenvSuite) TestReadEmptyErrors(c *C) { + modeenv, err := boot.ReadModeenv("/no/such/file") + c.Assert(os.IsNotExist(err), Equals, true) + c.Assert(modeenv, IsNil) +} + +func (s *modeenvSuite) makeMockModeenvFile(c *C, content string) { + err := os.MkdirAll(filepath.Dir(s.mockModeenvPath), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(s.mockModeenvPath, []byte(content), 0644) + c.Assert(err, IsNil) +} + +func (s *modeenvSuite) TestWasReadSanity(c *C) { + modeenv := &boot.Modeenv{} + c.Check(modeenv.WasRead(), Equals, false) +} + +func (s *modeenvSuite) TestReadEmpty(c *C) { + s.makeMockModeenvFile(c, "") + + modeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + c.Check(modeenv.Mode, Equals, "") + c.Check(modeenv.RecoverySystem, Equals, "") + // an empty modeenv still means the modeenv was set + c.Check(modeenv.WasRead(), Equals, true) +} + +func (s *modeenvSuite) TestReadMode(c *C) { + s.makeMockModeenvFile(c, "mode=run") + + modeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + c.Check(modeenv.Mode, Equals, "run") + c.Check(modeenv.RecoverySystem, Equals, "") + c.Check(modeenv.Base, Equals, "") +} + +func (s *modeenvSuite) TestReadModeWithRecoverySystem(c *C) { + s.makeMockModeenvFile(c, `mode=recovery +recovery_system=20191126 +`) + + modeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + c.Check(modeenv.Mode, Equals, "recovery") + c.Check(modeenv.RecoverySystem, Equals, "20191126") +} + +func (s *modeenvSuite) TestReadModeWithBase(c *C) { + s.makeMockModeenvFile(c, `mode=recovery +recovery_system=20191126 +base=core20_123.snap +try_base=core20_124.snap +base_status=try +`) + + modeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + c.Check(modeenv.Mode, Equals, "recovery") + c.Check(modeenv.RecoverySystem, Equals, "20191126") + c.Check(modeenv.Base, Equals, "core20_123.snap") + c.Check(modeenv.TryBase, Equals, "core20_124.snap") + c.Check(modeenv.BaseStatus, Equals, boot.TryStatus) +} + +func (s *modeenvSuite) TestReadModeWithGrade(c *C) { + s.makeMockModeenvFile(c, `mode=run +grade=dangerous +`) + modeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + c.Check(modeenv.Mode, Equals, "run") + c.Check(modeenv.Grade, Equals, "dangerous") + + s.makeMockModeenvFile(c, `mode=run +grade=some-random-grade-string +`) + modeenv, err = boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + c.Check(modeenv.Mode, Equals, "run") + c.Check(modeenv.Grade, Equals, "some-random-grade-string") +} + +func (s *modeenvSuite) TestReadModeWithModel(c *C) { + tt := []struct { + entry string + model, brand string + }{ + { + entry: "my-brand/my-model", + brand: "my-brand", + model: "my-model", + }, { + entry: "my-brand/", + }, { + entry: "my-model/", + }, { + entry: "foobar", + }, { + entry: "/", + }, { + entry: ",", + }, { + entry: "", + }, + } + + for _, t := range tt { + s.makeMockModeenvFile(c, `mode=run +model=`+t.entry+"\n") + modeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + c.Check(modeenv.Mode, Equals, "run") + c.Check(modeenv.Model, Equals, t.model) + c.Check(modeenv.BrandID, Equals, t.brand) + } +} + +func (s *modeenvSuite) TestReadModeWithCurrentKernels(c *C) { + + tt := []struct { + kernelString string + expectedKernels []string + }{ + { + "pc-kernel_1.snap", + []string{"pc-kernel_1.snap"}, + }, + { + "pc-kernel_1.snap,pc-kernel_2.snap", + []string{"pc-kernel_1.snap", "pc-kernel_2.snap"}, + }, + { + "pc-kernel_1.snap,,,,,pc-kernel_2.snap", + []string{"pc-kernel_1.snap", "pc-kernel_2.snap"}, + }, + // we should be robust in parsing the modeenv against garbage + { + `pc-kernel_1.snap,this-is-not-a-real-snap$%^&^%$#@#$%^%"$,pc-kernel_2.snap`, + []string{"pc-kernel_1.snap", `this-is-not-a-real-snap$%^&^%$#@#$%^%"$`, "pc-kernel_2.snap"}, + }, + {",,,", nil}, + {"", nil}, + } + + for _, t := range tt { + s.makeMockModeenvFile(c, `mode=recovery +recovery_system=20191126 +current_kernels=`+t.kernelString+"\n") + + modeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + c.Check(modeenv.Mode, Equals, "recovery") + c.Check(modeenv.RecoverySystem, Equals, "20191126") + c.Check(len(modeenv.CurrentKernels), Equals, len(t.expectedKernels)) + if len(t.expectedKernels) != 0 { + c.Check(modeenv.CurrentKernels, DeepEquals, t.expectedKernels) + } + } +} + +func (s *modeenvSuite) TestWriteToNonExisting(c *C) { + c.Assert(s.mockModeenvPath, testutil.FileAbsent) + + modeenv := &boot.Modeenv{Mode: "run"} + err := modeenv.WriteTo(s.tmpdir) + c.Assert(err, IsNil) + + c.Assert(s.mockModeenvPath, testutil.FileEquals, "mode=run\n") +} + +func (s *modeenvSuite) TestWriteToExisting(c *C) { + s.makeMockModeenvFile(c, "mode=run") + + modeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + modeenv.Mode = "recovery" + err = modeenv.WriteTo(s.tmpdir) + c.Assert(err, IsNil) + + c.Assert(s.mockModeenvPath, testutil.FileEquals, "mode=recovery\n") +} + +func (s *modeenvSuite) TestWriteExisting(c *C) { + s.makeMockModeenvFile(c, "mode=run") + + modeenv, err := boot.ReadModeenv(s.tmpdir) + c.Assert(err, IsNil) + modeenv.Mode = "recovery" + err = modeenv.Write() + c.Assert(err, IsNil) + + c.Assert(s.mockModeenvPath, testutil.FileEquals, "mode=recovery\n") +} + +func (s *modeenvSuite) TestWriteFreshError(c *C) { + modeenv := &boot.Modeenv{Mode: "recovery"} + + err := modeenv.Write() + c.Assert(err, ErrorMatches, `internal error: must use WriteTo with modeenv not read from disk`) +} + +func (s *modeenvSuite) TestWriteToNonExistingFull(c *C) { + c.Assert(s.mockModeenvPath, testutil.FileAbsent) + + modeenv := &boot.Modeenv{ + Mode: "run", + RecoverySystem: "20191128", + Base: "core20_321.snap", + TryBase: "core20_322.snap", + BaseStatus: boot.TryStatus, + CurrentKernels: []string{"pc-kernel_1.snap", "pc-kernel_2.snap"}, + } + err := modeenv.WriteTo(s.tmpdir) + c.Assert(err, IsNil) + + c.Assert(s.mockModeenvPath, testutil.FileEquals, `mode=run +recovery_system=20191128 +base=core20_321.snap +try_base=core20_322.snap +base_status=try +current_kernels=pc-kernel_1.snap,pc-kernel_2.snap +`) +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/androidbootenv/androidbootenv.go snapd-2.45.1ubuntu0.2/bootloader/androidbootenv/androidbootenv.go --- snapd-2.37.4ubuntu0.1/bootloader/androidbootenv/androidbootenv.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/androidbootenv/androidbootenv.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,90 @@ +// -*- 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 androidbootenv + +import ( + "bufio" + "bytes" + "fmt" + "os" + "strings" + + "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" +) + +type Env struct { + // Map with key-value strings + env map[string]string + // File for environment storage + path string +} + +func NewEnv(path string) *Env { + return &Env{ + env: make(map[string]string), + path: path, + } +} + +func (a *Env) Get(name string) string { + return a.env[name] +} + +func (a *Env) Set(key, value string) { + a.env[key] = value +} + +func (a *Env) Load() error { + file, err := os.Open(a.path) + if err != nil { + return err + } + defer file.Close() + + scanner := bufio.NewScanner(file) + for scanner.Scan() { + l := strings.SplitN(scanner.Text(), "=", 2) + // be liberal in what you accept + if len(l) < 2 { + logger.Noticef("WARNING: bad value while parsing %v (line: %q)", + a.path, scanner.Text()) + continue + } + a.env[l[0]] = l[1] + } + if err := scanner.Err(); err != nil { + return err + } + + return nil +} + +func (a *Env) Save() error { + var w bytes.Buffer + + for k, v := range a.env { + if _, err := fmt.Fprintf(&w, "%s=%s\n", k, v); err != nil { + return err + } + } + + return osutil.AtomicWriteFile(a.path, w.Bytes(), 0644, 0) +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/androidbootenv/androidbootenv_test.go snapd-2.45.1ubuntu0.2/bootloader/androidbootenv/androidbootenv_test.go --- snapd-2.37.4ubuntu0.1/bootloader/androidbootenv/androidbootenv_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/androidbootenv/androidbootenv_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,69 @@ +// -*- 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 androidbootenv_test + +import ( + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/bootloader/androidbootenv" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type androidbootenvTestSuite struct { + envPath string + env *androidbootenv.Env +} + +var _ = Suite(&androidbootenvTestSuite{}) + +func (a *androidbootenvTestSuite) SetUpTest(c *C) { + a.envPath = filepath.Join(c.MkDir(), "androidbootenv") + a.env = androidbootenv.NewEnv(a.envPath) + c.Assert(a.env, NotNil) +} + +func (a *androidbootenvTestSuite) TestSet(c *C) { + a.env.Set("key", "value") + c.Check(a.env.Get("key"), Equals, "value") +} + +func (a *androidbootenvTestSuite) TestSaveAndLoad(c *C) { + a.env.Set("key1", "value1") + a.env.Set("key2", "") + a.env.Set("key3", "value3") + + err := a.env.Save() + c.Assert(err, IsNil) + + env2 := androidbootenv.NewEnv(a.envPath) + c.Check(env2, NotNil) + + err = env2.Load() + c.Assert(err, IsNil) + + c.Assert(env2.Get("key1"), Equals, "value1") + c.Assert(env2.Get("key2"), Equals, "") + c.Assert(env2.Get("key3"), Equals, "value3") +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/androidboot.go snapd-2.45.1ubuntu0.2/bootloader/androidboot.go --- snapd-2.37.4ubuntu0.1/bootloader/androidboot.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/androidboot.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,101 @@ +// -*- 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 bootloader + +import ( + "os" + "path/filepath" + + "github.com/snapcore/snapd/bootloader/androidbootenv" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + +type androidboot struct { + rootdir string +} + +// newAndroidboot creates a new Androidboot bootloader object +func newAndroidBoot(rootdir string) Bootloader { + a := &androidboot{rootdir: rootdir} + if !osutil.FileExists(a.ConfigFile()) { + return nil + } + return a +} + +func (a *androidboot) Name() string { + return "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) InstallBootConfig(gadgetDir string, opts *Options) (bool, error) { + gadgetFile := filepath.Join(gadgetDir, a.Name()+".conf") + systemFile := a.ConfigFile() + return genericInstallBootConfig(gadgetFile, systemFile) +} + +func (a *androidboot) ConfigFile() string { + return filepath.Join(a.dir(), "androidboot.env") +} + +func (a *androidboot) GetBootVars(names ...string) (map[string]string, error) { + env := androidbootenv.NewEnv(a.ConfigFile()) + if err := env.Load(); err != nil { + return nil, err + } + + out := make(map[string]string, len(names)) + for _, name := range names { + out[name] = env.Get(name) + } + + return out, nil +} + +func (a *androidboot) SetBootVars(values map[string]string) error { + env := androidbootenv.NewEnv(a.ConfigFile()) + if err := env.Load(); err != nil && !os.IsNotExist(err) { + return err + } + for k, v := range values { + env.Set(k, v) + } + return env.Save() +} + +func (a *androidboot) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error { + return nil + +} + +func (a *androidboot) RemoveKernelAssets(s snap.PlaceInfo) error { + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/androidboot_test.go snapd-2.45.1ubuntu0.2/bootloader/androidboot_test.go --- snapd-2.37.4ubuntu0.1/bootloader/androidboot_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/androidboot_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,96 @@ +// -*- 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 bootloader_test + +import ( + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapfile" + "github.com/snapcore/snapd/snap/snaptest" +) + +type androidBootTestSuite struct { + baseBootenvTestSuite +} + +var _ = Suite(&androidBootTestSuite{}) + +func (s *androidBootTestSuite) SetUpTest(c *C) { + s.baseBootenvTestSuite.SetUpTest(c) + + // the file needs to exist for androidboot object to be created + bootloader.MockAndroidBootFile(c, s.rootdir, 0644) +} + +func (s *androidBootTestSuite) TestNewAndroidbootNoAndroidbootReturnsNil(c *C) { + a := bootloader.NewAndroidBoot("/something/not/there") + c.Assert(a, IsNil) +} + +func (s *androidBootTestSuite) TestNewAndroidboot(c *C) { + a := bootloader.NewAndroidBoot(s.rootdir) + c.Assert(a, NotNil) +} + +func (s *androidBootTestSuite) TestSetGetBootVar(c *C) { + a := bootloader.NewAndroidBoot(s.rootdir) + bootVars := map[string]string{"snap_mode": boot.TryStatus} + a.SetBootVars(bootVars) + + v, err := a.GetBootVars("snap_mode") + c.Assert(err, IsNil) + c.Check(v, HasLen, 1) + c.Check(v["snap_mode"], Equals, boot.TryStatus) +} + +func (s *androidBootTestSuite) TestExtractKernelAssetsNoUnpacksKernel(c *C) { + a := bootloader.NewAndroidBoot(s.rootdir) + + c.Assert(a, NotNil) + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snapfile.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = a.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // kernel is *not* here + kernimg := filepath.Join(s.rootdir, "boot", "androidboot", "ubuntu-kernel_42.snap", "kernel.img") + c.Assert(osutil.FileExists(kernimg), Equals, false) +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/bootloader.go snapd-2.45.1ubuntu0.2/bootloader/bootloader.go --- snapd-2.37.4ubuntu0.1/bootloader/bootloader.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/bootloader.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,258 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 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 ( + "errors" + "fmt" + "os" + "path/filepath" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + +var ( + // ErrBootloader is returned if the bootloader can not be determined. + ErrBootloader = errors.New("cannot determine bootloader") + + // ErrNoTryKernelRef is returned if the bootloader finds no enabled + // try-kernel. + ErrNoTryKernelRef = errors.New("no try-kernel referenced") +) + +// 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 + + // Recovery indicates to use the recovery bootloader. Note that + // UC16/18 do not have a recovery partition. + Recovery bool + + // NoSlashBoot indicates to use the run mode bootloader but + // under the native layout and not the /boot mount. + NoSlashBoot bool + + // ExtractedRunKernelImage is whether to force kernel asset extraction. + ExtractedRunKernelImage bool +} + +// Bootloader provides an interface to interact with the system +// bootloader. +type Bootloader interface { + // Return the value of the specified bootloader variable. + GetBootVars(names ...string) (map[string]string, error) + + // Set the value of the specified bootloader variable. + SetBootVars(values map[string]string) error + + // Name returns the bootloader name. + Name() string + + // ConfigFile returns the name of the config file. + ConfigFile() string + + // InstallBootConfig will try to install the boot config in the + // given gadgetDir to rootdir. If no boot config for this bootloader + // is found ok is false. + InstallBootConfig(gadgetDir string, opts *Options) (ok bool, err error) + + // ExtractKernelAssets extracts kernel assets from the given kernel snap. + 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) +} + +type RecoveryAwareBootloader interface { + Bootloader + SetRecoverySystemEnv(recoverySystemDir string, values map[string]string) error +} + +type ExtractedRecoveryKernelImageBootloader interface { + Bootloader + ExtractRecoveryKernelAssets(recoverySystemDir string, s snap.PlaceInfo, snapf snap.Container) error +} + +// ExtractedRunKernelImageBootloader is a Bootloader that also supports specific +// methods needed to setup booting from an extracted kernel, which is needed to +// implement encryption and/or secure boot. Prototypical implementation is UC20 +// grub implementation with FDE. +type ExtractedRunKernelImageBootloader interface { + Bootloader + + // EnableKernel enables the specified kernel on ubuntu-boot to be used + // during normal boots. The specified kernel should already have been + // extracted. This is usually implemented with a "kernel.efi" symlink + // pointing to the extracted kernel image. + EnableKernel(snap.PlaceInfo) error + + // EnableTryKernel enables the specified kernel on ubuntu-boot to be + // tried by the bootloader on a reboot, to be used in conjunction with + // setting "kernel_status" to "try". The specified kernel should already + // have been extracted. This is usually implemented with a + // "try-kernel.efi" symlink pointing to the extracted kernel image. + EnableTryKernel(snap.PlaceInfo) error + + // Kernel returns the current enabled kernel on the bootloader, not + // necessarily the kernel that was used to boot the current session, but the + // kernel that is enabled to boot on "normal" boots. + // If error is not nil, the first argument shall be non-nil. + Kernel() (snap.PlaceInfo, error) + + // TryKernel returns the current enabled try-kernel on the bootloader, if + // there is no such enabled try-kernel, then ErrNoTryKernelRef is returned. + // If error is not nil, the first argument shall be non-nil. + TryKernel() (snap.PlaceInfo, error) + + // DisableTryKernel disables the current enabled try-kernel on the + // bootloader, if it exists. It does not need to return an error if the + // enabled try-kernel does not exist or is in an inconsistent state before + // disabling it, errors should only be returned when the implementation + // fails to disable the try-kernel. + DisableTryKernel() error +} + +func genericInstallBootConfig(gadgetFile, systemFile string) (bool, error) { + if !osutil.FileExists(gadgetFile) { + return false, nil + } + if err := os.MkdirAll(filepath.Dir(systemFile), 0755); err != nil { + return true, err + } + return true, osutil.CopyFile(gadgetFile, systemFile, osutil.CopyFlagOverwrite) +} + +// InstallBootConfig installs the bootloader config from the gadget +// snap dir into the right place. +func InstallBootConfig(gadgetDir, rootDir string, opts *Options) error { + for _, bl := range []installableBootloader{&grub{}, &uboot{}, &androidboot{}, &lk{}} { + bl.setRootDir(rootDir) + ok, err := bl.InstallBootConfig(gadgetDir, opts) + if ok { + return err + } + } + + return fmt.Errorf("cannot find boot config in %q", gadgetDir) +} + +var ( + forcedBootloader Bootloader + forcedError error +) + +// Find returns the bootloader for the system +// or an error if no bootloader is found. +// +// The rootdir option is useful for image creation operations. It +// can also be used to find the recovery bootloader, e.g. on uc20: +// bootloader.Find("/run/mnt/ubuntu-seed") +func Find(rootdir string, opts *Options) (Bootloader, error) { + if forcedBootloader != nil || forcedError != nil { + return forcedBootloader, forcedError + } + + if rootdir == "" { + rootdir = dirs.GlobalRootDir + } + if opts == nil { + opts = &Options{} + } + + // try uboot + if uboot := newUboot(rootdir, opts); uboot != nil { + return uboot, nil + } + + // no, try grub + if grub := newGrub(rootdir, opts); grub != nil { + return grub, nil + } + + // no, try androidboot + 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 Find to always find the specified bootloader; use +// nil to reset to normal lookup. +func Force(booloader Bootloader) { + forcedBootloader = booloader + forcedError = nil +} + +// ForceError 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(dstDir string, snapf snap.Container, assets []string) error { + // now do the kernel specific bits + if err := os.MkdirAll(dstDir, 0755); err != nil { + return err + } + dir, err := os.Open(dstDir) + if err != nil { + return err + } + defer dir.Close() + + for _, src := range assets { + if err := snapf.Unpack(src, dstDir); err != nil { + return err + } + if err := dir.Sync(); err != nil { + return err + } + } + return nil +} + +func removeKernelAssetsFromBootDir(bootDir string, s snap.PlaceInfo) error { + // remove the kernel blob + blobName := s.Filename() + dstDir := filepath.Join(bootDir, blobName) + if err := os.RemoveAll(dstDir); err != nil { + return err + } + + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/bootloadertest/bootloadertest.go snapd-2.45.1ubuntu0.2/bootloader/bootloadertest/bootloadertest.go --- snapd-2.37.4ubuntu0.1/bootloader/bootloadertest/bootloadertest.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/bootloadertest/bootloadertest.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,337 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package bootloadertest + +import ( + "fmt" + "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 + SetBootVarsCalls int + SetErr error + GetErr error + + name string + bootdir string + + ExtractKernelAssetsCalls []snap.PlaceInfo + RemoveKernelAssetsCalls []snap.PlaceInfo + + InstallBootConfigCalled []string + InstallBootConfigResult bool + InstallBootConfigErr error + + panicMethods map[string]bool +} + +// ensure MockBootloader(s) implement the Bootloader interfaceces +var _ bootloader.Bootloader = (*MockBootloader)(nil) +var _ bootloader.RecoveryAwareBootloader = (*MockRecoveryAwareBootloader)(nil) +var _ bootloader.ExtractedRunKernelImageBootloader = (*MockExtractedRunKernelImageBootloader)(nil) +var _ bootloader.ExtractedRecoveryKernelImageBootloader = (*MockExtractedRecoveryKernelImageBootloader)(nil) + +func Mock(name, bootdir string) *MockBootloader { + return &MockBootloader{ + name: name, + bootdir: bootdir, + + BootVars: make(map[string]string), + + panicMethods: make(map[string]bool), + } +} + +func (b *MockBootloader) maybePanic(which string) { + if b.panicMethods[which] { + panic(fmt.Sprintf("mocked reboot panic in %s", which)) + } +} + +func (b *MockBootloader) SetBootVars(values map[string]string) error { + b.maybePanic("SetBootVars") + b.SetBootVarsCalls++ + for k, v := range values { + b.BootVars[k] = v + } + return b.SetErr +} + +func (b *MockBootloader) GetBootVars(keys ...string) (map[string]string, error) { + b.maybePanic("GetBootVars") + + 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 +} + +// InstallBootConfig installs the boot config in the gadget directory to the +// mock bootloader's root directory. +func (b *MockBootloader) InstallBootConfig(gadgetDir string, opts *bootloader.Options) (bool, error) { + b.InstallBootConfigCalled = append(b.InstallBootConfigCalled, gadgetDir) + return b.InstallBootConfigResult, b.InstallBootConfigErr +} + +// MockRecoveryAwareBootloader mocks a bootloader implementing the +// RecoveryAware interface. +type MockRecoveryAwareBootloader struct { + *MockBootloader + + RecoverySystemDir string + RecoverySystemBootVars map[string]string +} + +type ExtractedRecoveryKernelCall struct { + RecoverySystemDir string + S snap.PlaceInfo +} + +// MockExtractedRecoveryKernelImageBootloader mocks a bootloader implementing +// the ExtractedRecoveryKernelImage interface. +type MockExtractedRecoveryKernelImageBootloader struct { + *MockBootloader + + ExtractRecoveryKernelAssetsCalls []ExtractedRecoveryKernelCall +} + +// ExtractedRecoveryKernelImage derives a MockRecoveryAwareBootloader from a base +// MockBootloader. +func (b *MockBootloader) ExtractedRecoveryKernelImage() *MockExtractedRecoveryKernelImageBootloader { + return &MockExtractedRecoveryKernelImageBootloader{MockBootloader: b} +} + +// ExtractRecoveryKernelAssets extracts the kernel assets for the provided +// kernel snap into the specified recovery system dir; part of +// RecoveryAwareBootloader. +func (b *MockExtractedRecoveryKernelImageBootloader) ExtractRecoveryKernelAssets(recoverySystemDir string, s snap.PlaceInfo, snapf snap.Container) error { + if recoverySystemDir == "" { + panic("MockBootloader.ExtractRecoveryKernelAssets called without recoverySystemDir") + } + + b.ExtractRecoveryKernelAssetsCalls = append( + b.ExtractRecoveryKernelAssetsCalls, + ExtractedRecoveryKernelCall{ + S: s, + RecoverySystemDir: recoverySystemDir}, + ) + return nil +} + +// RecoveryAware derives a MockRecoveryAwareBootloader from a base +// MockBootloader. +func (b *MockBootloader) RecoveryAware() *MockRecoveryAwareBootloader { + return &MockRecoveryAwareBootloader{MockBootloader: b} +} + +// SetRecoverySystemEnv sets the recovery system environment bootloader +// variables; part of RecoveryAwareBootloader. +func (b *MockRecoveryAwareBootloader) SetRecoverySystemEnv(recoverySystemDir string, blVars map[string]string) error { + if recoverySystemDir == "" { + panic("MockBootloader.SetRecoverySystemEnv called without recoverySystemDir") + } + b.RecoverySystemDir = recoverySystemDir + b.RecoverySystemBootVars = blVars + return nil +} + +// MockExtractedRunKernelImageBootloader mocks a bootloader +// implementing the ExtractedRunKernelImageBootloader interface. +type MockExtractedRunKernelImageBootloader struct { + *MockBootloader + + runKernelImageEnableKernelCalls []snap.PlaceInfo + runKernelImageEnableTryKernelCalls []snap.PlaceInfo + runKernelImageDisableTryKernelCalls []snap.PlaceInfo + runKernelImageEnabledKernel snap.PlaceInfo + runKernelImageEnabledTryKernel snap.PlaceInfo + + runKernelImageMockedErrs map[string]error + runKernelImageMockedNumCalls map[string]int +} + +// WithExtractedRunKernelImage derives a MockExtractedRunKernelImageBootloader +// from a base MockBootloader. +func (b *MockBootloader) WithExtractedRunKernelImage() *MockExtractedRunKernelImageBootloader { + return &MockExtractedRunKernelImageBootloader{ + MockBootloader: b, + + runKernelImageMockedErrs: make(map[string]error), + runKernelImageMockedNumCalls: make(map[string]int), + } +} + +// SetRunKernelImageEnabledKernel sets the current kernel "symlink" as returned +// by Kernel(); returns' a restore function to set it back to what it was +// before. +func (b *MockExtractedRunKernelImageBootloader) SetRunKernelImageEnabledKernel(kernel snap.PlaceInfo) (restore func()) { + old := b.runKernelImageEnabledKernel + b.runKernelImageEnabledKernel = kernel + return func() { + b.runKernelImageEnabledKernel = old + } +} + +// SetRunKernelImageEnabledTryKernel sets the current try-kernel "symlink" as +// returned by TryKernel(). If set to nil, TryKernel()'s second return value +// will be false; returns' a restore function to set it back to what it was +// before. +func (b *MockExtractedRunKernelImageBootloader) SetRunKernelImageEnabledTryKernel(kernel snap.PlaceInfo) (restore func()) { + old := b.runKernelImageEnabledTryKernel + b.runKernelImageEnabledTryKernel = kernel + return func() { + b.runKernelImageEnabledTryKernel = old + } +} + +// SetRunKernelImageFunctionError allows setting an error to be returned for the +// specified function; it returns a restore function to set it back to what it +// was before. +func (b *MockExtractedRunKernelImageBootloader) SetRunKernelImageFunctionError(f string, err error) (restore func()) { + // check the function + switch f { + case "EnableKernel", "EnableTryKernel", "Kernel", "TryKernel", "DisableTryKernel": + old := b.runKernelImageMockedErrs[f] + b.runKernelImageMockedErrs[f] = err + return func() { + b.runKernelImageMockedErrs[f] = old + } + default: + panic(fmt.Sprintf("unknown ExtractedRunKernelImageBootloader method %q to mock error for", f)) + } +} + +// SetRunKernelImagePanic allows setting any method in the +// ExtractedRunKernelImageBootloader interface on +// MockExtractedRunKernelImageBootloader to panic instead of +// returning. This allows one to test what would happen if the system +// was rebooted during execution of a particular +// function. Specifically, the panic will be done immediately entering +// the function so setting SetBootVars to panic will emulate a reboot +// before any boot vars are set persistently +func (b *MockExtractedRunKernelImageBootloader) SetRunKernelImagePanic(f string) (restore func()) { + switch f { + case "EnableKernel", "EnableTryKernel", "Kernel", "TryKernel", "DisableTryKernel", "SetBootVars", "GetBootVars": + old := b.panicMethods[f] + b.panicMethods[f] = true + return func() { + b.panicMethods[f] = old + } + default: + panic(fmt.Sprintf("unknown ExtractedRunKernelImageBootloader method %q to mock reboot via panic for", f)) + } +} + +// GetRunKernelImageFunctionSnapCalls returns which snaps were specified during +// execution, in order of calls, as well as the number of calls for methods that +// don't take a snap to set. +func (b *MockExtractedRunKernelImageBootloader) GetRunKernelImageFunctionSnapCalls(f string) ([]snap.PlaceInfo, int) { + switch f { + case "EnableKernel": + l := b.runKernelImageEnableKernelCalls + return l, len(l) + case "EnableTryKernel": + l := b.runKernelImageEnableTryKernelCalls + return l, len(l) + case "Kernel", "TryKernel", "DisableTryKernel": + return nil, b.runKernelImageMockedNumCalls[f] + default: + panic(fmt.Sprintf("unknown ExtractedRunKernelImageBootloader method %q to return snap args for", f)) + } +} + +// EnableKernel enables the kernel; part of ExtractedRunKernelImageBootloader. +func (b *MockExtractedRunKernelImageBootloader) EnableKernel(s snap.PlaceInfo) error { + b.maybePanic("EnableKernel") + b.runKernelImageEnableKernelCalls = append(b.runKernelImageEnableKernelCalls, s) + b.runKernelImageEnabledKernel = s + return b.runKernelImageMockedErrs["EnableKernel"] +} + +// EnableTryKernel enables a try-kernel; part of +// ExtractedRunKernelImageBootloader. +func (b *MockExtractedRunKernelImageBootloader) EnableTryKernel(s snap.PlaceInfo) error { + b.maybePanic("EnableTryKernel") + b.runKernelImageEnableTryKernelCalls = append(b.runKernelImageEnableTryKernelCalls, s) + b.runKernelImageEnabledTryKernel = s + return b.runKernelImageMockedErrs["EnableTryKernel"] +} + +// Kernel returns the current kernel set in the bootloader; part of +// ExtractedRunKernelImageBootloader. +func (b *MockExtractedRunKernelImageBootloader) Kernel() (snap.PlaceInfo, error) { + b.maybePanic("Kernel") + b.runKernelImageMockedNumCalls["Kernel"]++ + err := b.runKernelImageMockedErrs["Kernel"] + if err != nil { + return nil, err + } + return b.runKernelImageEnabledKernel, nil +} + +// TryKernel returns the current kernel set in the bootloader; part of +// ExtractedRunKernelImageBootloader. +func (b *MockExtractedRunKernelImageBootloader) TryKernel() (snap.PlaceInfo, error) { + b.maybePanic("TryKernel") + b.runKernelImageMockedNumCalls["TryKernel"]++ + err := b.runKernelImageMockedErrs["TryKernel"] + if err != nil { + return nil, err + } + if b.runKernelImageEnabledTryKernel == nil { + return nil, bootloader.ErrNoTryKernelRef + } + return b.runKernelImageEnabledTryKernel, nil +} + +// DisableTryKernel removes the current try-kernel "symlink" set in the +// bootloader; part of ExtractedRunKernelImageBootloader. +func (b *MockExtractedRunKernelImageBootloader) DisableTryKernel() error { + b.maybePanic("DisableTryKernel") + b.runKernelImageMockedNumCalls["DisableTryKernel"]++ + b.runKernelImageEnabledTryKernel = nil + return b.runKernelImageMockedErrs["DisableTryKernel"] +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/bootloadertest/utf16.go snapd-2.45.1ubuntu0.2/bootloader/bootloadertest/utf16.go --- snapd-2.37.4ubuntu0.1/bootloader/bootloadertest/utf16.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/bootloadertest/utf16.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,37 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package bootloadertest + +import ( + "bytes" + "encoding/binary" + "unicode/utf16" +) + +// UTF16Bytes converts the given string into its UTF16 +// encoding. Convenient for use together with efi.MockVars. +func UTF16Bytes(s string) []byte { + r16 := utf16.Encode(bytes.Runes([]byte(s))) + b := bytes.NewBuffer(make([]byte, 0, (len(r16)+1)*2)) + binary.Write(b, binary.LittleEndian, r16) + // zero termination + binary.Write(b, binary.LittleEndian, uint16(0)) + return b.Bytes() +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/bootloader_test.go snapd-2.45.1ubuntu0.2/bootloader/bootloader_test.go --- snapd-2.37.4ubuntu0.1/bootloader/bootloader_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/bootloader_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,125 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 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 ( + "errors" + "io/ioutil" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/bootloader/bootloadertest" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +const packageKernel = ` +name: ubuntu-kernel +version: 4.0-1 +type: kernel +vendor: Someone +` + +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) {})) + s.rootdir = c.MkDir() +} + +type bootenvTestSuite struct { + baseBootenvTestSuite + + b *bootloadertest.MockBootloader +} + +var _ = Suite(&bootenvTestSuite{}) + +func (s *bootenvTestSuite) SetUpTest(c *C) { + s.baseBootenvTestSuite.SetUpTest(c) + + 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("", nil) + c.Assert(err, IsNil) + c.Check(got, Equals, s.b) +} + +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(), s.rootdir, nil) + c.Assert(err, ErrorMatches, `cannot find boot config in.*`) +} + +func (s *bootenvTestSuite) TestInstallBootloaderConfig(c *C) { + for _, t := range []struct { + name string + gadgetFile, sysFile string + gadgetFileContent []byte + opts *bootloader.Options + }{ + {name: "grub", gadgetFile: "grub.conf", sysFile: "/boot/grub/grub.cfg"}, + // traditional uboot.env - the uboot.env file needs to be non-empty + {name: "uboot.env", gadgetFile: "uboot.conf", sysFile: "/boot/uboot/uboot.env", gadgetFileContent: []byte{1}}, + // boot.scr in place of uboot.env means we create the boot.sel file + { + name: "uboot boot.scr", + gadgetFile: "uboot.conf", + sysFile: "/uboot/ubuntu/boot.sel", + opts: &bootloader.Options{NoSlashBoot: true}, + }, + {name: "androidboot", gadgetFile: "androidboot.conf", sysFile: "/boot/androidboot/androidboot.env"}, + {name: "lk", gadgetFile: "lk.conf", sysFile: "/boot/lk/snapbootsel.bin"}, + {name: "grub recovery", gadgetFile: "grub-recovery.conf", sysFile: "/EFI/ubuntu/grub.cfg", opts: &bootloader.Options{Recovery: true}}, + } { + mockGadgetDir := c.MkDir() + err := ioutil.WriteFile(filepath.Join(mockGadgetDir, t.gadgetFile), t.gadgetFileContent, 0644) + c.Assert(err, IsNil) + err = bootloader.InstallBootConfig(mockGadgetDir, s.rootdir, t.opts) + c.Assert(err, IsNil, Commentf("installing boot config for %s", t.name)) + fn := filepath.Join(s.rootdir, t.sysFile) + c.Assert(fn, testutil.FilePresent, Commentf("boot config missing for %s at %s", t.name, t.sysFile)) + } +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/efi/efi.go snapd-2.45.1ubuntu0.2/bootloader/efi/efi.go --- snapd-2.37.4ubuntu0.1/bootloader/efi/efi.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/efi/efi.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,188 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// Package efi supports reading EFI variables. +package efi + +import ( + "bytes" + "encoding/binary" + "errors" + "fmt" + "io" + "io/ioutil" + "os" + "path/filepath" + "strings" + "unicode/utf16" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +var ErrNoEFISystem = errors.New("not a supported EFI system") + +type VariableAttr uint32 + +// see https://git.kernel.org/pub/scm/linux/kernel/git/stable/linux.git/tree/include/linux/efi.h?h=v5.4.32 +const ( + VariableNonVolatile VariableAttr = 0x00000001 + VariableBootServiceAccess VariableAttr = 0x00000002 + VariableRuntimeAccess VariableAttr = 0x00000004 +) + +var ( + isSnapdTest = len(os.Args) > 0 && strings.HasSuffix(os.Args[0], ".test") + openEFIVar = openEFIVarImpl +) + +const expectedEFIvarfsDir = "/sys/firmware/efi/efivars" + +func openEFIVarImpl(name string) (r io.ReadCloser, attr VariableAttr, size int64, err error) { + mounts, err := osutil.LoadMountInfo() + if err != nil { + return nil, 0, 0, err + } + found := false + for _, mnt := range mounts { + if mnt.MountDir == expectedEFIvarfsDir { + if mnt.FsType == "efivarfs" { + found = true + break + } + } + } + if !found { + return nil, 0, 0, ErrNoEFISystem + } + varf, err := os.Open(filepath.Join(dirs.GlobalRootDir, expectedEFIvarfsDir, name)) + if err != nil { + return nil, 0, 0, err + } + defer func() { + if err != nil { + varf.Close() + } + }() + fi, err := varf.Stat() + if err != nil { + return nil, 0, 0, err + } + sz := fi.Size() + if sz < 4 { + return nil, 0, 0, fmt.Errorf("unexpected size: %d", sz) + } + + if err = binary.Read(varf, binary.LittleEndian, &attr); err != nil { + return nil, 0, 0, err + } + return varf, attr, sz - 4, nil +} + +func cannotReadError(name string, err error) error { + return fmt.Errorf("cannot read EFI var %q: %v", name, err) +} + +// ReadVarBytes will attempt to read the bytes of the value of the +// specified EFI variable, specified by its full name composed of the +// variable name and vendor ID. It also returns the attribute value +// attached to it. It expects to use the efivars filesystem at +// /sys/firmware/efi/efivars. +// https://www.kernel.org/doc/Documentation/filesystems/efivarfs.txt +// for more details. +func ReadVarBytes(name string) ([]byte, VariableAttr, error) { + varf, attr, _, err := openEFIVar(name) + if err != nil { + if err == ErrNoEFISystem { + return nil, 0, err + } + return nil, 0, cannotReadError(name, err) + } + defer varf.Close() + b, err := ioutil.ReadAll(varf) + if err != nil { + return nil, 0, cannotReadError(name, err) + } + return b, attr, nil +} + +// ReadVarString will attempt to read the string value of the +// specified EFI variable, specified by its full name composed of the +// variable name and vendor ID. The string value is expected to be +// encoded as UTF16. It also returns the attribute value attached to +// it. It expects to use the efivars filesystem at +// /sys/firmware/efi/efivars. +// https://www.kernel.org/doc/Documentation/filesystems/efivarfs.txt +// for more details. +func ReadVarString(name string) (string, VariableAttr, error) { + varf, attr, sz, err := openEFIVar(name) + if err != nil { + if err == ErrNoEFISystem { + return "", 0, err + } + return "", 0, cannotReadError(name, err) + } + defer varf.Close() + // TODO: consider using golang.org/x/text/encoding/unicode here + if sz%2 != 0 { + return "", 0, fmt.Errorf("EFI var %q is not a valid UTF16 string, it has an extra byte", name) + } + n := int(sz / 2) + if n == 0 { + return "", attr, nil + } + r16 := make([]uint16, n) + if err := binary.Read(varf, binary.LittleEndian, r16); err != nil { + return "", 0, cannotReadError(name, err) + } + if r16[n-1] == 0 { + n-- + } + b := &bytes.Buffer{} + for _, r := range utf16.Decode(r16[:n]) { + b.WriteRune(r) + } + return b.String(), attr, nil +} + +// MockVars mocks EFI variables as read by ReadVar*, only to be used +// from tests. Set vars to nil to mock a non-EFI system. +func MockVars(vars map[string][]byte, attrs map[string]VariableAttr) (restore func()) { + if !isSnapdTest { + panic("MockVars only to be used from tests") + } + old := openEFIVar + openEFIVar = func(name string) (io.ReadCloser, VariableAttr, int64, error) { + if vars == nil { + return nil, 0, 0, ErrNoEFISystem + } + if val, ok := vars[name]; ok { + attr, ok := attrs[name] + if !ok { + attr = VariableRuntimeAccess | VariableBootServiceAccess + } + return ioutil.NopCloser(bytes.NewBuffer(val)), attr, int64(len(val)), nil + } + return nil, 0, 0, fmt.Errorf("EFI variable %s not mocked", name) + } + + return func() { + openEFIVar = old + } +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/efi/efi_test.go snapd-2.45.1ubuntu0.2/bootloader/efi/efi_test.go --- snapd-2.37.4ubuntu0.1/bootloader/efi/efi_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/efi/efi_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,174 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package efi_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/bootloader/bootloadertest" + "github.com/snapcore/snapd/bootloader/efi" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/testutil" +) + +type efiVarsSuite struct { + testutil.BaseTest + + rootdir string +} + +var _ = Suite(&efiVarsSuite{}) + +func TestBoot(t *testing.T) { TestingT(t) } + +func (s *efiVarsSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + s.rootdir = c.MkDir() + dirs.SetRootDir(s.rootdir) + s.AddCleanup(func() { dirs.SetRootDir("") }) + + err := os.MkdirAll(filepath.Join(s.rootdir, "/sys/firmware/efi/efivars"), 0755) + c.Assert(err, IsNil) + + efivarfsMount := ` +38 24 0:32 / /sys/firmware/efi/efivars rw,nosuid,nodev,noexec,relatime shared:13 - efivarfs efivarfs rw +` + restore := osutil.MockMountInfo(strings.TrimSpace(efivarfsMount)) + s.AddCleanup(restore) +} + +func (s *efiVarsSuite) TestNoEFISystem(c *C) { + // no efivarfs + osutil.MockMountInfo("") + + _, _, err := efi.ReadVarBytes("my-cool-efi-var") + c.Check(err, Equals, efi.ErrNoEFISystem) + + _, _, err = efi.ReadVarString("my-cool-efi-var") + c.Check(err, Equals, efi.ErrNoEFISystem) +} + +func (s *efiVarsSuite) TestSizeError(c *C) { + // mock the efi var file + varPath := filepath.Join(s.rootdir, "/sys/firmware/efi/efivars", "my-cool-efi-var") + err := ioutil.WriteFile(varPath, []byte("\x06"), 0644) + c.Assert(err, IsNil) + + _, _, err = efi.ReadVarBytes("my-cool-efi-var") + c.Check(err, ErrorMatches, `cannot read EFI var "my-cool-efi-var": unexpected size: 1`) +} + +func (s *efiVarsSuite) TestReadVarBytes(c *C) { + // mock the efi var file + varPath := filepath.Join(s.rootdir, "/sys/firmware/efi/efivars", "my-cool-efi-var") + err := ioutil.WriteFile(varPath, []byte("\x06\x00\x00\x00\x01"), 0644) + c.Assert(err, IsNil) + + data, attr, err := efi.ReadVarBytes("my-cool-efi-var") + c.Assert(err, IsNil) + c.Check(attr, Equals, efi.VariableBootServiceAccess|efi.VariableRuntimeAccess) + c.Assert(string(data), Equals, "\x01") +} + +func (s *efiVarsSuite) TestReadVarString(c *C) { + // mock the efi var file + varPath := filepath.Join(s.rootdir, "/sys/firmware/efi/efivars", "my-cool-efi-var") + err := ioutil.WriteFile(varPath, []byte("\x06\x00\x00\x00A\x009\x00F\x005\x00C\x009\x004\x009\x00-\x00A\x00B\x008\x009\x00-\x005\x00B\x004\x007\x00-\x00A\x007\x00B\x00F\x00-\x005\x006\x00D\x00D\x002\x008\x00F\x009\x006\x00E\x006\x005\x00\x00\x00"), 0644) + c.Assert(err, IsNil) + + data, attr, err := efi.ReadVarString("my-cool-efi-var") + c.Assert(err, IsNil) + c.Check(attr, Equals, efi.VariableBootServiceAccess|efi.VariableRuntimeAccess) + c.Assert(data, Equals, "A9F5C949-AB89-5B47-A7BF-56DD28F96E65") +} + +func (s *efiVarsSuite) TestEmpty(c *C) { + // mock the efi var file + varPath := filepath.Join(s.rootdir, "/sys/firmware/efi/efivars", "my-cool-efi-var") + err := ioutil.WriteFile(varPath, []byte("\x06\x00\x00\x00"), 0644) + c.Assert(err, IsNil) + + b, _, err := efi.ReadVarBytes("my-cool-efi-var") + c.Assert(err, IsNil) + c.Check(b, HasLen, 0) + + v, _, err := efi.ReadVarString("my-cool-efi-var") + c.Assert(err, IsNil) + c.Check(v, HasLen, 0) +} + +func (s *efiVarsSuite) TestMockVars(c *C) { + restore := efi.MockVars(map[string][]byte{ + "a": []byte("\x01"), + "b": []byte("\x02"), + }, map[string]efi.VariableAttr{ + "b": efi.VariableNonVolatile | efi.VariableRuntimeAccess | efi.VariableBootServiceAccess, + }) + defer restore() + + b, attr, err := efi.ReadVarBytes("a") + c.Assert(err, IsNil) + c.Check(attr, Equals, efi.VariableBootServiceAccess|efi.VariableRuntimeAccess) + c.Assert(string(b), Equals, "\x01") + + b, attr, err = efi.ReadVarBytes("b") + c.Assert(err, IsNil) + c.Check(attr, Equals, efi.VariableBootServiceAccess|efi.VariableRuntimeAccess|efi.VariableNonVolatile) + c.Assert(string(b), Equals, "\x02") + +} + +func (s *efiVarsSuite) TestMockStringVars(c *C) { + restore := efi.MockVars(map[string][]byte{ + "a": bootloadertest.UTF16Bytes("foo-bar-baz"), + }, nil) + defer restore() + + v, attr, err := efi.ReadVarString("a") + c.Assert(err, IsNil) + c.Check(attr, Equals, efi.VariableBootServiceAccess|efi.VariableRuntimeAccess) + c.Assert(v, Equals, "foo-bar-baz") +} + +func (s *efiVarsSuite) TestMockVarsNoEFISystem(c *C) { + restore := efi.MockVars(nil, nil) + defer restore() + + _, _, err := efi.ReadVarBytes("a") + c.Check(err, Equals, efi.ErrNoEFISystem) +} + +func (s *efiVarsSuite) TestStringOddSize(c *C) { + restore := efi.MockVars(map[string][]byte{ + "a": []byte("\x0a"), + }, nil) + defer restore() + + _, _, err := efi.ReadVarString("a") + c.Check(err, ErrorMatches, `EFI var "a" is not a valid UTF16 string, it has an extra byte`) +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/export_test.go snapd-2.45.1ubuntu0.2/bootloader/export_test.go --- snapd-2.37.4ubuntu0.1/bootloader/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/export_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,104 @@ +// -*- 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 bootloader + +import ( + "io/ioutil" + "os" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/bootloader/lkenv" + "github.com/snapcore/snapd/bootloader/ubootenv" +) + +// creates a new Androidboot bootloader object +func NewAndroidBoot(rootdir string) Bootloader { + return newAndroidBoot(rootdir) +} + +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(rootdir string, blOpts *Options) ExtractedRecoveryKernelImageBootloader { + return newUboot(rootdir, blOpts) +} + +func MockUbootFiles(c *C, rootdir string, blOpts *Options) { + u := &uboot{rootdir: rootdir} + u.setDefaults() + u.processBlOpts(blOpts) + err := os.MkdirAll(u.dir(), 0755) + c.Assert(err, IsNil) + + // ensure that we have a valid uboot.env too + env, err := ubootenv.Create(u.envFile(), 4096) + c.Assert(err, IsNil) + err = env.Save() + c.Assert(err, IsNil) +} + +func NewGrub(rootdir string, opts *Options) RecoveryAwareBootloader { + return newGrub(rootdir, opts) +} + +func MockGrubFiles(c *C, rootdir string) { + err := os.MkdirAll(filepath.Join(rootdir, "/boot/grub"), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(rootdir, "/boot/grub/grub.cfg"), 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.37.4ubuntu0.1/bootloader/grubenv/grubenv.go snapd-2.45.1ubuntu0.2/bootloader/grubenv/grubenv.go --- snapd-2.37.4ubuntu0.1/bootloader/grubenv/grubenv.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/grubenv/grubenv.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,117 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 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 grubenv + +import ( + "bytes" + "fmt" + "io/ioutil" + "os" + + "github.com/snapcore/snapd/strutil" +) + +// FIXME: support for escaping (embedded \n in grubenv) missing +type Env struct { + env map[string]string + ordering []string + + path string +} + +func NewEnv(path string) *Env { + return &Env{ + env: make(map[string]string), + path: path, + } +} + +func (g *Env) Get(name string) string { + return g.env[name] +} + +func (g *Env) Set(key, value string) { + if !strutil.ListContains(g.ordering, key) { + g.ordering = append(g.ordering, key) + } + + g.env[key] = value +} + +func (g *Env) Load() error { + buf, err := ioutil.ReadFile(g.path) + if err != nil { + return err + } + if len(buf) != 1024 { + return fmt.Errorf("grubenv %q must be exactly 1024 byte, got %d", g.path, len(buf)) + } + if !bytes.HasPrefix(buf, []byte("# GRUB Environment Block\n")) { + return fmt.Errorf("cannot find grubenv header in %q", g.path) + } + rawEnv := bytes.Split(buf, []byte("\n")) + for _, env := range rawEnv[1:] { + l := bytes.SplitN(env, []byte("="), 2) + // be liberal in what you accept + if len(l) < 2 { + continue + } + k := string(l[0]) + v := string(l[1]) + g.env[k] = v + g.ordering = append(g.ordering, k) + } + + return nil +} + +func (g *Env) Save() error { + w := bytes.NewBuffer(nil) + w.Grow(1024) + + fmt.Fprintf(w, "# GRUB Environment Block\n") + for _, k := range g.ordering { + if _, err := fmt.Fprintf(w, "%s=%s\n", k, g.env[k]); err != nil { + return err + } + } + if w.Len() > 1024 { + return fmt.Errorf("cannot write grubenv %q: bigger than 1024 bytes (%d)", g.path, w.Len()) + } + content := w.Bytes()[:w.Cap()] + for i := w.Len(); i < len(content); i++ { + content[i] = '#' + } + + // write in place to avoid the file moving on disk + // (thats what grubenv is also doing) + f, err := os.Create(g.path) + if err != nil { + return err + } + if _, err := f.Write(content); err != nil { + return err + } + if err := f.Sync(); err != nil { + return err + } + + return f.Close() +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/grubenv/grubenv_test.go snapd-2.45.1ubuntu0.2/bootloader/grubenv/grubenv_test.go --- snapd-2.37.4ubuntu0.1/bootloader/grubenv/grubenv_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/grubenv/grubenv_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,92 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 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 grubenv_test + +import ( + "fmt" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/bootloader/grubenv" + "github.com/snapcore/snapd/testutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type grubenvTestSuite struct { + envPath string +} + +var _ = Suite(&grubenvTestSuite{}) + +func (g *grubenvTestSuite) SetUpTest(c *C) { + g.envPath = filepath.Join(c.MkDir(), "grubenv") +} + +func (g *grubenvTestSuite) TestSet(c *C) { + env := grubenv.NewEnv(g.envPath) + c.Check(env, NotNil) + + env.Set("key", "value") + c.Check(env.Get("key"), Equals, "value") +} + +func (g *grubenvTestSuite) TestSave(c *C) { + env := grubenv.NewEnv(g.envPath) + c.Check(env, NotNil) + + env.Set("key1", "value1") + env.Set("key2", "value2") + env.Set("key3", "value3") + env.Set("key4", "value4") + env.Set("key5", "value5") + env.Set("key6", "value6") + env.Set("key7", "value7") + // set "key1" again, ordering (position) does not change + env.Set("key1", "value1") + + err := env.Save() + c.Assert(err, IsNil) + + c.Assert(g.envPath, testutil.FileEquals, `# GRUB Environment Block +key1=value1 +key2=value2 +key3=value3 +key4=value4 +key5=value5 +key6=value6 +key7=value7 +###################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################################`) +} + +func (g *grubenvTestSuite) TestSaveOverflow(c *C) { + env := grubenv.NewEnv(g.envPath) + c.Check(env, NotNil) + + for i := 0; i < 101; i++ { + env.Set(fmt.Sprintf("key%d", i), "foo") + } + + err := env.Save() + c.Assert(err, ErrorMatches, `cannot write grubenv .*: bigger than 1024 bytes \(1026\)`) +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/grub.go snapd-2.45.1ubuntu0.2/bootloader/grub.go --- snapd-2.37.4ubuntu0.1/bootloader/grub.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/grub.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,302 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 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" + "os" + "path/filepath" + + "github.com/snapcore/snapd/bootloader/grubenv" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + +// sanity - grub implements the required interfaces +var ( + _ Bootloader = (*grub)(nil) + _ installableBootloader = (*grub)(nil) + _ RecoveryAwareBootloader = (*grub)(nil) + _ ExtractedRunKernelImageBootloader = (*grub)(nil) +) + +type grub struct { + rootdir string + + basedir string + + uefiRunKernelExtraction bool +} + +// newGrub create a new Grub bootloader object +func newGrub(rootdir string, opts *Options) RecoveryAwareBootloader { + g := &grub{rootdir: rootdir} + if opts != nil && (opts.Recovery || opts.NoSlashBoot) { + g.basedir = "EFI/ubuntu" + } else { + g.basedir = "boot/grub" + } + if !osutil.FileExists(g.ConfigFile()) { + return nil + } + if opts != nil { + g.uefiRunKernelExtraction = opts.ExtractedRunKernelImage + } + + return g +} + +func (g *grub) Name() string { + return "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, g.basedir) +} + +func (g *grub) InstallBootConfig(gadgetDir string, opts *Options) (bool, error) { + if opts != nil && opts.Recovery { + recoveryGrubCfg := filepath.Join(gadgetDir, g.Name()+"-recovery.conf") + systemFile := filepath.Join(g.rootdir, "/EFI/ubuntu/grub.cfg") + return genericInstallBootConfig(recoveryGrubCfg, systemFile) + } + gadgetFile := filepath.Join(gadgetDir, g.Name()+".conf") + systemFile := filepath.Join(g.rootdir, "/boot/grub/grub.cfg") + return genericInstallBootConfig(gadgetFile, systemFile) +} + +func (g *grub) SetRecoverySystemEnv(recoverySystemDir string, values map[string]string) error { + if recoverySystemDir == "" { + return fmt.Errorf("internal error: recoverySystemDir unset") + } + recoverySystemGrubEnv := filepath.Join(g.rootdir, recoverySystemDir, "grubenv") + if err := os.MkdirAll(filepath.Dir(recoverySystemGrubEnv), 0755); err != nil { + return err + } + genv := grubenv.NewEnv(recoverySystemGrubEnv) + for k, v := range values { + genv.Set(k, v) + } + return genv.Save() +} + +func (g *grub) ConfigFile() string { + return filepath.Join(g.dir(), "grub.cfg") +} + +func (g *grub) envFile() string { + return filepath.Join(g.dir(), "grubenv") +} + +func (g *grub) GetBootVars(names ...string) (map[string]string, error) { + out := make(map[string]string) + + env := grubenv.NewEnv(g.envFile()) + if err := env.Load(); err != nil { + return nil, err + } + + for _, name := range names { + out[name] = env.Get(name) + } + + return out, nil +} + +func (g *grub) SetBootVars(values map[string]string) error { + env := grubenv.NewEnv(g.envFile()) + if err := env.Load(); err != nil && !os.IsNotExist(err) { + return err + } + for k, v := range values { + env.Set(k, v) + } + return env.Save() +} + +func (g *grub) extractedKernelDir(prefix string, s snap.PlaceInfo) string { + return filepath.Join( + prefix, + s.Filename(), + ) +} + +func (g *grub) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error { + // default kernel assets are: + // - kernel.img + // - initrd.img + // - dtbs/* + var assets []string + if g.uefiRunKernelExtraction { + assets = []string{"kernel.efi"} + } else { + assets = []string{"kernel.img", "initrd.img", "dtbs/*"} + } + + // extraction can be forced through either a special file in the kernel snap + // or through an option in the bootloader + _, err := snapf.ReadFile("meta/force-kernel-extraction") + if g.uefiRunKernelExtraction || err == nil { + return extractKernelAssetsToBootDir( + g.extractedKernelDir(g.dir(), s), + snapf, + assets, + ) + } + return nil +} + +func (g *grub) RemoveKernelAssets(s snap.PlaceInfo) error { + return removeKernelAssetsFromBootDir(g.dir(), s) +} + +// ExtractedRunKernelImageBootloader helper methods + +func (g *grub) makeKernelEfiSymlink(s snap.PlaceInfo, name string) error { + // use a relative symlink destination so that it resolves properly, if grub + // is located at /run/mnt/ubuntu-boot or /boot/grub, etc. + target := filepath.Join( + s.Filename(), + "kernel.efi", + ) + + // the location of the destination symlink as an absolute filepath + source := filepath.Join(g.dir(), name) + + // check that the kernel snap has been extracted already so we don't + // inadvertently create a dangling symlink + // expand the relative symlink from g.dir() + if !osutil.FileExists(filepath.Join(g.dir(), target)) { + return fmt.Errorf( + "cannot enable %s at %s: %v", + name, + target, + os.ErrNotExist, + ) + } + + // the symlink doesn't exist so just create it + return osutil.AtomicSymlink(target, source) +} + +// unlinkKernelEfiSymlink will remove the specified symlink if it exists. Note +// that if the symlink is "dangling", it will still remove the symlink without +// returning an error. This is useful for example to disable a try-kernel that +// was incorrectly created. +func (g *grub) unlinkKernelEfiSymlink(name string) error { + symlink := filepath.Join(g.dir(), name) + err := os.Remove(symlink) + if err != nil && !os.IsNotExist(err) { + return err + } + return nil +} + +func (g *grub) readKernelSymlink(name string) (snap.PlaceInfo, error) { + // read the symlink from / to + // // and parse the + // directory (which is supposed to be the name of the snap) into the snap + link := filepath.Join(g.dir(), name) + + // check that the symlink is not dangling before continuing + if !osutil.FileExists(link) { + return nil, fmt.Errorf("cannot read dangling symlink %s", name) + } + + targetKernelEfi, err := os.Readlink(link) + if err != nil { + return nil, fmt.Errorf("cannot read %s symlink: %v", link, err) + } + + kernelSnapFileName := filepath.Base(filepath.Dir(targetKernelEfi)) + sn, err := snap.ParsePlaceInfoFromSnapFileName(kernelSnapFileName) + if err != nil { + return nil, fmt.Errorf( + "cannot parse kernel snap file name from symlink target %q: %v", + kernelSnapFileName, + err, + ) + } + return sn, nil +} + +// actual ExtractedRunKernelImageBootloader methods + +// EnableKernel will install a kernel.efi symlink in the bootloader partition, +// pointing to the referenced kernel snap. EnableKernel() will fail if the +// referenced kernel snap does not exist. +func (g *grub) EnableKernel(s snap.PlaceInfo) error { + // add symlink from ubuntuBootPartition/kernel.efi to + // /EFI/ubuntu/.snap/kernel.efi + // so that we are consistent between uc16/uc18 and uc20 with where we + // extract kernels + return g.makeKernelEfiSymlink(s, "kernel.efi") +} + +// EnableTryKernel will install a try-kernel.efi symlink in the bootloader +// partition, pointing towards the referenced kernel snap. EnableTryKernel() +// will fail if the referenced kernel snap does not exist. +func (g *grub) EnableTryKernel(s snap.PlaceInfo) error { + // add symlink from ubuntuBootPartition/kernel.efi to + // /EFI/ubuntu/.snap/kernel.efi + // so that we are consistent between uc16/uc18 and uc20 with where we + // extract kernels + return g.makeKernelEfiSymlink(s, "try-kernel.efi") +} + +// DisableTryKernel will remove the try-kernel.efi symlink if it exists. Note +// that when performing an update, you should probably first use EnableKernel(), +// then DisableTryKernel() for maximum safety. +func (g *grub) DisableTryKernel() error { + return g.unlinkKernelEfiSymlink("try-kernel.efi") +} + +// Kernel will return the kernel snap currently installed in the bootloader +// partition, pointed to by the kernel.efi symlink. +func (g *grub) Kernel() (snap.PlaceInfo, error) { + return g.readKernelSymlink("kernel.efi") +} + +// TryKernel will return the kernel snap currently being tried if it exists and +// false if there is not currently a try-kernel.efi symlink. Note if the symlink +// exists but does not point to an existing file an error will be returned. +func (g *grub) TryKernel() (snap.PlaceInfo, error) { + // check that the _symlink_ exists, not that it points to something real + // we check for whether it is a dangling symlink inside readKernelSymlink, + // which returns an error when the symlink is dangling + _, err := os.Lstat(filepath.Join(g.dir(), "try-kernel.efi")) + if err == nil { + p, err := g.readKernelSymlink("try-kernel.efi") + // if we failed to read the symlink, then the try kernel isn't usable, + // so return err because the symlink is there + if err != nil { + return nil, err + } + return p, nil + } + return nil, ErrNoTryKernelRef +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/grub_test.go snapd-2.45.1ubuntu0.2/bootloader/grub_test.go --- snapd-2.37.4ubuntu0.1/bootloader/grub_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/grub_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,571 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 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 ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + "github.com/mvo5/goconfigparser" + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/bootloader/grubenv" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapfile" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" +) + +type grubTestSuite struct { + baseBootenvTestSuite + + bootdir string +} + +var _ = Suite(&grubTestSuite{}) + +func (s *grubTestSuite) SetUpTest(c *C) { + s.baseBootenvTestSuite.SetUpTest(c) + bootloader.MockGrubFiles(c, s.rootdir) + + s.bootdir = filepath.Join(s.rootdir, "boot") +} + +// grubEditenvCmd finds the right grub{,2}-editenv command +func grubEditenvCmd() string { + for _, exe := range []string{"grub2-editenv", "grub-editenv"} { + if osutil.ExecutableExists(exe) { + return exe + } + } + return "" +} + +func grubEnvPath(rootdir string) string { + return filepath.Join(rootdir, "boot/grub/grubenv") +} + +func (s *grubTestSuite) grubEditenvSet(c *C, key, value string) { + if grubEditenvCmd() == "" { + c.Skip("grub{,2}-editenv is not available") + } + + output, err := exec.Command(grubEditenvCmd(), grubEnvPath(s.rootdir), "set", fmt.Sprintf("%s=%s", key, value)).CombinedOutput() + c.Check(err, IsNil) + c.Check(string(output), Equals, "") +} + +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(s.rootdir), "list").CombinedOutput() + c.Assert(err, IsNil) + cfg := goconfigparser.New() + cfg.AllowNoSectionHeader = true + err = cfg.ReadString(string(output)) + c.Assert(err, IsNil) + v, err := cfg.Get("", key) + c.Assert(err, IsNil) + return v +} + +func (s *grubTestSuite) makeFakeGrubEnv(c *C) { + s.grubEditenvSet(c, "k", "v") +} + +func (s *grubTestSuite) TestNewGrubNoGrubReturnsNil(c *C) { + g := bootloader.NewGrub("/something/not/there", nil) + c.Assert(g, IsNil) +} + +func (s *grubTestSuite) TestNewGrub(c *C) { + s.makeFakeGrubEnv(c) + + g := bootloader.NewGrub(s.rootdir, nil) + c.Assert(g, NotNil) + c.Assert(g.Name(), Equals, "grub") +} + +func (s *grubTestSuite) TestGetBootloaderWithGrub(c *C) { + s.makeFakeGrubEnv(c) + + 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) + s.grubEditenvSet(c, "snap_mode", "regular") + + g := bootloader.NewGrub(s.rootdir, nil) + v, err := g.GetBootVars("snap_mode") + c.Assert(err, IsNil) + c.Check(v, HasLen, 1) + c.Check(v["snap_mode"], Equals, "regular") +} + +func (s *grubTestSuite) TestSetBootVer(c *C) { + s.makeFakeGrubEnv(c) + + g := bootloader.NewGrub(s.rootdir, nil) + err := g.SetBootVars(map[string]string{ + "k1": "v1", + "k2": "v2", + }) + c.Assert(err, IsNil) + + 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(s.rootdir, nil) + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snapfile.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = g.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // kernel is *not* here + 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(s.rootdir, nil) + c.Assert(g, NotNil) + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"meta/force-kernel-extraction", ""}, + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snapfile.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = g.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // kernel is extracted + kernimg := filepath.Join(s.bootdir, "grub", "ubuntu-kernel_42.snap", "kernel.img") + c.Assert(osutil.FileExists(kernimg), Equals, true) + // initrd + 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 + err = g.RemoveKernelAssets(info) + c.Assert(err, IsNil) + exists, _, err := osutil.DirExists(filepath.Dir(kernimg)) + c.Assert(err, IsNil) + c.Check(exists, Equals, false) +} + +func (s *grubTestSuite) grubDir() string { + return filepath.Join(s.bootdir, "grub") +} + +func (s *grubTestSuite) grubRecoveryDir() string { + return filepath.Join(s.rootdir, "EFI/ubuntu") +} + +func (s *grubTestSuite) makeFakeGrubRecoveryEnv(c *C) { + err := os.MkdirAll(s.grubRecoveryDir(), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(filepath.Join(s.grubRecoveryDir(), "grub.cfg"), nil, 0644) + c.Assert(err, IsNil) +} + +func (s *grubTestSuite) TestNewGrubWithOptionRecovery(c *C) { + s.makeFakeGrubRecoveryEnv(c) + + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Recovery: true}) + c.Assert(g, NotNil) + c.Assert(g.Name(), Equals, "grub") +} + +func (s *grubTestSuite) TestNewGrubWithOptionRecoveryBootEnv(c *C) { + s.makeFakeGrubRecoveryEnv(c) + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Recovery: true}) + + // check that setting vars goes to the right place + c.Check(filepath.Join(s.grubRecoveryDir(), "grubenv"), testutil.FileAbsent) + err := g.SetBootVars(map[string]string{ + "k1": "v1", + "k2": "v2", + }) + c.Assert(err, IsNil) + c.Check(filepath.Join(s.grubRecoveryDir(), "grubenv"), testutil.FilePresent) + + env, err := g.GetBootVars("k1", "k2") + c.Assert(err, IsNil) + c.Check(env, DeepEquals, map[string]string{ + "k1": "v1", + "k2": "v2", + }) +} + +func (s *grubTestSuite) TestNewGrubWithOptionRecoveryNoEnv(c *C) { + // fake a *regular* grub env + s.makeFakeGrubEnv(c) + + // we can't create a recovery grub with that + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Recovery: true}) + c.Assert(g, IsNil) +} + +func (s *grubTestSuite) TestGrubSetRecoverySystemEnv(c *C) { + s.makeFakeGrubRecoveryEnv(c) + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{Recovery: true}) + + // check that we can set a recovery system specific bootenv + bvars := map[string]string{ + "snapd_recovery_kernel": "/snaps/pc-kernel_1.snap", + "other_options": "are-supported", + } + + err := g.SetRecoverySystemEnv("/systems/20191209", bvars) + c.Assert(err, IsNil) + recoverySystemGrubenv := filepath.Join(s.rootdir, "/systems/20191209/grubenv") + c.Assert(recoverySystemGrubenv, testutil.FilePresent) + + genv := grubenv.NewEnv(recoverySystemGrubenv) + err = genv.Load() + c.Assert(err, IsNil) + c.Check(genv.Get("snapd_recovery_kernel"), Equals, "/snaps/pc-kernel_1.snap") + c.Check(genv.Get("other_options"), Equals, "are-supported") +} + +func (s *grubTestSuite) makeKernelAssetSnap(c *C, snapFileName string) snap.PlaceInfo { + kernelSnap, err := snap.ParsePlaceInfoFromSnapFileName(snapFileName) + c.Assert(err, IsNil) + + // make a kernel.efi snap as it would be by ExtractKernelAssets() + kernelSnapExtractedAssetsDir := filepath.Join(s.grubDir(), snapFileName) + err = os.MkdirAll(kernelSnapExtractedAssetsDir, 0755) + c.Assert(err, IsNil) + + err = ioutil.WriteFile(filepath.Join(kernelSnapExtractedAssetsDir, "kernel.efi"), nil, 0644) + c.Assert(err, IsNil) + + return kernelSnap +} + +func (s *grubTestSuite) makeKernelAssetSnapAndSymlink(c *C, snapFileName, symlinkName string) snap.PlaceInfo { + kernelSnap := s.makeKernelAssetSnap(c, snapFileName) + + // make a kernel.efi symlink to the kernel.efi above + err := os.Symlink( + filepath.Join(snapFileName, "kernel.efi"), + filepath.Join(s.grubDir(), symlinkName), + ) + c.Assert(err, IsNil) + + return kernelSnap +} + +func (s *grubTestSuite) TestGrubExtractedRunKernelImageKernel(c *C) { + s.makeFakeGrubEnv(c) + g := bootloader.NewGrub(s.rootdir, nil) + eg, ok := g.(bootloader.ExtractedRunKernelImageBootloader) + c.Assert(ok, Equals, true) + + kernel := s.makeKernelAssetSnapAndSymlink(c, "pc-kernel_1.snap", "kernel.efi") + + // ensure that the returned kernel is the same as the one we put there + sn, err := eg.Kernel() + c.Assert(err, IsNil) + c.Assert(sn, DeepEquals, kernel) +} + +func (s *grubTestSuite) TestGrubExtractedRunKernelImageTryKernel(c *C) { + s.makeFakeGrubEnv(c) + g := bootloader.NewGrub(s.rootdir, nil) + eg, ok := g.(bootloader.ExtractedRunKernelImageBootloader) + c.Assert(ok, Equals, true) + + // ensure it doesn't return anything when the symlink doesn't exist + _, err := eg.TryKernel() + c.Assert(err, Equals, bootloader.ErrNoTryKernelRef) + + // when a bad kernel snap name is in the extracted path, it will complain + // appropriately + kernelSnapExtractedAssetsDir := filepath.Join(s.grubDir(), "bad_snap_rev_name") + badKernelSnapPath := filepath.Join(kernelSnapExtractedAssetsDir, "kernel.efi") + tryKernelSymlink := filepath.Join(s.grubDir(), "try-kernel.efi") + err = os.MkdirAll(kernelSnapExtractedAssetsDir, 0755) + c.Assert(err, IsNil) + + err = ioutil.WriteFile(badKernelSnapPath, nil, 0644) + c.Assert(err, IsNil) + + err = os.Symlink("bad_snap_rev_name/kernel.efi", tryKernelSymlink) + c.Assert(err, IsNil) + + _, err = eg.TryKernel() + c.Assert(err, ErrorMatches, "cannot parse kernel snap file name from symlink target \"bad_snap_rev_name\": .*") + + // remove the bad symlink + err = os.Remove(tryKernelSymlink) + c.Assert(err, IsNil) + + // make a real symlink + tryKernel := s.makeKernelAssetSnapAndSymlink(c, "pc-kernel_2.snap", "try-kernel.efi") + + // ensure that the returned kernel is the same as the one we put there + sn, err := eg.TryKernel() + c.Assert(err, IsNil) + c.Assert(sn, DeepEquals, tryKernel) + + // if the destination of the symlink is removed, we get an error + err = os.Remove(filepath.Join(s.grubDir(), "pc-kernel_2.snap", "kernel.efi")) + c.Assert(err, IsNil) + _, err = eg.TryKernel() + c.Assert(err, ErrorMatches, "cannot read dangling symlink try-kernel.efi") +} + +func (s *grubTestSuite) TestGrubExtractedRunKernelImageEnableKernel(c *C) { + s.makeFakeGrubEnv(c) + g := bootloader.NewGrub(s.rootdir, nil) + eg, ok := g.(bootloader.ExtractedRunKernelImageBootloader) + c.Assert(ok, Equals, true) + + // ensure we fail to create a dangling symlink to a kernel snap that was not + // actually extracted + nonExistSnap, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_12.snap") + c.Assert(err, IsNil) + err = eg.EnableKernel(nonExistSnap) + c.Assert(err, ErrorMatches, "cannot enable kernel.efi at pc-kernel_12.snap/kernel.efi: file does not exist") + + kernel := s.makeKernelAssetSnap(c, "pc-kernel_1.snap") + + // enable the Kernel we extracted + err = eg.EnableKernel(kernel) + c.Assert(err, IsNil) + + // ensure that the symlink was put where we expect it + asset, err := os.Readlink(filepath.Join(s.grubDir(), "kernel.efi")) + c.Assert(err, IsNil) + c.Assert(asset, DeepEquals, filepath.Join("pc-kernel_1.snap", "kernel.efi")) + + // create a new kernel snap and ensure that we can safely enable that one + // too + kernel2 := s.makeKernelAssetSnap(c, "pc-kernel_2.snap") + err = eg.EnableKernel(kernel2) + c.Assert(err, IsNil) + + // ensure that the symlink was put where we expect it + asset, err = os.Readlink(filepath.Join(s.grubDir(), "kernel.efi")) + c.Assert(err, IsNil) + c.Assert(asset, DeepEquals, filepath.Join("pc-kernel_2.snap", "kernel.efi")) +} + +func (s *grubTestSuite) TestGrubExtractedRunKernelImageEnableTryKernel(c *C) { + s.makeFakeGrubEnv(c) + g := bootloader.NewGrub(s.rootdir, nil) + eg, ok := g.(bootloader.ExtractedRunKernelImageBootloader) + c.Assert(ok, Equals, true) + + kernel := s.makeKernelAssetSnap(c, "pc-kernel_1.snap") + + // enable the Kernel we extracted + err := eg.EnableTryKernel(kernel) + c.Assert(err, IsNil) + + // ensure that the symlink was put where we expect it + asset, err := os.Readlink(filepath.Join(s.grubDir(), "try-kernel.efi")) + c.Assert(err, IsNil) + + c.Assert(asset, DeepEquals, filepath.Join("pc-kernel_1.snap", "kernel.efi")) +} + +func (s *grubTestSuite) TestGrubExtractedRunKernelImageDisableTryKernel(c *C) { + s.makeFakeGrubEnv(c) + g := bootloader.NewGrub(s.rootdir, nil) + eg, ok := g.(bootloader.ExtractedRunKernelImageBootloader) + c.Assert(ok, Equals, true) + + // trying to disable when the try-kernel.efi symlink is missing does not + // raise any errors + err := eg.DisableTryKernel() + c.Assert(err, IsNil) + + // make the symlink and check that the symlink is missing afterwards + s.makeKernelAssetSnapAndSymlink(c, "pc-kernel_1.snap", "try-kernel.efi") + // make sure symlink is there + c.Assert(filepath.Join(s.grubDir(), "try-kernel.efi"), testutil.FilePresent) + + err = eg.DisableTryKernel() + c.Assert(err, IsNil) + + // ensure that the symlink is no longer there + c.Assert(filepath.Join(s.grubDir(), "try-kernel.efi"), testutil.FileAbsent) + c.Assert(filepath.Join(s.grubDir(), "pc-kernel_1.snap/kernel.efi"), testutil.FilePresent) + + // try again but make sure that the directory cannot be written to + s.makeKernelAssetSnapAndSymlink(c, "pc-kernel_1.snap", "try-kernel.efi") + err = os.Chmod(s.grubDir(), 000) + c.Assert(err, IsNil) + defer os.Chmod(s.grubDir(), 0755) + + err = eg.DisableTryKernel() + c.Assert(err, ErrorMatches, "remove .*/grub/try-kernel.efi: permission denied") +} + +func (s *grubTestSuite) TestKernelExtractionRunImageKernel(c *C) { + s.makeFakeGrubEnv(c) + + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{ExtractedRunKernelImage: true}) + c.Assert(g, NotNil) + + files := [][]string{ + {"kernel.efi", "I'm a kernel"}, + {"another-kernel-file", "another kernel file"}, + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snapfile.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = g.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // kernel is extracted + kernefi := filepath.Join(s.bootdir, "grub", "ubuntu-kernel_42.snap", "kernel.efi") + c.Assert(kernefi, testutil.FilePresent) + // other file is not extracted + other := filepath.Join(s.bootdir, "grub", "ubuntu-kernel_42.snap", "another-kernel-file") + c.Assert(other, testutil.FileAbsent) + + // ensure that removal of assets also works + err = g.RemoveKernelAssets(info) + c.Assert(err, IsNil) + exists, _, err := osutil.DirExists(filepath.Dir(kernefi)) + c.Assert(err, IsNil) + c.Check(exists, Equals, false) +} + +func (s *grubTestSuite) TestKernelExtractionRunImageKernelNoSlashBoot(c *C) { + // this is ubuntu-boot but during install we use the native EFI/ubuntu + // layout, same as Recovery, without the /boot mount + s.makeFakeGrubRecoveryEnv(c) + + g := bootloader.NewGrub(s.rootdir, &bootloader.Options{ExtractedRunKernelImage: true, NoSlashBoot: true}) + c.Assert(g, NotNil) + + files := [][]string{ + {"kernel.efi", "I'm a kernel"}, + {"another-kernel-file", "another kernel file"}, + {"meta/kernel.yaml", "version: 4.2"}, + } + si := &snap.SideInfo{ + RealName: "ubuntu-kernel", + Revision: snap.R(42), + } + fn := snaptest.MakeTestSnapWithFiles(c, packageKernel, files) + snapf, err := snapfile.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = g.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // kernel is extracted + kernefi := filepath.Join(s.rootdir, "EFI/ubuntu", "ubuntu-kernel_42.snap", "kernel.efi") + c.Assert(kernefi, testutil.FilePresent) + // other file is not extracted + other := filepath.Join(s.rootdir, "EFI/ubuntu", "ubuntu-kernel_42.snap", "another-kernel-file") + c.Assert(other, testutil.FileAbsent) + + // enable the Kernel we extracted + eg, ok := g.(bootloader.ExtractedRunKernelImageBootloader) + c.Assert(ok, Equals, true) + err = eg.EnableKernel(info) + c.Assert(err, IsNil) + + // ensure that the symlink was put where we expect it + asset, err := os.Readlink(filepath.Join(s.rootdir, "EFI/ubuntu", "kernel.efi")) + c.Assert(err, IsNil) + + c.Assert(asset, DeepEquals, filepath.Join("ubuntu-kernel_42.snap", "kernel.efi")) + + // ensure that removal of assets also works + err = g.RemoveKernelAssets(info) + c.Assert(err, IsNil) + exists, _, err := osutil.DirExists(filepath.Dir(kernefi)) + c.Assert(err, IsNil) + c.Check(exists, Equals, false) +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/lkenv/export_test.go snapd-2.45.1ubuntu0.2/bootloader/lkenv/export_test.go --- snapd-2.37.4ubuntu0.1/bootloader/lkenv/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/lkenv/export_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/bootloader/lkenv/lkenv.go snapd-2.45.1ubuntu0.2/bootloader/lkenv/lkenv.go --- snapd-2.37.4ubuntu0.1/bootloader/lkenv/lkenv.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/lkenv/lkenv.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,427 @@ +// -*- 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/lk/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 populated 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 slot (first row, second column) + * - 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_try_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 { + 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.37.4ubuntu0.1/bootloader/lkenv/lkenv_test.go snapd-2.45.1ubuntu0.2/bootloader/lkenv/lkenv_test.go --- snapd-2.37.4ubuntu0.1/bootloader/lkenv/lkenv_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/lkenv/lkenv_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,338 @@ +// -*- 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" + "io" + "io/ioutil" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/boot" + "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", boot.TryStatus) + c.Check(env.Get("snap_mode"), Equals, boot.TryStatus) +} + +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.37.4ubuntu0.1/bootloader/lk.go snapd-2.45.1ubuntu0.2/bootloader/lk.go --- snapd-2.37.4ubuntu0.1/bootloader/lk.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/lk.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,216 @@ +// -*- 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/") + } + return filepath.Join(l.rootdir, "/boot/lk/") +} + +func (l *lk) InstallBootConfig(gadgetDir string, opts *Options) (bool, error) { + gadgetFile := filepath.Join(gadgetDir, l.Name()+".conf") + systemFile := l.ConfigFile() + return genericInstallBootConfig(gadgetFile, systemFile) +} + +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") + } + 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 := s.Filename() + + 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 := s.Filename() + 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.37.4ubuntu0.1/bootloader/lk_test.go snapd-2.45.1ubuntu0.2/bootloader/lk_test.go --- snapd-2.37.4ubuntu0.1/bootloader/lk_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/lk_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,236 @@ +// -*- 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/boot" + "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/snapfile" + "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": boot.TryStatus} + l.SetBootVars(bootVars) + + v, err := l.GetBootVars("snap_mode") + c.Assert(err, IsNil) + c.Check(v, HasLen, 1) + c.Check(v["snap_mode"], Equals, boot.TryStatus) +} + +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 := snapfile.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 := snapfile.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 := snapfile.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.37.4ubuntu0.1/bootloader/ubootenv/env.go snapd-2.45.1ubuntu0.2/bootloader/ubootenv/env.go --- snapd-2.37.4ubuntu0.1/bootloader/ubootenv/env.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/ubootenv/env.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,294 @@ +// -*- 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 ubootenv + +import ( + "bufio" + "bytes" + "encoding/binary" + "fmt" + "hash/crc32" + "io" + "io/ioutil" + "os" + "path/filepath" + "sort" + "strings" +) + +// FIXME: add config option for that so that the user can select if +// he/she wants env with or without flags +var headerSize = 5 + +// Env contains the data of the uboot environment +type Env struct { + fname string + size int + data map[string]string +} + +// little endian helpers +func readUint32(data []byte) uint32 { + var ret uint32 + buf := bytes.NewBuffer(data) + binary.Read(buf, binary.LittleEndian, &ret) + return ret +} + +func writeUint32(u uint32) []byte { + buf := bytes.NewBuffer(nil) + binary.Write(buf, binary.LittleEndian, &u) + return buf.Bytes() +} + +// Create a new empty uboot env file with the given size +func Create(fname string, size int) (*Env, error) { + f, err := os.Create(fname) + if err != nil { + return nil, err + } + defer f.Close() + + env := &Env{ + fname: fname, + size: size, + data: make(map[string]string), + } + + return env, nil +} + +// OpenFlags instructs open how to alter its behavior. +type OpenFlags int + +const ( + // OpenBestEffort instructs OpenWithFlags to skip malformed data without returning an error. + OpenBestEffort OpenFlags = 1 << iota +) + +// Open opens a existing uboot env file +func Open(fname string) (*Env, error) { + return OpenWithFlags(fname, OpenFlags(0)) +} + +// OpenWithFlags opens a existing uboot env file, passing additional flags. +func OpenWithFlags(fname string, flags OpenFlags) (*Env, error) { + f, err := os.Open(fname) + if err != nil { + return nil, err + } + defer f.Close() + + contentWithHeader, err := ioutil.ReadAll(f) + if err != nil { + return nil, err + } + crc := readUint32(contentWithHeader) + + payload := contentWithHeader[headerSize:] + actualCRC := crc32.ChecksumIEEE(payload) + if crc != actualCRC { + return nil, fmt.Errorf("cannot open %q: bad CRC %v != %v", fname, crc, actualCRC) + } + + if eof := bytes.Index(payload, []byte{0, 0}); eof >= 0 { + payload = payload[:eof] + } + + data, err := parseData(payload, flags) + if err != nil { + return nil, err + } + + env := &Env{ + fname: fname, + size: len(contentWithHeader), + data: data, + } + + return env, nil +} + +func parseData(data []byte, flags OpenFlags) (map[string]string, error) { + out := make(map[string]string) + + for _, envStr := range bytes.Split(data, []byte{0}) { + if len(envStr) == 0 || envStr[0] == 0 || envStr[0] == 255 { + continue + } + l := strings.SplitN(string(envStr), "=", 2) + if len(l) != 2 || l[0] == "" { + if flags&OpenBestEffort == OpenBestEffort { + continue + } + return nil, fmt.Errorf("cannot parse line %q as key=value pair", envStr) + } + key := l[0] + value := l[1] + out[key] = value + } + + return out, nil +} + +func (env *Env) String() string { + out := "" + + env.iterEnv(func(key, value string) { + out += fmt.Sprintf("%s=%s\n", key, value) + }) + + return out +} + +func (env *Env) Size() int { + return env.size +} + +// Get the value of the environment variable +func (env *Env) Get(name string) string { + return env.data[name] +} + +// Set an environment name to the given value, if the value is empty +// the variable will be removed from the environment +func (env *Env) Set(name, value string) { + if name == "" { + panic(fmt.Sprintf("Set() can not be called with empty key for value: %q", value)) + } + if value == "" { + delete(env.data, name) + return + } + env.data[name] = value +} + +// iterEnv calls the passed function f with key, value for environment +// vars. The order is guaranteed (unlike just iterating over the map) +func (env *Env) iterEnv(f func(key, value string)) { + keys := make([]string, 0, len(env.data)) + for k := range env.data { + keys = append(keys, k) + } + sort.Strings(keys) + + for _, k := range keys { + if k == "" { + panic("iterEnv iterating over a empty key") + } + + f(k, env.data[k]) + } +} + +// Save will write out the environment data +func (env *Env) Save() error { + w := bytes.NewBuffer(nil) + // will panic if the buffer can't grow, all writes to + // the buffer will be ok because we sized it correctly + w.Grow(env.size - headerSize) + + // write the payload + env.iterEnv(func(key, value string) { + w.Write([]byte(fmt.Sprintf("%s=%s", key, value))) + w.Write([]byte{0}) + }) + + // write double \0 to mark the end of the env + w.Write([]byte{0}) + + // no keys, so no previous \0 was written so we write one here + if len(env.data) == 0 { + w.Write([]byte{0}) + } + + // write ff into the remaining parts + writtenSoFar := w.Len() + for i := 0; i < env.size-headerSize-writtenSoFar; i++ { + w.Write([]byte{0xff}) + } + + // checksum + crc := crc32.ChecksumIEEE(w.Bytes()) + + // ensure dir sync + dir, err := os.Open(filepath.Dir(env.fname)) + if err != nil { + return err + } + defer dir.Close() + + // Note that we overwrite the existing file and do not do + // the usual write-rename. The rationale is that we want to + // minimize the amount of writes happening on a potential + // FAT partition where the env is loaded from. The file will + // always be of a fixed size so we know the writes will not + // fail because of ENOSPC. + // + // The size of the env file never changes so we do not + // truncate it. + // + // We also do not O_TRUNC to avoid reallocations on the FS + // to minimize risk of fs corruption. + f, err := os.OpenFile(env.fname, os.O_WRONLY, 0666) + if err != nil { + return err + } + defer f.Close() + + if _, err := f.Write(writeUint32(crc)); err != nil { + return err + } + // padding bytes (e.g. for redundant header) + pad := make([]byte, headerSize-binary.Size(crc)) + if _, err := f.Write(pad); err != nil { + return err + } + if _, err := f.Write(w.Bytes()); err != nil { + return err + } + + if err := f.Sync(); err != nil { + return err + } + + return dir.Sync() +} + +// Import is a helper that imports a given text file that contains +// "key=value" paris into the uboot env. Lines starting with ^# are +// ignored (like the input file on mkenvimage) +func (env *Env) Import(r io.Reader) error { + scanner := bufio.NewScanner(r) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "#") || len(line) == 0 { + continue + } + l := strings.SplitN(line, "=", 2) + if len(l) == 1 { + return fmt.Errorf("Invalid line: %q", line) + } + env.data[l[0]] = l[1] + + } + + return scanner.Err() +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/ubootenv/env_test.go snapd-2.45.1ubuntu0.2/bootloader/ubootenv/env_test.go --- snapd-2.37.4ubuntu0.1/bootloader/ubootenv/env_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/ubootenv/env_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,298 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package ubootenv_test + +import ( + "bytes" + "hash/crc32" + "io/ioutil" + "os" + "path/filepath" + "strings" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/bootloader/ubootenv" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +type uenvTestSuite struct { + envFile string +} + +var _ = Suite(&uenvTestSuite{}) + +func (u *uenvTestSuite) SetUpTest(c *C) { + u.envFile = filepath.Join(c.MkDir(), "uboot.env") +} + +func (u *uenvTestSuite) TestSetNoDuplicate(c *C) { + env, err := ubootenv.Create(u.envFile, 4096) + c.Assert(err, IsNil) + env.Set("foo", "bar") + env.Set("foo", "bar") + c.Assert(env.String(), Equals, "foo=bar\n") +} + +func (u *uenvTestSuite) TestOpenEnv(c *C) { + env, err := ubootenv.Create(u.envFile, 4096) + c.Assert(err, IsNil) + env.Set("foo", "bar") + c.Assert(env.String(), Equals, "foo=bar\n") + err = env.Save() + c.Assert(err, IsNil) + + env2, err := ubootenv.Open(u.envFile) + c.Assert(err, IsNil) + c.Assert(env2.String(), Equals, "foo=bar\n") +} + +func (u *uenvTestSuite) TestOpenEnvBadCRC(c *C) { + corrupted := filepath.Join(c.MkDir(), "corrupted.env") + + buf := make([]byte, 4096) + err := ioutil.WriteFile(corrupted, buf, 0644) + c.Assert(err, IsNil) + + _, err = ubootenv.Open(corrupted) + c.Assert(err, ErrorMatches, `cannot open ".*": bad CRC 0 != .*`) +} + +func (u *uenvTestSuite) TestGetSimple(c *C) { + env, err := ubootenv.Create(u.envFile, 4096) + c.Assert(err, IsNil) + env.Set("foo", "bar") + c.Assert(env.Get("foo"), Equals, "bar") +} + +func (u *uenvTestSuite) TestGetNoSuchEntry(c *C) { + env, err := ubootenv.Create(u.envFile, 4096) + c.Assert(err, IsNil) + c.Assert(env.Get("no-such-entry"), Equals, "") +} + +func (u *uenvTestSuite) TestImport(c *C) { + env, err := ubootenv.Create(u.envFile, 4096) + c.Assert(err, IsNil) + + r := strings.NewReader("foo=bar\n#comment\n\nbaz=baz") + err = env.Import(r) + c.Assert(err, IsNil) + // order is alphabetic + c.Assert(env.String(), Equals, "baz=baz\nfoo=bar\n") +} + +func (u *uenvTestSuite) TestImportHasError(c *C) { + env, err := ubootenv.Create(u.envFile, 4096) + c.Assert(err, IsNil) + + r := strings.NewReader("foxy") + err = env.Import(r) + c.Assert(err, ErrorMatches, "Invalid line: \"foxy\"") +} + +func (u *uenvTestSuite) TestSetEmptyUnsets(c *C) { + env, err := ubootenv.Create(u.envFile, 4096) + c.Assert(err, IsNil) + + env.Set("foo", "bar") + c.Assert(env.String(), Equals, "foo=bar\n") + env.Set("foo", "") + c.Assert(env.String(), Equals, "") +} + +func (u *uenvTestSuite) makeUbootEnvFromData(c *C, mockData []byte) { + w := bytes.NewBuffer(nil) + crc := crc32.ChecksumIEEE(mockData) + w.Write(ubootenv.WriteUint32(crc)) + w.Write([]byte{0}) + w.Write(mockData) + + f, err := os.Create(u.envFile) + c.Assert(err, IsNil) + defer f.Close() + _, err = f.Write(w.Bytes()) + c.Assert(err, IsNil) +} + +// ensure that the data after \0\0 is discarded (except for crc) +func (u *uenvTestSuite) TestReadStopsAfterDoubleNull(c *C) { + mockData := []byte{ + // foo=bar + 0x66, 0x6f, 0x6f, 0x3d, 0x62, 0x61, 0x72, + // eof + 0x00, 0x00, + // junk after eof as written by fw_setenv sometimes + // =b + 0x3d, 62, + // empty + 0xff, 0xff, + } + u.makeUbootEnvFromData(c, mockData) + + env, err := ubootenv.Open(u.envFile) + c.Assert(err, IsNil) + c.Assert(env.String(), Equals, "foo=bar\n") +} + +// ensure that the malformed data is not causing us to panic. +func (u *uenvTestSuite) TestErrorOnMalformedData(c *C) { + mockData := []byte{ + // foo + 0x66, 0x6f, 0x6f, + // eof + 0x00, 0x00, + } + u.makeUbootEnvFromData(c, mockData) + + env, err := ubootenv.Open(u.envFile) + c.Assert(err, ErrorMatches, `cannot parse line "foo" as key=value pair`) + c.Assert(env, IsNil) +} + +// ensure that the malformed data is not causing us to panic. +func (u *uenvTestSuite) TestOpenBestEffort(c *C) { + testCases := map[string][]byte{"noise": { + // key1=value1 + 0x6b, 0x65, 0x79, 0x31, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x31, 0x00, + // foo + 0x66, 0x6f, 0x6f, 0x00, + // key2=value2 + 0x6b, 0x65, 0x79, 0x32, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x00, + // eof + 0x00, 0x00, + }, "no-eof": { + // key1=value1 + 0x6b, 0x65, 0x79, 0x31, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x31, 0x00, + // key2=value2 + 0x6b, 0x65, 0x79, 0x32, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x00, + // NO EOF! + }, "noise-eof": { + // key1=value1 + 0x6b, 0x65, 0x79, 0x31, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x31, 0x00, + // key2=value2 + 0x6b, 0x65, 0x79, 0x32, 0x3d, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x32, 0x00, + // foo + 0x66, 0x6f, 0x6f, 0x00, + }} + for testName, mockData := range testCases { + u.makeUbootEnvFromData(c, mockData) + + env, err := ubootenv.OpenWithFlags(u.envFile, ubootenv.OpenBestEffort) + c.Assert(err, IsNil, Commentf(testName)) + c.Check(env.String(), Equals, "key1=value1\nkey2=value2\n", Commentf(testName)) + } +} + +func (u *uenvTestSuite) TestErrorOnMissingKeyInKeyValuePair(c *C) { + mockData := []byte{ + // =foo + 0x3d, 0x66, 0x6f, 0x6f, + // eof + 0x00, 0x00, + } + u.makeUbootEnvFromData(c, mockData) + + env, err := ubootenv.Open(u.envFile) + c.Assert(err, ErrorMatches, `cannot parse line "=foo" as key=value pair`) + c.Assert(env, IsNil) +} + +func (u *uenvTestSuite) TestReadEmptyFile(c *C) { + mockData := []byte{ + // eof + 0x00, 0x00, + // empty + 0xff, 0xff, + } + u.makeUbootEnvFromData(c, mockData) + + env, err := ubootenv.Open(u.envFile) + c.Assert(err, IsNil) + c.Assert(env.String(), Equals, "") +} + +func (u *uenvTestSuite) TestWritesEmptyFileWithDoubleNewline(c *C) { + env, err := ubootenv.Create(u.envFile, 12) + c.Assert(err, IsNil) + err = env.Save() + c.Assert(err, IsNil) + + r, err := os.Open(u.envFile) + c.Assert(err, IsNil) + defer r.Close() + content, err := ioutil.ReadAll(r) + c.Assert(err, IsNil) + c.Assert(content, DeepEquals, []byte{ + // crc + 0x11, 0x38, 0xb3, 0x89, + // redundant + 0x0, + // eof + 0x0, 0x0, + // footer + 0xff, 0xff, 0xff, 0xff, 0xff, + }) + + env, err = ubootenv.Open(u.envFile) + c.Assert(err, IsNil) + c.Assert(env.String(), Equals, "") +} + +func (u *uenvTestSuite) TestWritesContentCorrectly(c *C) { + totalSize := 16 + + env, err := ubootenv.Create(u.envFile, totalSize) + c.Assert(err, IsNil) + env.Set("a", "b") + env.Set("c", "d") + err = env.Save() + c.Assert(err, IsNil) + + r, err := os.Open(u.envFile) + c.Assert(err, IsNil) + defer r.Close() + content, err := ioutil.ReadAll(r) + c.Assert(err, IsNil) + c.Assert(content, DeepEquals, []byte{ + // crc + 0xc7, 0xd9, 0x6b, 0xc5, + // redundant + 0x0, + // a=b + 0x61, 0x3d, 0x62, + // eol + 0x0, + // c=d + 0x63, 0x3d, 0x64, + // eof + 0x0, 0x0, + // footer + 0xff, 0xff, + }) + + env, err = ubootenv.Open(u.envFile) + c.Assert(err, IsNil) + c.Assert(env.String(), Equals, "a=b\nc=d\n") + c.Assert(env.Size(), Equals, totalSize) +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/ubootenv/export_test.go snapd-2.45.1ubuntu0.2/bootloader/ubootenv/export_test.go --- snapd-2.37.4ubuntu0.1/bootloader/ubootenv/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/ubootenv/export_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,24 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2017 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package ubootenv + +var ( + WriteUint32 = writeUint32 +) diff -Nru snapd-2.37.4ubuntu0.1/bootloader/uboot.go snapd-2.45.1ubuntu0.2/bootloader/uboot.go --- snapd-2.37.4ubuntu0.1/bootloader/uboot.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/uboot.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,206 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 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" + "os" + "path/filepath" + + "github.com/snapcore/snapd/bootloader/ubootenv" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" +) + +type uboot struct { + rootdir string + basedir string + + ubootEnvFileName string +} + +func (u *uboot) setDefaults() { + u.basedir = "/boot/uboot/" + u.ubootEnvFileName = "uboot.env" +} + +func (u *uboot) processBlOpts(blOpts *Options) { + if blOpts != nil { + switch { + case blOpts.NoSlashBoot, blOpts.Recovery: + // Recovery or NoSlashBoot imply we use the "boot.sel" simple text + // format file in /uboot/ubuntu as it exists on the partition + // directly + u.basedir = "/uboot/ubuntu/" + fallthrough + case blOpts.ExtractedRunKernelImage: + // if just ExtractedRunKernelImage is defined, we expect to find + // /boot/uboot/boot.sel + u.ubootEnvFileName = "boot.sel" + } + } +} + +// newUboot create a new Uboot bootloader object +func newUboot(rootdir string, blOpts *Options) ExtractedRecoveryKernelImageBootloader { + u := &uboot{ + rootdir: rootdir, + } + u.setDefaults() + u.processBlOpts(blOpts) + + if !osutil.FileExists(u.envFile()) { + return nil + } + + return u +} + +func (u *uboot) Name() string { + return "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, u.basedir) +} + +func (u *uboot) InstallBootConfig(gadgetDir string, blOpts *Options) (bool, error) { + gadgetFile := filepath.Join(gadgetDir, u.Name()+".conf") + // if the gadget file is empty, then we don't install anything + // this is because there are some gadgets, namely the 20 pi gadget right + // now, that don't use a uboot.env to boot and instead use a boot.scr, and + // installing a uboot.env file of any form in the root directory will break + // the boot.scr, so for these setups we just don't install anything + // TODO:UC20: how can we do this better? maybe parse the file to get the + // actual format? + st, err := os.Stat(gadgetFile) + if err != nil { + return false, err + } + if st.Size() == 0 { + // we have an empty uboot.conf, and hence a uboot bootloader in the + // gadget, but nothing to copy in this case and instead just install our + // own boot.sel file + u.processBlOpts(blOpts) + + err := os.MkdirAll(filepath.Dir(u.envFile()), 0755) + if err != nil { + return false, err + } + + // TODO:UC20: what's a reasonable size for this file? + env, err := ubootenv.Create(u.envFile(), 4096) + if err != nil { + return false, err + } + + if err := env.Save(); err != nil { + return false, nil + } + + return true, nil + } + + // InstallBootConfig gets called on a uboot that does not come from newUboot + // so we need to apply the defaults here + u.setDefaults() + + if blOpts != nil && blOpts.Recovery { + // not supported yet, this is traditional uboot.env from gadget + // TODO:UC20: support this use-case + return false, fmt.Errorf("non-empty uboot.env not supported on UC20 yet") + } + + systemFile := u.ConfigFile() + return genericInstallBootConfig(gadgetFile, systemFile) +} + +func (u *uboot) ConfigFile() string { + return u.envFile() +} + +func (u *uboot) envFile() string { + return filepath.Join(u.dir(), u.ubootEnvFileName) +} + +func (u *uboot) SetBootVars(values map[string]string) error { + env, err := ubootenv.OpenWithFlags(u.envFile(), ubootenv.OpenBestEffort) + if err != nil { + return err + } + + 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 +} + +func (u *uboot) GetBootVars(names ...string) (map[string]string, error) { + out := map[string]string{} + + env, err := ubootenv.OpenWithFlags(u.envFile(), ubootenv.OpenBestEffort) + if err != nil { + return nil, err + } + + for _, name := range names { + out[name] = env.Get(name) + } + + return out, nil +} + +func (u *uboot) ExtractKernelAssets(s snap.PlaceInfo, snapf snap.Container) error { + dstDir := filepath.Join(u.dir(), s.Filename()) + assets := []string{"kernel.img", "initrd.img", "dtbs/*"} + return extractKernelAssetsToBootDir(dstDir, snapf, assets) +} + +func (u *uboot) ExtractRecoveryKernelAssets(recoverySystemDir string, s snap.PlaceInfo, snapf snap.Container) error { + if recoverySystemDir == "" { + return fmt.Errorf("internal error: recoverySystemDir unset") + } + + recoverySystemUbootKernelAssetsDir := filepath.Join(u.rootdir, recoverySystemDir, "kernel") + assets := []string{"kernel.img", "initrd.img", "dtbs/*"} + return extractKernelAssetsToBootDir(recoverySystemUbootKernelAssetsDir, snapf, assets) +} + +func (u *uboot) RemoveKernelAssets(s snap.PlaceInfo) error { + return removeKernelAssetsFromBootDir(u.dir(), s) +} diff -Nru snapd-2.37.4ubuntu0.1/bootloader/uboot_test.go snapd-2.45.1ubuntu0.2/bootloader/uboot_test.go --- snapd-2.37.4ubuntu0.1/bootloader/uboot_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/bootloader/uboot_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,267 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2014-2015 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 ( + "os" + "path/filepath" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/bootloader/ubootenv" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapfile" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" +) + +type ubootTestSuite struct { + baseBootenvTestSuite +} + +var _ = Suite(&ubootTestSuite{}) + +func (s *ubootTestSuite) TestNewUbootNoUbootReturnsNil(c *C) { + u := bootloader.NewUboot(s.rootdir, nil) + c.Assert(u, IsNil) +} + +func (s *ubootTestSuite) TestNewUboot(c *C) { + bootloader.MockUbootFiles(c, s.rootdir, nil) + u := bootloader.NewUboot(s.rootdir, nil) + c.Assert(u, NotNil) + c.Assert(u.Name(), Equals, "uboot") +} + +func (s *ubootTestSuite) TestUbootGetEnvVar(c *C) { + bootloader.MockUbootFiles(c, s.rootdir, nil) + u := bootloader.NewUboot(s.rootdir, nil) + c.Assert(u, NotNil) + err := u.SetBootVars(map[string]string{ + "snap_mode": "", + "snap_core": "4", + }) + c.Assert(err, IsNil) + + m, err := u.GetBootVars("snap_mode", "snap_core") + c.Assert(err, IsNil) + c.Assert(m, DeepEquals, map[string]string{ + "snap_mode": "", + "snap_core": "4", + }) +} + +func (s *ubootTestSuite) TestGetBootloaderWithUboot(c *C) { + bootloader.MockUbootFiles(c, s.rootdir, nil) + + 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, s.rootdir, nil) + u := bootloader.NewUboot(s.rootdir, nil) + c.Assert(u, NotNil) + + envFile := u.ConfigFile() + env, err := ubootenv.Create(envFile, 4096) + c.Assert(err, IsNil) + env.Set("snap_ab", "b") + env.Set("snap_mode", "") + err = env.Save() + c.Assert(err, IsNil) + + st, err := os.Stat(envFile) + c.Assert(err, IsNil) + time.Sleep(100 * time.Millisecond) + + // note that we set to the same var as above + err = u.SetBootVars(map[string]string{"snap_ab": "b"}) + c.Assert(err, IsNil) + + env, err = ubootenv.Open(envFile) + c.Assert(err, IsNil) + c.Assert(env.String(), Equals, "snap_ab=b\n") + + st2, err := os.Stat(envFile) + c.Assert(err, IsNil) + c.Assert(st.ModTime(), Equals, st2.ModTime()) +} + +func (s *ubootTestSuite) TestUbootSetBootVarFwEnv(c *C) { + bootloader.MockUbootFiles(c, s.rootdir, nil) + u := bootloader.NewUboot(s.rootdir, nil) + + err := u.SetBootVars(map[string]string{"key": "value"}) + c.Assert(err, IsNil) + + content, err := u.GetBootVars("key") + c.Assert(err, IsNil) + c.Assert(content, DeepEquals, map[string]string{"key": "value"}) +} + +func (s *ubootTestSuite) TestUbootGetBootVarFwEnv(c *C) { + bootloader.MockUbootFiles(c, s.rootdir, nil) + u := bootloader.NewUboot(s.rootdir, nil) + + err := u.SetBootVars(map[string]string{"key2": "value2"}) + c.Assert(err, IsNil) + + content, err := u.GetBootVars("key2") + c.Assert(err, IsNil) + c.Assert(content, DeepEquals, map[string]string{"key2": "value2"}) +} + +func (s *ubootTestSuite) TestExtractKernelAssetsAndRemove(c *C) { + bootloader.MockUbootFiles(c, s.rootdir, nil) + u := bootloader.NewUboot(s.rootdir, nil) + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"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 := snapfile.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + err = u.ExtractKernelAssets(info, snapf) + c.Assert(err, IsNil) + + // this is where the kernel/initrd is unpacked + kernelAssetsDir := filepath.Join(s.rootdir, "boot", "uboot", "ubuntu-kernel_42.snap") + + for _, def := range files { + if def[0] == "meta/kernel.yaml" { + break + } + + fullFn := filepath.Join(kernelAssetsDir, def[0]) + c.Check(fullFn, testutil.FileEquals, def[1]) + } + + // remove + err = u.RemoveKernelAssets(info) + c.Assert(err, IsNil) + + c.Check(osutil.FileExists(kernelAssetsDir), Equals, false) +} + +func (s *ubootTestSuite) TestExtractRecoveryKernelAssets(c *C) { + bootloader.MockUbootFiles(c, s.rootdir, nil) + u := bootloader.NewUboot(s.rootdir, nil) + + files := [][]string{ + {"kernel.img", "I'm a kernel"}, + {"initrd.img", "...and I'm an initrd"}, + {"dtbs/foo.dtb", "foo dtb"}, + {"dtbs/bar.dto", "bar dtbo"}, + // 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 := snapfile.Open(fn) + c.Assert(err, IsNil) + + info, err := snap.ReadInfoFromSnapFile(snapf, si) + c.Assert(err, IsNil) + + // try with empty recovery dir first to check the errors + err = u.ExtractRecoveryKernelAssets("", info, snapf) + c.Assert(err, ErrorMatches, "internal error: recoverySystemDir unset") + + // now the expected behavior + err = u.ExtractRecoveryKernelAssets("recovery-dir", info, snapf) + c.Assert(err, IsNil) + + // this is where the kernel/initrd is unpacked + kernelAssetsDir := filepath.Join(s.rootdir, "recovery-dir", "kernel") + + for _, def := range files { + if def[0] == "meta/kernel.yaml" { + break + } + + fullFn := filepath.Join(kernelAssetsDir, def[0]) + c.Check(fullFn, testutil.FileEquals, def[1]) + } +} + +func (s *ubootTestSuite) TestUbootUC20OptsPlacement(c *C) { + tt := []struct { + blOpts *bootloader.Options + expEnv string + comment string + }{ + { + nil, + "/boot/uboot/uboot.env", + "traditional uboot.env", + }, + { + &bootloader.Options{NoSlashBoot: true}, + "/uboot/ubuntu/boot.sel", + "uc20 install mode boot.sel", + }, + { + &bootloader.Options{ExtractedRunKernelImage: true}, + "/boot/uboot/boot.sel", + "uc20 run mode boot.sel", + }, + { + &bootloader.Options{Recovery: true}, + "/uboot/ubuntu/boot.sel", + "uc20 recovery boot.sel", + }, + } + + for _, t := range tt { + dir := c.MkDir() + bootloader.MockUbootFiles(c, dir, t.blOpts) + u := bootloader.NewUboot(dir, t.blOpts) + c.Assert(u, NotNil, Commentf(t.comment)) + c.Assert(u.ConfigFile(), Equals, filepath.Join(dir, t.expEnv), Commentf(t.comment)) + + // if we set boot vars on the uboot, we can open the config file and + // get the same variables + c.Assert(u.SetBootVars(map[string]string{"hello": "there"}), IsNil) + env, err := ubootenv.Open(filepath.Join(dir, t.expEnv)) + c.Assert(err, IsNil) + c.Assert(env.Get("hello"), Equals, "there") + } +} diff -Nru snapd-2.37.4ubuntu0.1/build-aux/snap/snapcraft.yaml snapd-2.45.1ubuntu0.2/build-aux/snap/snapcraft.yaml --- snapd-2.37.4ubuntu0.1/build-aux/snap/snapcraft.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/build-aux/snap/snapcraft.yaml 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,129 @@ +name: snapd +type: snapd +summary: Daemon and tooling that enable snap packages +description: | + Install, configure, refresh and remove snap packages. Snaps are + 'universal' packages that work across many different Linux systems, + enabling secure distribution of the latest apps and utilities for + cloud, servers, desktops and the internet of things. + + Start with 'snap list' to see installed snaps. +adopt-info: snapd-deb +# build-base is needed here for snapcraft to build this snap as with "modern" +# snapcraft +build-base: core +grade: stable +license: GPL-3.0 + +# Note that this snap is unusual in that it has no "apps" section. +# +# It is started via re-exec on classic systems and via special +# handling in the core18 snap on Ubuntu Core Systems. +# +# Because snapd itself manages snaps it must currently run totally +# unconfined (even devmode is not enough). +# +# See the comments from jdstrand in +# https://forum.snapcraft.io/t/5547/10 +parts: + snapd-deb: + plugin: nil + source: . + build-snaps: [go/1.10/stable] + override-pull: | + snapcraftctl pull + # install build dependencies + export DEBIAN_FRONTEND=noninteractive + export DEBCONF_NONINTERACTIVE_SEEN=true + sudo -E apt-get build-dep -y ./ + ./get-deps.sh --skip-unused-check + # set version after installing dependencies so we have all the tools here + snapcraftctl set-version "$(./mkversion.sh --output-only)" + override-build: | + # unset the LD_FLAGS and LD_LIBRARY_PATH vars that snapcraft sets for us + # as those will point to the $SNAPCRAFT_STAGE which on re-builds will + # contain things like libc and friends that confuse the debian package + # build system + # TODO: should we unset $PATH to not include $SNAPCRAFT_STAGE too? + unset LD_FLAGS + unset LD_LIBRARY_PATH + # if we are root, disable tests because a number of them fail when run as + # root + if [ "$(id -u)" = "0" ]; then + DEB_BUILD_OPTIONS=nocheck + export DEB_BUILD_OPTIONS + fi + # run the real build (but just build the binary package, and don't + # bother compressing it too much) + dpkg-buildpackage -b -Zgzip -zfast + dpkg-deb -x $(pwd)/../snapd_*.deb $SNAPCRAFT_PART_INSTALL + + # xdelta is used to enable delta downloads (even if the host does not have it) + xdelta3: + plugin: nil + stage-packages: + - xdelta3 + stage: + - usr/bin/* + - usr/lib/* + - lib/* + # squashfs-tools are used by `snap pack` + squashfs-tools: + plugin: nil + stage-packages: + - squashfs-tools + stage: + - usr/bin/* + - usr/lib/* + - lib/* + # liblzma5 is part of core but the snapd snap needs to run even without core + liblzma5: + plugin: nil + stage-packages: + - liblzma5 + stage: + - lib/* + # libc6 is part of core but we need it in the snapd snap for + # CommandFromSystemSnap + libc6: + plugin: nil + stage-packages: + - libc6 + - libc-bin + stage: + - lib/* + - usr/lib/* + - lib64/* + - etc/ld.so.conf + - etc/ld.so.conf.d/* + override-stage: | + snapcraftctl stage + # fix symlinks of ld.so to be relative + if [ "$(readlink -f lib64/ld-linux-x86-64.so.2)" = "/lib/x86_64-linux-gnu/ld-2.23.so" ]; then + ln -f -s ../lib/x86_64-linux-gnu/ld-2.23.so lib64/ld-linux-x86-64.so.2 + fi + if [ "$(readlink -f lib64/ld64.so.2)" = "/lib/powerpc64le-linux-gnu/ld-2.23.so" ]; then + ln -f -s ../lib/powerpc64le-linux-gnu/ld-2.23.so lib64/ld64.so.2 + fi + # the version in Ubuntu 16.04 (cache v6) + fontconfig-xenial: + plugin: nil + build-packages: [python3-apt] + source: https://github.com/snapcore/fc-cache-static-builder.git + override-build: | + ./build-from-security.py xenial + mkdir -p $SNAPCRAFT_PART_INSTALL/bin + cp -a fc-cache-xenial $SNAPCRAFT_PART_INSTALL/bin/fc-cache-v6 + prime: + - bin/fc-cache-v6 + # the version in Ubuntu 18.04 (cache v7) + fontconfig-bionic: + plugin: nil + build-packages: [python3-apt] + source: https://github.com/snapcore/fc-cache-static-builder.git + override-build: | + ./build-from-security.py bionic + mkdir -p $SNAPCRAFT_PART_INSTALL/bin + cp -a fc-cache-bionic $SNAPCRAFT_PART_INSTALL/bin/fc-cache-v7 + prime: + - bin/fc-cache-v7 diff -Nru snapd-2.37.4ubuntu0.1/check-pr-skip-spread.py snapd-2.45.1ubuntu0.2/check-pr-skip-spread.py --- snapd-2.37.4ubuntu0.1/check-pr-skip-spread.py 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/check-pr-skip-spread.py 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,101 @@ +#!/usr/bin/python3 + +import argparse +import urllib.request +import logging + +from html.parser import HTMLParser + +# PR label indicating that spread job should be skipped +LABEL_SKIP_SPREAD_JOB = "Skip spread" + + +class GithubLabelsParser(HTMLParser): + def __init__(self): + super().__init__() + self.in_labels = False + self.deep = 0 + self.labels = [] + + def handle_starttag(self, tag, attributes): + logging.debug(attributes) + + if not self.in_labels: + attr_class = [attr[1] for attr in attributes if attr[0] == "class"] + if len(attr_class) == 0: + return + # labels are in separate div defined like: + #
+ elems = attr_class[0].split(" ") + if "labels" in elems: + self.in_labels = True + self.deep = 1 + logging.debug("labels start") + else: + # nesting counter + self.deep += 1 + + # inside labels + # label entry has + # + attr_data_name = [attr[1] for attr in attributes if attr[0] == "data-name"] + if len(attr_data_name) == 0: + return + data_name = attr_data_name[0] + logging.debug("found label: %s", data_name) + self.labels.append(data_name) + + def handle_endtag(self, tag): + if self.in_labels: + self.deep -= 1 + if self.deep < 1: + logging.debug("labels end") + self.in_labels = False + + def handle_data(self, data): + if self.in_labels: + logging.debug("data: %s", data) + + +def grab_pr_labels(pr_number: int): + # ideally we would use the github API - however we can't because: + # a) its rate limiting and travis IPs hit the API a lot so we regularly + # get errors + # b) using a API token is tricky because travis will not allow the secure + # vars for forks + # so instead we just scrape the html title which is unlikely to change + # radically + parser = GithubLabelsParser() + with urllib.request.urlopen( + "https://github.com/snapcore/snapd/pull/{}".format(pr_number) + ) as f: + parser.feed(f.read().decode("utf-8")) + return parser.labels + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "pr_number", metavar="PR number", help="the github PR number to check" + ) + parser.add_argument( + "-d", "--debug", help="enable debug logging", action="store_true" + ) + args = parser.parse_args() + + lvl = logging.INFO + if args.debug: + lvl = logging.DEBUG + logging.basicConfig(level=lvl) + + labels = grab_pr_labels(args.pr_number) + print("labels:", labels) + + if LABEL_SKIP_SPREAD_JOB not in labels: + raise SystemExit(1) + + print("requested to skip the spread job") + + +if __name__ == "__main__": + main() diff -Nru snapd-2.37.4ubuntu0.1/check-pr-title.py snapd-2.45.1ubuntu0.2/check-pr-title.py --- snapd-2.37.4ubuntu0.1/check-pr-title.py 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/check-pr-title.py 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,78 @@ +#!/usr/bin/python3 + +import argparse +import re +import urllib.request + +from html.parser import HTMLParser + + +class InvalidPRTitle(Exception): + def __init__(self, invalid_title): + self.invalid_title = invalid_title + + +class GithubTitleParser(HTMLParser): + def __init__(self): + HTMLParser.__init__(self) + self._cur_tag = "" + self.title = "" + + def handle_starttag(self, tag, attributes): + self._cur_tag = tag + + def handle_endtag(self, tag): + self._cur_tag = "" + + def handle_data(self, data): + if self._cur_tag == "title": + self.title = data + + +def check_pr_title(pr_number: int): + # ideally we would use the github API - however we can't because: + # a) its rate limiting and travis IPs hit the API a lot so we regularly + # get errors + # b) using a API token is tricky because travis will not allow the secure + # vars for forks + # so instead we just scrape the html title which is unlikely to change + # radically + parser = GithubTitleParser() + with urllib.request.urlopen( + "https://github.com/snapcore/snapd/pull/{}".format(pr_number) + ) as f: + parser.feed(f.read().decode("utf-8")) + # the title has the format: + # "Added api endpoint for downloading snaps by glower · Pull Request #6958 · snapcore/snapd · GitHub" + # so we rsplit() once to get the title (rsplit to not get confused by + # possible "by" words in the real title) + title = parser.title.rsplit(" by ", maxsplit=1)[0] + print(title) + # cover most common cases: + # package: foo + # 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): + raise InvalidPRTitle(title) + + +def main(): + parser = argparse.ArgumentParser() + parser.add_argument( + "pr_number", metavar="PR number", help="the github PR number to check" + ) + args = parser.parse_args() + try: + check_pr_title(args.pr_number) + except InvalidPRTitle as e: + print('Invalid PR title: "{}"\n'.format(e.invalid_title)) + print("Please provide a title in the following format:") + print("module: short description") + print("E.g.:") + print("daemon: fix frobinator bug") + raise SystemExit(1) + + +if __name__ == "__main__": + main() diff -Nru snapd-2.37.4ubuntu0.1/.clang-format snapd-2.45.1ubuntu0.2/.clang-format --- snapd-2.37.4ubuntu0.1/.clang-format 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/.clang-format 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,3 @@ +BasedOnStyle: Google +IndentWidth: 4 +ColumnLimit: 120 diff -Nru snapd-2.37.4ubuntu0.1/client/aliases_test.go snapd-2.45.1ubuntu0.2/client/aliases_test.go --- snapd-2.37.4ubuntu0.1/client/aliases_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/aliases_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/client/apps.go snapd-2.45.1ubuntu0.2/client/apps.go --- snapd-2.37.4ubuntu0.1/client/apps.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/apps.go 2020-06-05 13:13:49.000000000 +0000 @@ -22,6 +22,7 @@ import ( "bufio" "bytes" + "context" "encoding/json" "errors" "fmt" @@ -119,7 +120,7 @@ query.Set("follow", strconv.FormatBool(opts.Follow)) } - rsp, err := client.raw("GET", "/v2/logs", query, nil, nil) + rsp, err := client.raw(context.Background(), "GET", "/v2/logs", query, nil, nil) if err != nil { return nil, err } @@ -130,7 +131,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.37.4ubuntu0.1/client/apps_test.go snapd-2.45.1ubuntu0.2/client/apps_test.go --- snapd-2.37.4ubuntu0.1/client/apps_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/apps_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -107,6 +107,11 @@ ch, err := cs.cli.Logs([]string{"foo", "bar"}, client.LogOptions{N: -1, Follow: false}) c.Check(cs.req.URL.Path, check.Equals, "/v2/logs") c.Check(cs.req.Method, check.Equals, "GET") + + // logs cannot have a deadline because of "-f" + _, ok := cs.req.Context().Deadline() + c.Check(ok, check.Equals, false) + query := cs.req.URL.Query() c.Check(query, check.HasLen, 2) c.Check(query.Get("names"), check.Equals, "foo,bar") @@ -213,6 +218,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 +280,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 +342,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.37.4ubuntu0.1/client/asserts.go snapd-2.45.1ubuntu0.2/client/asserts.go --- snapd-2.37.4ubuntu0.1/client/asserts.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/asserts.go 2020-06-05 13:13:49.000000000 +0000 @@ -21,12 +21,16 @@ import ( "bytes" + "context" "fmt" "io" "net/url" "strconv" + "golang.org/x/xerrors" + "github.com/snapcore/snapd/asserts" // for parsing + "github.com/snapcore/snapd/snap" ) // Ack tries to add an assertion to the system assertion @@ -49,14 +53,25 @@ } _, err := client.doSync("GET", "/v2/assertions", nil, nil, nil, &types) if err != nil { - return nil, fmt.Errorf("cannot get assertion type names: %v", err) + fmt := "cannot get assertion type names: %w" + return nil, xerrors.Errorf(fmt, err) } return types.Types, nil } +// KnownOptions represent the options of the Known call. +type KnownOptions struct { + // If Remote is true, the store is queried to find the assertion + Remote bool +} + // Known queries assertions with type assertTypeName and matching assertion headers. -func (client *Client) Known(assertTypeName string, headers map[string]string) ([]asserts.Assertion, error) { +func (client *Client) Known(assertTypeName string, headers map[string]string, opts *KnownOptions) ([]asserts.Assertion, error) { + if opts == nil { + opts = &KnownOptions{} + } + path := fmt.Sprintf("/v2/assertions/%s", assertTypeName) q := url.Values{} @@ -65,11 +80,18 @@ q.Set(k, v) } } + if opts.Remote { + q.Set("remote", "true") + } - response, err := client.raw("GET", path, q, nil, nil) + ctx, cancel := context.WithTimeout(context.Background(), doTimeout) + defer cancel() + response, err := client.raw(ctx, "GET", path, q, nil, nil) if err != nil { - return nil, fmt.Errorf("failed to query assertions: %v", err) + fmt := "failed to query assertions: %w" + return nil, xerrors.Errorf(fmt, err) } + defer response.Body.Close() if response.StatusCode != 200 { return nil, parseError(response) @@ -102,3 +124,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}, nil) + 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.37.4ubuntu0.1/client/asserts_test.go snapd-2.45.1ubuntu0.2/client/asserts_test.go --- snapd-2.37.4ubuntu0.1/client/asserts_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/asserts_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -25,9 +25,13 @@ "net/http" "net/url" + "golang.org/x/xerrors" + . "gopkg.in/check.v1" "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/snap" ) func (cs *clientSuite) TestClientAssert(c *C) { @@ -60,16 +64,23 @@ } func (cs *clientSuite) TestClientAssertsCallsEndpoint(c *C) { - _, _ = cs.cli.Known("snap-revision", nil) + _, _ = cs.cli.Known("snap-revision", nil, nil) + c.Check(cs.req.Method, Equals, "GET") + c.Check(cs.req.URL.Path, Equals, "/v2/assertions/snap-revision") +} + +func (cs *clientSuite) TestClientAssertsOptsCallsEndpoint(c *C) { + _, _ = cs.cli.Known("snap-revision", nil, &client.KnownOptions{Remote: true}) c.Check(cs.req.Method, Equals, "GET") c.Check(cs.req.URL.Path, Equals, "/v2/assertions/snap-revision") + c.Check(cs.req.URL.Query()["remote"], DeepEquals, []string{"true"}) } func (cs *clientSuite) TestClientAssertsCallsEndpointWithFilter(c *C) { _, _ = cs.cli.Known("snap-revision", map[string]string{ "snap-id": "snap-id-1", "snap-sha3-384": "sha3-384...", - }) + }, nil) u, err := url.ParseRequestURI(cs.req.URL.String()) c.Assert(err, IsNil) c.Check(u.Path, Equals, "/v2/assertions/snap-revision") @@ -81,7 +92,7 @@ func (cs *clientSuite) TestClientAssertsHttpError(c *C) { cs.err = errors.New("fail") - _, err := cs.cli.Known("snap-build", nil) + _, err := cs.cli.Known("snap-build", nil, nil) c.Assert(err, ErrorMatches, "failed to query assertions: cannot communicate with server: fail") } @@ -96,7 +107,7 @@ "message": "invalid" } }` - _, err := cs.cli.Known("snap-build", nil) + _, err := cs.cli.Known("snap-build", nil, nil) c.Assert(err, ErrorMatches, "invalid") } @@ -132,7 +143,7 @@ openpgp ... ` - a, err := cs.cli.Known("snap-revision", nil) + a, err := cs.cli.Known("snap-revision", nil, nil) c.Assert(err, IsNil) c.Check(a, HasLen, 2) @@ -144,7 +155,7 @@ cs.header.Add("X-Ubuntu-Assertions-Count", "0") cs.rsp = "" cs.status = 200 - a, err := cs.cli.Known("snap-revision", nil) + a, err := cs.cli.Known("snap-revision", nil, nil) c.Assert(err, IsNil) c.Check(a, HasLen, 0) } @@ -154,6 +165,75 @@ cs.header.Add("X-Ubuntu-Assertions-Count", "4") cs.rsp = "" cs.status = 200 - _, err := cs.cli.Known("snap-build", nil) + _, err := cs.cli.Known("snap-build", nil, 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") +} + +func (cs *clientSuite) TestClientAssertTypesErrIsWrapped(c *C) { + cs.err = errors.New("boom") + _, err := cs.cli.AssertionTypes() + var e xerrors.Wrapper + c.Assert(err, Implements, &e) +} + +func (cs *clientSuite) TestClientKnownErrIsWrapped(c *C) { + cs.err = errors.New("boom") + _, err := cs.cli.Known("foo", nil, nil) + var e xerrors.Wrapper + c.Assert(err, Implements, &e) +} diff -Nru snapd-2.37.4ubuntu0.1/client/client.go snapd-2.45.1ubuntu0.2/client/client.go --- snapd-2.37.4ubuntu0.1/client/client.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/client.go 2020-06-05 13:13:49.000000000 +0000 @@ -21,6 +21,7 @@ import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -70,6 +71,9 @@ // DisableKeepAlive indicates whether the connections should not be kept // alive for later reuse DisableKeepAlive bool + + // User-Agent to sent to the snapd daemon + UserAgent string } // A Client knows how to talk to the snappy daemon. @@ -84,6 +88,8 @@ warningCount int warningTimestamp time.Time + + userAgent string } // New returns a new instance of Client @@ -103,6 +109,7 @@ doer: &http.Client{Transport: transport}, disableAuth: config.DisableAuth, interactive: config.Interactive, + userAgent: config.UserAgent, } } @@ -115,6 +122,7 @@ doer: &http.Client{Transport: &http.Transport{DisableKeepAlives: config.DisableKeepAlive}}, disableAuth: config.DisableAuth, interactive: config.Interactive, + userAgent: config.UserAgent, } } @@ -172,10 +180,23 @@ return fmt.Sprintf("cannot add authorization: %v", e.error) } -type ConnectionError struct{ error } +type ConnectionError struct{ Err error } func (e ConnectionError) Error() string { - return fmt.Sprintf("cannot communicate with server: %v", e.error) + var errStr string + switch e.Err { + case context.DeadlineExceeded: + errStr = "timeout exceeded while waiting for response" + case context.Canceled: + errStr = "request canceled" + default: + errStr = e.Err.Error() + } + return fmt.Sprintf("cannot communicate with server: %s", errStr) +} + +func (e ConnectionError) Unwrap() error { + return e.Err } // AllowInteractionHeader is the HTTP request header used to indicate @@ -185,7 +206,7 @@ // raw performs a request and returns the resulting http.Response and // error you usually only need to call this directly if you expect the // response to not be JSON, otherwise you'd call Do(...) instead. -func (client *Client) raw(method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) { +func (client *Client) raw(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader) (*http.Response, error) { // fake a url to keep http.Client happy u := client.baseURL u.Path = path.Join(client.baseURL.Path, urlpath) @@ -194,6 +215,9 @@ if err != nil { return nil, RequestError{err} } + if client.userAgent != "" { + req.Header.Set("User-Agent", client.userAgent) + } for key, value := range headers { req.Header.Set(key, value) @@ -211,6 +235,10 @@ req.Header.Set(AllowInteractionHeader, "true") } + if ctx != nil { + req = req.WithContext(ctx) + } + rsp, err := client.doer.Do(req) if err != nil { return nil, ConnectionError{err} @@ -219,13 +247,36 @@ return rsp, nil } +// rawWithTimeout is like raw(), but sets a timeout for the whole of request and +// response (including rsp.Body() read) round trip. The caller is responsible +// for canceling the internal context to release the resources associated with +// the request by calling the returned cancel function. +func (client *Client) rawWithTimeout(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader, timeout time.Duration) (*http.Response, context.CancelFunc, error) { + if timeout == 0 { + return nil, nil, fmt.Errorf("internal error: timeout not set for rawWithTimeout") + } + + ctx, cancel := context.WithTimeout(ctx, timeout) + rsp, err := client.raw(ctx, method, urlpath, query, headers, body) + if err != nil && ctx.Err() != nil { + cancel() + return nil, nil, ConnectionError{ctx.Err()} + } + + return rsp, cancel, err +} + var ( - doRetry = 250 * time.Millisecond - doTimeout = 5 * time.Second + doRetry = 250 * time.Millisecond + // snapd may need to reach out to the store, where it uses a fixed 10s + // timeout for the whole of a single request to complete, requests are + // retried for up to 38s in total, make sure that the client timeout is + // not shorter than that + doTimeout = 50 * time.Second ) -// MockDoRetry mocks the delays used by the do retry loop. -func MockDoRetry(retry, timeout time.Duration) (restore func()) { +// MockDoTimings mocks the delay used by the do retry loop and request timeout. +func MockDoTimings(retry, timeout time.Duration) (restore func()) { oldRetry := doRetry oldTimeout := doTimeout doRetry = retry @@ -249,39 +300,56 @@ client.doer = hijacked{f} } +type doFlags struct { + NoTimeout bool +} + // 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{}, flags doFlags) (statusCode int, err error) { retry := time.NewTicker(doRetry) defer retry.Stop() - timeout := time.After(doTimeout) + timeout := time.NewTimer(doTimeout) + defer timeout.Stop() + var rsp *http.Response - var err error + var ctx context.Context = context.Background() for { - rsp, err = client.raw(method, path, query, headers, body) + if flags.NoTimeout { + rsp, err = client.raw(ctx, method, path, query, headers, body) + } else { + var cancel context.CancelFunc + // use the same timeout as for the whole of the retry + // loop to error out the whole do() call when a single + // request exceeds the deadline + rsp, cancel, err = client.rawWithTimeout(ctx, method, path, query, headers, body, doTimeout) + if err == nil { + defer cancel() + } + } if err == nil || method != "GET" { break } select { case <-retry.C: continue - case <-timeout: + case <-timeout.C: } 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 { @@ -303,10 +371,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, doFlags{}) + 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" { @@ -326,23 +395,28 @@ } func (client *Client) doAsync(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) { - _, changeID, err = client.doAsyncFull(method, path, query, headers, body) + _, changeID, err = client.doAsyncFull(method, path, query, headers, body, doFlags{}) return } -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 +func (client *Client) doAsyncNoTimeout(method, path string, query url.Values, headers map[string]string, body io.Reader) (changeID string, err error) { + _, changeID, err = client.doAsyncFull(method, path, query, headers, body, doFlags{NoTimeout: true}) + return changeID, err +} - if err := client.do(method, path, query, headers, body, &rsp); err != nil { +func (client *Client) doAsyncFull(method, path string, query url.Values, headers map[string]string, body io.Reader, flags doFlags) (result json.RawMessage, changeID string, err error) { + var rsp response + statusCode, err := client.do(method, path, query, headers, body, &rsp, flags) + 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 == "" { @@ -359,7 +433,9 @@ OSVersionID string OnClassic bool - KernelVersion string + KernelVersion string + Architecture string + Virtualization string } func (client *Client) ServerVersion() (*ServerVersion, error) { @@ -375,18 +451,18 @@ OSVersionID: sysInfo.OSRelease.VersionID, OnClassic: sysInfo.OnClassic, - KernelVersion: sysInfo.KernelVersion, + KernelVersion: sysInfo.KernelVersion, + Architecture: sysInfo.Architecture, + Virtualization: sysInfo.Virtualization, }, nil } // 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"` @@ -448,6 +524,10 @@ ErrorKindSystemRestart = "system-restart" ErrorKindDaemonRestart = "daemon-restart" + + ErrorKindAssertionNotFound = "assertion-not-found" + + ErrorKindUnsuccessful = "unsuccessful" ) // IsRetryable returns true if the given error is an error @@ -481,6 +561,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"` @@ -507,14 +598,16 @@ OnClassic bool `json:"on-classic"` Managed bool `json:"managed"` - KernelVersion string `json:"kernel-version,omitempty"` + KernelVersion string `json:"kernel-version,omitempty"` + Architecture string `json:"architecture,omitempty"` + Virtualization string `json:"virtualization,omitempty"` Refresh RefreshInfo `json:"refresh,omitempty"` Confinement string `json:"confinement"` 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) @@ -530,9 +623,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 } @@ -548,7 +641,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) } @@ -566,105 +659,6 @@ return &sysInfo, nil } -// CreateUserResult holds the result of a user creation. -type CreateUserResult struct { - Username string `json:"username"` - SSHKeys []string `json:"ssh-keys"` -} - -// CreateUserOptions holds options for creating a local system user. -// -// If Known is false, the provided email is used to query the store for -// username and SSH key details. -// -// If Known is true, the user will be created by looking through existing -// system-user assertions and looking for a matching email. If Email is -// empty then all such assertions are considered and multiple users may -// be created. -type CreateUserOptions struct { - Email string `json:"email,omitempty"` - Sudoer bool `json:"sudoer,omitempty"` - Known bool `json:"known,omitempty"` - ForceManaged bool `json:"force-managed,omitempty"` -} - -// CreateUser creates a local system user. See CreateUserOptions for details. -func (client *Client) CreateUser(options *CreateUserOptions) (*CreateUserResult, error) { - if options.Email == "" { - return nil, fmt.Errorf("cannot create a user without providing an email") - } - - var result CreateUserResult - data, err := json.Marshal(options) - if err != nil { - return nil, err - } - - if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil { - return nil, fmt.Errorf("while creating user: %v", err) - } - return &result, nil -} - -// CreateUsers creates multiple local system users. See CreateUserOptions for details. -// -// Results may be provided even if there are errors. -func (client *Client) CreateUsers(options []*CreateUserOptions) ([]*CreateUserResult, error) { - for _, opts := range options { - if opts.Email == "" && !opts.Known { - return nil, fmt.Errorf("cannot create user from store details without an email to query for") - } - } - - var results []*CreateUserResult - var errs []error - - for _, opts := range options { - data, err := json.Marshal(opts) - if err != nil { - return nil, err - } - - if opts.Email == "" { - var result []*CreateUserResult - if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil { - errs = append(errs, err) - } else { - results = append(results, result...) - } - } else { - var result *CreateUserResult - if _, err := client.doSync("POST", "/v2/create-user", nil, nil, bytes.NewReader(data), &result); err != nil { - errs = append(errs, err) - } else { - results = append(results, result) - } - } - } - - if len(errs) == 1 { - return results, errs[0] - } - if len(errs) > 1 { - var buf bytes.Buffer - for _, err := range errs { - fmt.Fprintf(&buf, "\n- %s", err) - } - return results, fmt.Errorf("while creating users:%s", buf.Bytes()) - } - return results, nil -} - -// Users returns the local users. -func (client *Client) Users() ([]*User, error) { - var result []*User - - if _, err := client.doSync("GET", "/v2/users", nil, nil, nil, &result); err != nil { - return nil, fmt.Errorf("while getting users: %v", err) - } - return result, nil -} - type debugAction struct { Action string `json:"action"` Params interface{} `json:"params,omitempty"` @@ -684,3 +678,12 @@ _, err = client.doSync("POST", "/v2/debug", nil, nil, bytes.NewReader(body), result) return err } + +func (client *Client) DebugGet(aspect string, result interface{}, params map[string]string) error { + urlParams := url.Values{"aspect": []string{aspect}} + for k, v := range params { + urlParams.Set(k, v) + } + _, err := client.doSync("GET", "/v2/debug", urlParams, nil, nil, &result) + return err +} diff -Nru snapd-2.37.4ubuntu0.1/client/client_test.go snapd-2.45.1ubuntu0.2/client/client_test.go --- snapd-2.37.4ubuntu0.1/client/client_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/client_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -26,6 +26,7 @@ "net" "net/http" "net/http/httptest" + "net/url" "os" "path/filepath" "strings" @@ -42,16 +43,17 @@ func Test(t *testing.T) { TestingT(t) } type clientSuite struct { - cli *client.Client - req *http.Request - reqs []*http.Request - rsp string - rsps []string - err error - doCalls int - header http.Header - status int - restore func() + cli *client.Client + req *http.Request + reqs []*http.Request + rsp string + rsps []string + err error + doCalls int + header http.Header + status int + contentLength int64 + restore func() } var _ = Suite(&clientSuite{}) @@ -69,10 +71,11 @@ cs.header = nil cs.status = 200 cs.doCalls = 0 + cs.contentLength = 0 dirs.SetRootDir(c.MkDir()) - cs.restore = client.MockDoRetry(time.Millisecond, 10*time.Millisecond) + cs.restore = client.MockDoTimings(time.Millisecond, 100*time.Millisecond) } func (cs *clientSuite) TearDownTest(c *C) { @@ -88,9 +91,10 @@ body = cs.rsps[cs.doCalls] } rsp := &http.Response{ - Body: ioutil.NopCloser(strings.NewReader(body)), - Header: cs.header, - StatusCode: cs.status, + Body: ioutil.NopCloser(strings.NewReader(body)), + Header: cs.header, + StatusCode: cs.status, + ContentLength: cs.contentLength, } cs.doCalls++ return rsp, cs.err @@ -99,12 +103,12 @@ func (cs *clientSuite) TestNewPanics(c *C) { c.Assert(func() { client.New(&client.Config{BaseURL: ":"}) - }, PanicMatches, `cannot parse server base URL: ":" \(parse :: missing protocol scheme\)`) + }, PanicMatches, `cannot parse server base URL: ":" \(parse \"?:\"?: missing protocol scheme\)`) } 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, client.DoFlags{}) c.Check(err, ErrorMatches, "cannot communicate with server: ouchie") if cs.doCalls < 2 { c.Fatalf("do did not retry") @@ -115,8 +119,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, client.DoFlags{}) 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, client.DoFlags{}) + 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) @@ -130,7 +151,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, client.DoFlags{}) c.Assert(cs.req, NotNil) authorization := cs.req.Header.Get("Authorization") c.Check(authorization, Equals, "") @@ -148,7 +169,7 @@ c.Assert(err, IsNil) var v string - _ = cs.cli.Do("GET", "/this", nil, nil, &v) + _, _ = cs.cli.Do("GET", "/this", nil, nil, &v, client.DoFlags{}) authorization := cs.req.Header.Get("Authorization") c.Check(authorization, Equals, `Macaroon root="macaroon", discharge="discharge"`) } @@ -167,7 +188,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, client.DoFlags{}) authorization := cs.req.Header.Get("Authorization") c.Check(authorization, Equals, "") } @@ -176,13 +197,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, client.DoFlags{}) 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, client.DoFlags{}) interactive = cs.req.Header.Get(client.AllowInteractionHeader) c.Check(interactive, Equals, "true") } @@ -220,6 +241,8 @@ "on-classic": true, "build-id": "1234", "confinement": "strict", + "architecture": "TI-99/4A", + "virtualization": "MESS", "sandbox-features": {"backend": ["feature-1", "feature-2"]}}}` sysInfo, err := cs.cli.SysInfo() c.Check(err, IsNil) @@ -235,7 +258,9 @@ SandboxFeatures: map[string][]string{ "backend": {"feature-1", "feature-2"}, }, - BuildID: "1234", + BuildID: "1234", + Architecture: "TI-99/4A", + Virtualization: "MESS", }) } @@ -243,14 +268,19 @@ cs.rsp = `{"type": "sync", "result": {"series": "16", "version": "2", - "os-release": {"id": "zyggy", "version-id": "123"}}}` + "os-release": {"id": "zyggy", "version-id": "123"}, + "architecture": "m32", + "virtualization": "qemu" +}}}` version, err := cs.cli.ServerVersion() c.Check(err, IsNil) c.Check(version, DeepEquals, &client.ServerVersion{ - Version: "2", - Series: "16", - OSID: "zyggy", - OSVersionID: "123", + Version: "2", + Series: "16", + OSID: "zyggy", + OSVersionID: "123", + Architecture: "m32", + Virtualization: "qemu", }) } @@ -277,7 +307,7 @@ cli := client.New(nil) si, err := cli.SysInfo() - c.Check(err, IsNil) + c.Assert(err, IsNil) c.Check(si.Series, Equals, "42") } @@ -317,12 +347,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", @@ -367,6 +399,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) @@ -431,105 +464,14 @@ c.Check(client.IsRetryable(&client.Error{Kind: client.ErrorKindChangeConflict}), Equals, true) } -func (cs *clientSuite) TestClientCreateUser(c *C) { - _, err := cs.cli.CreateUser(&client.CreateUserOptions{}) - c.Assert(err, ErrorMatches, "cannot create a user without providing an email") - - cs.rsp = `{ - "type": "sync", - "result": { - "username": "karl", - "ssh-keys": ["one", "two"] - } - }` - rsp, err := cs.cli.CreateUser(&client.CreateUserOptions{Email: "one@email.com", Sudoer: true, Known: true}) - c.Assert(cs.req.Method, Equals, "POST") - c.Assert(cs.req.URL.Path, Equals, "/v2/create-user") - c.Assert(err, IsNil) - - body, err := ioutil.ReadAll(cs.req.Body) - c.Assert(err, IsNil) - c.Assert(string(body), Equals, `{"email":"one@email.com","sudoer":true,"known":true}`) - - c.Assert(rsp, DeepEquals, &client.CreateUserResult{ - Username: "karl", - SSHKeys: []string{"one", "two"}, - }) -} - -var createUsersTests = []struct { - options []*client.CreateUserOptions - bodies []string - responses []string - results []*client.CreateUserResult - error string -}{{ - options: []*client.CreateUserOptions{{}}, - error: "cannot create user from store details without an email to query for", -}, { - options: []*client.CreateUserOptions{{ - Email: "one@example.com", - Sudoer: true, - }, { - Known: true, - }}, - bodies: []string{ - `{"email":"one@example.com","sudoer":true}`, - `{"known":true}`, - }, - responses: []string{ - `{"type": "sync", "result": {"username": "one", "ssh-keys":["a", "b"]}}`, - `{"type": "sync", "result": [{"username": "two"}, {"username": "three"}]}`, - }, - results: []*client.CreateUserResult{{ - Username: "one", - SSHKeys: []string{"a", "b"}, - }, { - Username: "two", - }, { - Username: "three", - }}, -}} - -func (cs *clientSuite) TestClientCreateUsers(c *C) { - for _, test := range createUsersTests { - cs.rsps = test.responses - - results, err := cs.cli.CreateUsers(test.options) - if test.error != "" { - c.Assert(err, ErrorMatches, test.error) - } - c.Assert(results, DeepEquals, test.results) - - var bodies []string - for _, req := range cs.reqs { - c.Assert(req.Method, Equals, "POST") - c.Assert(req.URL.Path, Equals, "/v2/create-user") - data, err := ioutil.ReadAll(req.Body) - c.Assert(err, IsNil) - bodies = append(bodies, string(data)) - } - - c.Assert(bodies, DeepEquals, test.bodies) - } -} - -func (cs *clientSuite) TestClientJSONError(c *C) { - cs.rsp = `some non-json error message` - _, err := cs.cli.SysInfo() - c.Assert(err, ErrorMatches, `cannot obtain system details: cannot decode "some non-json error message": invalid char.*`) -} +func (cs *clientSuite) TestUserAgent(c *C) { + cli := client.New(&client.Config{UserAgent: "some-agent/9.87"}) + cli.SetDoer(cs) -func (cs *clientSuite) TestUsers(c *C) { - cs.rsp = `{"type": "sync", "result": - [{"username": "foo","email":"foo@example.com"}, - {"username": "bar","email":"bar@example.com"}]}` - users, err := cs.cli.Users() - c.Check(err, IsNil) - c.Check(users, DeepEquals, []*client.User{ - {Username: "foo", Email: "foo@example.com"}, - {Username: "bar", Email: "bar@example.com"}, - }) + var v string + _, _ = cli.Do("GET", "/", nil, nil, &v, client.DoFlags{}) + c.Assert(cs.req, NotNil) + c.Check(cs.req.Header.Get("User-Agent"), Equals, "some-agent/9.87") } func (cs *clientSuite) TestDebugEnsureStateSoon(c *C) { @@ -558,3 +500,37 @@ c.Assert(err, IsNil) c.Check(string(data), DeepEquals, `{"action":"do-something","params":["param1","param2"]}`) } + +func (cs *clientSuite) TestDebugGet(c *C) { + cs.rsp = `{"type": "sync", "result":["res1","res2"]}` + + var result []string + err := cs.cli.DebugGet("do-something", &result, map[string]string{"foo": "bar"}) + c.Check(err, IsNil) + c.Check(result, DeepEquals, []string{"res1", "res2"}) + c.Check(cs.reqs, HasLen, 1) + c.Check(cs.reqs[0].Method, Equals, "GET") + c.Check(cs.reqs[0].URL.Path, Equals, "/v2/debug") + c.Check(cs.reqs[0].URL.Query(), DeepEquals, url.Values{"aspect": []string{"do-something"}, "foo": []string{"bar"}}) +} + +type integrationSuite struct{} + +var _ = Suite(&integrationSuite{}) + +func (cs *integrationSuite) TestClientTimeoutLP1837804(c *C) { + restore := client.MockDoTimings(time.Millisecond, 5*time.Millisecond) + defer restore() + + testServer := httptest.NewServer(http.HandlerFunc(func(res http.ResponseWriter, req *http.Request) { + time.Sleep(25 * time.Millisecond) + })) + defer func() { testServer.Close() }() + + cli := client.New(&client.Config{BaseURL: testServer.URL}) + _, err := cli.Do("GET", "/", nil, nil, nil, client.DoFlags{}) + c.Assert(err, ErrorMatches, `.* timeout exceeded while waiting for response`) + + _, err = cli.Do("POST", "/", nil, nil, nil, client.DoFlags{}) + c.Assert(err, ErrorMatches, `.* timeout exceeded while waiting for response`) +} diff -Nru snapd-2.37.4ubuntu0.1/client/cohort.go snapd-2.45.1ubuntu0.2/client/cohort.go --- snapd-2.37.4ubuntu0.1/client/cohort.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/cohort.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,50 @@ +// -*- 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 client + +import ( + "bytes" + "encoding/json" + "fmt" + + "golang.org/x/xerrors" +) + +type CohortAction struct { + Action string `json:"action"` + Snaps []string `json:"snaps"` +} + +func (client *Client) CreateCohorts(snaps []string) (map[string]string, error) { + data, err := json.Marshal(&CohortAction{Action: "create", Snaps: snaps}) + if err != nil { + return nil, fmt.Errorf("cannot request cohorts: %v", err) + } + + var cohorts map[string]string + + if _, err := client.doSync("POST", "/v2/cohorts", nil, nil, bytes.NewReader(data), &cohorts); err != nil { + fmt := "cannot create cohorts: %w" + return nil, xerrors.Errorf(fmt, err) + } + + return cohorts, nil + +} diff -Nru snapd-2.37.4ubuntu0.1/client/cohort_test.go snapd-2.45.1ubuntu0.2/client/cohort_test.go --- snapd-2.37.4ubuntu0.1/client/cohort_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/cohort_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,77 @@ +// -*- 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 client_test + +import ( + "encoding/json" + "errors" + "io/ioutil" + + "golang.org/x/xerrors" + + "gopkg.in/check.v1" +) + +func (cs *clientSuite) TestClientCreateCohortsEndpoint(c *check.C) { + cs.cli.CreateCohorts([]string{"foo", "bar"}) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/cohorts") + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + var jsonBody map[string]interface{} + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil) + c.Check(jsonBody, check.DeepEquals, map[string]interface{}{ + "action": "create", + "snaps": []interface{}{"foo", "bar"}, + }) +} + +func (cs *clientSuite) TestClientCreateCohorts(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": {"foo": "xyzzy", "bar": "what-what"} + }` + cohorts, err := cs.cli.CreateCohorts([]string{"foo", "bar"}) + c.Assert(err, check.IsNil) + c.Check(cohorts, check.DeepEquals, map[string]string{ + "foo": "xyzzy", + "bar": "what-what", + }) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + var jsonBody map[string]interface{} + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil) + c.Check(jsonBody, check.DeepEquals, map[string]interface{}{ + "action": "create", + "snaps": []interface{}{"foo", "bar"}, + }) +} + +func (cs *clientSuite) TestClientCreateCohortsErrIsWrapped(c *check.C) { + cs.err = errors.New("boom") + _, err := cs.cli.CreateCohorts([]string{"foo", "bar"}) + var e xerrors.Wrapper + c.Assert(err, check.Implements, &e) +} diff -Nru snapd-2.37.4ubuntu0.1/client/conf_test.go snapd-2.45.1ubuntu0.2/client/conf_test.go --- snapd-2.37.4ubuntu0.1/client/conf_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/conf_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/client/connections.go snapd-2.45.1ubuntu0.2/client/connections.go --- snapd-2.37.4ubuntu0.1/client/connections.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/connections.go 2020-06-05 13:13:49.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 client + +import ( + "net/url" +) + +// Connection describes a connection between a plug and a slot. +type Connection struct { + Slot SlotRef `json:"slot"` + Plug PlugRef `json:"plug"` + Interface string `json:"interface"` + // Manual is set for connections that were established manually. + Manual bool `json:"manual"` + // Gadget is set for connections that were enabled by the gadget snap. + Gadget bool `json:"gadget"` + // SlotAttrs is the list of attributes of the slot side of the connection. + SlotAttrs map[string]interface{} `json:"slot-attrs,omitempty"` + // PlugAttrs is the list of attributes of the plug side of the connection. + PlugAttrs map[string]interface{} `json:"plug-attrs,omitempty"` +} + +// Connections contains information about connections, as well as related plugs +// and slots. +type Connections struct { + // Established is the list of connections that are currently present. + Established []Connection `json:"established"` + // Undersired is a list of connections that are manually denied. + Undesired []Connection `json:"undesired"` + Plugs []Plug `json:"plugs"` + Slots []Slot `json:"slots"` +} + +// ConnectionOptions contains criteria for selecting matching connections, plugs +// and slots. +type ConnectionOptions struct { + // Snap selects connections with the snap on one of the sides, as well + // as plugs and slots of a given snap. + Snap string + // Interface selects connections, plugs or slots using given interface. + Interface string + // All when true, selects established and undesired connections as well + // as all disconnected plugs and slots. + All bool +} + +// Connections returns matching plugs, slots and their connections. Unless +// specified by matching options, returns established connections. +func (client *Client) Connections(opts *ConnectionOptions) (Connections, error) { + var conns Connections + query := url.Values{} + if opts != nil && opts.Snap != "" { + query.Set("snap", opts.Snap) + } + if opts != nil && opts.Interface != "" { + query.Set("interface", opts.Interface) + } + if opts != nil && opts.All { + query.Set("select", "all") + } + _, err := client.doSync("GET", "/v2/connections", query, nil, nil, &conns) + return conns, err +} diff -Nru snapd-2.37.4ubuntu0.1/client/connections_test.go snapd-2.45.1ubuntu0.2/client/connections_test.go --- snapd-2.37.4ubuntu0.1/client/connections_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/connections_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,270 @@ +// -*- 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 client_test + +import ( + "net/url" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +func (cs *clientSuite) TestClientConnectionsCallsEndpoint(c *check.C) { + _, _ = cs.cli.Connections(nil) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/connections") +} + +func (cs *clientSuite) TestClientConnectionsDefault(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": { + "established": [ + { + "slot": {"snap": "keyboard-lights", "slot": "capslock-led"}, + "plug": {"snap": "canonical-pi2", "plug": "pin-13"}, + "interface": "bool-file", + "gadget": true + } + ], + "plugs": [ + { + "snap": "canonical-pi2", + "plug": "pin-13", + "interface": "bool-file", + "label": "Pin 13", + "connections": [ + {"snap": "keyboard-lights", "slot": "capslock-led"} + ] + } + ], + "slots": [ + { + "snap": "keyboard-lights", + "slot": "capslock-led", + "interface": "bool-file", + "label": "Capslock indicator LED", + "connections": [ + {"snap": "canonical-pi2", "plug": "pin-13"} + ] + } + ] + } + }` + conns, err := cs.cli.Connections(nil) + c.Assert(err, check.IsNil) + c.Check(cs.req.URL.Path, check.Equals, "/v2/connections") + c.Check(conns, check.DeepEquals, client.Connections{ + Established: []client.Connection{ + { + Plug: client.PlugRef{Snap: "canonical-pi2", Name: "pin-13"}, + Slot: client.SlotRef{Snap: "keyboard-lights", Name: "capslock-led"}, + Interface: "bool-file", + Gadget: true, + }, + }, + Plugs: []client.Plug{ + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + Connections: []client.SlotRef{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + }, + }, + }, + Slots: []client.Slot{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + Interface: "bool-file", + Label: "Capslock indicator LED", + Connections: []client.PlugRef{ + { + Snap: "canonical-pi2", + Name: "pin-13", + }, + }, + }, + }, + }) +} + +func (cs *clientSuite) TestClientConnectionsAll(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": { + "established": [ + { + "slot": {"snap": "keyboard-lights", "slot": "capslock-led"}, + "plug": {"snap": "canonical-pi2", "plug": "pin-13"}, + "interface": "bool-file", + "gadget": true + } + ], + "undesired": [ + { + "slot": {"snap": "keyboard-lights", "slot": "numlock-led"}, + "plug": {"snap": "canonical-pi2", "plug": "pin-14"}, + "interface": "bool-file", + "gadget": true, + "manual": true + } + ], + "plugs": [ + { + "snap": "canonical-pi2", + "plug": "pin-13", + "interface": "bool-file", + "label": "Pin 13", + "connections": [ + {"snap": "keyboard-lights", "slot": "capslock-led"} + ] + }, + { + "snap": "canonical-pi2", + "plug": "pin-14", + "interface": "bool-file", + "label": "Pin 14" + } + ], + "slots": [ + { + "snap": "keyboard-lights", + "slot": "capslock-led", + "interface": "bool-file", + "label": "Capslock indicator LED", + "connections": [ + {"snap": "canonical-pi2", "plug": "pin-13"} + ] + }, + { + "snap": "keyboard-lights", + "slot": "numlock-led", + "interface": "bool-file", + "label": "Numlock LED" + } + ] + } + }` + conns, err := cs.cli.Connections(&client.ConnectionOptions{All: true}) + c.Assert(err, check.IsNil) + c.Check(cs.req.URL.Path, check.Equals, "/v2/connections") + c.Check(cs.req.URL.RawQuery, check.Equals, "select=all") + c.Check(conns, check.DeepEquals, client.Connections{ + Established: []client.Connection{ + { + Plug: client.PlugRef{Snap: "canonical-pi2", Name: "pin-13"}, + Slot: client.SlotRef{Snap: "keyboard-lights", Name: "capslock-led"}, + Interface: "bool-file", + Gadget: true, + }, + }, + Undesired: []client.Connection{ + { + Plug: client.PlugRef{Snap: "canonical-pi2", Name: "pin-14"}, + Slot: client.SlotRef{Snap: "keyboard-lights", Name: "numlock-led"}, + Interface: "bool-file", + Gadget: true, + Manual: true, + }, + }, + Plugs: []client.Plug{ + { + Snap: "canonical-pi2", + Name: "pin-13", + Interface: "bool-file", + Label: "Pin 13", + Connections: []client.SlotRef{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + }, + }, + }, + { + Snap: "canonical-pi2", + Name: "pin-14", + Interface: "bool-file", + Label: "Pin 14", + }, + }, + Slots: []client.Slot{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + Interface: "bool-file", + Label: "Capslock indicator LED", + Connections: []client.PlugRef{ + { + Snap: "canonical-pi2", + Name: "pin-13", + }, + }, + }, + { + Snap: "keyboard-lights", + Name: "numlock-led", + Interface: "bool-file", + Label: "Numlock LED", + }, + }, + }) +} + +func (cs *clientSuite) TestClientConnectionsFilter(c *check.C) { + cs.rsp = `{ + "type": "sync", + "result": { + "established": [], + "plugs": [], + "slots": [] + } + }` + + _, err := cs.cli.Connections(&client.ConnectionOptions{All: true}) + c.Assert(err, check.IsNil) + c.Check(cs.req.URL.Path, check.Equals, "/v2/connections") + c.Check(cs.req.URL.RawQuery, check.Equals, "select=all") + + _, err = cs.cli.Connections(&client.ConnectionOptions{Snap: "foo"}) + c.Assert(err, check.IsNil) + c.Check(cs.req.URL.Path, check.Equals, "/v2/connections") + c.Check(cs.req.URL.RawQuery, check.Equals, "snap=foo") + + _, err = cs.cli.Connections(&client.ConnectionOptions{Interface: "test"}) + c.Assert(err, check.IsNil) + c.Check(cs.req.URL.Path, check.Equals, "/v2/connections") + c.Check(cs.req.URL.RawQuery, check.Equals, "interface=test") + + _, err = cs.cli.Connections(&client.ConnectionOptions{All: true, Snap: "foo", Interface: "test"}) + c.Assert(err, check.IsNil) + query := cs.req.URL.Query() + c.Check(query, check.DeepEquals, url.Values{ + "select": []string{"all"}, + "interface": []string{"test"}, + "snap": []string{"foo"}, + }) +} diff -Nru snapd-2.37.4ubuntu0.1/client/export_test.go snapd-2.45.1ubuntu0.2/client/export_test.go --- snapd-2.37.4ubuntu0.1/client/export_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/export_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -30,9 +30,11 @@ client.doer = d } +type DoFlags = doFlags + // Do does do. -func (client *Client) Do(method, path string, query url.Values, body io.Reader, v interface{}) error { - return client.do(method, path, query, nil, body, v) +func (client *Client) Do(method, path string, query url.Values, body io.Reader, v interface{}, flags DoFlags) (statusCode int, err error) { + return client.do(method, path, query, nil, body, v, flags) } // expose parseError for testing @@ -49,3 +51,5 @@ err = json.NewDecoder(body).Decode(&act) return } + +type DownloadAction = downloadAction diff -Nru snapd-2.37.4ubuntu0.1/client/icons.go snapd-2.45.1ubuntu0.2/client/icons.go --- snapd-2.37.4ubuntu0.1/client/icons.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/icons.go 2020-06-05 13:13:49.000000000 +0000 @@ -20,9 +20,12 @@ package client import ( + "context" "fmt" "io/ioutil" "regexp" + + "golang.org/x/xerrors" ) // Icon represents the icon of an installed snap @@ -37,9 +40,12 @@ func (c *Client) Icon(pkgID string) (*Icon, error) { const errPrefix = "cannot retrieve icon" - response, err := c.raw("GET", fmt.Sprintf("/v2/icons/%s/icon", pkgID), nil, nil, nil) + ctx, cancel := context.WithTimeout(context.Background(), doTimeout) + defer cancel() + response, err := c.raw(ctx, "GET", fmt.Sprintf("/v2/icons/%s/icon", pkgID), nil, nil, nil) if err != nil { - return nil, fmt.Errorf("%s: failed to communicate with server: %s", errPrefix, err) + fmt := "%s: failed to communicate with server: %w" + return nil, xerrors.Errorf(fmt, errPrefix, err) } defer response.Body.Close() diff -Nru snapd-2.37.4ubuntu0.1/client/icons_test.go snapd-2.45.1ubuntu0.2/client/icons_test.go --- snapd-2.37.4ubuntu0.1/client/icons_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/icons_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -24,6 +24,8 @@ "fmt" "net/http" + "golang.org/x/xerrors" + . "gopkg.in/check.v1" ) @@ -63,3 +65,10 @@ c.Assert(icon.Filename, Equals, "myicon.png") c.Assert(icon.Content, DeepEquals, []byte("pixels")) } + +func (cs *clientSuite) TestClientIconErrIsWrapped(c *C) { + cs.err = errors.New("boom") + _, err := cs.cli.Icon("something") + var e xerrors.Wrapper + c.Assert(err, Implements, &e) +} diff -Nru snapd-2.37.4ubuntu0.1/client/interfaces.go snapd-2.45.1ubuntu0.2/client/interfaces.go --- snapd-2.37.4ubuntu0.1/client/interfaces.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/interfaces.go 2020-06-05 13:13:49.000000000 +0000 @@ -60,12 +60,6 @@ Name string `json:"slot"` } -// Connections contains information about all plugs, slots and their connections -type Connections struct { - Plugs []Plug `json:"plugs"` - Slots []Slot `json:"slots"` -} - // Interface holds information about a given interface and its instances. type Interface struct { Name string `json:"name,omitempty"` @@ -78,17 +72,11 @@ // InterfaceAction represents an action performed on the interface system. type InterfaceAction struct { Action string `json:"action"` + Forget bool `json:"forget,omitempty"` Plugs []Plug `json:"plugs,omitempty"` Slots []Slot `json:"slots,omitempty"` } -// Connections returns all plugs, slots and their connections. -func (client *Client) Connections() (Connections, error) { - var conns Connections - _, err := client.doSync("GET", "/v2/interfaces", nil, nil, nil, &conns) - return conns, err -} - // InterfaceOptions represents opt-in elements include in responses. type InterfaceOptions struct { Names []string @@ -98,6 +86,11 @@ Connected bool } +// DisconnectOptions represents extra options for disconnect op +type DisconnectOptions struct { + Forget bool +} + func (client *Client) Interfaces(opts *InterfaceOptions) ([]*Interface, error) { query := url.Values{} if opts != nil && len(opts.Names) > 0 { @@ -146,9 +139,10 @@ } // Disconnect breaks the connection between a plug and a slot. -func (client *Client) Disconnect(plugSnapName, plugName, slotSnapName, slotName string) (changeID string, err error) { +func (client *Client) Disconnect(plugSnapName, plugName, slotSnapName, slotName string, opts *DisconnectOptions) (changeID string, err error) { return client.performInterfaceAction(&InterfaceAction{ Action: "disconnect", + Forget: opts != nil && opts.Forget, Plugs: []Plug{{Snap: plugSnapName, Name: plugName}}, Slots: []Slot{{Snap: slotSnapName, Name: slotName}}, }) diff -Nru snapd-2.37.4ubuntu0.1/client/interfaces_test.go snapd-2.45.1ubuntu0.2/client/interfaces_test.go --- snapd-2.37.4ubuntu0.1/client/interfaces_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/interfaces_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -156,74 +156,6 @@ }) } -func (cs *clientSuite) TestClientConnectionsCallsEndpoint(c *check.C) { - _, _ = cs.cli.Connections() - c.Check(cs.req.Method, check.Equals, "GET") - c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") -} - -func (cs *clientSuite) TestClientConnections(c *check.C) { - cs.rsp = `{ - "type": "sync", - "result": { - "plugs": [ - { - "snap": "canonical-pi2", - "plug": "pin-13", - "interface": "bool-file", - "label": "Pin 13", - "connections": [ - {"snap": "keyboard-lights", "slot": "capslock-led"} - ] - } - ], - "slots": [ - { - "snap": "keyboard-lights", - "slot": "capslock-led", - "interface": "bool-file", - "label": "Capslock indicator LED", - "connections": [ - {"snap": "canonical-pi2", "plug": "pin-13"} - ] - } - ] - } - }` - conns, err := cs.cli.Connections() - c.Assert(err, check.IsNil) - c.Check(conns, check.DeepEquals, client.Connections{ - Plugs: []client.Plug{ - { - Snap: "canonical-pi2", - Name: "pin-13", - Interface: "bool-file", - Label: "Pin 13", - Connections: []client.SlotRef{ - { - Snap: "keyboard-lights", - Name: "capslock-led", - }, - }, - }, - }, - Slots: []client.Slot{ - { - Snap: "keyboard-lights", - Name: "capslock-led", - Interface: "bool-file", - Label: "Capslock indicator LED", - Connections: []client.PlugRef{ - { - Snap: "canonical-pi2", - Name: "pin-13", - }, - }, - }, - }, - }) -} - func (cs *clientSuite) TestClientConnectCallsEndpoint(c *check.C) { cs.cli.Connect("producer", "plug", "consumer", "slot") c.Check(cs.req.Method, check.Equals, "POST") @@ -231,6 +163,7 @@ } func (cs *clientSuite) TestClientConnect(c *check.C) { + cs.status = 202 cs.rsp = `{ "type": "async", "status-code": 202, @@ -262,19 +195,54 @@ } func (cs *clientSuite) TestClientDisconnectCallsEndpoint(c *check.C) { - cs.cli.Disconnect("producer", "plug", "consumer", "slot") + cs.cli.Disconnect("producer", "plug", "consumer", "slot", nil) c.Check(cs.req.Method, check.Equals, "POST") c.Check(cs.req.URL.Path, check.Equals, "/v2/interfaces") } func (cs *clientSuite) TestClientDisconnect(c *check.C) { + cs.status = 202 + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": { }, + "change": "42" + }` + opts := &client.DisconnectOptions{Forget: false} + id, err := cs.cli.Disconnect("producer", "plug", "consumer", "slot", opts) + c.Assert(err, check.IsNil) + c.Check(id, check.Equals, "42") + var body map[string]interface{} + decoder := json.NewDecoder(cs.req.Body) + err = decoder.Decode(&body) + c.Check(err, check.IsNil) + c.Check(body, check.DeepEquals, map[string]interface{}{ + "action": "disconnect", + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "slot": "slot", + }, + }, + }) +} + +func (cs *clientSuite) TestClientDisconnectForget(c *check.C) { + cs.status = 202 cs.rsp = `{ "type": "async", "status-code": 202, "result": { }, "change": "42" }` - id, err := cs.cli.Disconnect("producer", "plug", "consumer", "slot") + opts := &client.DisconnectOptions{Forget: true} + id, err := cs.cli.Disconnect("producer", "plug", "consumer", "slot", opts) c.Assert(err, check.IsNil) c.Check(id, check.Equals, "42") var body map[string]interface{} @@ -283,6 +251,7 @@ c.Check(err, check.IsNil) c.Check(body, check.DeepEquals, map[string]interface{}{ "action": "disconnect", + "forget": true, "plugs": []interface{}{ map[string]interface{}{ "snap": "producer", diff -Nru snapd-2.37.4ubuntu0.1/client/model.go snapd-2.45.1ubuntu0.2/client/model.go --- snapd-2.37.4ubuntu0.1/client/model.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/model.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,105 @@ +// -*- 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 client + +import ( + "bytes" + "context" + "encoding/json" + "fmt" + "net/url" + + "golang.org/x/xerrors" + + "github.com/snapcore/snapd/asserts" +) + +type remodelData struct { + NewModel string `json:"new-model"` +} + +// Remodel tries to remodel the system with the given assertion data +func (client *Client) Remodel(b []byte) (changeID string, err error) { + data, err := json.Marshal(&remodelData{ + NewModel: string(b), + }) + if err != nil { + return "", fmt.Errorf("cannot marshal remodel data: %v", err) + } + headers := map[string]string{ + "Content-Type": "application/json", + } + + 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{} + + ctx, cancel := context.WithTimeout(context.Background(), doTimeout) + defer cancel() + response, err := client.raw(ctx, "GET", path, q, nil, nil) + if err != nil { + fmt := "failed to query current assertion: %w" + return nil, xerrors.Errorf(fmt, 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.37.4ubuntu0.1/client/model_test.go snapd-2.45.1ubuntu0.2/client/model_test.go --- snapd-2.37.4ubuntu0.1/client/model_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/model_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,176 @@ +// -*- 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 client_test + +import ( + "encoding/json" + "errors" + "io/ioutil" + "net/http" + + "golang.org/x/xerrors" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/asserts" +) + +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") + c.Check(cs.req.URL.Path, Equals, "/v2/model") +} + +func (cs *clientSuite) TestClientRemodel(c *C) { + cs.status = 202 + cs.rsp = `{ + "type": "async", + "status-code": 202, + "result": {}, + "change": "d728" + }` + remodelJsonData := []byte(`{"new-model": "some-model"}`) + id, err := cs.cli.Remodel(remodelJsonData) + c.Assert(err, IsNil) + c.Check(id, Equals, "d728") + c.Assert(cs.req.Header.Get("Content-Type"), Equals, "application/json") + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, IsNil) + jsonBody := make(map[string]string) + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, IsNil) + 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) +} + +func (cs *clientSuite) TestClientCurrentModelAssertionErrIsWrapped(c *C) { + cs.err = errors.New("boom") + _, err := cs.cli.CurrentModelAssertion() + var e xerrors.Wrapper + c.Assert(err, Implements, &e) +} diff -Nru snapd-2.37.4ubuntu0.1/client/packages.go snapd-2.45.1ubuntu0.2/client/packages.go --- snapd-2.37.4ubuntu0.1/client/packages.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/packages.go 2020-06-05 13:13:49.000000000 +0000 @@ -27,6 +27,8 @@ "strings" "time" + "golang.org/x/xerrors" + "github.com/snapcore/snapd/snap" ) @@ -42,6 +44,7 @@ InstallDate time.Time `json:"install-date,omitempty"` Name string `json:"name"` Publisher *snap.StoreAccount `json:"publisher,omitempty"` + StoreURL string `json:"store-url,omitempty"` // Developer is also the publisher's username for historic reasons. Developer string `json:"developer"` Status string `json:"status"` @@ -63,6 +66,8 @@ License string `json:"license,omitempty"` CommonIDs []string `json:"common-ids,omitempty"` MountedFrom string `json:"mounted-from,omitempty"` + CohortKey string `json:"cohort-key,omitempty"` + Website string `json:"website,omitempty"` Prices map[string]float64 `json:"prices,omitempty"` Screenshots []snap.ScreenshotInfo `json:"screenshots,omitempty"` @@ -73,6 +78,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) { @@ -117,12 +132,17 @@ // - Private: return snaps that are private // - Query: only return snaps that match the query string type FindOptions struct { - Refresh bool - Private bool - Prefix bool - Query string + // Query is a term to search by or a prefix (if Prefix is true) + Query string + Prefix bool + + CommonID string + Section string + Private bool Scope string + + Refresh bool } var ErrNoSnapsInstalled = errors.New("no snaps installed") @@ -163,7 +183,8 @@ var sections []string _, err := client.doSync("GET", "/v2/sections", nil, nil, nil, §ions) if err != nil { - return nil, fmt.Errorf("cannot get snap sections: %s", err) + fmt := "cannot get snap sections: %w" + return nil, xerrors.Errorf(fmt, err) } return sections, nil } @@ -179,8 +200,14 @@ if opts.Prefix { q.Set("name", opts.Query+"*") } else { - q.Set("q", opts.Query) + if opts.CommonID != "" { + q.Set("common-id", opts.CommonID) + } + if opts.Query != "" { + q.Set("q", opts.Query) + } } + switch { case opts.Refresh && opts.Private: return nil, nil, fmt.Errorf("cannot specify refresh and private together") @@ -205,7 +232,8 @@ snaps, ri, err := client.snapsFromPath("/v2/find", q) if err != nil { - return nil, nil, fmt.Errorf("cannot find snap %q: %s", name, err) + fmt := "cannot find snap %q: %w" + return nil, nil, xerrors.Errorf(fmt, name, err) } if len(snaps) == 0 { @@ -222,7 +250,8 @@ return nil, nil, e } if err != nil { - return nil, nil, fmt.Errorf("cannot list snaps: %s", err) + fmt := "cannot list snaps: %w" + return nil, nil, xerrors.Errorf(fmt, err) } return snaps, ri, nil } @@ -234,7 +263,8 @@ path := fmt.Sprintf("/v2/snaps/%s", name) ri, err := client.doSync("GET", path, nil, nil, nil, &snap) if err != nil { - return nil, nil, fmt.Errorf("cannot retrieve snap %q: %s", name, err) + fmt := "cannot retrieve snap %q: %w" + return nil, nil, xerrors.Errorf(fmt, name, err) } return snap, ri, nil } diff -Nru snapd-2.37.4ubuntu0.1/client/packages_test.go snapd-2.45.1ubuntu0.2/client/packages_test.go --- snapd-2.37.4ubuntu0.1/client/packages_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/packages_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -21,10 +21,14 @@ import ( "encoding/json" + "errors" "fmt" + "io/ioutil" "net/url" + "os" "time" + "golang.org/x/xerrors" "gopkg.in/check.v1" "github.com/snapcore/snapd/client" @@ -45,7 +49,7 @@ c.Check(cs.req.Method, check.Equals, "GET") c.Check(cs.req.URL.Path, check.Equals, "/v2/find") c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{ - "q": []string{""}, "select": []string{"refresh"}, + "select": []string{"refresh"}, }) } @@ -57,7 +61,7 @@ c.Check(cs.req.Method, check.Equals, "GET") c.Check(cs.req.URL.Path, check.Equals, "/v2/find") c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{ - "q": []string{""}, "section": []string{"mysection"}, "select": []string{"refresh"}, + "section": []string{"mysection"}, "select": []string{"refresh"}, }) } @@ -68,7 +72,7 @@ c.Check(cs.req.Method, check.Equals, "GET") c.Check(cs.req.URL.Path, check.Equals, "/v2/find") c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{ - "q": []string{""}, "section": []string{"mysection"}, + "section": []string{"mysection"}, }) } @@ -89,7 +93,7 @@ c.Check(cs.req.Method, check.Equals, "GET") c.Check(cs.req.URL.Path, check.Equals, "/v2/find") c.Check(cs.req.URL.Query(), check.DeepEquals, url.Values{ - "q": []string{""}, "scope": []string{"mouthwash"}, + "scope": []string{"mouthwash"}, }) } @@ -115,6 +119,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 +131,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 +160,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", @@ -185,6 +203,12 @@ c.Check(cs.req.URL.RawQuery, check.Equals, "name=foo%2A") // 2A is `*` } +func (cs *clientSuite) TestClientFindCommonID(c *check.C) { + _, _, _ = cs.cli.Find(&client.FindOptions{CommonID: "org.kde.ktuberling.desktop"}) + c.Check(cs.req.URL.Path, check.Equals, "/v2/find") + c.Check(cs.req.URL.RawQuery, check.Equals, "common-id=org.kde.ktuberling.desktop") +} + func (cs *clientSuite) TestClientFindOne(c *check.C) { _, _, _ = cs.cli.FindOne("foo") c.Check(cs.req.URL.Path, check.Equals, "/v2/find") @@ -198,6 +222,7 @@ func (cs *clientSuite) TestClientSnap(c *check.C) { // example data obtained via // printf "GET /v2/find?name=test-snapd-tools HTTP/1.0\r\n\r\n" | nc -U -q 1 /run/snapd.socket|grep '{'|python3 -m json.tool + // XXX: update / sync with what daemon is actually putting out cs.rsp = `{ "type": "sync", "result": { @@ -236,7 +261,10 @@ {"type": "screenshot", "url":"http://example.com/shot1.png", "width":640, "height":480}, {"type": "screenshot", "url":"http://example.com/shot2.png"} ], - "common-ids": ["org.funky.snap"] + "cohort-key": "some-long-cohort-key", + "website": "http://example.com/funky", + "common-ids": ["org.funky.snap"], + "store-url": "https://snapcraft.io/chatroom" } }` pkg, _, err := cs.cli.Snap(pkgName) @@ -279,6 +307,9 @@ {Type: "screenshot", URL: "http://example.com/shot2.png"}, }, CommonIDs: []string{"org.funky.snap"}, + CohortKey: "some-long-cohort-key", + Website: "http://example.com/funky", + StoreURL: "https://snapcraft.io/chatroom", }) } @@ -333,3 +364,44 @@ c.Check(app.Name, check.Equals, "hello") c.Check(app.IsService(), check.Equals, true) } + +func (cs *clientSuite) TestClientSectionsErrIsWrapped(c *check.C) { + cs.err = errors.New("boom") + _, err := cs.cli.Sections() + var e xerrors.Wrapper + c.Assert(err, check.Implements, &e) +} + +func (cs *clientSuite) TestClientFindOneErrIsWrapped(c *check.C) { + cs.err = errors.New("boom") + _, _, err := cs.cli.FindOne("snap") + var e xerrors.Wrapper + c.Assert(err, check.Implements, &e) +} + +func (cs *clientSuite) TestClientSnapErrIsWrapped(c *check.C) { + // setting cs.err will trigger a "client.ClientError" + cs.err = errors.New("boom") + _, _, err := cs.cli.Snap("snap") + var e xerrors.Wrapper + c.Assert(err, check.Implements, &e) +} + +func (cs *clientSuite) TestClientFindFromPathErrIsWrapped(c *check.C) { + var e client.AuthorizationError + + // this will trigger a "client.AuthorizationError" + err := ioutil.WriteFile(client.TestStoreAuthFilename(os.Getenv("HOME")), []byte("rubbish"), 0644) + c.Assert(err, check.IsNil) + + // check that all the functions that use snapsFromPath() get a + // wrapped error + _, _, err = cs.cli.FindOne("snap") + c.Assert(xerrors.As(err, &e), check.Equals, true) + + _, _, err = cs.cli.Find(nil) + c.Assert(xerrors.As(err, &e), check.Equals, true) + + _, err = cs.cli.List([]string{"snap"}, nil) + c.Assert(xerrors.As(err, &e), check.Equals, true) +} diff -Nru snapd-2.37.4ubuntu0.1/client/snap_op.go snapd-2.45.1ubuntu0.2/client/snap_op.go --- snapd-2.37.4ubuntu0.1/client/snap_op.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/snap_op.go 2020-06-05 13:13:49.000000000 +0000 @@ -21,6 +21,7 @@ import ( "bytes" + "context" "encoding/json" "fmt" "io" @@ -30,15 +31,18 @@ ) type SnapOptions struct { - Amend bool `json:"amend,omitempty"` Channel string `json:"channel,omitempty"` Revision string `json:"revision,omitempty"` + CohortKey string `json:"cohort-key,omitempty"` + LeaveCohort bool `json:"leave-cohort,omitempty"` DevMode bool `json:"devmode,omitempty"` JailMode bool `json:"jailmode,omitempty"` Classic bool `json:"classic,omitempty"` Dangerous bool `json:"dangerous,omitempty"` IgnoreValidation bool `json:"ignore-validation,omitempty"` Unaliased bool `json:"unaliased,omitempty"` + Purge bool `json:"purge,omitempty"` + Amend bool `json:"amend,omitempty"` Users []string `json:"users,omitempty"` } @@ -200,7 +204,7 @@ "Content-Type": "application/json", } - return client.doAsyncFull("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data)) + return client.doAsyncFull("POST", "/v2/snaps", nil, headers, bytes.NewBuffer(data), doFlags{}) } // InstallPath sideloads the snap with the given path under optional provided name, @@ -226,7 +230,7 @@ "Content-Type": mw.FormDataContentType(), } - return client.doAsync("POST", "/v2/snaps", nil, headers, pr) + return client.doAsyncNoTimeout("POST", "/v2/snaps", nil, headers, pr) } // Try @@ -302,3 +306,89 @@ mw.Close() pw.Close() } + +type snapRevisionOptions struct { + Channel string `json:"channel,omitempty"` + Revision string `json:"revision,omitempty"` + CohortKey string `json:"cohort-key,omitempty"` +} + +type downloadAction struct { + SnapName string `json:"snap-name"` + + snapRevisionOptions + + HeaderPeek bool `json:"header-peek,omitempty"` + ResumeToken string `json:"resume-token,omitempty"` +} + +type DownloadInfo struct { + SuggestedFileName string + Size int64 + Sha3_384 string + ResumeToken string +} + +type DownloadOptions struct { + SnapOptions + + HeaderPeek bool + ResumeToken string + Resume int64 +} + +// Download will stream the given snap to the client +func (client *Client) Download(name string, options *DownloadOptions) (dlInfo *DownloadInfo, r io.ReadCloser, err error) { + if options == nil { + options = &DownloadOptions{} + } + action := downloadAction{ + SnapName: name, + snapRevisionOptions: snapRevisionOptions{ + Channel: options.Channel, + CohortKey: options.CohortKey, + Revision: options.Revision, + }, + HeaderPeek: options.HeaderPeek, + ResumeToken: options.ResumeToken, + } + data, err := json.Marshal(&action) + if err != nil { + return nil, nil, fmt.Errorf("cannot marshal snap action: %s", err) + } + headers := map[string]string{ + "Content-Type": "application/json", + } + if options.Resume > 0 { + headers["range"] = fmt.Sprintf("bytes: %d-", options.Resume) + } + + // no deadline for downloads + ctx := context.Background() + rsp, err := client.raw(ctx, "POST", "/v2/download", nil, headers, bytes.NewBuffer(data)) + if err != nil { + return nil, nil, err + } + + if rsp.StatusCode != 200 { + var r response + defer rsp.Body.Close() + if err := decodeInto(rsp.Body, &r); err != nil { + return nil, nil, err + } + return nil, nil, r.err(client, rsp.StatusCode) + } + matches := contentDispositionMatcher(rsp.Header.Get("Content-Disposition")) + if matches == nil || matches[1] == "" { + return nil, nil, fmt.Errorf("cannot determine filename") + } + + dlInfo = &DownloadInfo{ + SuggestedFileName: matches[1], + Size: rsp.ContentLength, + Sha3_384: rsp.Header.Get("Snap-Sha3-384"), + ResumeToken: rsp.Header.Get("Snap-Download-Token"), + } + + return dlInfo, rsp.Body, nil +} diff -Nru snapd-2.37.4ubuntu0.1/client/snap_op_test.go snapd-2.45.1ubuntu0.2/client/snap_op_test.go --- snapd-2.37.4ubuntu0.1/client/snap_op_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/snap_op_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -27,6 +27,7 @@ "io/ioutil" "mime" "mime/multipart" + "net/http" "path/filepath" "gopkg.in/check.v1" @@ -77,21 +78,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 +117,7 @@ } func (cs *clientSuite) TestClientOpSnapNoChange(c *check.C) { + cs.status = 202 cs.rsp = `{ "status-code": 202, "type": "async" @@ -125,6 +129,7 @@ } func (cs *clientSuite) TestClientOpSnap(c *check.C) { + cs.status = 202 cs.rsp = `{ "change": "d728", "status-code": 202, @@ -136,6 +141,9 @@ c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json", check.Commentf(s.action)) + _, ok := cs.req.Context().Deadline() + c.Check(ok, check.Equals, true) + body, err := ioutil.ReadAll(cs.req.Body) c.Assert(err, check.IsNil, check.Commentf(s.action)) jsonBody := make(map[string]string) @@ -151,6 +159,7 @@ } func (cs *clientSuite) TestClientMultiOpSnap(c *check.C) { + cs.status = 202 cs.rsp = `{ "change": "d728", "status-code": 202, @@ -179,6 +188,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 +213,7 @@ } func (cs *clientSuite) TestClientOpInstallPath(c *check.C) { + cs.status = 202 cs.rsp = `{ "change": "66b3", "status-code": 202, @@ -226,10 +237,13 @@ c.Check(cs.req.Method, check.Equals, "POST") c.Check(cs.req.URL.Path, check.Equals, fmt.Sprintf("/v2/snaps")) c.Assert(cs.req.Header.Get("Content-Type"), check.Matches, "multipart/form-data; boundary=.*") + _, ok := cs.req.Context().Deadline() + c.Assert(ok, check.Equals, false) c.Check(id, check.Equals, "66b3") } func (cs *clientSuite) TestClientOpInstallPathInstance(c *check.C) { + cs.status = 202 cs.rsp = `{ "change": "66b3", "status-code": 202, @@ -258,6 +272,7 @@ } func (cs *clientSuite) TestClientOpInstallDangerous(c *check.C) { + cs.status = 202 cs.rsp = `{ "change": "66b3", "status-code": 202, @@ -294,6 +309,7 @@ } func (cs *clientSuite) TestClientOpInstallUnaliased(c *check.C) { + cs.status = 202 cs.rsp = `{ "change": "66b3", "status-code": 202, @@ -344,6 +360,7 @@ } func (cs *clientSuite) TestClientOpTryMode(c *check.C) { + cs.status = 202 cs.rsp = `{ "change": "66b3", "status-code": 202, @@ -400,3 +417,122 @@ _, err := cs.cli.Try(snapdir, &client.SnapOptions{Dangerous: true}) c.Assert(err, check.Equals, client.ErrDangerousNotApplicable) } + +func (cs *clientSuite) TestSnapOptionsSerialises(c *check.C) { + tests := map[string]client.SnapOptions{ + "{}": {}, + `{"channel":"edge"}`: {Channel: "edge"}, + `{"revision":"42"}`: {Revision: "42"}, + `{"cohort-key":"what"}`: {CohortKey: "what"}, + `{"leave-cohort":true}`: {LeaveCohort: true}, + `{"devmode":true}`: {DevMode: true}, + `{"jailmode":true}`: {JailMode: true}, + `{"classic":true}`: {Classic: true}, + `{"dangerous":true}`: {Dangerous: true}, + `{"ignore-validation":true}`: {IgnoreValidation: true}, + `{"unaliased":true}`: {Unaliased: true}, + `{"purge":true}`: {Purge: true}, + `{"amend":true}`: {Amend: true}, + } + for expected, opts := range tests { + buf, err := json.Marshal(&opts) + c.Assert(err, check.IsNil, check.Commentf("%s", expected)) + c.Check(string(buf), check.Equals, expected) + } +} + +func (cs *clientSuite) TestClientOpDownload(c *check.C) { + cs.status = 200 + cs.header = http.Header{ + "Content-Disposition": {"attachment; filename=foo_2.snap"}, + "Snap-Sha3-384": {"sha3sha3sha3"}, + "Snap-Download-Token": {"some-token"}, + } + cs.contentLength = 1234 + + cs.rsp = `lots-of-foo-data` + + dlInfo, rc, err := cs.cli.Download("foo", &client.DownloadOptions{ + SnapOptions: client.SnapOptions{ + Revision: "2", + Channel: "edge", + }, + HeaderPeek: true, + }) + c.Check(err, check.IsNil) + c.Check(dlInfo, check.DeepEquals, &client.DownloadInfo{ + SuggestedFileName: "foo_2.snap", + Size: 1234, + Sha3_384: "sha3sha3sha3", + ResumeToken: "some-token", + }) + + // check we posted the right stuff + c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") + c.Assert(cs.req.Header.Get("range"), check.Equals, "") + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + var jsonBody client.DownloadAction + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil) + c.Check(jsonBody.SnapName, check.DeepEquals, "foo") + c.Check(jsonBody.Revision, check.Equals, "2") + c.Check(jsonBody.Channel, check.Equals, "edge") + c.Check(jsonBody.HeaderPeek, check.Equals, true) + + // ensure we can read the response + content, err := ioutil.ReadAll(rc) + c.Assert(err, check.IsNil) + c.Check(string(content), check.Equals, cs.rsp) + // and we can close it + c.Check(rc.Close(), check.IsNil) +} + +func (cs *clientSuite) TestClientOpDownloadResume(c *check.C) { + cs.status = 200 + cs.header = http.Header{ + "Content-Disposition": {"attachment; filename=foo_2.snap"}, + "Snap-Sha3-384": {"sha3sha3sha3"}, + } + // we resume + cs.contentLength = 1234 - 64 + + cs.rsp = `lots-of-foo-data` + + dlInfo, rc, err := cs.cli.Download("foo", &client.DownloadOptions{ + SnapOptions: client.SnapOptions{ + Revision: "2", + Channel: "edge", + }, + HeaderPeek: true, + ResumeToken: "some-token", + Resume: 64, + }) + c.Check(err, check.IsNil) + c.Check(dlInfo, check.DeepEquals, &client.DownloadInfo{ + SuggestedFileName: "foo_2.snap", + Size: 1234 - 64, + Sha3_384: "sha3sha3sha3", + }) + + // check we posted the right stuff + c.Assert(cs.req.Header.Get("Content-Type"), check.Equals, "application/json") + c.Assert(cs.req.Header.Get("range"), check.Equals, "bytes: 64-") + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + var jsonBody client.DownloadAction + err = json.Unmarshal(body, &jsonBody) + c.Assert(err, check.IsNil) + c.Check(jsonBody.SnapName, check.DeepEquals, "foo") + c.Check(jsonBody.Revision, check.Equals, "2") + c.Check(jsonBody.Channel, check.Equals, "edge") + c.Check(jsonBody.HeaderPeek, check.Equals, true) + c.Check(jsonBody.ResumeToken, check.Equals, "some-token") + + // ensure we can read the response + content, err := ioutil.ReadAll(rc) + c.Assert(err, check.IsNil) + c.Check(string(content), check.Equals, cs.rsp) + // and we can close it + c.Check(rc.Close(), check.IsNil) +} diff -Nru snapd-2.37.4ubuntu0.1/client/snapshot.go snapd-2.45.1ubuntu0.2/client/snapshot.go --- snapd-2.37.4ubuntu0.1/client/snapshot.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/snapshot.go 2020-06-05 13:13:49.000000000 +0000 @@ -72,6 +72,9 @@ Size int64 `json:"size,omitempty"` // if the snapshot failed to open this will be the reason why Broken string `json:"broken,omitempty"` + + // set if the snapshot was created automatically on snap removal + Auto bool `json:"auto,omitempty"` } // IsValid checks whether the snapshot is missing information that diff -Nru snapd-2.37.4ubuntu0.1/client/snapshot_test.go snapd-2.45.1ubuntu0.2/client/snapshot_test.go --- snapd-2.37.4ubuntu0.1/client/snapshot_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/snapshot_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/client/systems.go snapd-2.45.1ubuntu0.2/client/systems.go --- snapd-2.37.4ubuntu0.1/client/systems.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/systems.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,105 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" + + "golang.org/x/xerrors" + + "github.com/snapcore/snapd/snap" +) + +// SystemModelData contains information about the model +type SystemModelData struct { + // Model as the model assertion + Model string `json:"model,omitempty"` + // BrandID corresponds to brand-id in the model assertion + BrandID string `json:"brand-id,omitempty"` + // DisplayName is human friendly name, corresponds to display-name in + // the model assertion + DisplayName string `json:"display-name,omitempty"` +} + +type System struct { + // Current is true when the system running now was installed from that + // recovery seed + Current bool `json:"current,omitempty"` + // Label of the recovery system + Label string `json:"label,omitempty"` + // Model information + Model SystemModelData `json:"model,omitempty"` + // Brand information + Brand snap.StoreAccount `json:"brand,omitempty"` + // Actions available for this system + Actions []SystemAction `json:"actions,omitempty"` +} + +type SystemAction struct { + // Title is a user presentable action description + Title string `json:"title,omitempty"` + // Mode given action can be executed in + Mode string `json:"mode,omitempty"` +} + +// ListSystems list all systems available for seeding or recovery. +func (client *Client) ListSystems() ([]System, error) { + type systemsResponse struct { + Systems []System `json:"systems,omitempty"` + } + + var rsp systemsResponse + + if _, err := client.doSync("GET", "/v2/systems", nil, nil, nil, &rsp); err != nil { + return nil, xerrors.Errorf("cannot list recovery systems: %v", err) + } + return rsp.Systems, nil +} + +// DoSystemAction issues a request to perform an action using the given seed +// system and its mode. +func (client *Client) DoSystemAction(systemLabel string, action *SystemAction) error { + if systemLabel == "" { + return fmt.Errorf("cannot request an action without the system") + } + if action == nil { + return fmt.Errorf("cannot request an action without one") + } + // deeper verification is done by the backend + + req := struct { + Action string `json:"action"` + *SystemAction + }{ + Action: "do", + SystemAction: action, + } + + var body bytes.Buffer + if err := json.NewEncoder(&body).Encode(&req); err != nil { + return err + } + if _, err := client.doSync("POST", "/v2/systems/"+systemLabel, nil, nil, &body, nil); err != nil { + return xerrors.Errorf("cannot request system action: %v", err) + } + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/client/systems_test.go snapd-2.45.1ubuntu0.2/client/systems_test.go --- snapd-2.37.4ubuntu0.1/client/systems_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/systems_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,171 @@ +// -*- 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 client_test + +import ( + "encoding/json" + "io/ioutil" + + "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/snap" +) + +func (cs *clientSuite) TestListSystemsSome(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": { + "systems": [ + { + "current": true, + "label": "20200101", + "model": { + "model": "this-is-model-id", + "brand-id": "brand-id-1", + "display-name": "wonky model" + }, + "brand": { + "id": "brand-id-1", + "username": "brand", + "display-name": "wonky publishing" + }, + "actions": [ + {"title": "recover", "mode": "recover"}, + {"title": "reinstall", "mode": "install"} + ] + }, { + "label": "20200311", + "model": { + "model": "different-model-id", + "brand-id": "bulky-brand-id-1", + "display-name": "bulky model" + }, + "brand": { + "id": "bulky-brand-id-1", + "username": "bulky-brand", + "display-name": "bulky publishing" + }, + "actions": [ + {"title": "factory-reset", "mode": "install"} + ] + } + ] + } + }` + systems, err := cs.cli.ListSystems() + c.Assert(err, check.IsNil) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/systems") + c.Check(systems, check.DeepEquals, []client.System{ + { + Current: true, + Label: "20200101", + Model: client.SystemModelData{ + Model: "this-is-model-id", + BrandID: "brand-id-1", + DisplayName: "wonky model", + }, + Brand: snap.StoreAccount{ + ID: "brand-id-1", + Username: "brand", + DisplayName: "wonky publishing", + }, + Actions: []client.SystemAction{ + {Title: "recover", Mode: "recover"}, + {Title: "reinstall", Mode: "install"}, + }, + }, { + Label: "20200311", + Model: client.SystemModelData{ + Model: "different-model-id", + BrandID: "bulky-brand-id-1", + DisplayName: "bulky model", + }, + Brand: snap.StoreAccount{ + ID: "bulky-brand-id-1", + Username: "bulky-brand", + DisplayName: "bulky publishing", + }, + Actions: []client.SystemAction{ + {Title: "factory-reset", Mode: "install"}, + }, + }, + }) +} + +func (cs *clientSuite) TestListSystemsNone(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": {} + }` + systems, err := cs.cli.ListSystems() + c.Assert(err, check.IsNil) + c.Check(cs.req.Method, check.Equals, "GET") + c.Check(cs.req.URL.Path, check.Equals, "/v2/systems") + c.Check(systems, check.HasLen, 0) +} + +func (cs *clientSuite) TestRequestSystemActionHappy(c *check.C) { + cs.rsp = `{ + "type": "sync", + "status-code": 200, + "result": {} + }` + err := cs.cli.DoSystemAction("1234", &client.SystemAction{ + Title: "reinstall", + Mode: "install", + }) + c.Assert(err, check.IsNil) + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/systems/1234") + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, check.IsNil) + var req map[string]interface{} + err = json.Unmarshal(body, &req) + c.Assert(err, check.IsNil) + c.Assert(req, check.DeepEquals, map[string]interface{}{ + "action": "do", + "title": "reinstall", + "mode": "install", + }) +} + +func (cs *clientSuite) TestRequestSystemActionError(c *check.C) { + cs.rsp = `{ + "type": "error", + "status-code": 500, + "result": {"message": "failed"} + }` + err := cs.cli.DoSystemAction("1234", &client.SystemAction{Mode: "install"}) + c.Assert(err, check.ErrorMatches, "cannot request system action: failed") + c.Check(cs.req.Method, check.Equals, "POST") + c.Check(cs.req.URL.Path, check.Equals, "/v2/systems/1234") +} + +func (cs *clientSuite) TestRequestSystemActionInvalid(c *check.C) { + err := cs.cli.DoSystemAction("", &client.SystemAction{}) + c.Assert(err, check.ErrorMatches, "cannot request an action without the system") + err = cs.cli.DoSystemAction("1234", nil) + c.Assert(err, check.ErrorMatches, "cannot request an action without one") +} diff -Nru snapd-2.37.4ubuntu0.1/client/users.go snapd-2.45.1ubuntu0.2/client/users.go --- snapd-2.37.4ubuntu0.1/client/users.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/users.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,143 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package client + +import ( + "bytes" + "encoding/json" + "fmt" +) + +// CreateUserResult holds the result of a user creation. +type CreateUserResult struct { + Username string `json:"username"` + SSHKeys []string `json:"ssh-keys"` +} + +// CreateUserOptions holds options for creating a local system user. +// +// If Known is false, the provided email is used to query the store for +// username and SSH key details. +// +// If Known is true, the user will be created by looking through existing +// system-user assertions and looking for a matching email. If Email is +// empty then all such assertions are considered and multiple users may +// be created. +type CreateUserOptions struct { + Email string `json:"email,omitempty"` + Sudoer bool `json:"sudoer,omitempty"` + Known bool `json:"known,omitempty"` + ForceManaged bool `json:"force-managed,omitempty"` +} + +// RemoveUserOptions holds options for removing a local system user. +type RemoveUserOptions struct { + // Username indicates which user to remove. + Username string `json:"username,omitempty"` +} + +type userAction struct { + Action string `json:"action"` + *CreateUserOptions + *RemoveUserOptions +} + +func (client *Client) doUserAction(act *userAction, result interface{}) error { + data, err := json.Marshal(act) + if err != nil { + return err + } + + _, err = client.doSync("POST", "/v2/users", nil, nil, bytes.NewReader(data), result) + return err +} + +// CreateUser creates a local system user. See CreateUserOptions for details. +func (client *Client) CreateUser(options *CreateUserOptions) (*CreateUserResult, error) { + if options == nil || options.Email == "" { + return nil, fmt.Errorf("cannot create a user without providing an email") + } + + var result []*CreateUserResult + err := client.doUserAction(&userAction{Action: "create", CreateUserOptions: options}, &result) + if err != nil { + return nil, fmt.Errorf("while creating user: %v", err) + } + return result[0], nil +} + +// CreateUsers creates multiple local system users. See CreateUserOptions for details. +// +// Results may be provided even if there are errors. +func (client *Client) CreateUsers(options []*CreateUserOptions) ([]*CreateUserResult, error) { + for _, opts := range options { + if opts == nil || (opts.Email == "" && !opts.Known) { + return nil, fmt.Errorf("cannot create user from store details without an email to query for") + } + } + + var results []*CreateUserResult + var errs []error + for _, opts := range options { + var result []*CreateUserResult + err := client.doUserAction(&userAction{Action: "create", CreateUserOptions: opts}, &result) + if err != nil { + errs = append(errs, err) + } else { + results = append(results, result...) + } + } + + if len(errs) == 1 { + return results, errs[0] + } + if len(errs) > 1 { + var buf bytes.Buffer + for _, err := range errs { + fmt.Fprintf(&buf, "\n- %s", err) + } + return results, fmt.Errorf("while creating users:%s", buf.Bytes()) + } + return results, nil +} + +// RemoveUser removes a local system user. +func (client *Client) RemoveUser(options *RemoveUserOptions) (removed []*User, err error) { + if options == nil || options.Username == "" { + return nil, fmt.Errorf("cannot remove a user without providing a username") + } + var result struct { + Removed []*User `json:"removed"` + } + if err := client.doUserAction(&userAction{Action: "remove", RemoveUserOptions: options}, &result); err != nil { + return nil, err + } + return result.Removed, nil +} + +// Users returns the local users. +func (client *Client) Users() ([]*User, error) { + var result []*User + + if _, err := client.doSync("GET", "/v2/users", nil, nil, nil, &result); err != nil { + return nil, fmt.Errorf("while getting users: %v", err) + } + return result, nil +} diff -Nru snapd-2.37.4ubuntu0.1/client/users_test.go snapd-2.45.1ubuntu0.2/client/users_test.go --- snapd-2.37.4ubuntu0.1/client/users_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/client/users_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,184 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2015-2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package client_test + +import ( + "io/ioutil" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" +) + +func (cs *clientSuite) TestClientRemoveUser(c *C) { + removed, err := cs.cli.RemoveUser(&client.RemoveUserOptions{}) + c.Assert(err, ErrorMatches, "cannot remove a user without providing a username") + c.Assert(removed, IsNil) + + cs.rsp = `{ + "type": "sync", + "result": { + "removed": [{"id": 11, "username": "one-user", "email": "user@test.com"}] + } + }` + removed, err = cs.cli.RemoveUser(&client.RemoveUserOptions{Username: "one-user"}) + c.Assert(cs.req.Method, Equals, "POST") + c.Assert(cs.req.URL.Path, Equals, "/v2/users") + c.Assert(err, IsNil) + c.Assert(removed, DeepEquals, []*client.User{ + {ID: 11, Username: "one-user", Email: "user@test.com"}, + }) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, IsNil) + c.Assert(string(body), Equals, `{"action":"remove","username":"one-user"}`) +} + +func (cs *clientSuite) TestClientRemoveUserError(c *C) { + removed, err := cs.cli.RemoveUser(nil) + c.Assert(err, ErrorMatches, "cannot remove a user without providing a username") + c.Assert(removed, IsNil) + removed, err = cs.cli.RemoveUser(&client.RemoveUserOptions{}) + c.Assert(err, ErrorMatches, "cannot remove a user without providing a username") + c.Assert(removed, IsNil) + + cs.rsp = `{ + "type": "error", + "result": {"message": "no can do"} + }` + removed, err = cs.cli.RemoveUser(&client.RemoveUserOptions{Username: "one-user"}) + c.Assert(cs.req.Method, Equals, "POST") + c.Assert(cs.req.URL.Path, Equals, "/v2/users") + c.Assert(err, ErrorMatches, "no can do") + c.Assert(removed, IsNil) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, IsNil) + c.Assert(string(body), Equals, `{"action":"remove","username":"one-user"}`) +} + +func (cs *clientSuite) TestClientCreateUser(c *C) { + _, err := cs.cli.CreateUser(nil) + c.Assert(err, ErrorMatches, "cannot create a user without providing an email") + _, err = cs.cli.CreateUser(&client.CreateUserOptions{}) + c.Assert(err, ErrorMatches, "cannot create a user without providing an email") + + cs.rsp = `{ + "type": "sync", + "result": [{ + "username": "karl", + "ssh-keys": ["one", "two"] + }] + }` + rsp, err := cs.cli.CreateUser(&client.CreateUserOptions{Email: "one@email.com", Sudoer: true, Known: true}) + c.Assert(cs.req.Method, Equals, "POST") + c.Assert(cs.req.URL.Path, Equals, "/v2/users") + c.Assert(err, IsNil) + + body, err := ioutil.ReadAll(cs.req.Body) + c.Assert(err, IsNil) + c.Assert(string(body), Equals, `{"action":"create","email":"one@email.com","sudoer":true,"known":true}`) + + c.Assert(rsp, DeepEquals, &client.CreateUserResult{ + Username: "karl", + SSHKeys: []string{"one", "two"}, + }) +} + +var createUsersTests = []struct { + options []*client.CreateUserOptions + bodies []string + responses []string + results []*client.CreateUserResult + error string +}{{ + // nothing in -> nothing out + options: nil, +}, { + options: []*client.CreateUserOptions{nil}, + error: "cannot create user from store details without an email to query for", +}, { + options: []*client.CreateUserOptions{{}}, + error: "cannot create user from store details without an email to query for", +}, { + options: []*client.CreateUserOptions{{ + Email: "one@example.com", + Sudoer: true, + }, { + Known: true, + }}, + bodies: []string{ + `{"action":"create","email":"one@example.com","sudoer":true}`, + `{"action":"create","known":true}`, + }, + responses: []string{ + `{"type": "sync", "result": [{"username": "one", "ssh-keys":["a", "b"]}]}`, + `{"type": "sync", "result": [{"username": "two"}, {"username": "three"}]}`, + }, + results: []*client.CreateUserResult{{ + Username: "one", + SSHKeys: []string{"a", "b"}, + }, { + Username: "two", + }, { + Username: "three", + }}, +}} + +func (cs *clientSuite) TestClientCreateUsers(c *C) { + for _, test := range createUsersTests { + cs.rsps = test.responses + + results, err := cs.cli.CreateUsers(test.options) + if test.error != "" { + c.Assert(err, ErrorMatches, test.error) + } + c.Assert(results, DeepEquals, test.results) + + var bodies []string + for _, req := range cs.reqs { + c.Assert(req.Method, Equals, "POST") + c.Assert(req.URL.Path, Equals, "/v2/users") + data, err := ioutil.ReadAll(req.Body) + c.Assert(err, IsNil) + bodies = append(bodies, string(data)) + } + + c.Assert(bodies, DeepEquals, test.bodies) + } +} + +func (cs *clientSuite) TestClientJSONError(c *C) { + cs.rsp = `some non-json error message` + _, err := cs.cli.SysInfo() + c.Assert(err, ErrorMatches, `cannot obtain system details: cannot decode "some non-json error message": invalid char.*`) +} + +func (cs *clientSuite) TestUsers(c *C) { + cs.rsp = `{"type": "sync", "result": + [{"username": "foo","email":"foo@example.com"}, + {"username": "bar","email":"bar@example.com"}]}` + users, err := cs.cli.Users() + c.Check(err, IsNil) + c.Check(users, DeepEquals, []*client.User{ + {Username: "foo", Email: "foo@example.com"}, + {Username: "bar", Email: "bar@example.com"}, + }) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/appinfo.go snapd-2.45.1ubuntu0.2/cmd/appinfo.go --- snapd-2.37.4ubuntu0.1/cmd/appinfo.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/appinfo.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,133 +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 cmd - -import ( - "fmt" - "path/filepath" - "strings" - - "github.com/snapcore/snapd/client" - "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/progress" - "github.com/snapcore/snapd/snap" - "github.com/snapcore/snapd/systemd" -) - -func ClientAppInfoNotes(app *client.AppInfo) string { - if !app.IsService() { - return "-" - } - - var notes = make([]string, 0, 2) - var seenTimer, seenSocket bool - for _, act := range app.Activators { - switch act.Type { - case "timer": - seenTimer = true - case "socket": - seenSocket = true - } - } - if seenTimer { - notes = append(notes, "timer-activated") - } - if seenSocket { - notes = append(notes, "socket-activated") - } - if len(notes) == 0 { - return "-" - } - return strings.Join(notes, ",") -} - -func ClientAppInfosFromSnapAppInfos(apps []*snap.AppInfo) ([]client.AppInfo, error) { - // TODO: pass in an actual notifier here instead of null - // (Status doesn't _need_ it, but benefits from it) - sysd := systemd.New(dirs.GlobalRootDir, progress.Null) - - out := make([]client.AppInfo, 0, len(apps)) - for _, app := range apps { - appInfo := client.AppInfo{ - Snap: app.Snap.InstanceName(), - Name: app.Name, - CommonID: app.CommonID, - } - if fn := app.DesktopFile(); osutil.FileExists(fn) { - appInfo.DesktopFile = fn - } - - appInfo.Daemon = app.Daemon - if !app.IsService() || !app.Snap.IsActive() { - out = append(out, appInfo) - continue - } - - // collect all services for a single call to systemctl - serviceNames := make([]string, 0, 1+len(app.Sockets)+1) - serviceNames = append(serviceNames, app.ServiceName()) - - sockSvcFileToName := make(map[string]string, len(app.Sockets)) - for _, sock := range app.Sockets { - sockUnit := filepath.Base(sock.File()) - sockSvcFileToName[sockUnit] = sock.Name - serviceNames = append(serviceNames, sockUnit) - } - if app.Timer != nil { - timerUnit := filepath.Base(app.Timer.File()) - serviceNames = append(serviceNames, timerUnit) - } - - // sysd.Status() makes sure that we get only the units we asked - // for and raises an error otherwise - sts, err := sysd.Status(serviceNames...) - if err != nil { - return nil, fmt.Errorf("cannot get status of services of app %q: %v", app.Name, err) - } - if len(sts) != len(serviceNames) { - return nil, fmt.Errorf("cannot get status of services of app %q: expected %v results, got %v", app.Name, len(serviceNames), len(sts)) - } - for _, st := range sts { - switch filepath.Ext(st.UnitName) { - case ".service": - appInfo.Enabled = st.Enabled - appInfo.Active = st.Active - case ".timer": - appInfo.Activators = append(appInfo.Activators, client.AppActivator{ - Name: app.Name, - Enabled: st.Enabled, - Active: st.Active, - Type: "timer", - }) - case ".socket": - appInfo.Activators = append(appInfo.Activators, client.AppActivator{ - Name: sockSvcFileToName[st.UnitName], - Enabled: st.Enabled, - Active: st.Active, - Type: "socket", - }) - } - } - out = append(out, appInfo) - } - - return out, nil -} diff -Nru snapd-2.37.4ubuntu0.1/cmd/appinfo_test.go snapd-2.45.1ubuntu0.2/cmd/appinfo_test.go --- snapd-2.37.4ubuntu0.1/cmd/appinfo_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/appinfo_test.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,71 +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 cmd_test - -import ( - "gopkg.in/check.v1" - - "github.com/snapcore/snapd/client" - "github.com/snapcore/snapd/cmd" -) - -func (*cmdSuite) TestAppStatusNotes(c *check.C) { - ai := client.AppInfo{} - c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "-") - - ai = client.AppInfo{ - Daemon: "oneshot", - } - c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "-") - - ai = client.AppInfo{ - Daemon: "oneshot", - Activators: []client.AppActivator{ - {Type: "timer"}, - }, - } - c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated") - - ai = client.AppInfo{ - Daemon: "oneshot", - Activators: []client.AppActivator{ - {Type: "socket"}, - }, - } - c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "socket-activated") - - // check that the output is stable regardless of the order of activators - ai = client.AppInfo{ - Daemon: "oneshot", - Activators: []client.AppActivator{ - {Type: "timer"}, - {Type: "socket"}, - }, - } - c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated,socket-activated") - ai = client.AppInfo{ - Daemon: "oneshot", - Activators: []client.AppActivator{ - {Type: "socket"}, - {Type: "timer"}, - }, - } - c.Check(cmd.ClientAppInfoNotes(&ai), check.Equals, "timer-activated,socket-activated") -} diff -Nru snapd-2.37.4ubuntu0.1/cmd/autogen.sh snapd-2.45.1ubuntu0.2/cmd/autogen.sh --- snapd-2.37.4ubuntu0.1/cmd/autogen.sh 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/autogen.sh 2020-06-05 13:13:49.000000000 +0000 @@ -33,13 +33,13 @@ extra_opts="--libexecdir=/usr/lib/snapd" ;; ubuntu) - extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-multiarch --enable-static-libcap --enable-static-libapparmor --enable-static-libseccomp --with-host-arch-triplet=$(dpkg-architecture -qDEB_HOST_MULTIARCH)" + extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-multiarch --enable-static-libcap --enable-static-libapparmor --with-host-arch-triplet=$(dpkg-architecture -qDEB_HOST_MULTIARCH)" if [ "$(dpkg-architecture -qDEB_HOST_ARCH)" = "amd64" ]; then extra_opts="$extra_opts --with-host-arch-32bit-triplet=$(dpkg-architecture -ai386 -qDEB_HOST_MULTIARCH)" fi ;; fedora|centos|rhel) - extra_opts="--libexecdir=/usr/libexec/snapd --with-snap-mount-dir=/var/lib/snapd/snap --enable-merged-usr --disable-apparmor" + extra_opts="--libexecdir=/usr/libexec/snapd --with-snap-mount-dir=/var/lib/snapd/snap --enable-merged-usr --disable-apparmor --enable-selinux" ;; opensuse|opensuse-tumbleweed) extra_opts="--libexecdir=/usr/lib/snapd --enable-nvidia-biarch --with-32bit-libdir=/usr/lib --enable-merged-usr" diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmd_linux.go snapd-2.45.1ubuntu0.2/cmd/cmd_linux.go --- snapd-2.37.4ubuntu0.1/cmd/cmd_linux.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/cmd_linux.go 2020-06-05 13:13:49.000000000 +0000 @@ -20,14 +20,13 @@ package cmd import ( - "bytes" - "io/ioutil" "log" "os" "path/filepath" "strings" "syscall" + "github.com/snapcore/snapd/cmd/cmdutil" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" @@ -73,34 +72,18 @@ 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")) - content, err := ioutil.ReadFile(fullInfo) +func coreSupportsReExec(coreOrSnapdPath string) bool { + infoPath := filepath.Join(coreOrSnapdPath, filepath.Join(dirs.CoreLibExecDir, "info")) + ver, err := cmdutil.SnapdVersionFromInfoFile(infoPath) if err != nil { - if !os.IsNotExist(err) { - logger.Noticef("cannot open snapd info file %q: %s", fullInfo, err) - } + logger.Noticef("%v", err) return false } - if !bytes.HasPrefix(content, []byte("VERSION=")) { - idx := bytes.Index(content, []byte("\nVERSION=")) - if idx < 0 { - logger.Noticef("cannot find snapd version information in %q", content) - return false - } - content = content[idx+1:] - } - content = content[8:] - idx := bytes.IndexByte(content, '\n') - if idx > -1 { - content = content[:idx] - } - ver := string(content) // > 0 means our Version is bigger than the version of snapd in core res, err := strutil.VersionCompare(Version, ver) if err != nil { @@ -108,20 +91,21 @@ 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 } +// TODO: move to cmd/cmdutil/ +// // InternalToolPath returns the path of an internal snapd tool. The tool -// *must* be located inside /usr/lib/snapd/. +// *must* be located inside the same tree as the current binary. // // The return value is either the path of the tool in the current distribution -// or in the core snap (or the ubuntu-core snap). This handles spiritual -// "re-exec" where we run the tool from the core snap if the environment allows -// us to do so. -func InternalToolPath(tool string) string { +// or in the core/snapd snap (or the ubuntu-core snap) if the current binary is +// ran from that location. +func InternalToolPath(tool string) (string, error) { distroTool := filepath.Join(dirs.DistroLibExecDir, tool) // find the internal path relative to the running snapd, this @@ -129,23 +113,34 @@ // having a valid "current" symlink). exe, err := osReadlink("/proc/self/exe") if err != nil { - logger.Noticef("cannot read /proc/self/exe: %v, using tool outside core", err) - return distroTool + return "", err } - // ensure we never use this helper from anything but - if !strings.HasSuffix(exe, "/snapd") && !strings.HasSuffix(exe, ".test") { - log.Panicf("InternalToolPath can only be used from snapd, got: %s", exe) - } + if !strings.HasPrefix(exe, dirs.DistroLibExecDir) { + // either running from mounted location or /usr/bin/snap* - if !strings.HasPrefix(exe, dirs.SnapMountDir) { - logger.Debugf("exe doesn't have snap mount dir prefix: %q vs %q", exe, dirs.SnapMountDir) - return distroTool + // find the local prefix to the snap: + // /snap/snapd/123/usr/bin/snap -> /snap/snapd/123 + // /snap/core/234/usr/lib/snapd/snapd -> /snap/core/234 + idx := strings.LastIndex(exe, "/usr/") + if idx > 0 { + // only assume mounted location when path contains + // /usr/, but does not start with one + prefix := exe[:idx] + return filepath.Join(prefix, "/usr/lib/snapd", tool), nil + } + if idx == -1 { + // or perhaps some other random location, make sure the tool + // exists there and is an executable + maybeTool := filepath.Join(filepath.Dir(exe), tool) + if osutil.IsExecutable(maybeTool) { + return maybeTool, nil + } + } } - // if we are re-execed, then the tool is at the same location - // as snapd - return filepath.Join(filepath.Dir(exe), tool) + // fallback to distro tool + return distroTool, nil } // mustUnsetenv will unset the given environment key or panic if it @@ -194,10 +189,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 @@ -205,10 +200,19 @@ } // 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 } logger.Debugf("restarting into %q", full) panic(syscallExec(full, os.Args, os.Environ())) } + +// MockOsReadlink is for use in tests +func MockOsReadlink(f func(string) (string, error)) func() { + realOsReadlink := osReadlink + osReadlink = f + return func() { + osReadlink = realOsReadlink + } +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmd_other.go snapd-2.45.1ubuntu0.2/cmd/cmd_other.go --- snapd-2.37.4ubuntu0.1/cmd/cmd_other.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/cmd_other.go 2020-06-05 13:13:49.000000000 +0000 @@ -20,9 +20,21 @@ package cmd +import ( + "errors" +) + // ExecInSnapdOrCoreSnap makes sure you're executing the binary that ships in // the snapd/core snap. // On this OS this is a stub. func ExecInSnapdOrCoreSnap() { return } + +// InternalToolPath returns the path of an internal snapd tool. The tool +// *must* be located inside the same tree as the current binary. +// +// On this OS this is a stub and always returns an error. +func InternalToolPath(tool string) (string, error) { + return "", errors.New("unsupported on non-Linux systems") +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmd_test.go snapd-2.45.1ubuntu0.2/cmd/cmd_test.go --- snapd-2.37.4ubuntu0.1/cmd/cmd_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/cmd_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -206,7 +206,9 @@ }) defer restore() - c.Check(cmd.InternalToolPath("potato"), Equals, filepath.Join(dirs.DistroLibExecDir, "potato")) + path, err := cmd.InternalToolPath("potato") + c.Check(err, IsNil) + c.Check(path, Equals, filepath.Join(dirs.DistroLibExecDir, "potato")) } func (s *cmdSuite) TestInternalToolPathWithReexec(c *C) { @@ -216,16 +218,126 @@ }) defer restore() - c.Check(cmd.InternalToolPath("potato"), Equals, filepath.Join(dirs.SnapMountDir, "snapd/42/usr/lib/snapd/potato")) + path, err := cmd.InternalToolPath("potato") + c.Check(err, IsNil) + c.Check(path, Equals, filepath.Join(dirs.SnapMountDir, "snapd/42/usr/lib/snapd/potato")) } -func (s *cmdSuite) TestInternalToolPathFromIncorrectHelper(c *C) { +func (s *cmdSuite) TestInternalToolPathWithOtherLocation(c *C) { + s.fakeInternalTool(c, s.snapdPath, "potato") + restore := cmd.MockOsReadlink(func(string) (string, error) { + return filepath.Join("/tmp/tmp.foo_1234/usr/lib/snapd/snapd"), nil + }) + defer restore() + + path, err := cmd.InternalToolPath("potato") + c.Check(err, IsNil) + c.Check(path, Equals, "/tmp/tmp.foo_1234/usr/lib/snapd/potato") +} + +func (s *cmdSuite) TestInternalToolSnapPathWithOtherLocation(c *C) { + restore := cmd.MockOsReadlink(func(string) (string, error) { + return filepath.Join("/tmp/tmp.foo_1234/usr/bin/snap"), nil + }) + defer restore() + + path, err := cmd.InternalToolPath("potato") + c.Check(err, IsNil) + c.Check(path, Equals, "/tmp/tmp.foo_1234/usr/lib/snapd/potato") +} + +func (s *cmdSuite) TestInternalToolPathWithOtherCrazyLocation(c *C) { + restore := cmd.MockOsReadlink(func(string) (string, error) { + return filepath.Join("/usr/foo/usr/tmp/tmp.foo_1234/usr/bin/snap"), nil + }) + defer restore() + + path, err := cmd.InternalToolPath("potato") + c.Check(err, IsNil) + c.Check(path, Equals, "/usr/foo/usr/tmp/tmp.foo_1234/usr/lib/snapd/potato") +} + +func (s *cmdSuite) TestInternalToolPathWithDevLocationFallback(c *C) { + restore := cmd.MockOsReadlink(func(string) (string, error) { + return filepath.Join("/home/dev/snapd/snapd"), nil + }) + defer restore() + + path, err := cmd.InternalToolPath("potato") + c.Check(err, IsNil) + c.Check(path, Equals, filepath.Join(dirs.DistroLibExecDir, "potato")) +} + +func (s *cmdSuite) TestInternalToolPathWithOtherDevLocationWhenExecutable(c *C) { restore := cmd.MockOsReadlink(func(string) (string, error) { - return "/usr/bin/potato", nil + return filepath.Join(dirs.GlobalRootDir, "/tmp/snapd"), nil + }) + defer restore() + + devTool := filepath.Join(dirs.GlobalRootDir, "/tmp/potato") + err := os.MkdirAll(filepath.Dir(devTool), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(devTool, []byte(""), 0755) + c.Assert(err, IsNil) + + path, err := cmd.InternalToolPath("potato") + c.Check(err, IsNil) + c.Check(path, Equals, filepath.Join(dirs.GlobalRootDir, "/tmp/potato")) +} + +func (s *cmdSuite) TestInternalToolPathWithOtherDevLocationNonExecutable(c *C) { + restore := cmd.MockOsReadlink(func(string) (string, error) { + return filepath.Join(dirs.GlobalRootDir, "/tmp/snapd"), nil + }) + defer restore() + + devTool := filepath.Join(dirs.GlobalRootDir, "/tmp/non-executable-potato") + err := os.MkdirAll(filepath.Dir(devTool), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(devTool, []byte(""), 0644) + c.Assert(err, IsNil) + + path, err := cmd.InternalToolPath("non-executable-potato") + c.Check(err, IsNil) + c.Check(path, Equals, filepath.Join(dirs.DistroLibExecDir, "non-executable-potato")) +} + +func (s *cmdSuite) TestInternalToolPathSnapdPathReexec(c *C) { + restore := cmd.MockOsReadlink(func(string) (string, error) { + return filepath.Join(dirs.SnapMountDir, "core/111/usr/bin/snap"), nil + }) + defer restore() + + p, err := cmd.InternalToolPath("snapd") + c.Assert(err, IsNil) + c.Check(p, Equals, filepath.Join(dirs.SnapMountDir, "/core/111/usr/lib/snapd/snapd")) +} + +func (s *cmdSuite) TestInternalToolPathSnapdSnap(c *C) { + restore := cmd.MockOsReadlink(func(string) (string, error) { + return filepath.Join(dirs.SnapMountDir, "snapd/22/usr/bin/snap"), nil + }) + defer restore() + p, err := cmd.InternalToolPath("snapd") + c.Assert(err, IsNil) + c.Check(p, Equals, filepath.Join(dirs.SnapMountDir, "/snapd/22/usr/lib/snapd/snapd")) +} + +func (s *cmdSuite) TestInternalToolPathWithLibexecdirLocation(c *C) { + defer dirs.SetRootDir(s.fakeroot) + restore := release.MockReleaseInfo(&release.OS{ID: "fedora"}) + defer restore() + // reload directory paths + dirs.SetRootDir("/") + + restore = cmd.MockOsReadlink(func(string) (string, error) { + return filepath.Join("/usr/bin/snap"), nil }) defer restore() - c.Check(func() { cmd.InternalToolPath("potato") }, PanicMatches, "InternalToolPath can only be used from snapd, got: /usr/bin/potato") + path, err := cmd.InternalToolPath("potato") + c.Check(err, IsNil) + c.Check(path, Equals, filepath.Join("/usr/libexec/snapd/potato")) } func (s *cmdSuite) TestExecInSnapdOrCoreSnap(c *C) { diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmdutil/cmdutil.go snapd-2.45.1ubuntu0.2/cmd/cmdutil/cmdutil.go --- snapd-2.37.4ubuntu0.1/cmd/cmdutil/cmdutil.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/cmdutil/cmdutil.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,140 @@ +// -*- 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 cmdutil + +import ( + "bufio" + "bytes" + "debug/elf" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +func elfInterp(cmd string) (string, error) { + el, err := elf.Open(cmd) + if err != nil { + return "", err + } + defer el.Close() + + for _, prog := range el.Progs { + if prog.Type == elf.PT_INTERP { + r := prog.Open() + interp, err := ioutil.ReadAll(r) + if err != nil { + return "", nil + } + + return string(bytes.Trim(interp, "\x00")), nil + } + } + + return "", fmt.Errorf("cannot find PT_INTERP header") +} + +func parseLdSoConf(root string, confPath string) []string { + f, err := os.Open(filepath.Join(root, confPath)) + if err != nil { + return nil + } + defer f.Close() + + var out []string + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + switch { + case strings.HasPrefix(line, "#"): + // nothing + case strings.TrimSpace(line) == "": + // nothing + case strings.HasPrefix(line, "include "): + l := strings.SplitN(line, "include ", 2) + files, err := filepath.Glob(filepath.Join(root, l[1])) + if err != nil { + return nil + } + for _, f := range files { + out = append(out, parseLdSoConf(root, f[len(root):])...) + } + default: + out = append(out, filepath.Join(root, line)) + } + + } + if err := scanner.Err(); err != nil { + return nil + } + + return out +} + +// CommandFromSystemSnap runs a command from the snapd/core snap +// using the proper interpreter and library paths. +// +// At the moment it can only run ELF files, expects a standard ld.so +// interpreter, and can't handle RPATH. +func CommandFromSystemSnap(name string, cmdArgs ...string) (*exec.Cmd, error) { + from := "snapd" + root := filepath.Join(dirs.SnapMountDir, "/snapd/current") + if !osutil.FileExists(root) { + from = "core" + root = filepath.Join(dirs.SnapMountDir, "/core/current") + } + + cmdPath := filepath.Join(root, name) + interp, err := elfInterp(cmdPath) + if err != nil { + return nil, err + } + coreLdSo := filepath.Join(root, interp) + // we cannot use EvalSymlink here because we need to resolve + // relative and an absolute symlinks differently. A absolute + // symlink is relative to root of the snapd/core snap. + seen := map[string]bool{} + for osutil.IsSymlink(coreLdSo) { + link, err := os.Readlink(coreLdSo) + if err != nil { + return nil, err + } + if filepath.IsAbs(link) { + coreLdSo = filepath.Join(root, link) + } else { + coreLdSo = filepath.Join(filepath.Dir(coreLdSo), link) + } + if seen[coreLdSo] { + return nil, fmt.Errorf("cannot run command from %s: symlink cycle found", from) + } + seen[coreLdSo] = true + } + + ldLibraryPathForCore := parseLdSoConf(root, "/etc/ld.so.conf") + + ldSoArgs := []string{"--library-path", strings.Join(ldLibraryPathForCore, ":"), cmdPath} + allArgs := append(ldSoArgs, cmdArgs...) + return exec.Command(coreLdSo, allArgs...), nil +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmdutil/cmdutil_test.go snapd-2.45.1ubuntu0.2/cmd/cmdutil/cmdutil_test.go --- snapd-2.37.4ubuntu0.1/cmd/cmdutil/cmdutil_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/cmdutil/cmdutil_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,118 @@ +// -*- 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 cmdutil_test + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "strings" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/cmd/cmdutil" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" +) + +// Hook up check.v1 into the "go test" runner +func Test(t *testing.T) { TestingT(t) } + +var truePath = osutil.LookPathDefault("true", "/bin/true") + +type cmdutilSuite struct{} + +var _ = Suite(&cmdutilSuite{}) + +func (s *cmdutilSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) +} + +func (s *cmdutilSuite) TearDownTest(c *C) { + dirs.SetRootDir("") +} + +func (s *cmdutilSuite) makeMockLdSoConf(c *C, root string) { + ldSoConf := filepath.Join(root, "/etc/ld.so.conf") + ldSoConfD := ldSoConf + ".d" + + err := os.MkdirAll(filepath.Dir(ldSoConf), 0755) + c.Assert(err, IsNil) + err = os.MkdirAll(ldSoConfD, 0755) + c.Assert(err, IsNil) + + err = ioutil.WriteFile(ldSoConf, []byte("include /etc/ld.so.conf.d/*.conf"), 0644) + c.Assert(err, IsNil) + + ldSoConf1 := filepath.Join(ldSoConfD, "x86_64-linux-gnu.conf") + + err = ioutil.WriteFile(ldSoConf1, []byte(` +# Multiarch support +/lib/x86_64-linux-gnu +/usr/lib/x86_64-linux-gnu`), 0644) + c.Assert(err, IsNil) +} + +func (s *cmdutilSuite) TestCommandFromSystemSnap(c *C) { + for _, snap := range []string{"core", "snapd"} { + + root := filepath.Join(dirs.SnapMountDir, snap, "current") + s.makeMockLdSoConf(c, root) + + os.MkdirAll(filepath.Join(root, "/usr/bin"), 0755) + osutil.CopyFile(truePath, filepath.Join(root, "/usr/bin/xdelta3"), 0) + cmd, err := cmdutil.CommandFromSystemSnap("/usr/bin/xdelta3", "--some-xdelta-arg") + c.Assert(err, IsNil) + + out, err := exec.Command("/bin/sh", "-c", fmt.Sprintf("readelf -l %s |grep interpreter:|cut -f2 -d:|cut -f1 -d]", truePath)).Output() + c.Assert(err, IsNil) + interp := strings.TrimSpace(string(out)) + + c.Check(cmd.Args, DeepEquals, []string{ + filepath.Join(root, interp), + "--library-path", + fmt.Sprintf("%s/lib/x86_64-linux-gnu:%s/usr/lib/x86_64-linux-gnu", root, root), + filepath.Join(root, "/usr/bin/xdelta3"), + "--some-xdelta-arg", + }) + } +} + +func (s *cmdutilSuite) TestCommandFromCoreSymlinkCycle(c *C) { + root := filepath.Join(dirs.SnapMountDir, "/core/current") + s.makeMockLdSoConf(c, root) + + os.MkdirAll(filepath.Join(root, "/usr/bin"), 0755) + osutil.CopyFile(truePath, filepath.Join(root, "/usr/bin/xdelta3"), 0) + + out, err := exec.Command("/bin/sh", "-c", "readelf -l /bin/true |grep interpreter:|cut -f2 -d:|cut -f1 -d]").Output() + c.Assert(err, IsNil) + interp := strings.TrimSpace(string(out)) + + coreInterp := filepath.Join(root, interp) + c.Assert(os.MkdirAll(filepath.Dir(coreInterp), 0755), IsNil) + c.Assert(os.Symlink(filepath.Base(coreInterp), coreInterp), IsNil) + + _, err = cmdutil.CommandFromSystemSnap("/usr/bin/xdelta3", "--some-xdelta-arg") + c.Assert(err, ErrorMatches, "cannot run command from core: symlink cycle found") +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmdutil/version.go snapd-2.45.1ubuntu0.2/cmd/cmdutil/version.go --- snapd-2.37.4ubuntu0.1/cmd/cmdutil/version.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/cmdutil/version.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,53 @@ +// -*- 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 cmdutil + +import ( + "bytes" + "fmt" + "io/ioutil" +) + +// SnapdVersionFromInfoFile returns snapd version read for the +// given info" file, pointed by infoPath. +// The format of the "info" file is a single line with "VERSION=..." +// in it. The file is produced by mkversion.sh and normally installed +// along snapd binary in /usr/lib/snapd. +func SnapdVersionFromInfoFile(infoPath string) (string, error) { + content, err := ioutil.ReadFile(infoPath) + if err != nil { + return "", fmt.Errorf("cannot open snapd info file %q: %s", infoPath, err) + } + + if !bytes.HasPrefix(content, []byte("VERSION=")) { + idx := bytes.Index(content, []byte("\nVERSION=")) + if idx < 0 { + return "", fmt.Errorf("cannot find snapd version information in %q", content) + } + content = content[idx+1:] + } + content = content[8:] + idx := bytes.IndexByte(content, '\n') + if idx > -1 { + content = content[:idx] + } + + return string(content), nil +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/cmdutil/version_test.go snapd-2.45.1ubuntu0.2/cmd/cmdutil/version_test.go --- snapd-2.37.4ubuntu0.1/cmd/cmdutil/version_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/cmdutil/version_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,57 @@ +// -*- 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 cmdutil_test + +import ( + "io/ioutil" + "path/filepath" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/cmd/cmdutil" +) + +type versionSuite struct{} + +var _ = Suite(&versionSuite{}) + +func (s *versionSuite) TestNoVersionFile(c *C) { + _, err := cmdutil.SnapdVersionFromInfoFile("/non-existing-file") + c.Assert(err, ErrorMatches, `cannot open snapd info file "/non-existing-file":.*`) +} + +func (s *versionSuite) TestNoVersionData(c *C) { + top := c.MkDir() + infoFile := filepath.Join(top, "info") + c.Assert(ioutil.WriteFile(infoFile, []byte("foo"), 0644), IsNil) + + _, err := cmdutil.SnapdVersionFromInfoFile(infoFile) + c.Assert(err, ErrorMatches, `cannot find snapd version information in "foo"`) +} + +func (s *versionSuite) TestVersionHappy(c *C) { + top := c.MkDir() + infoFile := filepath.Join(top, "info") + c.Assert(ioutil.WriteFile(infoFile, []byte("VERSION=1.2.3"), 0644), IsNil) + + ver, err := cmdutil.SnapdVersionFromInfoFile(infoFile) + c.Assert(err, IsNil) + c.Check(ver, Equals, "1.2.3") +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/configure.ac snapd-2.45.1ubuntu0.2/cmd/configure.ac --- snapd-2.37.4ubuntu0.1/cmd/configure.ac 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/configure.ac 2020-06-05 13:13:49.000000000 +0000 @@ -64,34 +64,26 @@ esac], [enable_apparmor=yes]) AM_CONDITIONAL([APPARMOR], [test "x$enable_apparmor" = "xyes"]) -# Allow to build without seccomp support by calling: -# ./configure --disable-seccomp -# This is separate because seccomp support is generally very good and it -# provides useful confinement for unsafe system calls. -AC_ARG_ENABLE([seccomp], - AS_HELP_STRING([--disable-seccomp], [Disable seccomp support]), - [case "${enableval}" in - yes) enable_seccomp=yes ;; - no) enable_seccomp=no ;; - *) AC_MSG_ERROR([bad value ${enableval} for --disable-seccomp]) - esac], [enable_seccomp=yes]) -AM_CONDITIONAL([SECCOMP], [test "x$enable_seccomp" = "xyes"]) +# Allow to build with SELinux support by calling: +# ./configure --enable-selinux +AC_ARG_ENABLE([selinux], + AS_HELP_STRING([--enable-selinux], [Enable SELinux support]), + [case "${enableval}" in + yes) enable_selinux=yes ;; + no) enable_selinux=no ;; + *) AC_MSG_ERROR([bad value ${enableval} for --enable-selinux]) + esac], [enable_selinux=no]) +AM_CONDITIONAL([SELINUX], [test "x$enable_selinux" = "xyes"]) # Enable older tests only when confinement is enabled and we're building for PC # The tests are of smaller value as we port more and more tests to spread. -AM_CONDITIONAL([CONFINEMENT_TESTS], [test "x$enable_apparmor" = "xyes" && test "x$enable_seccomp" = "xyes" && ((test "x$host_cpu" = "xx86_64" && test "x$build_cpu" = "xx86_64") || (test "x$host_cpu" = "xi686" && test "x$build_cpu" = "xi686"))]) +AM_CONDITIONAL([CONFINEMENT_TESTS], [test "x$enable_apparmor" = "xyes" && ((test "x$host_cpu" = "xx86_64" && test "x$build_cpu" = "xx86_64") || (test "x$host_cpu" = "xi686" && test "x$build_cpu" = "xi686"))]) # Check for glib that we use for unit testing AS_IF([test "x$with_unit_tests" = "xyes"], [ PKG_CHECK_MODULES([GLIB], [glib-2.0]) ]) -# Check if seccomp userspace library is available -AS_IF([test "x$enable_seccomp" = "xyes"], [ - PKG_CHECK_MODULES([SECCOMP], [libseccomp], [ - AC_DEFINE([HAVE_SECCOMP], [1], [Build with seccomp support])]) -]) - # Check if apparmor userspace library is available. AS_IF([test "x$enable_apparmor" = "xyes"], [ PKG_CHECK_MODULES([APPARMOR], [libapparmor], [ @@ -105,6 +97,12 @@ XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX]) ]) +# Check if SELinux userspace library is available. +AS_IF([test "x$enable_selinux" = "xyes"], [ +PKG_CHECK_MODULES([SELINUX], [libselinux], [ +AC_DEFINE([HAVE_SELINUX], [1], [Build with SELinux support])]) +]) + # Check if udev and libudev are available. # Those are now used unconditionally even if apparmor is disabled. PKG_CHECK_MODULES([LIBUDEV], [libudev]) @@ -161,18 +159,9 @@ AC_SUBST(SNAP_MOUNT_DIR) AC_DEFINE_UNQUOTED([SNAP_MOUNT_DIR], "${SNAP_MOUNT_DIR}", [Location of the snap mount points]) -AC_ARG_ENABLE([caps-over-setuid], - AS_HELP_STRING([--enable-caps-over-setuid], [Use capabilities rather than setuid bit]), - [case "${enableval}" in - yes) enable_caps_over_setuid=yes ;; - no) enable_caps_over_setuid=no ;; - *) AC_MSG_ERROR([bad value ${enableval} for --enable-caps-over-setuid]) - esac], [enable_caps_over_setuid=no]) -AM_CONDITIONAL([CAPS_OVER_SETUID], [test "x$enable_caps_over_setuid" = "xyes"]) - -AS_IF([test "x$enable_caps_over_setuid" = "xyes"], [ - AC_DEFINE([CAPS_OVER_SETUID], [1], - [Use capabilities rather than setuid bit])]) +SNAP_MOUNT_DIR_SYSTEMD_UNIT="$(systemd-escape -p "$SNAP_MOUNT_DIR")" +AC_SUBST([SNAP_MOUNT_DIR_SYSTEMD_UNIT]) +AC_DEFINE_UNQUOTED([SNAP_MOUNT_DIR_SYSTEMD_UNIT], "${SNAP_MOUNT_DIR_SYSTEMD_UNIT}", [Systemd unit name for snap mount points location]) AC_PATH_PROGS([HAVE_RST2MAN],[rst2man rst2man.py]) AS_IF([test "x$HAVE_RST2MAN" = "x"], [AC_MSG_WARN(["cannot find the rst2man tool, install python-docutils or similar"])]) @@ -182,6 +171,7 @@ AM_CONDITIONAL([HAVE_VALGRIND], [test "x${HAVE_VALGRIND}" != "x"]) AS_IF([test "x$HAVE_VALGRIND" = "x"], [AC_MSG_WARN(["cannot find the valgrind tool, will not run unit tests through valgrind"])]) +# Allow linking selected libraries statically for reexec. AC_ARG_ENABLE([static-libcap], AS_HELP_STRING([--enable-static-libcap], [Link libcap statically]), [case "${enableval}" in @@ -200,14 +190,14 @@ esac], [enable_static_libapparmor=no]) AM_CONDITIONAL([STATIC_LIBAPPARMOR], [test "x$enable_static_libapparmor" = "xyes"]) -AC_ARG_ENABLE([static-libseccomp], - AS_HELP_STRING([--enable-static-libseccomp], [Link libseccomp statically]), - [case "${enableval}" in - yes) enable_static_libseccomp=yes ;; - no) enable_static_libseccomp=no ;; - *) AC_MSG_ERROR([bad value ${enableval} for --enable-static-libseccomp]) - esac], [enable_static_libseccomp=no]) -AM_CONDITIONAL([STATIC_LIBSECCOMP], [test "x$enable_static_libseccomp" = "xyes"]) +AC_ARG_ENABLE([static-libselinux], +AS_HELP_STRING([--enable-static-libselinux], [Link libselinux statically]), +[case "${enableval}" in +yes) enable_static_libselinux=yes ;; +no) enable_static_libselinux=no ;; +*) AC_MSG_ERROR([bad value ${enableval} for --enable-static-libselinux]) +esac], [enable_static_libselinux=no]) +AM_CONDITIONAL([STATIC_LIBSELINUX], [test "x$enable_static_libselinux" = "xyes"]) LIB32_DIR="${prefix}/lib32" AC_ARG_WITH([32bit-libdir], diff -Nru snapd-2.37.4ubuntu0.1/cmd/export_test.go snapd-2.45.1ubuntu0.2/cmd/export_test.go --- snapd-2.37.4ubuntu0.1/cmd/export_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/export_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -50,11 +50,3 @@ syscallExec = oldSyscallExec } } - -func MockOsReadlink(f func(string) (string, error)) func() { - realOsReadlink := osReadlink - osReadlink = f - return func() { - osReadlink = realOsReadlink - } -} diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-freezer-support.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-freezer-support.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-freezer-support.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-freezer-support.c 2020-06-05 13:13:49.000000000 +0000 @@ -1,3 +1,20 @@ +/* + * 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 . + * + */ + // For AT_EMPTY_PATH and O_PATH #define _GNU_SOURCE @@ -11,6 +28,7 @@ #include #include +#include "cgroup-support.h" #include "cleanup-funcs.h" #include "string-utils.h" #include "utils.h" @@ -19,51 +37,9 @@ void sc_cgroup_freezer_join(const char *snap_name, pid_t pid) { - // Format the name of the cgroup hierarchy. char buf[PATH_MAX] = { 0 }; sc_must_snprintf(buf, sizeof buf, "snap.%s", snap_name); - - // Open the freezer cgroup directory. - int cgroup_fd SC_CLEANUP(sc_cleanup_close) = -1; - cgroup_fd = open(freezer_cgroup_dir, - O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); - if (cgroup_fd < 0) { - die("cannot open freezer cgroup (%s)", freezer_cgroup_dir); - } - // Create the freezer hierarchy for the given snap. - if (mkdirat(cgroup_fd, buf, 0755) < 0 && errno != EEXIST) { - die("cannot create freezer cgroup hierarchy for snap %s", - snap_name); - } - // Open the hierarchy directory for the given snap. - int hierarchy_fd SC_CLEANUP(sc_cleanup_close) = -1; - hierarchy_fd = openat(cgroup_fd, buf, - O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); - if (hierarchy_fd < 0) { - die("cannot open freezer cgroup hierarchy for snap %s", - snap_name); - } - // Since we may be running from a setuid but not setgid executable, ensure - // that the group and owner of the hierarchy directory is root.root. - if (fchownat(hierarchy_fd, "", 0, 0, AT_EMPTY_PATH) < 0) { - die("cannot change owner of freezer cgroup hierarchy for snap %s to root.root", snap_name); - } - // Open the tasks file. - int tasks_fd SC_CLEANUP(sc_cleanup_close) = -1; - tasks_fd = openat(hierarchy_fd, "tasks", - O_WRONLY | O_NOFOLLOW | O_CLOEXEC); - if (tasks_fd < 0) { - die("cannot open tasks file for freezer cgroup hierarchy for snap %s", snap_name); - } - // Write the process (task) number to the tasks file. Linux task IDs are - // limited to 2^29 so a long int is enough to represent it. - // See include/linux/threads.h in the kernel source tree for details. - int n = sc_must_snprintf(buf, sizeof buf, "%ld", (long)pid); - if (write(tasks_fd, buf, n) < n) { - die("cannot move process %ld to freezer cgroup hierarchy for snap %s", (long)pid, snap_name); - } - debug("moved process %ld to freezer cgroup hierarchy for snap %s", - (long)pid, snap_name); + sc_cgroup_create_and_join(freezer_cgroup_dir, buf, pid); } bool sc_cgroup_freezer_occupied(const char *snap_name) @@ -108,7 +84,7 @@ FILE *cgroup_procs SC_CLEANUP(sc_cleanup_file) = NULL; cgroup_procs = fdopen(cgroup_procs_fd, "r"); if (cgroup_procs == NULL) { - die("cannot convert tasks file descriptor to FILE"); + die("cannot convert cgroups.procs file descriptor to FILE"); } cgroup_procs_fd = -1; // cgroup_procs_fd will now be closed by fclose. @@ -116,7 +92,7 @@ size_t line_buf_size = 0; ssize_t num_read; struct stat statbuf; - do { + for (;;) { num_read = getline(&line_buf, &line_buf_size, cgroup_procs); if (num_read < 0 && errno != 0) { die("cannot read next PID belonging to snap %s", @@ -143,7 +119,7 @@ debug("found process %s belonging to user %d", line_buf, statbuf.st_uid); return true; - } while (num_read > 0); + } return false; } diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-freezer-support.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-freezer-support.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-freezer-support.h 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-freezer-support.h 2020-06-05 13:13:49.000000000 +0000 @@ -1,3 +1,20 @@ +/* + * 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_CGROUP_FREEZER_SUPPORT_H #define SC_CGROUP_FREEZER_SUPPORT_H @@ -14,7 +31,7 @@ * allows us to track processes belonging to a given snap. This makes the * measurement "are any processes of this snap still alive" very simple. * - * The "tasks" file belonging to the cgroup contains the set of all the + * The "cgroup.procs" file belonging to the cgroup contains the set of all the * processes that originate from the given snap. Examining that file one can * reliably determine if the set is empty or not. * diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-pids-support.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-pids-support.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-pids-support.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-pids-support.c 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,26 @@ +/* + * 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 "cgroup-pids-support.h" + +#include "cgroup-support.h" + +static const char *pids_cgroup_dir = "/sys/fs/cgroup/pids"; + +void sc_cgroup_pids_join(const char *snap_security_tag, pid_t pid) { + sc_cgroup_create_and_join(pids_cgroup_dir, snap_security_tag, pid); +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-pids-support.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-pids-support.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-pids-support.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-pids-support.h 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,43 @@ +/* + * 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_CGROUP_PIDS_SUPPORT_H +#define SC_CGROUP_PIDS_SUPPORT_H + +#include + +/** + * Join the pid cgroup for the given snap application. + * + * This function adds the specified task to the pid cgroup specific to the + * given snap. The name of the cgroup is "snap.$snap_name.$app_name" for apps + * or "snap.$snap_name.hook.$hook_name" for hooks. + * + * The "tasks" file belonging to the cgroup contains the set of all the + * threads that originate from the given snap app or hook. Examining that + * file one can reliably determine if the set is empty or not. + * + * Similarly the "cgroup.procs" file belonging to the same directory contains + * the set of all the processes that originate from the given snap app or + * hook. + * + * For more details please review: + * https://www.kernel.org/doc/Documentation/cgroup-v1/pids.txt + **/ +void sc_cgroup_pids_join(const char *snap_security_tag, pid_t pid); + +#endif diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-support.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-support.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-support.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-support.c 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,99 @@ +/* + * 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 . + * + */ + +// For AT_EMPTY_PATH and O_PATH +#define _GNU_SOURCE + +#include "cgroup-support.h" + +#include +#include +#include +#include +#include +#include +#include + +#include "cleanup-funcs.h" +#include "string-utils.h" +#include "utils.h" + +void sc_cgroup_create_and_join(const char *parent, const char *name, pid_t pid) { + int parent_fd SC_CLEANUP(sc_cleanup_close) = -1; + parent_fd = open(parent, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (parent_fd < 0) { + die("cannot open cgroup hierarchy %s", parent); + } + // Since we may be running from a setuid but not setgid executable, switch + // to the effective group to root so that the mkdirat call creates a cgroup + // that is always owned by root.root. + sc_identity old = sc_set_effective_identity(sc_root_group_identity()); + if (mkdirat(parent_fd, name, 0755) < 0 && errno != EEXIST) { + die("cannot create cgroup hierarchy %s/%s", parent, name); + } + (void)sc_set_effective_identity(old); + int hierarchy_fd SC_CLEANUP(sc_cleanup_close) = -1; + hierarchy_fd = openat(parent_fd, name, O_PATH | O_DIRECTORY | O_NOFOLLOW | O_CLOEXEC); + if (hierarchy_fd < 0) { + die("cannot open cgroup hierarchy %s/%s", parent, name); + } + // Open the cgroup.procs file. + int procs_fd SC_CLEANUP(sc_cleanup_close) = -1; + procs_fd = openat(hierarchy_fd, "cgroup.procs", O_WRONLY | O_NOFOLLOW | O_CLOEXEC); + if (procs_fd < 0) { + die("cannot open file %s/%s/cgroup.procs", parent, name); + } + // Write the process (task) number to the procs file. Linux task IDs are + // limited to 2^29 so a long int is enough to represent it. + // See include/linux/threads.h in the kernel source tree for details. + char buf[22] = {0}; // 2^64 base10 + 2 for NUL and '-' for long + int n = sc_must_snprintf(buf, sizeof buf, "%ld", (long)pid); + if (write(procs_fd, buf, n) < n) { + die("cannot move process %ld to cgroup hierarchy %s/%s", (long)pid, parent, name); + } + 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.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-support.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-support.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cgroup-support.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cgroup-support.h 2020-06-05 13:13:49.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 . + * + */ + +#ifndef SC_CGROUP_SUPPORT_H +#define SC_CGROUP_SUPPORT_H + +#include +#include + +/** + * sc_cgroup_create_and_join joins, perhaps creating, a cgroup hierarchy. + * + * The code assumes that an existing hierarchy rooted at "parent". It follows + * up with a sub-hierarchy called "name", creating it if necessary. The created + * sub-hierarchy is made to belong to root.root and the specified process is + * moved there. + **/ +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.37.4ubuntu0.1/cmd/libsnap-confine-private/classic.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/classic.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/classic.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/classic.c 2020-06-05 13:13:49.000000000 +0000 @@ -56,8 +56,3 @@ return SC_DISTRO_CLASSIC; } } - -bool sc_should_use_normal_mode(sc_distro distro, const char *base_snap_name) -{ - return distro != SC_DISTRO_CORE16 || !sc_streq(base_snap_name, "core"); -} diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/classic.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/classic.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/classic.h 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/classic.h 2020-06-05 13:13:49.000000000 +0000 @@ -30,6 +30,4 @@ sc_distro sc_classify_distro(void); -bool sc_should_use_normal_mode(sc_distro distro, const char *base_snap_name); - #endif diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/classic-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/classic-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/classic-test.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/classic-test.c 2020-06-05 13:13:49.000000000 +0000 @@ -176,19 +176,7 @@ g_assert_cmpint(sc_classify_distro(), ==, SC_DISTRO_CORE_OTHER); } -static void test_should_use_normal_mode(void) -{ - g_assert_false(sc_should_use_normal_mode(SC_DISTRO_CORE16, "core")); - g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CORE_OTHER, "core")); - g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CLASSIC, "core")); - - g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CORE16, "core18")); - g_assert_true(sc_should_use_normal_mode - (SC_DISTRO_CORE_OTHER, "core18")); - g_assert_true(sc_should_use_normal_mode(SC_DISTRO_CLASSIC, "core18")); -} - -static void __attribute__ ((constructor)) init(void) +static void __attribute__((constructor)) init(void) { g_test_add_func("/classic/on-classic", test_is_on_classic); g_test_add_func("/classic/on-classic-with-long-line", @@ -199,6 +187,4 @@ g_test_add_func("/classic/on-fedora-base", test_is_on_fedora_base); g_test_add_func("/classic/on-fedora-ws", test_is_on_fedora_ws); g_test_add_func("/classic/on-custom-base", test_is_on_custom_base); - g_test_add_func("/classic/should-use-normal-mode", - test_should_use_normal_mode); } diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cleanup-funcs.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cleanup-funcs.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cleanup-funcs.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cleanup-funcs.c 2020-06-05 13:13:49.000000000 +0000 @@ -22,8 +22,9 @@ void sc_cleanup_string(char **ptr) { - if (ptr != NULL) { + if (ptr != NULL && *ptr != NULL) { free(*ptr); + *ptr = NULL; } } @@ -31,6 +32,7 @@ { if (ptr != NULL && *ptr != NULL) { fclose(*ptr); + *ptr = NULL; } } @@ -38,6 +40,7 @@ { if (ptr != NULL && *ptr != NULL) { endmntent(*ptr); + *ptr = NULL; } } @@ -45,6 +48,7 @@ { if (ptr != NULL && *ptr != NULL) { closedir(*ptr); + *ptr = NULL; } } @@ -52,5 +56,6 @@ { if (ptr != NULL && *ptr != -1) { close(*ptr); + *ptr = -1; } } diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cleanup-funcs.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cleanup-funcs.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cleanup-funcs.h 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cleanup-funcs.h 2020-06-05 13:13:49.000000000 +0000 @@ -34,40 +34,45 @@ /** * Free a dynamically allocated string. * - * This function is designed to be used with - * __attribute__((cleanup(sc_cleanup_string))). + * This function is designed to be used with SC_CLEANUP() macro. + * The variable MUST be initialized for correct operation. + * The safe initialisation value is NULL. **/ void sc_cleanup_string(char **ptr); /** * Close an open file. * - * This function is designed to be used with - * __attribute__((cleanup(sc_cleanup_file))). + * This function is designed to be used with SC_CLEANUP() macro. + * The variable MUST be initialized for correct operation. + * The safe initialisation value is NULL. **/ void sc_cleanup_file(FILE ** ptr); /** * Close an open file with endmntent(3) * - * This function is designed to be used with - * __attribute__((cleanup(sc_cleanup_endmntent))). + * This function is designed to be used with SC_CLEANUP() macro. + * The variable MUST be initialized for correct operation. + * The safe initialisation value is NULL. **/ void sc_cleanup_endmntent(FILE ** ptr); /** * Close an open directory with closedir(3) * - * This function is designed to be used with - * __attribute__((cleanup(sc_cleanup_closedir))). + * This function is designed to be used with SC_CLEANUP() macro. + * The variable MUST be initialized for correct operation. + * The safe initialisation value is NULL. **/ void sc_cleanup_closedir(DIR ** ptr); /** * Close an open file descriptor with close(2) * - * This function is designed to be used with - * __attribute__((cleanup(sc_cleanup_close))). + * This function is designed to be used with SC_CLEANUP() macro. + * The variable MUST be initialized for correct operation. + * The safe initialisation value is -1. **/ void sc_cleanup_close(int *ptr); diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cleanup-funcs-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cleanup-funcs-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/cleanup-funcs-test.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/cleanup-funcs-test.c 2020-06-05 13:13:49.000000000 +0000 @@ -20,6 +20,8 @@ #include +#include + static int called = 0; static void cleanup_fn(int *ptr) @@ -38,7 +40,94 @@ g_assert_cmpint(called, ==, 1); } -static void __attribute__ ((constructor)) init(void) +static void test_cleanup_string(void) +{ + /* It is safe to use with a NULL pointer to a string. */ + sc_cleanup_string(NULL); + + /* It is safe to use with a NULL string. */ + char *str = NULL; + sc_cleanup_string(&str); + + /* It is safe to use with a non-NULL string. */ + str = malloc(1); + g_assert_nonnull(str); + sc_cleanup_string(&str); + g_assert_null(str); +} + +static void test_cleanup_file(void) +{ + /* It is safe to use with a NULL pointer to a FILE. */ + sc_cleanup_file(NULL); + + /* It is safe to use with a NULL FILE. */ + FILE *f = NULL; + sc_cleanup_file(&f); + + /* It is safe to use with a non-NULL FILE. */ + f = fmemopen(NULL, 10, "rt"); + g_assert_nonnull(f); + sc_cleanup_file(&f); + g_assert_null(f); +} + +static void test_cleanup_endmntent(void) +{ + /* It is safe to use with a NULL pointer to a FILE. */ + sc_cleanup_endmntent(NULL); + + /* It is safe to use with a NULL FILE. */ + FILE *f = NULL; + sc_cleanup_endmntent(&f); + + /* It is safe to use with a non-NULL FILE. */ + f = setmntent("/etc/fstab", "rt"); + g_assert_nonnull(f); + sc_cleanup_endmntent(&f); + g_assert_null(f); +} + +static void test_cleanup_closedir(void) +{ + /* It is safe to use with a NULL pointer to a DIR. */ + sc_cleanup_closedir(NULL); + + /* It is safe to use with a NULL DIR. */ + DIR *d = NULL; + sc_cleanup_closedir(&d); + + /* It is safe to use with a non-NULL DIR. */ + d = opendir("."); + g_assert_nonnull(d); + sc_cleanup_closedir(&d); + g_assert_null(d); +} + +static void test_cleanup_close(void) +{ + /* It is safe to use with a NULL pointer to an int. */ + sc_cleanup_close(NULL); + + /* It is safe to use with a -1 file descriptor. */ + int fd = -1; + sc_cleanup_close(&fd); + + /* It is safe to use with a non-invalid file descriptor. */ + /* Timerfd is a simple to use and widely available object that can be + * created and closed without interacting with the filesystem. */ + fd = timerfd_create(CLOCK_MONOTONIC, TFD_CLOEXEC); + g_assert_cmpint(fd, !=, -1); + sc_cleanup_close(&fd); + g_assert_cmpint(fd, ==, -1); +} + +static void __attribute__((constructor)) init(void) { g_test_add_func("/cleanup/sanity", test_cleanup_sanity); + g_test_add_func("/cleanup/string", test_cleanup_string); + g_test_add_func("/cleanup/file", test_cleanup_file); + g_test_add_func("/cleanup/endmntent", test_cleanup_endmntent); + g_test_add_func("/cleanup/closedir", test_cleanup_closedir); + g_test_add_func("/cleanup/close", test_cleanup_close); } diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/error.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/error.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/error.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/error.c 2020-06-05 13:13:49.000000000 +0000 @@ -26,20 +26,10 @@ #include #include -struct sc_error { - // Error domain defines a scope for particular error codes. - const char *domain; - // Code differentiates particular errors for the programmer. - // The code may be zero if the particular meaning is not relevant. - int code; - // Message carries a formatted description of the problem. - char *msg; -}; - -static struct sc_error *sc_error_initv(const char *domain, int code, - const char *msgfmt, va_list ap) +static sc_error *sc_error_initv(const char *domain, int code, + const char *msgfmt, va_list ap) { - struct sc_error *err = calloc(1, sizeof *err); + sc_error *err = calloc(1, sizeof *err); if (err == NULL) { die("cannot allocate memory for error object"); } @@ -51,28 +41,45 @@ return err; } -struct sc_error *sc_error_init(const char *domain, int code, const char *msgfmt, - ...) +sc_error *sc_error_init(const char *domain, int code, const char *msgfmt, ...) +{ + va_list ap; + va_start(ap, msgfmt); + sc_error *err = sc_error_initv(domain, code, msgfmt, ap); + va_end(ap); + return err; +} + +sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt, ...) +{ + va_list ap; + va_start(ap, msgfmt); + sc_error *err = sc_error_initv(SC_ERRNO_DOMAIN, errno_copy, msgfmt, ap); + va_end(ap); + return err; +} + +sc_error *sc_error_init_simple(const char *msgfmt, ...) { va_list ap; va_start(ap, msgfmt); - struct sc_error *err = sc_error_initv(domain, code, msgfmt, ap); + sc_error *err = sc_error_initv(SC_LIBSNAP_DOMAIN, + SC_UNSPECIFIED_ERROR, msgfmt, ap); va_end(ap); return err; } -struct sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt, - ...) +sc_error *sc_error_init_api_misuse(const char *msgfmt, ...) { va_list ap; va_start(ap, msgfmt); - struct sc_error *err = - sc_error_initv(SC_ERRNO_DOMAIN, errno_copy, msgfmt, ap); + sc_error *err = sc_error_initv(SC_LIBSNAP_DOMAIN, + SC_API_MISUSE, msgfmt, ap); va_end(ap); return err; } -const char *sc_error_domain(struct sc_error *err) +const char *sc_error_domain(sc_error * err) { if (err == NULL) { die("cannot obtain error domain from NULL error"); @@ -80,7 +87,7 @@ return err->domain; } -int sc_error_code(struct sc_error *err) +int sc_error_code(sc_error * err) { if (err == NULL) { die("cannot obtain error code from NULL error"); @@ -88,7 +95,7 @@ return err->code; } -const char *sc_error_msg(struct sc_error *err) +const char *sc_error_msg(sc_error * err) { if (err == NULL) { die("cannot obtain error message from NULL error"); @@ -96,7 +103,7 @@ return err->msg; } -void sc_error_free(struct sc_error *err) +void sc_error_free(sc_error * err) { if (err != NULL) { free(err->msg); @@ -105,36 +112,36 @@ } } -void sc_cleanup_error(struct sc_error **ptr) +void sc_cleanup_error(sc_error ** ptr) { sc_error_free(*ptr); *ptr = NULL; } -void sc_die_on_error(struct sc_error *error) +void sc_die_on_error(sc_error * error) { 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); } } -void sc_error_forward(struct sc_error **recipient, struct sc_error *error) +int sc_error_forward(sc_error ** recipient, sc_error * error) { if (recipient != NULL) { *recipient = error; } else { sc_die_on_error(error); } + return error != NULL ? -1 : 0; } -bool sc_error_match(struct sc_error *error, const char *domain, int code) +bool sc_error_match(sc_error * error, const char *domain, int code) { if (domain == NULL) { die("cannot match error to a NULL domain"); diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/error.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/error.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/error.h 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/error.h 2020-06-05 13:13:49.000000000 +0000 @@ -43,9 +43,17 @@ **/ /** - * Opaque error structure. + * Error structure. **/ -struct sc_error; +typedef struct sc_error { + // Error domain defines a scope for particular error codes. + const char *domain; + // Code differentiates particular errors for the programmer. + // The code may be zero if the particular meaning is not relevant. + int code; + // Message carries a formatted description of the problem. + char *msg; +} sc_error; /** * Error domain for errors related to system errno. @@ -53,6 +61,21 @@ #define SC_ERRNO_DOMAIN "errno" /** + * Error domain for errors in the libsnap-confine-private library. + **/ +#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 { + /** SC_UNSPECIFIED_ERROR indicates an error not worthy of a distinct code. */ + SC_UNSPECIFIED_ERROR = 0, + /** SC_API_MISUSE indicates that public API was called incorrectly. */ + SC_API_MISUSE, + /** SC_BUG indicates that private API was called incorrectly. */ + SC_BUG, +} sc_libsnap_error; + +/** * Initialize a new error object. * * The domain is a cookie-like string that allows the caller to distinguish @@ -62,10 +85,29 @@ * * This function calls die() in case of memory allocation failure. **/ -__attribute__ ((warn_unused_result, - format(printf, 3, 4) SC_APPEND_RETURNS_NONNULL)) -struct sc_error *sc_error_init(const char *domain, int code, const char *msgfmt, - ...); +__attribute__((warn_unused_result, + format(printf, 3, 4) SC_APPEND_RETURNS_NONNULL)) +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. @@ -75,10 +117,9 @@ * * This function calls die() in case of memory allocation failure. **/ -__attribute__ ((warn_unused_result, - format(printf, 2, 3) SC_APPEND_RETURNS_NONNULL)) -struct sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt, - ...); +__attribute__((warn_unused_result, + format(printf, 2, 3) SC_APPEND_RETURNS_NONNULL)) +sc_error *sc_error_init_from_errno(int errno_copy, const char *msgfmt, ...); /** * Get the error domain out of an error object. @@ -86,8 +127,8 @@ * The error domain acts as a namespace for error codes. * No change of ownership takes place. **/ -__attribute__ ((warn_unused_result SC_APPEND_RETURNS_NONNULL)) -const char *sc_error_domain(struct sc_error *err); +__attribute__((warn_unused_result SC_APPEND_RETURNS_NONNULL)) +const char *sc_error_domain(sc_error * err); /** * Get the error code out of an error object. @@ -99,8 +140,8 @@ * can rely on programmatically. This can be used to return an error message * without having to allocate a distinct code for each one. **/ -__attribute__ ((warn_unused_result)) -int sc_error_code(struct sc_error *err); +__attribute__((warn_unused_result)) +int sc_error_code(sc_error * err); /** * Get the error message out of an error object. @@ -108,15 +149,15 @@ * The error message is bound to the life-cycle of the error object. * No change of ownership takes place. **/ -__attribute__ ((warn_unused_result SC_APPEND_RETURNS_NONNULL)) -const char *sc_error_msg(struct sc_error *err); +__attribute__((warn_unused_result SC_APPEND_RETURNS_NONNULL)) +const char *sc_error_msg(sc_error * err); /** * Free an error object. * * The error object can be NULL. **/ -void sc_error_free(struct sc_error *error); +void sc_error_free(sc_error * error); /** * Cleanup an error with sc_error_free() @@ -124,8 +165,8 @@ * This function is designed to be used with * __attribute__((cleanup(sc_cleanup_error))). **/ -__attribute__ ((nonnull)) -void sc_cleanup_error(struct sc_error **ptr); +__attribute__((nonnull)) +void sc_cleanup_error(sc_error ** ptr); /** * @@ -136,7 +177,7 @@ * The error message is derived from the data in the error, using the special * errno domain to provide additional information if that is available. **/ -void sc_die_on_error(struct sc_error *error); +void sc_die_on_error(sc_error * error); /** * Forward an error to the caller. @@ -146,10 +187,14 @@ * sc_die_on_error() is called as a safety measure. * * Change of ownership takes place and the error is now stored in the recipient. + * + * The return value -1 if error is non-NULL and 0 otherwise. The return value + * makes it convenient to `return sc_error_forward(err_out, err);` as the last + * line of a function. **/ // NOTE: There's no nonnull(1) attribute as the recipient *can* be NULL. With // the attribute in place GCC optimizes some things out and tests fail. -void sc_error_forward(struct sc_error **recipient, struct sc_error *error); +int sc_error_forward(sc_error ** recipient, sc_error * error); /** * Check if a given error matches the specified domain and code. @@ -157,7 +202,7 @@ * It is okay to match a NULL error, the function simply returns false in that * case. The domain cannot be NULL though. **/ -__attribute__ ((warn_unused_result)) -bool sc_error_match(struct sc_error *error, const char *domain, int code); +__attribute__((warn_unused_result)) +bool sc_error_match(sc_error * error, const char *domain, int code); #endif diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/error-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/error-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/error-test.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/error-test.c 2020-06-05 13:13:49.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. @@ -161,8 +189,10 @@ // Check that forwarding NULL does exactly that. struct sc_error *recipient = (void *)0xDEADBEEF; struct sc_error *err = NULL; - sc_error_forward(&recipient, err); + int rc; + rc = sc_error_forward(&recipient, err); g_assert_null(recipient); + g_assert_cmpint(rc, ==, 0); } static void test_sc_error_forward__something_somewhere(void) @@ -170,10 +200,12 @@ // Check that forwarding a real error works OK. struct sc_error *recipient = NULL; struct sc_error *err = sc_error_init("domain", 42, "just testing"); + int rc; g_test_queue_destroy((GDestroyNotify) sc_error_free, err); g_assert_nonnull(err); - sc_error_forward(&recipient, err); + rc = sc_error_forward(&recipient, err); g_assert_nonnull(recipient); + g_assert_cmpint(rc, ==, -1); } static void test_sc_error_forward__something_nowhere(void) @@ -227,11 +259,15 @@ g_test_trap_assert_stderr("cannot match error to a NULL domain\n"); } -static void __attribute__ ((constructor)) init(void) +static void __attribute__((constructor)) init(void) { 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.37.4ubuntu0.1/cmd/libsnap-confine-private/fault-injection.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/fault-injection.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/fault-injection.h 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/fault-injection.h 2020-06-05 13:13:49.000000000 +0000 @@ -37,7 +37,7 @@ struct sc_fault_state; -typedef bool(*sc_fault_fn) (struct sc_fault_state * state, void *ptr); +typedef bool (*sc_fault_fn)(struct sc_fault_state * state, void *ptr); struct sc_fault_state { int ncalls; diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/fault-injection-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/fault-injection-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/fault-injection-test.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/fault-injection-test.c 2020-06-05 13:13:49.000000000 +0000 @@ -57,7 +57,7 @@ sc_reset_faults(); } -static void __attribute__ ((constructor)) init(void) +static void __attribute__((constructor)) init(void) { g_test_add_func("/fault-injection", test_fault_injection); } diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/feature.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/feature.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/feature.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/feature.c 2020-06-05 13:13:49.000000000 +0000 @@ -34,15 +34,23 @@ { const char *file_name; switch (flag) { - case SC_PER_USER_MOUNT_NAMESPACE: + case SC_FEATURE_PER_USER_MOUNT_NAMESPACE: file_name = "per-user-mount-namespace"; break; + 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); } int dirfd SC_CLEANUP(sc_cleanup_close) = -1; - dirfd = open(feature_flag_dir, O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW | O_PATH); + dirfd = + open(feature_flag_dir, + O_CLOEXEC | O_DIRECTORY | O_NOFOLLOW | O_PATH); if (dirfd < 0 && errno == ENOENT) { return false; } diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/feature.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/feature.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/feature.h 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/feature.h 2020-06-05 13:13:49.000000000 +0000 @@ -21,7 +21,9 @@ #include typedef enum sc_feature_flag { - SC_PER_USER_MOUNT_NAMESPACE, + 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.37.4ubuntu0.1/cmd/libsnap-confine-private/feature-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/feature-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/feature-test.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/feature-test.c 2020-06-05 13:13:49.000000000 +0000 @@ -54,14 +54,14 @@ char subd[PATH_MAX]; sc_must_snprintf(subd, sizeof subd, "%s/absent", d); sc_mock_feature_flag_dir(subd); - g_assert(!sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE)); + g_assert(!sc_feature_enabled(SC_FEATURE_PER_USER_MOUNT_NAMESPACE)); } static void test_feature_enabled__missing_file(void) { const char *d = sc_testdir(); sc_mock_feature_flag_dir(d); - g_assert(!sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE)); + g_assert(!sc_feature_enabled(SC_FEATURE_PER_USER_MOUNT_NAMESPACE)); } static void test_feature_enabled__present_file(void) @@ -72,10 +72,24 @@ sc_must_snprintf(pname, sizeof pname, "%s/per-user-mount-namespace", d); g_file_set_contents(pname, "", -1, NULL); - g_assert(sc_feature_enabled(SC_PER_USER_MOUNT_NAMESPACE)); + g_assert(sc_feature_enabled(SC_FEATURE_PER_USER_MOUNT_NAMESPACE)); } -static void __attribute__ ((constructor)) init(void) +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", test_feature_enabled__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.37.4ubuntu0.1/cmd/libsnap-confine-private/infofile.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/infofile.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/infofile.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/infofile.c 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,104 @@ +/* + * 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 "infofile.h" + +#include +#include +#include +#include + +#include "../libsnap-confine-private/cleanup-funcs.h" +#include "../libsnap-confine-private/error.h" +#include "../libsnap-confine-private/string-utils.h" +#include "../libsnap-confine-private/utils.h" + +int sc_infofile_get_key(FILE *stream, const char *key, char **value, sc_error **err_out) { + sc_error *err = NULL; + size_t line_size = 0; + char *line_buf SC_CLEANUP(sc_cleanup_string) = NULL; + + if (stream == NULL) { + err = sc_error_init_api_misuse("stream cannot be NULL"); + goto out; + } + if (key == NULL) { + err = sc_error_init_api_misuse("key cannot be NULL"); + goto out; + } + if (value == NULL) { + err = sc_error_init_api_misuse("value cannot be NULL"); + goto out; + } + + /* Store NULL in case we don't find the key. + * This makes the value always well-defined. */ + *value = NULL; + + /* This loop advances through subsequent lines. */ + for (int lineno = 1;; ++lineno) { + errno = 0; + ssize_t nread = getline(&line_buf, &line_size, stream); + if (nread < 0 && errno != 0) { + err = sc_error_init_from_errno(errno, "cannot read beyond line %d", lineno); + goto out; + } + if (nread <= 0) { + break; /* There is nothing more to read. */ + } + /* NOTE: beyond this line the buffer is never empty (ie, nread > 0). */ + + /* 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_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_DOMAIN, 0, "line %d does not end with a newline", lineno); + goto out; + } + /* Replace the trailing newline character with the NUL byte. */ + line_buf[nread - 1] = '\0'; + /* Guard against malformed input that does not contain '=' byte */ + char *eq_ptr = memchr(line_buf, '=', nread); + if (eq_ptr == NULL) { + 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_simple("line %d contains empty key", lineno); + goto out; + } + /* Replace the first '=' with string terminator byte. */ + *eq_ptr = '\0'; + + /* If the key matches the one we are looking for, store it and stop scanning. */ + const char *scanned_key = line_buf; + const char *scanned_value = eq_ptr + 1; + if (sc_streq(scanned_key, key)) { + *value = sc_strdup(scanned_value); + break; + } + } + +out: + return sc_error_forward(err_out, err); +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/infofile.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/infofile.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/infofile.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/infofile.h 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,36 @@ +/* + * 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 SNAP_CONFINE_INFOFILE_H +#define SNAP_CONFINE_INFOFILE_H + +#include + +#include "../libsnap-confine-private/error.h" + +/** + * sc_infofile_get_key extracts a single value of a key=value pair from a given + * stream. + * + * On success the return value is zero and err_out, if not NULL, is deferences + * and set to NULL. On failure the return value is -1 is and detailed error + * information is stored by dereferencing err_out. If an error occurs and + * err_out is NULL then the program dies, printing the error message. + **/ +int sc_infofile_get_key(FILE *stream, const char *key, char **value, sc_error **err_out); + +#endif diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/infofile-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/infofile-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/infofile-test.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/infofile-test.c 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,165 @@ +/* + * 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 "infofile.h" +#include "infofile.c" + +#include +#include + +static void test_infofile_get_key(void) { + int rc; + sc_error *err; + + char text[] = + "key=value\n" + "other-key=other-value\n" + "dup-key=value-one\n" + "dup-key=value-two\n"; + FILE *stream = fmemopen(text, sizeof text - 1, "r"); + g_assert_nonnull(stream); + + char *value; + + /* Caller must provide the stream to scan. */ + 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_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); + + /* Caller must provide the key to look for. */ + 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_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); + + /* Caller must provide storage for the value. */ + 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_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); + + /* Keys that are not found get NULL values. */ + value = (void *)0xfefefefe; + rewind(stream); + rc = sc_infofile_get_key(stream, "missing-key", &value, &err); + g_assert_cmpint(rc, ==, 0); + g_assert_null(err); + g_assert_null(value); + + /* Keys that are found get strdup-duplicated values. */ + value = NULL; + rewind(stream); + rc = sc_infofile_get_key(stream, "key", &value, &err); + g_assert_cmpint(rc, ==, 0); + g_assert_null(err); + g_assert_nonnull(value); + g_assert_cmpstr(value, ==, "value"); + free(value); + + /* When duplicate keys are present the first value is extracted. */ + char *dup_value; + rewind(stream); + rc = sc_infofile_get_key(stream, "dup-key", &dup_value, &err); + g_assert_cmpint(rc, ==, 0); + g_assert_null(err); + g_assert_nonnull(dup_value); + g_assert_cmpstr(dup_value, ==, "value-one"); + free(dup_value); + + fclose(stream); + + /* Key without a value. */ + char *tricky_value; + char tricky1[] = "key\n"; + stream = fmemopen(tricky1, sizeof tricky1 - 1, "r"); + g_assert_nonnull(stream); + 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_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); + sc_error_free(err); + fclose(stream); + + /* Key-value pair with embedded NUL byte. */ + char tricky2[] = "key=value\0garbage\n"; + stream = fmemopen(tricky2, sizeof tricky2 - 1, "r"); + g_assert_nonnull(stream); + 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_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); + sc_error_free(err); + fclose(stream); + + /* Key with empty value but without trailing newline. */ + char tricky3[] = "key="; + stream = fmemopen(tricky3, sizeof tricky3 - 1, "r"); + g_assert_nonnull(stream); + 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_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); + sc_error_free(err); + fclose(stream); + + /* Key with empty value with a trailing newline (which is also valid). */ + char tricky4[] = "key=\n"; + stream = fmemopen(tricky4, sizeof tricky4 - 1, "r"); + g_assert_nonnull(stream); + rc = sc_infofile_get_key(stream, "key", &tricky_value, &err); + g_assert_cmpint(rc, ==, 0); + g_assert_null(err); + g_assert_cmpstr(tricky_value, ==, ""); + sc_error_free(err); + fclose(stream); + free(tricky_value); + + /* The equals character alone (key is empty) */ + char tricky5[] = "=\n"; + stream = fmemopen(tricky5, sizeof tricky5 - 1, "r"); + g_assert_nonnull(stream); + 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_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); + sc_error_free(err); + fclose(stream); +} + +static void __attribute__((constructor)) init(void) { g_test_add_func("/infofile/get_key", test_infofile_get_key); } diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/locking.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/locking.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/locking.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/locking.c 2020-06-05 13:13:49.000000000 +0000 @@ -94,12 +94,14 @@ { // Create (if required) and open the lock directory. debug("creating lock directory %s (if missing)", sc_lock_dir); + sc_identity old = sc_set_effective_identity(sc_root_group_identity()); if (sc_nonfatal_mkpath(sc_lock_dir, 0755) < 0) { die("cannot create lock directory %s", sc_lock_dir); } debug("opening lock directory %s", sc_lock_dir); int dir_fd = open(sc_lock_dir, O_DIRECTORY | O_PATH | O_CLOEXEC | O_NOFOLLOW); + (void)sc_set_effective_identity(old); if (dir_fd < 0) { die("cannot open lock directory"); } @@ -131,8 +133,10 @@ // Open the lock file and acquire an exclusive lock. debug("opening lock file: %s/%s", sc_lock_dir, lock_fname); + sc_identity old = sc_set_effective_identity(sc_root_group_identity()); lock_fd = openat(dir_fd, lock_fname, O_CREAT | O_RDWR | O_CLOEXEC | O_NOFOLLOW, 0600); + (void)sc_set_effective_identity(old); if (lock_fd < 0) { die("cannot open lock file: %s/%s", sc_lock_dir, lock_fname); } diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/locking-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/locking-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/locking-test.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/locking-test.c 2020-06-05 13:13:49.000000000 +0000 @@ -68,6 +68,11 @@ // Check that locking a namespace actually flock's the mutex with LOCK_EX static void test_sc_lock_unlock(void) { + if (geteuid() != 0) { + g_test_skip("this test only runs as root"); + return; + } + const char *lock_dir = sc_test_use_fake_lock_dir(); int fd = sc_lock_generic("foo", 123); // Construct the name of the lock file @@ -95,6 +100,11 @@ // Check that holding a lock is properly detected. static void test_sc_verify_snap_lock__locked(void) { + if (geteuid() != 0) { + g_test_skip("this test only runs as root"); + return; + } + (void)sc_test_use_fake_lock_dir(); int fd = sc_lock_snap("foo"); sc_verify_snap_lock("foo"); @@ -104,6 +114,11 @@ // Check that holding a lock is properly detected. static void test_sc_verify_snap_lock__unlocked(void) { + if (geteuid() != 0) { + g_test_skip("this test only runs as root"); + return; + } + (void)sc_test_use_fake_lock_dir(); if (g_test_subprocess()) { sc_verify_snap_lock("foo"); @@ -117,6 +132,11 @@ static void test_sc_enable_sanity_timeout(void) { + if (geteuid() != 0) { + g_test_skip("this test only runs as root"); + return; + } + if (g_test_subprocess()) { sc_enable_sanity_timeout(); debug("waiting..."); @@ -128,10 +148,11 @@ g_test_trap_subprocess(NULL, 1 * G_USEC_PER_SEC, G_TEST_SUBPROCESS_INHERIT_STDERR); g_test_trap_assert_failed(); - g_test_trap_assert_stderr ("sanity timeout expired: Interrupted system call\n"); + g_test_trap_assert_stderr + ("sanity timeout expired: Interrupted system call\n"); } -static void __attribute__ ((constructor)) init(void) +static void __attribute__((constructor)) init(void) { g_test_add_func("/locking/sc_lock_unlock", test_sc_lock_unlock); g_test_add_func("/locking/sc_enable_sanity_timeout", diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mountinfo.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mountinfo.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mountinfo.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mountinfo.c 2020-06-05 13:13:49.000000000 +0000 @@ -17,6 +17,7 @@ #include "mountinfo.h" #include +#include #include #include #include @@ -43,35 +44,34 @@ * (10) mount source: filesystem specific information or "none" * (11) super options: per super block options **/ -static struct sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line) - __attribute__ ((nonnull(1))); +static sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line) + __attribute__((nonnull(1))); /** * Free a sc_mountinfo structure and all its entries. **/ -static void sc_free_mountinfo(struct sc_mountinfo *info) - __attribute__ ((nonnull(1))); +static void sc_free_mountinfo(sc_mountinfo * info) + __attribute__((nonnull(1))); /** * Free a sc_mountinfo entry. **/ -static void sc_free_mountinfo_entry(struct sc_mountinfo_entry *entry) - __attribute__ ((nonnull(1))); +static void sc_free_mountinfo_entry(sc_mountinfo_entry * entry) + __attribute__((nonnull(1))); -struct sc_mountinfo_entry *sc_first_mountinfo_entry(struct sc_mountinfo *info) +sc_mountinfo_entry *sc_first_mountinfo_entry(sc_mountinfo * info) { return info->first; } -struct sc_mountinfo_entry *sc_next_mountinfo_entry(struct sc_mountinfo_entry - *entry) +sc_mountinfo_entry *sc_next_mountinfo_entry(sc_mountinfo_entry * entry) { return entry->next; } -struct sc_mountinfo *sc_parse_mountinfo(const char *fname) +sc_mountinfo *sc_parse_mountinfo(const char *fname) { - struct sc_mountinfo *info = calloc(1, sizeof *info); + sc_mountinfo *info = calloc(1, sizeof *info); if (info == NULL) { return NULL; } @@ -86,7 +86,7 @@ } char *line SC_CLEANUP(sc_cleanup_string) = NULL; size_t line_size = 0; - struct sc_mountinfo_entry *entry, *last = NULL; + sc_mountinfo_entry *entry, *last = NULL; for (;;) { errno = 0; if (getline(&line, &line_size, f) == -1) { @@ -112,7 +112,7 @@ } static void show_buffers(const char *line, int offset, - struct sc_mountinfo_entry *entry) + sc_mountinfo_entry * entry) { #ifdef MOUNTINFO_DEBUG fprintf(stderr, "Input buffer (first), with offset arrow\n"); @@ -127,7 +127,7 @@ fprintf(stderr, ">%s<\n", line); fputc('>', stderr); - for (int i = 0; i < strlen(line); ++i) { + for (size_t i = 0; i < strlen(line); ++i) { int c = entry->line_buf[i]; fputc(c == 0 ? '@' : c == 1 ? '#' : c, stderr); } @@ -135,37 +135,98 @@ fputc('\n', stderr); fputc('>', stderr); - for (int i = 0; i < strlen(line); ++i) + for (size_t i = 0; i < strlen(line); ++i) fputc('=', stderr); fputc('<', stderr); fputc('\n', stderr); #endif // MOUNTINFO_DEBUG } -static char *parse_next_string_field(struct sc_mountinfo_entry *entry, - const char *line, int *offset) +static bool is_octal_digit(char c) { - int offset_delta = 0; - char *field = &entry->line_buf[0] + *offset; - if (line[*offset] == ' ') { - // Special case for empty fields which cannot be parsed with %s. - *field = '\0'; - *offset += 1; - } else { - int nscanned = - sscanf(line + *offset, "%s%n", field, &offset_delta); - if (nscanned != 1) - return NULL; - *offset += offset_delta; - if (line[*offset] == ' ') { - *offset += 1; + return c >= '0' && c <= '7'; +} + +static char *parse_next_string_field(sc_mountinfo_entry * entry, + const char *line, size_t *offset) +{ + const char *input = &line[*offset]; + char *output = &entry->line_buf[*offset]; + size_t input_idx = 0; // reading index + size_t output_idx = 0; // writing index + + // Scan characters until we run out of memory to scan or we find a + // space. The kernel uses simple octal escape sequences for the + // following: space, tab, newline, backwards slash. Everything else is + // copied verbatim. + for (;;) { + int c = input[input_idx]; + if (c == '\0') { + // The string is over before we see anything then + // return NULL. This is an indication of end-of-input + // to the caller. + if (output_idx == 0) { + return NULL; + } + // The scanned line is NUL terminated. This ensures that the + // terminator is copied to the output buffer. + output[output_idx] = '\0'; + // NOTE: we must not advance the reading index since we + // reached the end of the buffer. + break; + } else if (c == ' ') { + // Fields are space delimited or end-of-string terminated. + // Represent either as the end-of-string marker, skip over it, + // and stop parsing by terminating the output, then + // breaking out of the loop but advancing the reading + // index which is needed for subsequent calls. + output[output_idx] = '\0'; + input_idx++; + break; + } else if (c == '\\') { + // Three *more* octal digits required for the escape + // sequence. For reference see mangle_path() in + // fs/seq_file.c. Note that is_octal_digit returns + // false on the string terminator character NUL and the + // short-circuiting behavior of && makes this check + // correct even if '\\' is the last character of the + // string. + const char *s = &input[input_idx]; + if (is_octal_digit(s[1]) && is_octal_digit(s[2]) + && is_octal_digit(s[3])) { + // Unescape the octal value encoded in s[1], + // s[2] and s[3]. Because we are working with + // byte values there are no issues related to + // byte order. + output[output_idx++] = + ((s[1] - '0') << 6) | + ((s[2] - '0') << 3) | ((s[3] - '0')); + // Advance the reading index by the length of the escape + // sequence. + input_idx += 4; + } else { + // Partial escape sequence, copy verbatim and + // continue (since we don't use this). + output[output_idx++] = c; + input_idx++; + } + } else { + // All other characters are simply copied verbatim. + output[output_idx++] = c; + input_idx++; } } + *offset += input_idx; +#ifdef MOUNTINFO_DEBUG + fprintf(stderr, + "\nscanned: >%s< (%zd bytes), input idx: %zd, output idx: %zd\n", + output, strlen(output), input_idx, output_idx); +#endif show_buffers(line, *offset, entry); - return field; + return output; } -static struct sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line) +static sc_mountinfo_entry *sc_parse_mountinfo_entry(const char *line) { // NOTE: the sc_mountinfo structure is allocated along with enough extra // storage to hold the whole line we are parsing. This is used as backing @@ -189,8 +250,7 @@ // // If MOUNTINFO_DEBUG is defined then extra debugging is printed to stderr // and this allows for visual analysis of what is going on. - struct sc_mountinfo_entry *entry = - calloc(1, sizeof *entry + strlen(line) + 1); + sc_mountinfo_entry *entry = calloc(1, sizeof *entry + strlen(line) + 1); if (entry == NULL) { return NULL; } @@ -199,14 +259,15 @@ // by show_buffers() below. This is "unaltered" memory. memset(entry->line_buf, 1, strlen(line)); #endif // MOUNTINFO_DEBUG - int nscanned; - int offset_delta, offset = 0; + int nscanned, initial_offset = 0; + size_t offset = 0; nscanned = sscanf(line, "%d %d %u:%u %n", &entry->mount_id, &entry->parent_id, - &entry->dev_major, &entry->dev_minor, &offset_delta); + &entry->dev_major, &entry->dev_minor, + &initial_offset); if (nscanned != 4) goto fail; - offset += offset_delta; + offset += initial_offset; show_buffers(line, offset, entry); @@ -243,14 +304,13 @@ if ((entry->super_opts = parse_next_string_field(entry, line, &offset)) == NULL) goto fail; - show_buffers(line, offset, entry); return entry; fail: free(entry); return NULL; } -void sc_cleanup_mountinfo(struct sc_mountinfo **ptr) +void sc_cleanup_mountinfo(sc_mountinfo ** ptr) { if (*ptr != NULL) { sc_free_mountinfo(*ptr); @@ -258,9 +318,9 @@ } } -static void sc_free_mountinfo(struct sc_mountinfo *info) +static void sc_free_mountinfo(sc_mountinfo * info) { - struct sc_mountinfo_entry *entry, *next; + sc_mountinfo_entry *entry, *next; for (entry = info->first; entry != NULL; entry = next) { next = entry->next; sc_free_mountinfo_entry(entry); @@ -268,7 +328,7 @@ free(info); } -static void sc_free_mountinfo_entry(struct sc_mountinfo_entry *entry) +static void sc_free_mountinfo_entry(sc_mountinfo_entry * entry) { free(entry); } diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mountinfo.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mountinfo.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mountinfo.h 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mountinfo.h 2020-06-05 13:13:49.000000000 +0000 @@ -18,16 +18,9 @@ #define SNAP_CONFINE_MOUNTINFO_H /** - * Structure describing entire /proc/self/sc_mountinfo file - **/ -struct sc_mountinfo { - struct sc_mountinfo_entry *first; -}; - -/** * Structure describing a single entry in /proc/self/sc_mountinfo **/ -struct sc_mountinfo_entry { +typedef struct sc_mountinfo_entry { /** * The mount identifier of a given mount entry. **/ @@ -91,7 +84,14 @@ // along with the structure itself and does not need to be freed // separately. char line_buf[0]; -}; +} sc_mountinfo_entry; + +/** + * Structure describing entire /proc/self/sc_mountinfo file + **/ +typedef struct sc_mountinfo { + sc_mountinfo_entry *first; +} sc_mountinfo; /** * Parse a file in according to sc_mountinfo syntax. @@ -100,7 +100,7 @@ * implicitly parse /proc/self/sc_mountinfo, that is the mount information * associated with the current process. **/ -struct sc_mountinfo *sc_parse_mountinfo(const char *fname); +sc_mountinfo *sc_parse_mountinfo(const char *fname); /** * Free a sc_mountinfo structure. @@ -108,8 +108,8 @@ * This function is designed to be used with __attribute__((cleanup)) so it * takes a pointer to the freed object (which is also a pointer). **/ -void sc_cleanup_mountinfo(struct sc_mountinfo **ptr) - __attribute__ ((nonnull(1))); +void sc_cleanup_mountinfo(sc_mountinfo ** ptr) + __attribute__((nonnull(1))); /** * Get the first sc_mountinfo entry. @@ -118,8 +118,8 @@ * returned value is bound to the lifecycle of the whole sc_mountinfo structure * and should not be freed explicitly. **/ -struct sc_mountinfo_entry *sc_first_mountinfo_entry(struct sc_mountinfo *info) - __attribute__ ((nonnull(1))); +sc_mountinfo_entry *sc_first_mountinfo_entry(sc_mountinfo * info) + __attribute__((nonnull(1))); /** * Get the next sc_mountinfo entry. @@ -128,8 +128,7 @@ * was the last entry. The returned value is bound to the lifecycle of the * whole sc_mountinfo structure and should not be freed explicitly. **/ -struct sc_mountinfo_entry *sc_next_mountinfo_entry(struct sc_mountinfo_entry - *entry) - __attribute__ ((nonnull(1))); +sc_mountinfo_entry *sc_next_mountinfo_entry(sc_mountinfo_entry * entry) + __attribute__((nonnull(1))); #endif diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mountinfo-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mountinfo-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mountinfo-test.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mountinfo-test.c 2020-06-05 13:13:49.000000000 +0000 @@ -24,7 +24,7 @@ { const char *line = "19 25 0:18 / /sys rw,nosuid,nodev,noexec,relatime shared:7 - sysfs sysfs rw"; - struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); g_assert_cmpint(entry->mount_id, ==, 19); @@ -48,7 +48,7 @@ { const char *line = "104 23 0:19 /snapd/ns /run/snapd/ns rw,nosuid,noexec,relatime - tmpfs tmpfs rw,size=99840k,mode=755"; - struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); g_assert_cmpint(entry->mount_id, ==, 104); @@ -69,7 +69,7 @@ { const char *line = "256 104 0:3 mnt:[4026532509] /run/snapd/ns/hello-world.mnt rw - nsfs nsfs rw"; - struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); g_assert_cmpint(entry->mount_id, ==, 256); @@ -89,7 +89,7 @@ static void test_parse_mountinfo_entry__garbage(void) { const char *line = "256 104 0:3"; - struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_null(entry); } @@ -97,7 +97,7 @@ { const char *line = "1 2 3:4 root mount-dir mount-opts - fs-type mount-source super-opts"; - struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); g_assert_cmpint(entry->mount_id, ==, 1); @@ -118,7 +118,7 @@ { const char *line = "1 2 3:4 root mount-dir mount-opts tag:1 - fs-type mount-source super-opts"; - struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); g_assert_cmpint(entry->mount_id, ==, 1); @@ -139,7 +139,7 @@ { const char *line = "1 2 3:4 root mount-dir mount-opts tag:1 tag:2 tag:3 tag:4 - fs-type mount-source super-opts"; - struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); g_assert_cmpint(entry->mount_id, ==, 1); @@ -160,7 +160,7 @@ { const char *line = "304 301 0:45 / /snap/test-snapd-content-advanced-plug/x1 rw,relatime - tmpfs rw"; - struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); g_assert_nonnull(entry); g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); g_assert_cmpint(entry->mount_id, ==, 304); @@ -178,7 +178,85 @@ g_assert_null(entry->next); } -static void __attribute__ ((constructor)) init(void) +static void test_parse_mountinfo_entry__octal_escaping(void) +{ + const char *line; + struct sc_mountinfo_entry *entry; + + // The kernel escapes spaces as \040 + line = "2 1 0:54 / /tmp rw - tmpfs tricky\\040path rw"; + entry = sc_parse_mountinfo_entry(line); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_nonnull(entry); + g_assert_cmpstr(entry->mount_source, ==, "tricky path"); + + // kernel escapes newlines as \012 + line = "2 1 0:54 / /tmp rw - tmpfs tricky\\012path rw"; + entry = sc_parse_mountinfo_entry(line); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_nonnull(entry); + g_assert_cmpstr(entry->mount_source, ==, "tricky\npath"); + + // kernel escapes tabs as \011 + line = "2 1 0:54 / /tmp rw - tmpfs tricky\\011path rw"; + entry = sc_parse_mountinfo_entry(line); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_nonnull(entry); + g_assert_cmpstr(entry->mount_source, ==, "tricky\tpath"); + + // kernel escapes forward slashes as \057 + line = "2 1 0:54 / /tmp rw - tmpfs tricky\\057path rw"; + entry = sc_parse_mountinfo_entry(line); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_nonnull(entry); + g_assert_cmpstr(entry->mount_source, ==, "tricky/path"); +} + +static void test_parse_mountinfo_entry__broken_octal_escaping(void) +{ + // Invalid octal escape sequences are left intact. + const char *line = + "2074 27 0:54 / /tmp/strange-dir rw,relatime shared:1039 - tmpfs no\\888thing rw\\"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 2074); + g_assert_cmpint(entry->parent_id, ==, 27); + g_assert_cmpint(entry->dev_major, ==, 0); + g_assert_cmpint(entry->dev_minor, ==, 54); + g_assert_cmpstr(entry->root, ==, "/"); + g_assert_cmpstr(entry->mount_dir, ==, "/tmp/strange-dir"); + g_assert_cmpstr(entry->mount_opts, ==, "rw,relatime"); + g_assert_cmpstr(entry->optional_fields, ==, "shared:1039"); + g_assert_cmpstr(entry->fs_type, ==, "tmpfs"); + g_assert_cmpstr(entry->mount_source, ==, "no\\888thing"); + g_assert_cmpstr(entry->super_opts, ==, "rw\\"); + g_assert_null(entry->next); +} + +static void test_parse_mountinfo_entry__unescaped_whitespace(void) +{ + // The kernel does not escape '\r' + const char *line = + "2074 27 0:54 / /tmp/strange\rdir rw,relatime shared:1039 - tmpfs tmpfs rw"; + struct sc_mountinfo_entry *entry = sc_parse_mountinfo_entry(line); + g_assert_nonnull(entry); + g_test_queue_destroy((GDestroyNotify) sc_free_mountinfo_entry, entry); + g_assert_cmpint(entry->mount_id, ==, 2074); + g_assert_cmpint(entry->parent_id, ==, 27); + g_assert_cmpint(entry->dev_major, ==, 0); + g_assert_cmpint(entry->dev_minor, ==, 54); + g_assert_cmpstr(entry->root, ==, "/"); + g_assert_cmpstr(entry->mount_dir, ==, "/tmp/strange\rdir"); + g_assert_cmpstr(entry->mount_opts, ==, "rw,relatime"); + g_assert_cmpstr(entry->optional_fields, ==, "shared:1039"); + g_assert_cmpstr(entry->fs_type, ==, "tmpfs"); + g_assert_cmpstr(entry->mount_source, ==, "tmpfs"); + g_assert_cmpstr(entry->super_opts, ==, "rw"); + g_assert_null(entry->next); +} + +static void __attribute__((constructor)) init(void) { g_test_add_func("/mountinfo/parse_mountinfo_entry/sysfs", test_parse_mountinfo_entry__sysfs); @@ -197,4 +275,11 @@ g_test_add_func ("/mountinfo/parse_mountinfo_entry/empty_source", test_parse_mountinfo_entry__empty_source); + g_test_add_func("/mountinfo/parse_mountinfo_entry/octal_escaping", + test_parse_mountinfo_entry__octal_escaping); + g_test_add_func + ("/mountinfo/parse_mountinfo_entry/broken_octal_escaping", + test_parse_mountinfo_entry__broken_octal_escaping); + g_test_add_func("/mountinfo/parse_mountinfo_entry/unescaped_whitespace", + test_parse_mountinfo_entry__unescaped_whitespace); } diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mount-opt-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mount-opt-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/mount-opt-test.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/mount-opt-test.c 2020-06-05 13:13:49.000000000 +0000 @@ -319,7 +319,7 @@ } } -static void __attribute__ ((constructor)) init(void) +static void __attribute__((constructor)) init(void) { g_test_add_func("/mount/sc_mount_opt2str", test_sc_mount_opt2str); g_test_add_func("/mount/sc_mount_cmd", test_sc_mount_cmd); diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/panic.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/panic.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/panic.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/panic.c 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/libsnap-confine-private/panic.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/panic.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/panic.h 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/panic.h 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/libsnap-confine-private/panic-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/panic-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/panic-test.c 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/panic-test.c 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/libsnap-confine-private/privs-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/privs-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/privs-test.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/privs-test.c 2020-06-05 13:13:49.000000000 +0000 @@ -61,7 +61,7 @@ g_test_trap_assert_passed(); } -static void __attribute__ ((constructor)) init(void) +static void __attribute__((constructor)) init(void) { g_test_add_func("/privs/sc_privs_drop", test_sc_privs_drop); } diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/secure-getenv.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/secure-getenv.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/secure-getenv.h 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/secure-getenv.h 2020-06-05 13:13:49.000000000 +0000 @@ -30,7 +30,7 @@ * only used when glibc is not available. **/ char *secure_getenv(const char *name) - __attribute__ ((nonnull(1), warn_unused_result)); + __attribute__((nonnull(1), warn_unused_result)); #endif // ! HAVE_SECURE_GETENV #endif diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/snap.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/snap.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/snap.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/snap.c 2020-06-05 13:13:49.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); @@ -98,12 +98,11 @@ return 0; } -void sc_instance_name_validate(const char *instance_name, - struct sc_error **errorp) +void sc_instance_name_validate(const char *instance_name, sc_error ** errorp) { // NOTE: This function should be synchronized with the two other // implementations: validate_instance_name and snap.ValidateInstanceName. - struct sc_error *err = NULL; + sc_error *err = NULL; // Ensure that name is not NULL if (instance_name == NULL) { @@ -112,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; @@ -142,12 +140,11 @@ sc_error_forward(errorp, err); } -void sc_instance_key_validate(const char *instance_key, - struct sc_error **errorp) +void sc_instance_key_validate(const char *instance_key, sc_error ** errorp) { // NOTE: see snap.ValidateInstanceName for reference of a valid instance key // format - struct sc_error *err = NULL; + sc_error *err = NULL; // Ensure that name is not NULL if (instance_key == NULL) { @@ -177,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"); @@ -186,11 +183,11 @@ sc_error_forward(errorp, err); } -void sc_snap_name_validate(const char *snap_name, struct sc_error **errorp) +void sc_snap_name_validate(const char *snap_name, sc_error ** errorp) { // NOTE: This function should be synchronized with the two other // implementations: validate_snap_name and snap.ValidateName. - struct sc_error *err = NULL; + sc_error *err = NULL; // Ensure that name is not NULL if (snap_name == NULL) { @@ -255,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.37.4ubuntu0.1/cmd/libsnap-confine-private/snap.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/snap.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/snap.h 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/snap.h 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/libsnap-confine-private/snap-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/snap-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/snap-test.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/snap-test.c 2020-06-05 13:13:49.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) @@ -111,13 +115,13 @@ static void test_sc_snap_or_instance_name_validate(gconstpointer data) { - typedef void (*validate_func_t) (const char *, struct sc_error **); + typedef void (*validate_func_t)(const char *, sc_error **); validate_func_t validate = (validate_func_t) data; bool is_instance = (validate == sc_instance_name_validate) ? true : false; - struct sc_error *err = NULL; + sc_error *err = NULL; // Smoke test, a valid snap name validate("hello-world", &err); @@ -267,7 +271,7 @@ static void test_sc_instance_name_validate(void) { - struct sc_error *err = NULL; + sc_error *err = NULL; sc_instance_name_validate("hello-world", &err); g_assert_null(err); @@ -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, @@ -530,7 +548,7 @@ g_assert_cmpstr(instance, ==, ""); } -static void __attribute__ ((constructor)) init(void) +static void __attribute__((constructor)) init(void) { g_test_add_func("/snap/verify_security_tag", test_verify_security_tag); g_test_add_func("/snap/sc_is_hook_security_tag", @@ -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.37.4ubuntu0.1/cmd/libsnap-confine-private/string-utils.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/string-utils.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/string-utils.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/string-utils.c 2020-06-05 13:13:49.000000000 +0000 @@ -56,6 +56,22 @@ return strncmp(str - xlen + slen, suffix, xlen) == 0; } +bool sc_startswith(const char *str, const char *prefix) +{ + if (!str || !prefix) { + return false; + } + + size_t xlen = strlen(prefix); + size_t slen = strlen(str); + + if (slen < xlen) { + return false; + } + + return strncmp(str, prefix, xlen) == 0; +} + char *sc_strdup(const char *str) { size_t len; @@ -81,7 +97,7 @@ n = vsnprintf(str, size, format, va); va_end(va); - if (n < 0 || (size_t) n >= size) + if (n < 0 || (size_t)n >= size) die("cannot format string: %s", str); return n; diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/string-utils.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/string-utils.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/string-utils.h 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/string-utils.h 2020-06-05 13:13:49.000000000 +0000 @@ -32,6 +32,11 @@ bool sc_endswith(const char *str, const char *suffix); /** + * Check if a string has a given prefix. + **/ +bool sc_startswith(const char *str, const char *prefix); + +/** * Allocate and return a copy of a string. **/ char *sc_strdup(const char *str); @@ -41,7 +46,7 @@ * * This version dies on any error condition. **/ -__attribute__ ((format(printf, 3, 4))) +__attribute__((format(printf, 3, 4))) int sc_must_snprintf(char *str, size_t size, const char *format, ...); /** diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/string-utils-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/string-utils-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/string-utils-test.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/string-utils-test.c 2020-06-05 13:13:49.000000000 +0000 @@ -53,6 +53,27 @@ g_assert_false(sc_endswith("ba", "bar")); } +static void test_sc_startswith(void) +{ + // NULL doesn't start with anything, nothing starts with NULL + g_assert_false(sc_startswith("", NULL)); + g_assert_false(sc_startswith(NULL, "")); + g_assert_false(sc_startswith(NULL, NULL)); + // Empty string starts with an empty string + g_assert_true(sc_startswith("", "")); + // Starts-with (matches) + g_assert_true(sc_startswith("foobar", "foo")); + g_assert_true(sc_startswith("foobar", "fo")); + g_assert_true(sc_startswith("foobar", "f")); + g_assert_true(sc_startswith("foobar", "")); + g_assert_true(sc_startswith("bar", "bar")); + // Starts-with (non-matches) + g_assert_false(sc_startswith("foobar", "quux")); + g_assert_false(sc_startswith("", "bar")); + g_assert_false(sc_startswith("b", "bar")); + g_assert_false(sc_startswith("ba", "bar")); +} + static void test_sc_must_snprintf(void) { char buf[5] = { 0 }; @@ -86,7 +107,8 @@ }; } data = { .buf = { - 'f', '\0', 0xFF, 0xFF},.canary1 = ~0,.canary2 = ~0,}; + 'f', '\0', 0xFF, 0xFF},.canary1 = ~0,.canary2 = ~0, + }; // Sanity check, ensure that the layout of structures is as spelled above. // (first canary1, then buf and finally canary2. @@ -117,7 +139,8 @@ }; } data = { .buf = { - 'f', 'o', 'o', '\0'},.canary1 = ~0,.canary2 = ~0,}; + 'f', 'o', 'o', '\0'},.canary1 = ~0,.canary2 = ~0, + }; // Sanity check, ensure that the layout of structures is as spelled above. // (first canary1, then buf and finally canary2. @@ -788,10 +811,11 @@ free(s); } -static void __attribute__ ((constructor)) init(void) +static void __attribute__((constructor)) init(void) { g_test_add_func("/string-utils/sc_streq", test_sc_streq); g_test_add_func("/string-utils/sc_endswith", test_sc_endswith); + g_test_add_func("/string-utils/sc_startswith", test_sc_startswith); g_test_add_func("/string-utils/sc_must_snprintf", test_sc_must_snprintf); g_test_add_func("/string-utils/sc_must_snprintf/fail", diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/test-utils.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/test-utils.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/test-utils.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/test-utils.c 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/libsnap-confine-private/test-utils.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/test-utils.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/test-utils.h 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/test-utils.h 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/libsnap-confine-private/test-utils-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/test-utils-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/test-utils-test.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/test-utils-test.c 2020-06-05 13:13:49.000000000 +0000 @@ -39,7 +39,31 @@ g_test_trap_assert_failed(); } -static void __attribute__ ((constructor)) init(void) +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.37.4ubuntu0.1/cmd/libsnap-confine-private/tool.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/tool.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/tool.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/tool.c 2020-06-05 13:13:49.000000000 +0000 @@ -88,9 +88,14 @@ snap_name_copy, NULL }; char *envp[] = { "SNAPD_DEBUG=x", NULL }; + + /* Switch the group to root so that directories, files and locks created by + * snap-update-ns are owned by the root group. */ + sc_identity old = sc_set_effective_identity(sc_root_group_identity()); sc_call_snapd_tool_with_apparmor(snap_update_ns_fd, "snap-update-ns", apparmor, aa_profile, argv, envp); + (void)sc_set_effective_identity(old); } void sc_call_snap_update_ns_as_user(int snap_update_ns_fd, @@ -105,7 +110,7 @@ snap_name); const char *xdg_runtime_dir = getenv("XDG_RUNTIME_DIR"); - char xdg_runtime_dir_env[PATH_MAX+strlen("XDG_RUNTIME_DIR=")]; + char xdg_runtime_dir_env[PATH_MAX + strlen("XDG_RUNTIME_DIR=")]; if (xdg_runtime_dir != NULL) { sc_must_snprintf(xdg_runtime_dir_env, sizeof(xdg_runtime_dir_env), @@ -115,7 +120,7 @@ char *argv[] = { "snap-update-ns", /* This tells snap-update-ns we are calling from snap-confine and locking is in place */ - /* TODO: enable this in sync with snap-update-ns changes, "--from-snap-confine", */ + "--from-snap-confine", /* This tells snap-update-ns that we want to process the per-user profile */ "--user-mounts", snap_name_copy, NULL }; @@ -145,7 +150,11 @@ /* SNAPD_DEBUG=x is replaced by sc_call_snapd_tool_with_apparmor with * either SNAPD_DEBUG=0 or SNAPD_DEBUG=1, see that function for details. */ char *envp[] = { "SNAPD_DEBUG=x", NULL }; + /* Switch the group to root so that directories and locks created by + * snap-discard-ns are owned by the root group. */ + sc_identity old = sc_set_effective_identity(sc_root_group_identity()); sc_call_snapd_tool(snap_discard_ns_fd, "snap-discard-ns", argv, envp); + (void)sc_set_effective_identity(old); } static int sc_open_snapd_tool(const char *tool_name) diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/utils.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/utils.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/utils.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/utils.c 2020-06-05 13:13:49.000000000 +0000 @@ -23,33 +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); -} - -bool error(const char *msg, ...) -{ - va_list va; - va_start(va, msg); - vfprintf(stderr, msg, va); - va_end(va); - - return false; + va_list ap; + va_start(ap, msg); + sc_panicv(msg, ap); + va_end(ap); } struct sc_bool_name { @@ -74,7 +57,7 @@ * * If the text cannot be recognized, the default value is used. **/ -static int parse_bool(const char *text, bool * value, bool default_value) +static int parse_bool(const char *text, bool *value, bool default_value) { if (value == NULL) { errno = EFAULT; @@ -157,6 +140,43 @@ die("fclose failed"); } +sc_identity sc_set_effective_identity(sc_identity identity) +{ + debug("set_effective_identity uid:%d (change: %s), gid:%d (change: %s)", + identity.uid, identity.change_uid ? "yes" : "no", + identity.gid, identity.change_gid ? "yes" : "no"); + /* We are being careful not to return a value instructing us to change GID + * or UID by accident. */ + sc_identity old = { + .change_gid = 0, + .change_uid = 0, + }; + + if (identity.change_gid) { + old.gid = getegid(); + old.change_gid = 1; + if (setegid(identity.gid) < 0) { + die("cannot set effective group to %d", identity.gid); + } + if (getegid() != identity.gid) { + die("effective group change from %d to %d has failed", + old.gid, identity.gid); + } + } + if (identity.change_uid) { + old.uid = geteuid(); + old.change_uid = 1; + if (seteuid(identity.uid) < 0) { + die("cannot set effective user to %d", identity.uid); + } + if (geteuid() != identity.uid) { + die("effective user change from %d to %d has failed", + old.uid, identity.uid); + } + } + return old; +} + int sc_nonfatal_mkpath(const char *const path, mode_t mode) { // If asked to create an empty path, return immediately. diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/utils.h snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/utils.h --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/utils.h 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/utils.h 2020-06-05 13:13:49.000000000 +0000 @@ -20,14 +20,11 @@ #include #include -__attribute__ ((noreturn)) - __attribute__ ((format(printf, 1, 2))) +__attribute__((noreturn)) + __attribute__((format(printf, 1, 2))) void die(const char *fmt, ...); -__attribute__ ((format(printf, 1, 2))) -bool error(const char *fmt, ...); - -__attribute__ ((format(printf, 1, 2))) +__attribute__((format(printf, 1, 2))) void debug(const char *fmt, ...); /** @@ -42,6 +39,50 @@ **/ bool sc_is_reexec_enabled(void); +/** + * sc_identity describes the user performing certain operation. + * + * UID and GID represent user and group accounts numbers and are controlled by + * change_uid and change_gid flags. +**/ +typedef struct sc_identity { + uid_t uid; + gid_t gid; + unsigned change_uid:1; + unsigned change_gid:1; +} sc_identity; + +/** + * Identity of the root group. + * + * The return value is suitable for passing to sc_set_effective_identity. It + * causes the effective group to change to the root group. No change is made to + * effective user identity. + **/ +static inline sc_identity sc_root_group_identity(void) +{ + sc_identity id = { + /* Explicitly set our intent of changing just the GID. + * Refactoring of this code must retain this property. */ + .change_uid = 0, + .change_gid = 1, + .gid = 0, + }; + return id; +} + +/** + * Set the effective user and group IDs to given values. + * + * Effective user and group identifiers are applied to the system. The + * current values are returned as another identity that can be restored via + * another call to sc_set_effective_identity. + * + * The fields change_uid and change_gid control if user and group ID is changed. + * The returned old identity has identical values of both use flags. +**/ +sc_identity sc_set_effective_identity(sc_identity identity); + void write_string_to_file(const char *filepath, const char *buf); /** @@ -58,6 +99,6 @@ * * The function returns -1 in case of any error. **/ -__attribute__ ((warn_unused_result)) +__attribute__((warn_unused_result)) int sc_nonfatal_mkpath(const char *const path, mode_t mode); #endif diff -Nru snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/utils-test.c snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/utils-test.c --- snapd-2.37.4ubuntu0.1/cmd/libsnap-confine-private/utils-test.c 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/libsnap-confine-private/utils-test.c 2020-06-05 13:13:49.000000000 +0000 @@ -191,7 +191,7 @@ _test_sc_nonfatal_mkpath(dirname, subdirname); } -static void __attribute__ ((constructor)) init(void) +static void __attribute__((constructor)) init(void) { g_test_add_func("/utils/parse_bool", test_parse_bool); g_test_add_func("/utils/die", test_die); diff -Nru snapd-2.37.4ubuntu0.1/cmd/Makefile.am snapd-2.45.1ubuntu0.2/cmd/Makefile.am --- snapd-2.37.4ubuntu0.1/cmd/Makefile.am 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/Makefile.am 2020-06-05 13:13:49.000000000 +0000 @@ -55,33 +55,51 @@ endif new_format = \ + libsnap-confine-private/cgroup-pids-support.c \ + libsnap-confine-private/cgroup-pids-support.h \ + libsnap-confine-private/cgroup-support.c \ + libsnap-confine-private/cgroup-support.h \ + 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 + +# NOTE: clang-format is using project-wide .clang-format file. .PHONY: fmt fmt:: $(filter $(addprefix %,$(new_format)),$(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch]))) - clang-format -style='{BasedOnStyle: Google, IndentWidth: 4, ColumnLimit: 120}' -i $^ + clang-format -i $^ fmt:: $(filter-out $(addprefix %,$(new_format)),$(foreach dir,$(subdirs),$(wildcard $(srcdir)/$(dir)/*.[ch]))) HOME=$(srcdir) indent $^ -# The hack target helps devlopers work on snap-confine on their live system by +# The hack target helps developers work on snap-confine on their live system by # installing a fresh copy of snap confine and the appropriate apparmor profile. .PHONY: hack hack: snap-confine/snap-confine-debug snap-confine/snap-confine.apparmor snap-update-ns/snap-update-ns snap-seccomp/snap-seccomp snap-discard-ns/snap-discard-ns - sudo install -D -m 6755 snap-confine/snap-confine-debug $(DESTDIR)$(libexecdir)/snap-confine - sudo install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine.real + sudo install -D -m 4755 snap-confine/snap-confine-debug $(DESTDIR)$(libexecdir)/snap-confine + if [ -d /etc/apparmor.d ]; then sudo install -m 644 snap-confine/snap-confine.apparmor $(DESTDIR)/etc/apparmor.d/$(patsubst .%,%,$(subst /,.,$(libexecdir))).snap-confine.real; fi sudo install -d -m 755 $(DESTDIR)/var/lib/snapd/apparmor/snap-confine/ - sudo apparmor_parser -r snap-confine/snap-confine.apparmor + if [ "$$(command -v apparmor_parser)" != "" ]; then sudo apparmor_parser -r snap-confine/snap-confine.apparmor; fi sudo install -m 755 snap-update-ns/snap-update-ns $(DESTDIR)$(libexecdir)/snap-update-ns sudo install -m 755 snap-discard-ns/snap-discard-ns $(DESTDIR)$(libexecdir)/snap-discard-ns sudo install -m 755 snap-seccomp/snap-seccomp $(DESTDIR)$(libexecdir)/snap-seccomp + if [ "$$(command -v restorecon)" != "" ]; then sudo restorecon -R -v $(DESTDIR)$(libexecdir)/; fi # 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 @@ -94,6 +112,10 @@ libsnap-confine-private/apparmor-support.h \ libsnap-confine-private/cgroup-freezer-support.c \ libsnap-confine-private/cgroup-freezer-support.h \ + libsnap-confine-private/cgroup-pids-support.c \ + libsnap-confine-private/cgroup-pids-support.h \ + libsnap-confine-private/cgroup-support.c \ + libsnap-confine-private/cgroup-support.h \ libsnap-confine-private/classic.c \ libsnap-confine-private/classic.h \ libsnap-confine-private/cleanup-funcs.c \ @@ -104,12 +126,15 @@ libsnap-confine-private/fault-injection.h \ libsnap-confine-private/feature.c \ libsnap-confine-private/feature.h \ + libsnap-confine-private/infofile.c \ libsnap-confine-private/locking.c \ libsnap-confine-private/locking.h \ libsnap-confine-private/mount-opt.c \ 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 \ @@ -136,9 +161,11 @@ libsnap-confine-private/error-test.c \ libsnap-confine-private/fault-injection-test.c \ libsnap-confine-private/feature-test.c \ + libsnap-confine-private/infofile-test.c \ 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 \ @@ -216,8 +243,14 @@ snap-confine/mount-support.h \ snap-confine/ns-support.c \ snap-confine/ns-support.h \ + snap-confine/seccomp-support-ext.c \ + snap-confine/seccomp-support-ext.h \ + snap-confine/seccomp-support.c \ + snap-confine/seccomp-support.h \ snap-confine/snap-confine-args.c \ snap-confine/snap-confine-args.h \ + snap-confine/snap-confine-invocation.c \ + snap-confine/snap-confine-invocation.h \ snap-confine/snap-confine.c \ snap-confine/udev-support.c \ snap-confine/udev-support.h \ @@ -255,20 +288,6 @@ snap_confine_snap_confine_CFLAGS += $(SUID_CFLAGS) snap_confine_snap_confine_LDFLAGS += $(SUID_LDFLAGS) -if SECCOMP -snap_confine_snap_confine_SOURCES += \ - snap-confine/seccomp-support-ext.c \ - snap-confine/seccomp-support-ext.h \ - snap-confine/seccomp-support.c \ - snap-confine/seccomp-support.h -snap_confine_snap_confine_CFLAGS += $(SECCOMP_CFLAGS) -if STATIC_LIBSECCOMP -snap_confine_snap_confine_STATIC += $(shell $(PKG_CONFIG) --static --libs libseccomp) -else -snap_confine_snap_confine_extra_libs += $(SECCOMP_LIBS) -endif # STATIC_LIBSECCOMP -endif # SECCOMP - if APPARMOR snap_confine_snap_confine_CFLAGS += $(APPARMOR_CFLAGS) if STATIC_LIBAPPARMOR @@ -278,6 +297,18 @@ endif # STATIC_LIBAPPARMOR endif # APPARMOR +if SELINUX +snap_confine_snap_confine_SOURCES += \ + snap-confine/selinux-support.c \ + snap-confine/selinux-support.h +snap_confine_snap_confine_CFLAGS += $(SELINUX_CFLAGS) +if STATIC_LIBSELINUX +snap_confine_snap_confine_STATIC += $(shell $(PKG_CONFIG) --static --libs libselinux) +else +snap_confine_snap_confine_extra_libs += $(SELINUX_LIBS) +endif # STATIC_LIBSELINUX +endif # SELINUX + # an extra build that has additional debugging enabled at compile time noinst_PROGRAMS += snap-confine/snap-confine-debug @@ -306,6 +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-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) @@ -326,7 +358,7 @@ endif snap-confine/snap-confine.apparmor: snap-confine/snap-confine.apparmor.in Makefile - sed -e 's,[@]LIBEXECDIR[@],$(libexecdir),g' -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@ + sed -e 's,[@]LIBEXECDIR[@],$(libexecdir),g' -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),g' <$< >$@ # Install the apparmor profile # @@ -340,18 +372,13 @@ endif install -d -m 755 $(DESTDIR)/var/lib/snapd/apparmor/snap-confine/ -# NOTE: The 'void' directory *has to* be chmod 000 +# NOTE: The 'void' directory *has to* be chmod 111 install-data-local:: - install -d -m 000 $(DESTDIR)/var/lib/snapd/void + install -d -m 111 $(DESTDIR)/var/lib/snapd/void install-exec-hook:: -if CAPS_OVER_SETUID -# Ensure that snap-confine has CAP_SYS_ADMIN capability - setcap cap_sys_admin=pe $(DESTDIR)$(libexecdir)/snap-confine -else -# Ensure that snap-confine is u+s,g+s (setuid and setgid) - chmod 6755 $(DESTDIR)$(libexecdir)/snap-confine -endif +# Ensure that snap-confine is u+s (setuid) + chmod 4755 $(DESTDIR)$(libexecdir)/snap-confine ## ## snap-mgmt @@ -367,6 +394,17 @@ snap-mgmt/snap-mgmt: snap-mgmt/snap-mgmt.sh.in Makefile snap-mgmt/$(am__dirstamp) sed -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@ +if SELINUX +## +## snap-mgmt-selinux +## + +libexec_SCRIPTS += snap-mgmt/snap-mgmt-selinux +CLEANFILES += snap-mgmt/$(am__dirstamp) snap-mgmt/snap-mgmt-selinux + +snap-mgmt/snap-mgmt-selinux: snap-mgmt/snap-mgmt-selinux.sh.in Makefile snap-mgmt/$(am__dirstamp) + sed -e 's,[@]SNAP_MOUNT_DIR[@],$(SNAP_MOUNT_DIR),' <$< >$@ +endif ## ## ubuntu-core-launcher diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_advise.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_advise.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_advise.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_advise.go 2020-06-05 13:13:49.000000000 +0000 @@ -26,6 +26,7 @@ "io" "net" "os" + "sort" "strconv" "github.com/jessevdk/go-flags" @@ -47,6 +48,9 @@ // FromApt tells advise that it got started from an apt hook // and needs to communicate over a socket FromApt bool `long:"from-apt"` + + // DumpDb dumps the whole advise database + DumpDb bool `long:"dump-db"` } var shortAdviseSnapHelp = i18n.G("Advise on available snaps") @@ -64,7 +68,9 @@ // TRANSLATORS: This should not start with a lowercase letter. "command": i18n.G("Advise on snaps that provide the given command"), // TRANSLATORS: This should not start with a lowercase letter. - "from-apt": i18n.G("Advise will talk to apt via an apt hook"), + "dump-db": i18n.G("Dump advise database for use by command-not-found."), + // TRANSLATORS: This should not start with a lowercase letter. + "from-apt": i18n.G("Run as an apt hook"), // TRANSLATORS: This should not start with a lowercase letter. "format": i18n.G("Use the given output format"), }, []argDesc{ @@ -112,7 +118,6 @@ Method string `json:"method"` Params struct { Command string `json:"command"` - SearchTerms []string `json:"search-terms"` UnknownPackages []string `json:"unknown-packages"` } } @@ -214,11 +219,55 @@ return nil } +type Snap struct { + Snap string + Version string + Command string +} + +func dumpDbHook() error { + commands, err := advisor.DumpCommands() + if err != nil { + return err + } + + commands_processed := make([]string, 0) + var b []Snap + + var sortedCmds []string + for cmd := range commands { + sortedCmds = append(sortedCmds, cmd) + } + sort.Strings(sortedCmds) + + for _, key := range sortedCmds { + value := commands[key] + err := json.Unmarshal([]byte(value), &b) + if err != nil { + return err + } + for i := range b { + var s = fmt.Sprintf("%s %s %s\n", key, b[i].Snap, b[i].Version) + commands_processed = append(commands_processed, s) + } + } + + for _, value := range commands_processed { + fmt.Fprint(Stdout, value) + } + + return nil +} + func (x *cmdAdviseSnap) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } + if x.DumpDb { + return dumpDbHook() + } + if x.FromApt { return adviseViaAptHook() } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_advise_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_advise_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_advise_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_advise_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -33,6 +33,7 @@ "github.com/snapcore/snapd/advisor" snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/dirs" ) type sillyFinder struct{} @@ -98,6 +99,23 @@ c.Assert(s.Stderr(), Equals, "") } +func (s *SnapSuite) TestAdviseCommandDumpDb(c *C) { + dirs.SetRootDir(c.MkDir()) + c.Assert(os.MkdirAll(dirs.SnapCacheDir, 0755), IsNil) + defer dirs.SetRootDir("") + + db, err := advisor.Create() + c.Assert(err, IsNil) + c.Assert(db.AddSnap("foo", "1.0", "foo summary", []string{"foo", "bar"}), IsNil) + c.Assert(db.Commit(), IsNil) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"advise-snap", "--dump-db"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stdout(), Matches, `bar foo 1.0\nfoo foo 1.0\n`) +} + func (s *SnapSuite) TestAdviseCommandMisspellText(c *C) { restore := advisor.ReplaceCommandsFinder(mkSillyFinder) defer restore() diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_alias_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_alias_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_alias_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_alias_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/snap/cmd_auto_import.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_auto_import.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_auto_import.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_auto_import.go 2020-06-05 13:13:49.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2016 Canonical Ltd + * Copyright (C) 2014-2020 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -28,17 +28,20 @@ "os" "os/exec" "path/filepath" + "sort" "strings" "syscall" "github.com/jessevdk/go-flags" + "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" + "github.com/snapcore/snapd/snapdenv" ) const autoImportsName = "auto-import.assert" @@ -56,6 +59,8 @@ } defer f.Close() + isTesting := snapdenv.Testing() + scanner := bufio.NewScanner(f) for scanner.Scan() { l := strings.Fields(scanner.Text()) @@ -85,7 +90,7 @@ continue } // skip all ram disks (unless in tests) - if !osutil.GetenvBool("SNAPPY_TESTING") && strings.HasPrefix(mountSrc, "/dev/ram") { + if !isTesting && strings.HasPrefix(mountSrc, "/dev/ram") { continue } @@ -183,8 +188,10 @@ return added, nil } +var ioutilTempDir = ioutil.TempDir + func tryMount(deviceName string) (string, error) { - tmpMountTarget, err := ioutil.TempDir("", "snapd-auto-import-mount-") + tmpMountTarget, err := ioutilTempDir("", "snapd-auto-import-mount-") if err != nil { err = fmt.Errorf("cannot create temporary mount point: %v", err) logger.Noticef("error: %v", err) @@ -206,8 +213,10 @@ return tmpMountTarget, nil } +var syscallUnmount = syscall.Unmount + func doUmount(mp string) error { - if err := syscall.Unmount(mp, 0); err != nil { + if err := syscallUnmount(mp, 0); err != nil { return err } return os.Remove(mp) @@ -259,6 +268,55 @@ return cmd.Execute(nil) } +func removableBlockDevices() (removableDevices []string) { + // eg. /sys/block/sda/removable + removable, err := filepath.Glob(filepath.Join(dirs.GlobalRootDir, "/sys/block/*/removable")) + if err != nil { + return nil + } + for _, removableAttr := range removable { + val, err := ioutil.ReadFile(removableAttr) + if err != nil || string(val) != "1\n" { + // non removable + continue + } + // let's see if it has partitions + dev := filepath.Base(filepath.Dir(removableAttr)) + + pattern := fmt.Sprintf(filepath.Join(dirs.GlobalRootDir, "/sys/block/%s/%s*/partition"), dev, dev) + // eg. /sys/block/sda/sda1/partition + partitionAttrs, _ := filepath.Glob(pattern) + + if len(partitionAttrs) == 0 { + // not partitioned? try to use the main device + removableDevices = append(removableDevices, fmt.Sprintf("/dev/%s", dev)) + continue + } + + for _, partAttr := range partitionAttrs { + val, err := ioutil.ReadFile(partAttr) + if err != nil || string(val) != "1\n" { + // non partition? + continue + } + pdev := filepath.Base(filepath.Dir(partAttr)) + removableDevices = append(removableDevices, fmt.Sprintf("/dev/%s", pdev)) + // hasPartitions = true + } + } + sort.Strings(removableDevices) + return removableDevices +} + +// inInstallmode returns true if it's UC20 system in install mode +func inInstallMode() bool { + mode, _, err := boot.ModeAndRecoverySystemFromKernelCommandLine() + if err != nil { + return false + } + return mode == "install" +} + func (x *cmdAutoImport) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs @@ -268,8 +326,19 @@ fmt.Fprintf(Stderr, "auto-import is disabled on classic\n") return nil } + // TODO:UC20: workaround for LP: #1860231 + if inInstallMode() { + fmt.Fprintf(Stderr, "auto-import is disabled in install-mode\n") + return nil + } + + devices := x.Mount + if len(devices) == 0 { + // coldplug scenario, try all removable devices + devices = removableBlockDevices() + } - for _, path := range x.Mount { + for _, path := range devices { // udev adds new /dev/loopX devices on the fly when a // loop mount happens and there is no loop device left. // diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_auto_import_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_auto_import_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_auto_import_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_auto_import_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -28,11 +28,14 @@ . "gopkg.in/check.v1" + "github.com/snapcore/snapd/boot" 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/release" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" ) func makeMockMountInfo(c *C, content string) string { @@ -62,10 +65,10 @@ n++ case 1: c.Check(r.Method, Equals, "POST") - c.Check(r.URL.Path, Equals, "/v2/create-user") + c.Check(r.URL.Path, Equals, "/v2/users") postData, err := ioutil.ReadAll(r.Body) c.Assert(err, IsNil) - c.Check(string(postData), Equals, `{"sudoer":true,"known":true}`) + c.Check(string(postData), Equals, `{"action":"create","sudoer":true,"known":true}`) fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`) n++ @@ -239,10 +242,10 @@ n++ case 1: c.Check(r.Method, Equals, "POST") - c.Check(r.URL.Path, Equals, "/v2/create-user") + c.Check(r.URL.Path, Equals, "/v2/users") postData, err := ioutil.ReadAll(r.Body) c.Assert(err, IsNil) - c.Check(string(postData), Equals, `{"sudoer":true,"known":true}`) + c.Check(string(postData), Equals, `{"action":"create","sudoer":true,"known":true}`) fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "foo"}]}`) n++ @@ -300,3 +303,163 @@ _, err = snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) c.Assert(err, ErrorMatches, "cannot queue .*, file size too big: 656384") } + +func (s *SnapSuite) TestAutoImportUnhappyInInstallMode(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + _, restoreLogger := logger.MockLogger() + defer restoreLogger() + + mockProcCmdlinePath := filepath.Join(c.MkDir(), "cmdline") + err := ioutil.WriteFile(mockProcCmdlinePath, []byte("foo=bar snapd_recovery_mode=install snapd_recovery_system=20191118"), 0644) + c.Assert(err, IsNil) + + restore = boot.MockProcCmdline(mockProcCmdlinePath) + defer restore() + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "auto-import is disabled in install-mode\n") +} + +var mountStatic = []string{"mount", "-t", "ext4,vfat", "-o", "ro", "--make-private"} + +func (s *SnapSuite) TestAutoImportFromRemovable(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + _, restoreLogger := logger.MockLogger() + defer restoreLogger() + + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + + var umounts []string + restore = snap.MockSyscallUmount(func(p string, _ int) error { + umounts = append(umounts, p) + return nil + }) + defer restore() + + var tmpdirIdx int + restore = snap.MockIoutilTempDir(func(where string, p string) (string, error) { + c.Check(where, Equals, "") + tmpdirIdx++ + return filepath.Join(rootdir, fmt.Sprintf("/tmp/%s%d", p, tmpdirIdx)), nil + }) + defer restore() + + mountCmd := testutil.MockCommand(c, "mount", "") + defer mountCmd.Restore() + + snaptest.PopulateDir(rootdir, [][]string{ + // removable without partitions + {"sys/block/sdremovable/removable", "1\n"}, + // fixed disk + {"sys/block/sdfixed/removable", "0\n"}, + // removable with partitions + {"sys/block/sdpart/removable", "1\n"}, + {"sys/block/sdpart/sdpart1/partition", "1\n"}, + {"sys/block/sdpart/sdpart2/partition", "0\n"}, + {"sys/block/sdpart/sdpart3/partition", "1\n"}, + // removable but subdevices are not partitions? + {"sys/block/sdother/removable", "1\n"}, + {"sys/block/sdother/sdother1/partition", "0\n"}, + }) + + // do not mock mountinfo contents, we just want to observe whether we + // try to mount and umount the right stuff + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") + c.Check(mountCmd.Calls(), DeepEquals, [][]string{ + append(mountStatic, "/dev/sdpart1", filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-1")), + append(mountStatic, "/dev/sdpart3", filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-2")), + append(mountStatic, "/dev/sdremovable", filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-3")), + }) + c.Check(umounts, DeepEquals, []string{ + filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-3"), + filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-2"), + filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-1"), + }) +} + +func (s *SnapSuite) TestAutoImportNoRemovable(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + + var umounts []string + restore = snap.MockSyscallUmount(func(p string, _ int) error { + return fmt.Errorf("unexpected call") + }) + defer restore() + + mountCmd := testutil.MockCommand(c, "mount", "exit 1") + defer mountCmd.Restore() + + snaptest.PopulateDir(rootdir, [][]string{ + // fixed disk + {"sys/block/sdfixed/removable", "0\n"}, + // removable but subdevices are not partitions? + {"sys/block/sdother/removable", "1\n"}, + {"sys/block/sdother/sdother1/partition", "0\n"}, + }) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") + c.Check(mountCmd.Calls(), HasLen, 0) + c.Check(umounts, HasLen, 0) +} + +func (s *SnapSuite) TestAutoImportFromMount(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + _, restoreLogger := logger.MockLogger() + defer restoreLogger() + + mountCmd := testutil.MockCommand(c, "mount", "") + + rootdir := c.MkDir() + dirs.SetRootDir(rootdir) + + var umounts []string + restore = snap.MockSyscallUmount(func(p string, _ int) error { + c.Assert(umounts, HasLen, 0) + umounts = append(umounts, p) + return nil + }) + defer restore() + + var tmpdircalls int + restore = snap.MockIoutilTempDir(func(where string, p string) (string, error) { + c.Check(where, Equals, "") + c.Assert(tmpdircalls, Equals, 0) + tmpdircalls++ + return filepath.Join(rootdir, fmt.Sprintf("/tmp/%s1", p)), nil + }) + defer restore() + + // do not mock mountinfo contents, we just want to observe whether we + // try to mount and umount the right stuff + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"auto-import", "--mount", "/dev/foobar"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, "") + c.Check(s.Stderr(), Equals, "") + c.Check(mountCmd.Calls(), DeepEquals, [][]string{ + append(mountStatic, "/dev/foobar", filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-1")), + }) + c.Check(umounts, DeepEquals, []string{ + filepath.Join(rootdir, "/tmp/snapd-auto-import-mount-1"), + }) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_blame_generated.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_blame_generated.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_blame_generated.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_blame_generated.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,7 +0,0 @@ -package main - -// generated by mkauthors.sh; do not edit - -func init() { - authors = []string{"Mark Shuttleworth", "Gustavo Niemeyer", "Sergio Schvezov", "Simon Fels", "Kyle Fazzari", "Leo Arias", "Sergio Cazzolato", "Gustavo Niemeyer", "Federico Gimenez", "Maciej Borzecki", "Jamie Strandboge", "Pawel Stolowski", "John R. Lenton", "Samuele Pedroni", "Zygmunt Krynicki", "Michael Vogt"} -} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_blame.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_blame.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_blame.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_blame.go 1970-01-01 00:00:00.000000000 +0000 @@ -1,55 +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 main - -//go:generate mkauthors.sh - -import ( - "fmt" - "math/rand" - - "github.com/jessevdk/go-flags" -) - -type cmdBlame struct{} - -var authors []string - -func init() { - cmd := addCommand("blame", - "", - "", - func() flags.Commander { - return &cmdBlame{} - }, nil, nil) - cmd.hidden = true -} - -func (x *cmdBlame) Execute(args []string) error { - if len(args) > 0 { - return ErrExtraArgs - } - if len(authors) == 0 { - return nil - } - - fmt.Fprintf(Stdout, "It's all %s's fault.\n", authors[rand.Intn(len(authors))]) - return nil -} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_booted.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_booted.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_booted.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_booted.go 2020-06-05 13:13:49.000000000 +0000 @@ -29,7 +29,7 @@ func init() { cmd := addCommand("booted", - "Internal", + "Deprecated (hidden)", "The booted command is only retained for backwards compatibility.", func() flags.Commander { return &cmdBooted{} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connections.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connections.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connections.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connections.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,214 @@ +// -*- 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 main + +import ( + "fmt" + "sort" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +type cmdConnections struct { + clientMixin + All bool `long:"all"` + Positionals struct { + Snap installedSnapName + } `positional-args:"true"` +} + +var shortConnectionsHelp = i18n.G("List interface connections") +var longConnectionsHelp = i18n.G(` +The connections command lists connections between plugs and slots +in the system. + +Unless is provided, the listing is for connected plugs and +slots for all snaps in the system. In this mode, pass --all to also +list unconnected plugs and slots. + +$ snap connections + +Lists connected and unconnected plugs and slots for the specified +snap. +`) + +func init() { + addCommand("connections", shortConnectionsHelp, longConnectionsHelp, func() flags.Commander { + return &cmdConnections{} + }, map[string]string{ + "all": i18n.G("Show connected and unconnected plugs and slots"), + }, []argDesc{{ + // TRANSLATORS: This needs to be wrapped in <>s. + name: "", + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Constrain listing to a specific snap"), + }}) +} + +func isSystemSnap(snap string) bool { + return snap == "core" || snap == "snapd" || snap == "system" +} + +func endpoint(snap, name string) string { + if isSystemSnap(snap) { + return ":" + name + } + return snap + ":" + name +} + +type connection struct { + slot string + plug string + interfaceName string + interfaceDeterminant string + manual bool + gadget bool +} + +func (cn connection) String() string { + opts := []string{} + if cn.manual { + opts = append(opts, "manual") + } + if cn.gadget { + opts = append(opts, "gadget") + } + if len(opts) == 0 { + return "-" + } + return strings.Join(opts, ",") +} + +type byConnectionData []connection + +func (b byConnectionData) Len() int { return len(b) } +func (b byConnectionData) Swap(i, j int) { b[i], b[j] = b[j], b[i] } +func (b byConnectionData) Less(i, j int) bool { + iCon, jCon := b[i], b[j] + if iCon.interfaceName != jCon.interfaceName { + return iCon.interfaceName < jCon.interfaceName + } + if iCon.plug != jCon.plug { + return iCon.plug < jCon.plug + } + return iCon.slot < jCon.slot +} + +func interfaceDeterminant(conn *client.Connection) string { + var value string + + switch conn.Interface { + case "content": + value, _ = conn.PlugAttrs["content"].(string) + if value == "" { + value, _ = conn.SlotAttrs["content"].(string) + } + } + if value == "" { + return "" + } + return fmt.Sprintf("[%v]", value) +} + +func (x *cmdConnections) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + opts := client.ConnectionOptions{ + All: x.All, + } + wanted := string(x.Positionals.Snap) + if wanted != "" { + if x.All { + // passing a snap name already implies --all, error out + // when it was passed explicitly + return fmt.Errorf(i18n.G("cannot use --all with snap name")) + } + // when asking for a single snap, include its disconnected plugs + // and slots + opts.Snap = wanted + opts.All = true + // print all slots + x.All = true + } + + connections, err := x.client.Connections(&opts) + if err != nil { + return err + } + if len(connections.Plugs) == 0 && len(connections.Slots) == 0 { + return nil + } + + annotatedConns := make([]connection, 0, len(connections.Established)+len(connections.Undesired)) + for _, conn := range connections.Established { + annotatedConns = append(annotatedConns, connection{ + plug: endpoint(conn.Plug.Snap, conn.Plug.Name), + slot: endpoint(conn.Slot.Snap, conn.Slot.Name), + manual: conn.Manual, + gadget: conn.Gadget, + interfaceName: conn.Interface, + interfaceDeterminant: interfaceDeterminant(&conn), + }) + } + + w := tabWriter() + fmt.Fprintln(w, i18n.G("Interface\tPlug\tSlot\tNotes")) + + for _, plug := range connections.Plugs { + if len(plug.Connections) == 0 && x.All { + annotatedConns = append(annotatedConns, connection{ + plug: endpoint(plug.Snap, plug.Name), + slot: "-", + interfaceName: plug.Interface, + }) + } + } + for _, slot := range connections.Slots { + if !isSystemSnap(wanted) && isSystemSnap(slot.Snap) { + // displaying unconnected system snap slots is boring, + // unless explicitly asked to show them + continue + } + if len(slot.Connections) == 0 && x.All { + annotatedConns = append(annotatedConns, connection{ + plug: "-", + slot: endpoint(slot.Snap, slot.Name), + interfaceName: slot.Interface, + }) + } + } + + sort.Sort(byConnectionData(annotatedConns)) + + for _, note := range annotatedConns { + fmt.Fprintf(w, "%s%s\t%s\t%s\t%s\n", note.interfaceName, note.interfaceDeterminant, note.plug, note.slot, note) + } + + if len(annotatedConns) > 0 { + w.Flush() + } + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connections_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connections_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connections_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connections_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,853 @@ +// -*- 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 main_test + +import ( + "fmt" + "io/ioutil" + "net/http" + "net/url" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + . "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestConnectionsNoneConnected(c *C) { + result := client.Connections{} + query := url.Values{} + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, query) + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": result, + }) + }) + _, err := Parser(Client()).ParseArgs([]string{"connections"}) + c.Check(err, IsNil) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") + + s.ResetStdStreams() + + query = url.Values{ + "select": []string{"all"}, + } + _, err = Parser(Client()).ParseArgs([]string{"connections", "--all"}) + c.Check(err, IsNil) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsNotInstalled(c *C) { + query := url.Values{ + "snap": []string{"foo"}, + "select": []string{"all"}, + } + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, query) + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + fmt.Fprintln(w, `{"type": "error", "result": {"message": "not found", "value": "foo", "kind": "snap-not-found"}, "status-code": 404}`) + }) + _, err := Parser(Client()).ParseArgs([]string{"connections", "foo"}) + c.Check(err, ErrorMatches, `not found`) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsNoneConnectedPlugs(c *C) { + query := url.Values{ + "select": []string{"all"}, + } + result := client.Connections{ + Plugs: []client.Plug{ + { + Snap: "keyboard-lights", + Name: "capslock-led", + Interface: "leds", + }, + }, + } + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, query) + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": result, + }) + }) + + rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Interface Plug Slot Notes\n" + + "leds keyboard-lights:capslock-led - -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") + + s.ResetStdStreams() + + query = url.Values{ + "select": []string{"all"}, + "snap": []string{"keyboard-lights"}, + } + + rest, err = Parser(Client()).ParseArgs([]string{"connections", "keyboard-lights"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout = "" + + "Interface Plug Slot Notes\n" + + "leds keyboard-lights:capslock-led - -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsNoneConnectedSlots(c *C) { + result := client.Connections{} + query := url.Values{} + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, query) + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": result, + }) + }) + _, err := Parser(Client()).ParseArgs([]string{"connections"}) + c.Check(err, IsNil) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") + + s.ResetStdStreams() + + query = url.Values{ + "select": []string{"all"}, + } + result = client.Connections{ + Slots: []client.Slot{ + { + Snap: "leds-provider", + Name: "capslock-led", + Interface: "leds", + }, + }, + } + rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Interface Plug Slot Notes\n" + + "leds - leds-provider:capslock-led -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsSomeConnected(c *C) { + result := client.Connections{ + Established: []client.Connection{ + { + Plug: client.PlugRef{Snap: "keyboard-lights", Name: "capslock"}, + Slot: client.SlotRef{Snap: "leds-provider", Name: "capslock-led"}, + Interface: "leds", + Gadget: true, + }, { + Plug: client.PlugRef{Snap: "keyboard-lights", Name: "numlock"}, + Slot: client.SlotRef{Snap: "core", Name: "numlock-led"}, + Interface: "leds", + Manual: true, + }, { + Plug: client.PlugRef{Snap: "keyboard-lights", Name: "scrollock"}, + Slot: client.SlotRef{Snap: "core", Name: "scrollock-led"}, + Interface: "leds", + }, + }, + Plugs: []client.Plug{ + { + Snap: "keyboard-lights", + Name: "capslock", + Interface: "leds", + Connections: []client.SlotRef{{ + Snap: "leds-provider", + Name: "capslock-led", + }}, + }, { + Snap: "keyboard-lights", + Name: "numlock", + Interface: "leds", + Connections: []client.SlotRef{{ + Snap: "core", + Name: "numlock-led", + }}, + }, { + Snap: "keyboard-lights", + Name: "scrollock", + Interface: "leds", + Connections: []client.SlotRef{{ + Snap: "core", + Name: "scrollock-led", + }}, + }, + }, + Slots: []client.Slot{ + { + Snap: "core", + Name: "numlock-led", + Interface: "leds", + Connections: []client.PlugRef{{ + Snap: "keyuboard-lights", + Name: "numlock", + }}, + }, { + Snap: "core", + Name: "scrollock-led", + Interface: "leds", + Connections: []client.PlugRef{{ + Snap: "keyuboard-lights", + Name: "scrollock", + }}, + }, { + Snap: "leds-provider", + Name: "capslock-led", + Interface: "leds", + Connections: []client.PlugRef{{ + Snap: "keyuboard-lights", + Name: "capslock", + }}, + }, + }, + } + query := url.Values{} + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, query) + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": result, + }) + }) + rest, err := Parser(Client()).ParseArgs([]string{"connections"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Interface Plug Slot Notes\n" + + "leds keyboard-lights:capslock leds-provider:capslock-led gadget\n" + + "leds keyboard-lights:numlock :numlock-led manual\n" + + "leds keyboard-lights:scrollock :scrollock-led -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsSomeDisconnected(c *C) { + result := client.Connections{ + Established: []client.Connection{ + { + Plug: client.PlugRef{Snap: "keyboard-lights", Name: "scrollock"}, + Slot: client.SlotRef{Snap: "core", Name: "scrollock-led"}, + Interface: "leds", + }, { + Plug: client.PlugRef{Snap: "keyboard-lights", Name: "capslock"}, + Slot: client.SlotRef{Snap: "leds-provider", Name: "capslock-led"}, + Interface: "leds", + }, + }, + Undesired: []client.Connection{ + { + Plug: client.PlugRef{Snap: "keyboard-lights", Name: "numlock"}, + Slot: client.SlotRef{Snap: "core", Name: "numlock-led"}, + Interface: "leds", + Manual: true, + }, + }, + Plugs: []client.Plug{ + { + Snap: "keyboard-lights", + Name: "capslock", + Interface: "leds", + Connections: []client.SlotRef{{ + Snap: "leds-provider", + Name: "capslock-led", + }}, + }, { + Snap: "keyboard-lights", + Name: "numlock", + Interface: "leds", + }, { + Snap: "keyboard-lights", + Name: "scrollock", + Interface: "leds", + Connections: []client.SlotRef{{ + Snap: "core", + Name: "scrollock-led", + }}, + }, + }, + Slots: []client.Slot{ + { + Snap: "core", + Name: "capslock-led", + Interface: "leds", + }, { + Snap: "core", + Name: "numlock-led", + Interface: "leds", + }, { + Snap: "core", + Name: "scrollock-led", + Interface: "leds", + Connections: []client.PlugRef{{ + Snap: "keyuboard-lights", + Name: "scrollock", + }}, + }, { + Snap: "leds-provider", + Name: "capslock-led", + Interface: "leds", + Connections: []client.PlugRef{{ + Snap: "keyuboard-lights", + Name: "capslock", + }}, + }, { + Snap: "leds-provider", + Name: "numlock-led", + Interface: "leds", + }, + }, + } + query := url.Values{ + "select": []string{"all"}, + } + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, query) + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": result, + }) + }) + + rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Interface Plug Slot Notes\n" + + "leds - leds-provider:numlock-led -\n" + + "leds keyboard-lights:capslock leds-provider:capslock-led -\n" + + "leds keyboard-lights:numlock - -\n" + + "leds keyboard-lights:scrollock :scrollock-led -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsOnlyDisconnected(c *C) { + result := client.Connections{ + Undesired: []client.Connection{ + { + Plug: client.PlugRef{Snap: "keyboard-lights", Name: "numlock"}, + Slot: client.SlotRef{Snap: "leds-provider", Name: "numlock-led"}, + Interface: "leds", + Manual: true, + }, + }, + Slots: []client.Slot{ + { + Snap: "leds-provider", + Name: "capslock-led", + Interface: "leds", + }, { + Snap: "leds-provider", + Name: "numlock-led", + Interface: "leds", + }, + }, + } + query := url.Values{ + "snap": []string{"leds-provider"}, + "select": []string{"all"}, + } + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, query) + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": result, + }) + }) + + rest, err := Parser(Client()).ParseArgs([]string{"connections", "leds-provider"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Interface Plug Slot Notes\n" + + "leds - leds-provider:capslock-led -\n" + + "leds - leds-provider:numlock-led -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsFiltering(c *C) { + result := client.Connections{} + query := url.Values{ + "select": []string{"all"}, + } + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, query) + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": result, + }) + }) + + query = url.Values{ + "select": []string{"all"}, + "snap": []string{"mouse-buttons"}, + } + rest, err := Parser(Client()).ParseArgs([]string{"connections", "mouse-buttons"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + + rest, err = Parser(Client()).ParseArgs([]string{"connections", "mouse-buttons", "--all"}) + c.Assert(err, ErrorMatches, "cannot use --all with snap name") + c.Assert(rest, DeepEquals, []string{"--all"}) +} + +func (s *SnapSuite) TestConnectionsSorting(c *C) { + result := client.Connections{ + Established: []client.Connection{ + { + Plug: client.PlugRef{Snap: "foo", Name: "plug"}, + Slot: client.SlotRef{Snap: "a-content-provider", Name: "data"}, + Interface: "content", + }, { + Plug: client.PlugRef{Snap: "foo", Name: "plug"}, + Slot: client.SlotRef{Snap: "b-content-provider", Name: "data"}, + Interface: "content", + }, { + Plug: client.PlugRef{Snap: "foo", Name: "desktop-plug"}, + Slot: client.SlotRef{Snap: "core", Name: "desktop"}, + Interface: "desktop", + }, { + Plug: client.PlugRef{Snap: "foo", Name: "x11-plug"}, + Slot: client.SlotRef{Snap: "core", Name: "x11"}, + Interface: "x11", + }, { + Plug: client.PlugRef{Snap: "foo", Name: "a-x11-plug"}, + Slot: client.SlotRef{Snap: "core", Name: "x11"}, + Interface: "x11", + }, { + Plug: client.PlugRef{Snap: "a-foo", Name: "plug"}, + Slot: client.SlotRef{Snap: "a-content-provider", Name: "data"}, + Interface: "content", + }, { + Plug: client.PlugRef{Snap: "keyboard-app", Name: "x11"}, + Slot: client.SlotRef{Snap: "core", Name: "x11"}, + Interface: "x11", + Manual: true, + }, + }, + Undesired: []client.Connection{ + { + Plug: client.PlugRef{Snap: "foo", Name: "plug"}, + Slot: client.SlotRef{Snap: "c-content-provider", Name: "data"}, + Interface: "content", + Manual: true, + }, + }, + Plugs: []client.Plug{ + { + Snap: "foo", + Name: "plug", + Interface: "content", + Connections: []client.SlotRef{{ + Snap: "a-content-provider", + Name: "data", + }, { + Snap: "b-content-provider", + Name: "data", + }}, + }, { + Snap: "foo", + Name: "desktop-plug", + Interface: "desktop", + Connections: []client.SlotRef{{ + Snap: "core", + Name: "desktop", + }}, + }, { + Snap: "foo", + Name: "x11-plug", + Interface: "x11", + Connections: []client.SlotRef{{ + Snap: "core", + Name: "x11", + }}, + }, { + Snap: "foo", + Name: "a-x11-plug", + Interface: "x11", + Connections: []client.SlotRef{{ + Snap: "core", + Name: "x11", + }}, + }, { + Snap: "a-foo", + Name: "plug", + Interface: "content", + Connections: []client.SlotRef{{ + Snap: "a-content-provider", + Name: "data", + }}, + }, { + Snap: "keyboard-app", + Name: "x11", + Interface: "x11", + Connections: []client.SlotRef{{ + Snap: "core", + Name: "x11", + }}, + }, { + Snap: "keyboard-lights", + Name: "numlock", + Interface: "leds", + }, + }, + Slots: []client.Slot{ + { + Snap: "c-content-provider", + Name: "data", + Interface: "content", + }, { + Snap: "a-content-provider", + Name: "data", + Interface: "content", + Connections: []client.PlugRef{{ + Snap: "foo", + Name: "plug", + }, { + Snap: "a-foo", + Name: "plug", + }}, + }, { + Snap: "b-content-provider", + Name: "data", + Interface: "content", + Connections: []client.PlugRef{{ + Snap: "foo", + Name: "plug", + }}, + }, { + Snap: "core", + Name: "x11", + Interface: "x11", + Connections: []client.PlugRef{{ + Snap: "foo", + Name: "a-x11-plug", + }, { + Snap: "foo", + Name: "x11-plug", + }, { + Snap: "keyboard-app", + Name: "x11", + }}, + }, { + Snap: "leds-provider", + Name: "numlock-led", + Interface: "leds", + }, + }, + } + query := url.Values{ + "select": []string{"all"}, + } + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, query) + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": result, + }) + }) + + rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Interface Plug Slot Notes\n" + + "content - c-content-provider:data -\n" + + "content a-foo:plug a-content-provider:data -\n" + + "content foo:plug a-content-provider:data -\n" + + "content foo:plug b-content-provider:data -\n" + + "desktop foo:desktop-plug :desktop -\n" + + "leds - leds-provider:numlock-led -\n" + + "leds keyboard-lights:numlock - -\n" + + "x11 foo:a-x11-plug :x11 -\n" + + "x11 foo:x11-plug :x11 -\n" + + "x11 keyboard-app:x11 :x11 manual\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestConnectionsDefiningAttribute(c *C) { + result := client.Connections{ + Established: []client.Connection{ + { + Plug: client.PlugRef{Snap: "foo", Name: "a-plug"}, + Slot: client.SlotRef{Snap: "a-content-provider", Name: "data"}, + Interface: "content", + PlugAttrs: map[string]interface{}{ + "content": "plug-some-data", + "target": "$SNAP/foo", + }, + SlotAttrs: map[string]interface{}{ + "content": "slot-some-data", + "source": map[string]interface{}{ + "read": []string{"$SNAP/bar"}, + }, + }, + }, { + Plug: client.PlugRef{Snap: "foo", Name: "b-plug"}, + Slot: client.SlotRef{Snap: "b-content-provider", Name: "data"}, + Interface: "content", + PlugAttrs: map[string]interface{}{ + // no content attribute for plug, falls back to slot + "target": "$SNAP/foo", + }, + SlotAttrs: map[string]interface{}{ + "content": "slot-some-data", + "source": map[string]interface{}{ + "read": []string{"$SNAP/bar"}, + }, + }, + }, { + Plug: client.PlugRef{Snap: "foo", Name: "c-plug"}, + Slot: client.SlotRef{Snap: "c-content-provider", Name: "data"}, + Interface: "content", + PlugAttrs: map[string]interface{}{ + // no content attribute for plug + "target": "$SNAP/foo", + }, + SlotAttrs: map[string]interface{}{ + // no content attribute for slot either + "source": map[string]interface{}{ + "read": []string{"$SNAP/bar"}, + }, + }, + }, { + Plug: client.PlugRef{Snap: "foo", Name: "d-plug"}, + Slot: client.SlotRef{Snap: "d-content-provider", Name: "data"}, + Interface: "content", + // no attributes at all + }, { + Plug: client.PlugRef{Snap: "foo", Name: "desktop-plug"}, + Slot: client.SlotRef{Snap: "core", Name: "desktop"}, + // desktop interface does not have any defining attributes + Interface: "desktop", + PlugAttrs: map[string]interface{}{ + "this-is-ignored": "foo", + }, + SlotAttrs: map[string]interface{}{ + "this-is-ignored-too": "foo", + }, + }, + }, + Plugs: []client.Plug{ + { + Snap: "foo", + Name: "a-plug", + Interface: "content", + Connections: []client.SlotRef{{ + Snap: "a-content-provider", + Name: "data", + }}, + Attrs: map[string]interface{}{ + "content": "plug-some-data", + "target": "$SNAP/foo", + }, + }, { + Snap: "foo", + Name: "b-plug", + Interface: "content", + Connections: []client.SlotRef{{ + Snap: "b-content-provider", + Name: "data", + }}, + Attrs: map[string]interface{}{ + // no content attribute for plug, falls back to slot + "target": "$SNAP/foo", + }, + }, { + Snap: "foo", + Name: "c-plug", + Interface: "content", + Connections: []client.SlotRef{{ + Snap: "c-content-provider", + Name: "data", + }}, + Attrs: map[string]interface{}{ + // no content attribute for plug + "target": "$SNAP/foo", + }, + }, { + Snap: "foo", + Name: "d-plug", + Interface: "content", + Connections: []client.SlotRef{{ + Snap: "d-content-provider", + Name: "data", + }}, + }, { + Snap: "foo", + Name: "desktop-plug", + Interface: "desktop", + Connections: []client.SlotRef{{ + Snap: "core", + Name: "desktop", + }}, + }, + }, + Slots: []client.Slot{ + { + Snap: "a-content-provider", + Name: "data", + Interface: "content", + Connections: []client.PlugRef{{ + Snap: "foo", + Name: "a-plug", + }}, + Attrs: map[string]interface{}{ + "content": "slot-some-data", + "source": map[string]interface{}{ + "read": []string{"$SNAP/bar"}, + }, + }, + }, { + Snap: "b-content-provider", + Name: "data", + Interface: "content", + Connections: []client.PlugRef{{ + Snap: "foo", + Name: "a-plug", + }}, + Attrs: map[string]interface{}{ + "content": "slot-some-data", + "source": map[string]interface{}{ + "read": []string{"$SNAP/bar"}, + }, + }, + }, { + Snap: "c-content-provider", + Name: "data", + Interface: "content", + Connections: []client.PlugRef{{ + Snap: "foo", + Name: "a-plug", + }}, + Attrs: map[string]interface{}{ + "source": map[string]interface{}{ + "read": []string{"$SNAP/bar"}, + }, + }, + }, { + Snap: "a-content-provider", + Name: "data", + Interface: "content", + Connections: []client.PlugRef{{ + Snap: "foo", + Name: "a-plug", + }}, + }, { + Snap: "core", + Name: "desktop", + Interface: "desktop", + Connections: []client.PlugRef{{ + Snap: "foo", + Name: "desktop-plug", + }}, + }, + }, + } + query := url.Values{ + "select": []string{"all"}, + } + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, query) + body, err := ioutil.ReadAll(r.Body) + c.Check(err, IsNil) + c.Check(body, DeepEquals, []byte{}) + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": result, + }) + }) + + rest, err := Parser(Client()).ParseArgs([]string{"connections", "--all"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + expectedStdout := "" + + "Interface Plug Slot Notes\n" + + "content[plug-some-data] foo:a-plug a-content-provider:data -\n" + + "content[slot-some-data] foo:b-plug b-content-provider:data -\n" + + "content foo:c-plug c-content-provider:data -\n" + + "content foo:d-plug d-content-provider:data -\n" + + "desktop foo:desktop-plug :desktop -\n" + c.Assert(s.Stdout(), Equals, expectedStdout) + c.Assert(s.Stderr(), Equals, "") +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connectivity_check.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connectivity_check.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connectivity_check.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connectivity_check.go 2020-06-05 13:13:49.000000000 +0000 @@ -44,10 +44,9 @@ } var status struct { - Connectivity bool - Unreachable []string + Unreachable []string } - if err := x.client.Debug("connectivity", nil, &status); err != nil { + if err := x.client.DebugGet("connectivity", &status, nil); err != nil { return err } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connectivity_check_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connectivity_check_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connectivity_check_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connectivity_check_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -34,12 +34,12 @@ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: - c.Check(r.Method, check.Equals, "POST") + c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/debug") - c.Check(r.URL.RawQuery, check.Equals, "") + c.Check(r.URL.RawQuery, check.Equals, "aspect=connectivity") data, err := ioutil.ReadAll(r.Body) c.Check(err, check.IsNil) - c.Check(data, check.DeepEquals, []byte(`{"action":"connectivity"}`)) + c.Check(data, check.HasLen, 0) fmt.Fprintln(w, `{"type": "sync", "result": {}}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) @@ -61,12 +61,12 @@ s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch n { case 0: - c.Check(r.Method, check.Equals, "POST") + c.Check(r.Method, check.Equals, "GET") c.Check(r.URL.Path, check.Equals, "/v2/debug") - c.Check(r.URL.RawQuery, check.Equals, "") + c.Check(r.URL.RawQuery, check.Equals, "aspect=connectivity") data, err := ioutil.ReadAll(r.Body) c.Check(err, check.IsNil) - c.Check(data, check.DeepEquals, []byte(`{"action":"connectivity"}`)) + c.Check(data, check.HasLen, 0) fmt.Fprintln(w, `{"type": "sync", "result": {"connectivity":false,"unreachable":["foo.bar.com"]}}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connect_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connect_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_connect_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_connect_test.go 2020-06-05 13:13:49.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") @@ -276,7 +280,7 @@ func (s *SnapSuite) TestConnectCompletion(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/v2/interfaces": + case "/v2/connections": c.Assert(r.Method, Equals, "GET") EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_cohort.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_cohort.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_cohort.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_cohort.go 2020-06-05 13:13:49.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" + "gopkg.in/yaml.v2" + + "github.com/snapcore/snapd/i18n" +) + +var shortCreateCohortHelp = i18n.G("Create cohort keys for a series of snaps") +var longCreateCohortHelp = i18n.G(` +The create-cohort command creates a set of cohort keys for a given set of snaps. + +A cohort is a view or snapshot of a snap's "channel map" at a given point in +time that fixes the set of revisions for the snap given other constraints +(e.g. channel or architecture). The cohort is then identified by an opaque +per-snap key that works across systems. Installations or refreshes of the snap +using a given cohort key would use a fixed revision for up to 90 days, after +which a new set of revisions would be fixed under that same cohort key and a +new 90 days window started. +`) + +type cmdCreateCohort struct { + clientMixin + Positional struct { + Snaps []anySnapName `positional-arg-name:"" required:"1"` + } `positional-args:"yes" required:"yes"` +} + +func init() { + addCommand("create-cohort", shortCreateCohortHelp, longCreateCohortHelp, func() flags.Commander { return &cmdCreateCohort{} }, nil, nil) +} + +// output should be YAML, so we use these two as helpers to get that done easy +type cohortInnerYAML struct { + CohortKey string `yaml:"cohort-key"` +} +type cohortOutYAML struct { + Cohorts map[string]cohortInnerYAML `yaml:"cohorts"` +} + +func (x *cmdCreateCohort) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snaps := make([]string, len(x.Positional.Snaps)) + for i, s := range x.Positional.Snaps { + snaps[i] = string(s) + } + + cohorts, err := x.client.CreateCohorts(snaps) + if len(cohorts) == 0 || err != nil { + return err + } + + var out cohortOutYAML + out.Cohorts = make(map[string]cohortInnerYAML, len(cohorts)) + for k, v := range cohorts { + out.Cohorts[k] = cohortInnerYAML{v} + } + + enc := yaml.NewEncoder(Stdout) + defer enc.Close() + return enc.Encode(out) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_cohort_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_cohort_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_cohort_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_cohort_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,87 @@ +// -*- 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" + "gopkg.in/yaml.v2" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestCreateCohort(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + fmt.Fprintln(w, `{ +"type": "sync", +"status-code": 200, +"status": "OK", +"result": {"foo": "what", "bar": "this"}}`) + + }) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-cohort", "foo", "bar"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.HasLen, 0) + + var v map[string]map[string]map[string]string + c.Assert(yaml.Unmarshal(s.stdout.Bytes(), &v), check.IsNil) + c.Check(v, check.DeepEquals, map[string]map[string]map[string]string{ + "cohorts": { + "foo": {"cohort-key": "what"}, + "bar": {"cohort-key": "this"}, + }, + }) + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestCreateCohortNoSnaps(c *check.C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + panic("shouldn't be called") + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-cohort"}) + c.Check(err, check.ErrorMatches, "the required argument .* was not provided") +} + +func (s *SnapSuite) TestCreateCohortNotFound(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + fmt.Fprintln(w, `{"type": "error", "result": {"message": "snap not found", "kind": "snap-not-found"}, "status-code": 404}`) + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-cohort", "foo", "bar"}) + c.Check(err, check.ErrorMatches, "cannot create cohorts: snap not found") + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestCreateCohortError(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + n++ + fmt.Fprintln(w, `{"type": "error", "result": {"message": "something went wrong"}}`) + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"create-cohort", "foo", "bar"}) + c.Check(err, check.ErrorMatches, "cannot create cohorts: something went wrong") + c.Check(n, check.Equals, 1) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_key.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_key.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_key.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_key.go 2020-06-05 13:13:49.000000000 +0000 @@ -52,6 +52,7 @@ desc: i18n.G("Name of key to create; defaults to 'default'"), }}) cmd.hidden = true + cmd.completeHidden = true } func (x *cmdCreateKey) Execute(args []string) error { diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_user.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_user.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_user.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_user.go 2020-06-05 13:13:49.000000000 +0000 @@ -23,10 +23,10 @@ "encoding/json" "fmt" + "github.com/jessevdk/go-flags" + "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" - - "github.com/jessevdk/go-flags" ) var shortCreateUserHelp = i18n.G("Create a local system user") diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_user_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_user_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_create_user_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_create_user_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -35,13 +35,15 @@ switch *n { case 0: c.Check(r.Method, check.Equals, "POST") - c.Check(r.URL.Path, check.Equals, "/v2/create-user") + c.Check(r.URL.Path, check.Equals, "/v2/users") var gotBody map[string]interface{} dec := json.NewDecoder(r.Body) err := dec.Decode(&gotBody) c.Assert(err, check.IsNil) - wantBody := make(map[string]interface{}) + wantBody := map[string]interface{}{ + "action": "create", + } if email != "" { wantBody["email"] = "one@email.com" } @@ -56,7 +58,7 @@ if email == "" { fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "karl", "ssh-keys": ["a","b"]}]}`) } else { - fmt.Fprintln(w, `{"type": "sync", "result": {"username": "karl", "ssh-keys": ["a","b"]}}`) + fmt.Fprintln(w, `{"type": "sync", "result": [{"username": "karl", "ssh-keys": ["a","b"]}]}`) } default: c.Fatalf("got too many requests (now on %d)", *n+1) diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_bootvars.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_bootvars.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_bootvars.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_bootvars.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,57 @@ +// -*- 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" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/release" +) + +type cmdBootvars struct { + UC20 bool `long:"uc20"` + RootDir string `long:"root-dir"` +} + +func init() { + cmd := addDebugCommand("boot-vars", + "(internal) obtain the snapd boot variables", + "(internal) obtain the snapd boot variables", + func() flags.Commander { + return &cmdBootvars{} + }, map[string]string{ + "uc20": i18n.G("Whether to use uc20 boot vars or not"), + "root-dir": i18n.G("Root directory to look for boot variables in"), + }, nil) + if release.OnClassic { + cmd.hidden = true + } +} + +func (x *cmdBootvars) Execute(args []string) error { + if release.OnClassic { + return errors.New(`the "boot-vars" command is not available on classic systems`) + } + return boot.DumpBootVars(Stdout, x.RootDir, x.UC20) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_bootvars_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_bootvars_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_bootvars_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_bootvars_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,62 @@ +// -*- 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" + + "github.com/snapcore/snapd/bootloader" + "github.com/snapcore/snapd/bootloader/bootloadertest" + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/release" +) + +func (s *SnapSuite) TestDebugBootvars(c *check.C) { + restore := release.MockOnClassic(false) + defer restore() + bloader := bootloadertest.Mock("mock", c.MkDir()) + bootloader.Force(bloader) + bloader.BootVars = map[string]string{ + "snap_mode": "try", + "unrelated": "thing", + "snap_core": "core18_1.snap", + "snap_try_core": "core18_2.snap", + "snap_kernel": "pc-kernel_3.snap", + "snap_try_kernel": "pc-kernel_4.snap", + } + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "boot-vars"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `snap_mode=try +snap_core=core18_1.snap +snap_try_core=core18_2.snap +snap_kernel=pc-kernel_3.snap +snap_try_kernel=pc-kernel_4.snap +`) + c.Check(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestDebugBootvarsNotOnClassic(c *check.C) { + restore := release.MockOnClassic(true) + defer restore() + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "boot-vars"}) + c.Assert(err, check.ErrorMatches, `the "boot-vars" command is not available on classic systems`) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_model.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_model.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_model.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_model.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,54 @@ +// -*- 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" + + "github.com/jessevdk/go-flags" +) + +type cmdGetModel struct { + clientMixin +} + +func init() { + cmd := addDebugCommand("model", + "(internal) obtain the active model assertion", + "(internal) obtain the active model assertion", + func() flags.Commander { + return &cmdGetModel{} + }, nil, nil) + cmd.hidden = true +} + +func (x *cmdGetModel) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + var resp struct { + Model string `json:"model"` + } + if err := x.client.DebugGet("model", &resp, nil); err != nil { + return err + } + fmt.Fprintf(Stdout, "%s\n", resp.Model) + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_model_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_model_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_model_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_model_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,56 @@ +// -*- 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" + "io/ioutil" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestGetModel(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/debug") + c.Check(r.URL.RawQuery, check.Equals, "aspect=model") + data, err := ioutil.ReadAll(r.Body) + c.Check(err, check.IsNil) + c.Check(string(data), check.Equals, "") + fmt.Fprintln(w, `{"type": "sync", "result": {"model": "some-model-json"}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "model"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "some-model-json\n") + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 1) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_state.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_state.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_state.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_state.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,335 @@ +// -*- 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"` + + IsSeeded bool `long:"is-seeded"` + + // 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"), + "is-seeded": i18n.G("Output seeding status (true or false)"), + }), 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) showIsSeeded(st *state.State) error { + st.Lock() + defer st.Unlock() + + var isSeeded bool + err := st.Get("seeded", &isSeeded) + if err != nil && err != state.ErrNoState { + return err + } + fmt.Fprintf(Stdout, "%v\n", isSeeded) + + 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 c.IsSeeded != false { + cmds = append(cmds, "--is-seeded") + } + if len(cmds) > 1 { + return fmt.Errorf("cannot use %s and %s together", cmds[0], cmds[1]) + } + + if c.IsSeeded { + return c.showIsSeeded(st) + } + + 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.37.4ubuntu0.1/cmd/snap/cmd_debug_state_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_state_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_state_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_state_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,232 @@ +// -*- 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" + + main "github.com/snapcore/snapd/cmd/snap" +) + +var stateJSON = []byte(` +{ + "last-task-id": 31, + "last-change-id": 2, + + "data": { + "snaps": {}, + "seeded": true + }, + "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", "--abs-time", "--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-01T00:00:00Z 0001-01-01T00:00:00Z install-snap install a snap\n"+ + "2 Done 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z 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") + + _, err = main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--change=1", "--is-seeded", stateFile}) + c.Check(err, ErrorMatches, "cannot use --change= and --is-seeded 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", "--abs-time", "--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-01T00:00:00Z 0001-01-01T00:00:00Z download-snap Download snap a from channel edge\n"+ + "0 12 Do 0001-01-01T00:00:00Z 0001-01-01T00:00:00Z 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") +} + +func (s *SnapSuite) TestDebugIsSeededHappy(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", "--is-seeded", stateFile}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Matches, "true\n") + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestDebugIsSeededNo(c *C) { + dir := c.MkDir() + stateFile := filepath.Join(dir, "test-state.json") + c.Assert(ioutil.WriteFile(stateFile, []byte("{}"), 0644), IsNil) + + rest, err := main.Parser(main.Client()).ParseArgs([]string{"debug", "state", "--is-seeded", stateFile}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Check(s.Stdout(), Matches, "false\n") + c.Check(s.Stderr(), Equals, "") +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_timings.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_timings.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_timings.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_timings.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,294 @@ +// -*- 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" + "io" + "sort" + "strings" + "time" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +type cmdChangeTimings struct { + changeIDMixin + EnsureTag string `long:"ensure" choice:"auto-refresh" choice:"become-operational" choice:"refresh-catalogs" choice:"refresh-hints" choice:"seed"` + All bool `long:"all"` + StartupTag string `long:"startup" choice:"load-state" choice:"ifacemgr"` + Verbose bool `long:"verbose"` +} + +func init() { + addDebugCommand("timings", + i18n.G("Get the timings of the tasks of a change"), + i18n.G("The timings command displays details about the time each task runs."), + func() flags.Commander { + return &cmdChangeTimings{} + }, changeIDMixinOptDesc.also(map[string]string{ + "ensure": i18n.G("Show timings for a change related to the given Ensure activity (one of: auto-refresh, become-operational, refresh-catalogs, refresh-hints, seed)"), + "all": i18n.G("Show timings for all executions of the given Ensure or startup activity, not just the latest"), + "startup": i18n.G("Show timings for the startup of given subsystem (one of: load-state, ifacemgr)"), + // TRANSLATORS: This should not start with a lowercase letter. + "verbose": i18n.G("Show more information"), + }), changeIDMixinArgDesc) +} + +type Timing struct { + Level int `json:"level,omitempty"` + Label string `json:"label,omitempty"` + Summary string `json:"summary,omitempty"` + Duration time.Duration `json:"duration,omitempty"` +} + +func formatDuration(dur time.Duration) string { + return fmt.Sprintf("%dms", dur/time.Millisecond) +} + +func printTiming(w io.Writer, verbose bool, nestLevel int, id, status, doingTimeStr, undoingTimeStr, label, summary string) { + // don't display id for nesting>1, instead show nesting indicator + if nestLevel > 0 { + id = strings.Repeat(" ", nestLevel) + "^" + } + // Duration formats to 17m14.342s or 2.038s or 970ms, so with + // 11 chars we can go up to 59m59.999s + if verbose { + fmt.Fprintf(w, "%s\t%s\t%11s\t%11s\t%s\t%s\n", id, status, doingTimeStr, undoingTimeStr, label, strings.Repeat(" ", 2*nestLevel)+summary) + } else { + fmt.Fprintf(w, "%s\t%s\t%11s\t%11s\t%s\n", id, status, doingTimeStr, undoingTimeStr, strings.Repeat(" ", 2*nestLevel)+summary) + } +} + +func printTaskTiming(w io.Writer, t *Timing, verbose, doing bool) { + var doingTimeStr, undoingTimeStr string + if doing { + doingTimeStr = formatDuration(t.Duration) + undoingTimeStr = "-" + } else { + if doing { + doingTimeStr = "-" + undoingTimeStr = formatDuration(t.Duration) + } + } + printTiming(w, verbose, t.Level+1, "", "", doingTimeStr, undoingTimeStr, t.Label, t.Summary) +} + +// sortTimingsTasks sorts tasks from changeTimings by lane and ready time with special treatment of lane 0 tasks: +// - tasks from lanes >0 are grouped by lanes and sorted by ready time. +// - tasks from lane 0 are sorted by ready time and inserted before and after other lanes based on the min/max +// ready times of non-zero lanes. +// - tasks from lane 0 with ready time between non-zero lane tasks are not really expected in our system and will +// appear after non-zero lane tasks. +func sortTimingsTasks(timings map[string]changeTimings) []string { + tasks := make([]string, 0, len(timings)) + + var minReadyTime time.Time + // determine min ready time from all non-zero lane tasks + for taskID, taskData := range timings { + if taskData.Lane > 0 { + if minReadyTime.IsZero() { + minReadyTime = taskData.ReadyTime + } + if taskData.ReadyTime.Before(minReadyTime) { + minReadyTime = taskData.ReadyTime + } + } + tasks = append(tasks, taskID) + } + + sort.Slice(tasks, func(i, j int) bool { + t1 := timings[tasks[i]] + t2 := timings[tasks[j]] + if t1.Lane != t2.Lane { + // if either t1 or t2 is from lane 0, then it comes before or after non-zero lane tasks + if t1.Lane == 0 { + return t1.ReadyTime.Before(minReadyTime) + } + if t2.Lane == 0 { + return !t2.ReadyTime.Before(minReadyTime) + } + // different lanes (but neither of them is 0), order by lane + return t1.Lane < t2.Lane + } + + // same lane - order by ready time + return t1.ReadyTime.Before(t2.ReadyTime) + }) + + return tasks +} + +func (x *cmdChangeTimings) printChangeTimings(w io.Writer, timing *timingsData) error { + tasks := sortTimingsTasks(timing.ChangeTimings) + + for _, taskID := range tasks { + chgTiming := timing.ChangeTimings[taskID] + doingTime := formatDuration(timing.ChangeTimings[taskID].DoingTime) + if chgTiming.DoingTime == 0 { + doingTime = "-" + } + undoingTime := formatDuration(timing.ChangeTimings[taskID].UndoingTime) + if chgTiming.UndoingTime == 0 { + undoingTime = "-" + } + + printTiming(w, x.Verbose, 0, taskID, chgTiming.Status, doingTime, undoingTime, chgTiming.Kind, chgTiming.Summary) + for _, nested := range timing.ChangeTimings[taskID].DoingTimings { + showDoing := true + printTaskTiming(w, &nested, x.Verbose, showDoing) + } + for _, nested := range timing.ChangeTimings[taskID].UndoingTimings { + showDoing := false + printTaskTiming(w, &nested, x.Verbose, showDoing) + } + } + + return nil +} + +func (x *cmdChangeTimings) printEnsureTimings(w io.Writer, timings []*timingsData) error { + for _, td := range timings { + printTiming(w, x.Verbose, 0, x.EnsureTag, "", formatDuration(td.TotalDuration), "-", "", "") + for _, t := range td.EnsureTimings { + printTiming(w, x.Verbose, t.Level+1, "", "", formatDuration(t.Duration), "-", t.Label, t.Summary) + } + + // change is optional for ensure timings + if td.ChangeID != "" { + x.printChangeTimings(w, td) + } + } + return nil +} + +func (x *cmdChangeTimings) printStartupTimings(w io.Writer, timings []*timingsData) error { + for _, td := range timings { + printTiming(w, x.Verbose, 0, x.StartupTag, "", formatDuration(td.TotalDuration), "-", "", "") + for _, t := range td.StartupTimings { + printTiming(w, x.Verbose, t.Level+1, "", "", formatDuration(t.Duration), "-", t.Label, t.Summary) + } + } + return nil +} + +type changeTimings struct { + Status string `json:"status,omitempty"` + Kind string `json:"kind,omitempty"` + Summary string `json:"summary,omitempty"` + Lane int `json:"lane,omitempty"` + ReadyTime time.Time `json:"ready-time,omitempty"` + DoingTime time.Duration `json:"doing-time,omitempty"` + UndoingTime time.Duration `json:"undoing-time,omitempty"` + DoingTimings []Timing `json:"doing-timings,omitempty"` + UndoingTimings []Timing `json:"undoing-timings,omitempty"` +} + +type timingsData struct { + ChangeID string `json:"change-id"` + EnsureTimings []Timing `json:"ensure-timings,omitempty"` + StartupTimings []Timing `json:"startup-timings,omitempty"` + TotalDuration time.Duration `json:"total-duration,omitempty"` + // ChangeTimings are indexed by task id + ChangeTimings map[string]changeTimings `json:"change-timings,omitempty"` +} + +func (x *cmdChangeTimings) checkConflictingFlags() error { + var i int + for _, opt := range []string{string(x.Positional.ID), x.StartupTag, x.EnsureTag} { + if opt != "" { + i++ + if i > 1 { + return fmt.Errorf("cannot use change id, 'startup' or 'ensure' together") + } + } + } + + if x.All && (x.Positional.ID != "" || x.LastChangeType != "") { + return fmt.Errorf("cannot use 'all' with change id or 'last'") + } + return nil +} + +func (x *cmdChangeTimings) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + if err := x.checkConflictingFlags(); err != nil { + return err + } + + var chgid string + var err error + + if x.EnsureTag == "" && x.StartupTag == "" { + if x.Positional.ID == "" && x.LastChangeType == "" { + // GetChangeID() below checks for empty change ID / --last, check them early here to provide more helpful error message + return fmt.Errorf("please provide change ID or type with --last=, or query for --ensure= or --startup=") + } + + // GetChangeID takes care of --last=... if change ID was not specified by the user + chgid, err = x.GetChangeID() + if err != nil { + return err + } + } + + // gather debug timings first + var timings []*timingsData + var allEnsures string + if x.All { + allEnsures = "true" + } else { + allEnsures = "false" + } + if err := x.client.DebugGet("change-timings", &timings, map[string]string{"change-id": chgid, "ensure": x.EnsureTag, "all": allEnsures, "startup": x.StartupTag}); err != nil { + return err + } + + w := tabWriter() + if x.Verbose { + fmt.Fprintf(w, "ID\tStatus\t%11s\t%11s\tLabel\tSummary\n", "Doing", "Undoing") + } else { + fmt.Fprintf(w, "ID\tStatus\t%11s\t%11s\tSummary\n", "Doing", "Undoing") + } + + // If a specific change was requested, we expect exactly one timingsData element. + // If "ensure" activity was requested, we may get multiple elements (for multiple executions of the ensure) + if chgid != "" && len(timings) > 0 { + x.printChangeTimings(w, timings[0]) + } + + if x.EnsureTag != "" { + x.printEnsureTimings(w, timings) + } + + if x.StartupTag != "" { + x.printStartupTimings(w, timings) + } + + w.Flush() + fmt.Fprintln(Stdout) + + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_timings_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_timings_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_timings_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_timings_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,337 @@ +// -*- 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" + "strings" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/cmd/snap" +) + +type timingsCmdArgs struct { + args, stdout, stderr, error string +} + +var timingsTests = []timingsCmdArgs{{ + args: "debug timings", + error: "please provide change ID or type with --last=, or query for --ensure= or --startup=", +}, { + args: "debug timings --ensure=seed 9", + error: "cannot use change id, 'startup' or 'ensure' together", +}, { + args: "debug timings --ensure=seed --startup=ifacemgr", + error: "cannot use change id, 'startup' or 'ensure' together", +}, { + args: "debug timings --last=install --all", + error: "cannot use 'all' with change id or 'last'", +}, { + args: "debug timings --last=remove", + error: `no changes of type "remove" found`, +}, { + args: "debug timings --startup=load-state 9", + error: "cannot use change id, 'startup' or 'ensure' together", +}, { + args: "debug timings --all 9", + error: "cannot use 'all' with change id or 'last'", +}, { + args: "debug timings --last=install", + stdout: "ID Status Doing Undoing Summary\n" + + "40 Doing 910ms - lane 0 task bar summary\n" + + " ^ 1ms - foo summary\n" + + " ^ 1ms - bar summary\n" + + "41 Done 210ms - lane 1 task baz summary\n" + + "42 Done 310ms - lane 1 task boo summary\n" + + "43 Done 310ms - lane 0 task doh summary\n\n", +}, { + args: "debug timings 1", + stdout: "ID Status Doing Undoing Summary\n" + + "40 Doing 910ms - lane 0 task bar summary\n" + + " ^ 1ms - foo summary\n" + + " ^ 1ms - bar summary\n" + + "41 Done 210ms - lane 1 task baz summary\n" + + "42 Done 310ms - lane 1 task boo summary\n" + + "43 Done 310ms - lane 0 task doh summary\n\n", +}, { + args: "debug timings 1 --verbose", + stdout: "ID Status Doing Undoing Label Summary\n" + + "40 Doing 910ms - bar lane 0 task bar summary\n" + + " ^ 1ms - foo foo summary\n" + + " ^ 1ms - bar bar summary\n" + + "41 Done 210ms - baz lane 1 task baz summary\n" + + "42 Done 310ms - boo lane 1 task boo summary\n" + + "43 Done 310ms - doh lane 0 task doh summary\n\n", +}, { + args: "debug timings --ensure=seed", + stdout: "ID Status Doing Undoing Summary\n" + + "seed 8ms - \n" + + " ^ 8ms - baz summary\n" + + " ^ 8ms - booze summary\n" + + "40 Doing 910ms - task bar summary\n" + + " ^ 1ms - foo summary\n" + + " ^ 1ms - bar summary\n\n", +}, { + args: "debug timings --ensure=seed --all", + stdout: "ID Status Doing Undoing Summary\n" + + "seed 8ms - \n" + + " ^ 8ms - bar summary 1\n" + + " ^ 8ms - bar summary 2\n" + + "40 Doing 910ms - task bar summary\n" + + " ^ 1ms - foo summary\n" + + " ^ 1ms - bar summary\n" + + "seed 7ms - \n" + + " ^ 7ms - baz summary 2\n" + + "60 Doing 910ms - task bar summary\n" + + " ^ 1ms - foo summary\n" + + " ^ 1ms - bar summary\n\n", +}, { + args: "debug timings --ensure=seed --all --verbose", + stdout: "ID Status Doing Undoing Label Summary\n" + + "seed 8ms - \n" + + " ^ 8ms - abc bar summary 1\n" + + " ^ 8ms - abc bar summary 2\n" + + "40 Doing 910ms - bar task bar summary\n" + + " ^ 1ms - foo foo summary\n" + + " ^ 1ms - bar bar summary\n" + + "seed 7ms - \n" + + " ^ 7ms - ghi baz summary 2\n" + + "60 Doing 910ms - bar task bar summary\n" + + " ^ 1ms - foo foo summary\n" + + " ^ 1ms - bar bar summary\n\n", +}, { + args: "debug timings --startup=ifacemgr", + stdout: "ID Status Doing Undoing Summary\n" + + "ifacemgr 8ms - \n" + + " ^ 8ms - baz summary\n" + + " ^ 8ms - booze summary\n\n", +}, { + args: "debug timings --startup=ifacemgr --all", + stdout: "ID Status Doing Undoing Summary\n" + + "ifacemgr 8ms - \n" + + " ^ 8ms - baz summary\n" + + "ifacemgr 9ms - \n" + + " ^ 9ms - baz summary\n\n", +}} + +func (s *SnapSuite) TestGetDebugTimings(c *C) { + s.mockCmdTimingsAPI(c) + + restore := main.MockIsStdinTTY(true) + defer restore() + + for _, test := range timingsTests { + s.stdout.Truncate(0) + s.stderr.Truncate(0) + + c.Logf("Test: %s", test.args) + + _, err := main.Parser(main.Client()).ParseArgs(strings.Fields(test.args)) + if test.error != "" { + c.Check(err, ErrorMatches, test.error) + } else { + c.Check(err, IsNil) + c.Check(s.Stderr(), Equals, test.stderr) + c.Check(s.Stdout(), Equals, test.stdout) + } + } +} + +func (s *SnapSuite) mockCmdTimingsAPI(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.Method, Equals, "GET") + + if r.URL.Path == "/v2/debug" { + q := r.URL.Query() + aspect := q.Get("aspect") + c.Assert(aspect, Equals, "change-timings") + + changeID := q.Get("change-id") + ensure := q.Get("ensure") + startup := q.Get("startup") + all := q.Get("all") + + switch { + case changeID == "1": + // lane 0 and lane 1 tasks, interleaved + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[ + {"change-id":"1", "change-timings":{ + "41":{"doing-time":210000000, "status": "Done", "lane": 1, "ready-time": "2016-04-22T01:02:04Z", "kind": "baz", "summary": "lane 1 task baz summary"}, + "43":{"doing-time":310000000, "status": "Done", "ready-time": "2016-04-25T01:02:04Z", "kind": "doh", "summary": "lane 0 task doh summary"}, + "40":{"doing-time":910000000, "status": "Doing", "ready-time": "2016-04-20T00:00:00Z", "kind": "bar", "summary": "lane 0 task bar summary", + "doing-timings":[ + {"label":"foo", "summary": "foo summary", "duration": 1000001}, + {"level":1, "label":"bar", "summary": "bar summary", "duration": 1000002} + ]}, + "42":{"doing-time":310000000, "status": "Done", "lane": 1, "ready-time": "2016-04-23T01:02:04Z", "kind": "boo", "summary": "lane 1 task boo summary"} + }}]}`) + case ensure == "seed" && all == "false": + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[ + {"change-id":"1", + "total-duration": 8000002, + "ensure-timings": [ + {"label":"baz", "summary": "baz summary", "duration": 8000001}, + {"level":1, "label":"booze", "summary": "booze summary", "duration": 8000002} + ], + "change-timings":{ + "40":{"doing-time":910000000, "status": "Doing", "kind": "bar", "summary": "task bar summary", + "doing-timings":[ + {"label":"foo", "summary": "foo summary", "duration": 1000001}, + {"level":1, "label":"bar", "summary": "bar summary", "duration": 1000002} + ]}}}]}`) + case ensure == "seed" && all == "true": + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[ + {"change-id":"1", + "total-duration": 8000002, + "ensure-timings": [ + {"label":"abc", "summary": "bar summary 1", "duration": 8000001}, + {"label":"abc", "summary": "bar summary 2", "duration": 8000002} + ], + "change-timings":{ + "40":{"doing-time":910000000, "status": "Doing", "kind": "bar", "summary": "task bar summary", + "doing-timings":[ + {"label":"foo", "summary": "foo summary", "duration": 1000001}, + {"level":1, "label":"bar", "summary": "bar summary", "duration": 1000002} + ]}}}, + {"change-id":"2", + "total-duration": 7000002, + "ensure-timings": [{"label":"ghi", "summary": "baz summary 2", "duration": 7000002}], + "change-timings":{ + "60":{"doing-time":910000000, "status": "Doing", "kind": "bar", "summary": "task bar summary", + "doing-timings":[ + {"label":"foo", "summary": "foo summary", "duration": 1000001}, + {"level":1, "label":"bar", "summary": "bar summary", "duration": 1000002} + ]}}}]}`) + case startup == "ifacemgr" && all == "false": + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[ + {"total-duration": 8000002, "startup-timings": [ + {"label":"baz", "summary": "baz summary", "duration": 8000001}, + {"level":1, "label":"booze", "summary": "booze summary", "duration": 8000002} + ]}]}`) + case startup == "ifacemgr" && all == "true": + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[ + {"total-duration": 8000002, "startup-timings": [ + {"label":"baz", "summary": "baz summary", "duration": 8000001} + ]}, + {"total-duration": 9000002, "startup-timings": [ + {"label":"baz", "summary": "baz summary", "duration": 9000001} + ]}]}`) + default: + c.Errorf("unexpected request: %s, %s, %s", changeID, ensure, all) + } + return + } + + // request for all changes on --last=... + if r.URL.Path == "/v2/changes" { + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":[{ + "id": "1", + "kind": "install-snap", + "summary": "a", + "status": "Doing", + "ready": false, + "spawn-time": "2016-04-21T01:02:03Z", + "ready-time": "2016-04-21T01:02:04Z", + "tasks": [{"id":"99", "kind": "bar", "summary": ".", "status": "Doing", "progress": {"done": 0, "total": 1}, "spawn-time": "2016-04-21T01:02:03Z", "ready-time": "2016-04-21T01:02:04Z"}] + }]}`) + return + } + c.Errorf("unexpected path %q", r.URL.Path) + }) +} + +type TaskDef struct { + TaskID string + Lane int + ReadyTime time.Time +} + +func (s *SnapSuite) TestSortTimingsTasks(c *C) { + mkTime := func(timeStr string) time.Time { + t, err := time.Parse(time.RFC3339, timeStr) + c.Assert(err, IsNil) + return t + } + + testData := []struct { + ChangeTimings map[string]main.ChangeTimings + Expected []string + }{{ + // nothing to do + ChangeTimings: map[string]main.ChangeTimings{}, + Expected: []string{}, + }, { + ChangeTimings: map[string]main.ChangeTimings{ + // tasks in lane 0 only + "1": {ReadyTime: mkTime("2019-04-21T00:00:00Z")}, + "2": {ReadyTime: mkTime("2019-05-21T00:00:00Z")}, + "3": {ReadyTime: mkTime("2019-02-21T00:00:00Z")}, + "4": {ReadyTime: mkTime("2019-03-21T00:00:00Z")}, + "5": {ReadyTime: mkTime("2019-01-21T00:00:00Z")}, + }, + Expected: []string{"5", "3", "4", "1", "2"}, + }, { + // task in lane 1 with a task in lane 0 before and after it + ChangeTimings: map[string]main.ChangeTimings{ + "1": {Lane: 1, ReadyTime: mkTime("2019-01-21T00:00:00Z")}, + "2": {Lane: 0, ReadyTime: mkTime("2019-01-20T00:00:00Z")}, + "3": {Lane: 0, ReadyTime: mkTime("2019-01-22T00:00:00Z")}, + }, + Expected: []string{"2", "1", "3"}, + }, { + // tasks in lane 1 only + ChangeTimings: map[string]main.ChangeTimings{ + "1": {Lane: 1, ReadyTime: mkTime("2019-01-21T00:00:00Z")}, + "2": {Lane: 1, ReadyTime: mkTime("2019-01-20T00:00:00Z")}, + "3": {Lane: 1, ReadyTime: mkTime("2019-01-16T00:00:00Z")}, + }, + Expected: []string{"3", "2", "1"}, + }, { + // tasks in lanes 0, 1, 2 with tasks from line 0 before and after lanes 1, 2 + ChangeTimings: map[string]main.ChangeTimings{ + "1": {Lane: 1, ReadyTime: mkTime("2019-01-21T00:00:00Z")}, + "2": {Lane: 0, ReadyTime: mkTime("2019-01-19T00:00:00Z")}, + "3": {Lane: 2, ReadyTime: mkTime("2019-01-20T00:00:00Z")}, + "4": {Lane: 0, ReadyTime: mkTime("2019-01-25T00:00:00Z")}, + "5": {Lane: 1, ReadyTime: mkTime("2019-01-20T00:00:00Z")}, + "6": {Lane: 2, ReadyTime: mkTime("2019-01-21T00:00:00Z")}, + "7": {Lane: 0, ReadyTime: mkTime("2019-01-18T00:00:00Z")}, + "8": {Lane: 0, ReadyTime: mkTime("2019-01-27T00:00:00Z")}, + }, + Expected: []string{"7", "2", "5", "1", "3", "6", "4", "8"}, + }, { + // pathological case: lane 0 tasks have ready-time between lane 1 tasks + ChangeTimings: map[string]main.ChangeTimings{ + "1": {Lane: 1, ReadyTime: mkTime("2019-01-20T00:00:00Z")}, + "2": {Lane: 1, ReadyTime: mkTime("2019-01-30T00:00:00Z")}, + "3": {Lane: 0, ReadyTime: mkTime("2019-01-27T00:00:00Z")}, + "4": {Lane: 0, ReadyTime: mkTime("2019-01-25T00:00:00Z")}, + }, + Expected: []string{"1", "2", "4", "3"}, + }} + + for _, data := range testData { + tasks := main.SortTimingsTasks(data.ChangeTimings) + c.Check(tasks, DeepEquals, data.Expected) + } +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_validate_seed.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_validate_seed.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_validate_seed.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_validate_seed.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,50 @@ +// -*- 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/seed" +) + +type cmdValidateSeed struct { + Positionals struct { + SeedYamlPath flags.Filename `positional-arg-name:""` + } `positional-args:"true" required:"true"` +} + +func init() { + cmd := addDebugCommand("validate-seed", + "(internal) validate seed.yaml", + "(internal) validate seed.yaml", + func() flags.Commander { + return &cmdValidateSeed{} + }, nil, nil) + cmd.hidden = true +} + +func (x *cmdValidateSeed) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + return seed.ValidateFromYaml(string(x.Positionals.SeedYamlPath)) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_validate_seed_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_validate_seed_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_debug_validate_seed_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_debug_validate_seed_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,45 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2019-2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main_test + +import ( + "io/ioutil" + "path/filepath" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestDebugValidateCannotValidate(c *C) { + tmpf := filepath.Join(c.MkDir(), "seed.yaml") + err := ioutil.WriteFile(tmpf, []byte(` +snaps: + - + name: core + channel: stable + file: core_6673.snap +`), 0644) + c.Assert(err, IsNil) + + _, err = snap.Parser(snap.Client()).ParseArgs([]string{"debug", "validate-seed", tmpf}) + c.Assert(err, ErrorMatches, `cannot validate seed: + - no seed assertions`) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_delete_key.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_delete_key.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_delete_key.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_delete_key.go 2020-06-05 13:13:49.000000000 +0000 @@ -48,6 +48,7 @@ desc: i18n.G("Name of key to delete"), }}) cmd.hidden = true + cmd.completeHidden = true } func (x *cmdDeleteKey) Execute(args []string) error { diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_disconnect.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_disconnect.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_disconnect.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_disconnect.go 2020-06-05 13:13:49.000000000 +0000 @@ -30,6 +30,7 @@ type cmdDisconnect struct { waitMixin + Forget bool `long:"forget"` Positionals struct { Offer disconnectSlotOrPlugSpec `required:"true"` Use disconnectSlotSpec @@ -49,12 +50,17 @@ Disconnects everything from the provided plug or slot. The snap name may be omitted for the core snap. + +When an automatic connection is manually disconnected, its disconnected state +is retained after a snap refresh. The --forget flag can be added to the +disconnect command to reset this behaviour, and consequently re-enable +an automatic reconnection after a snap refresh. `) func init() { addCommand("disconnect", shortDisconnectHelp, longDisconnectHelp, func() flags.Commander { return &cmdDisconnect{} - }, waitDescs, []argDesc{ + }, waitDescs.also(map[string]string{"forget": "Forget remembered state about the given connection."}), []argDesc{ // TRANSLATORS: This needs to begin with < and end with > {name: i18n.G(":")}, // TRANSLATORS: This needs to begin with < and end with > @@ -80,7 +86,8 @@ return fmt.Errorf("please provide the plug or slot name to disconnect from snap %q", use.Snap) } - id, err := x.client.Disconnect(offer.Snap, offer.Name, use.Snap, use.Name) + opts := &client.DisconnectOptions{Forget: x.Forget} + id, err := x.client.Disconnect(offer.Snap, offer.Name, use.Snap, use.Name, opts) if err != nil { if client.IsInterfacesUnchangedError(err) { fmt.Fprintf(Stdout, i18n.G("No connections to disconnect")) diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_disconnect_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_disconnect_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_disconnect_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_disconnect_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -46,9 +46,15 @@ Disconnects everything from the provided plug or slot. The snap name may be omitted for the core snap. +When an automatic connection is manually disconnected, its disconnected state +is retained after a snap refresh. The --forget flag can be added to the +disconnect command to reset this behaviour, and consequently re-enable +an automatic reconnection after a snap refresh. + [disconnect command options] --no-wait Do not wait for the operation to finish but just print the change id. + --forget Forget remembered state about the given connection. ` s.testSubCommandHelp(c, "disconnect", msg) } @@ -73,6 +79,7 @@ }, }, }) + w.WriteHeader(202) fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) case "/v2/changes/zzz": c.Check(r.Method, Equals, "GET") @@ -88,6 +95,43 @@ c.Assert(s.Stderr(), Equals, "") } +func (s *SnapSuite) TestDisconnectWithForgetFlag(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/interfaces": + c.Check(r.Method, Equals, "POST") + c.Check(DecodedRequestBody(c, r), DeepEquals, map[string]interface{}{ + "action": "disconnect", + "forget": true, + "plugs": []interface{}{ + map[string]interface{}{ + "snap": "consumer", + "plug": "plug", + }, + }, + "slots": []interface{}{ + map[string]interface{}{ + "snap": "producer", + "slot": "slot", + }, + }, + }) + w.WriteHeader(202) + fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) + case "/v2/changes/zzz": + c.Check(r.Method, Equals, "GET") + fmt.Fprintln(w, `{"type":"sync", "result":{"ready": true, "status": "Done"}}`) + default: + c.Fatalf("unexpected path %q", r.URL.Path) + } + }) + rest, err := Parser(Client()).ParseArgs([]string{"disconnect", "--forget", "consumer:plug", "producer:slot"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + func (s *SnapSuite) TestDisconnectEverythingFromSpecificSlot(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { @@ -108,6 +152,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 +188,7 @@ }, }, }) + w.WriteHeader(202) fmt.Fprintln(w, `{"type":"async", "status-code": 202, "change": "zzz"}`) case "/v2/changes/zzz": c.Check(r.Method, Equals, "GET") @@ -172,7 +218,7 @@ func (s *SnapSuite) TestDisconnectCompletion(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/v2/interfaces": + case "/v2/connections": c.Assert(r.Method, Equals, "GET") EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_download.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_download.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_download.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_download.go 2020-06-05 13:13:49.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2017 Canonical Ltd + * Copyright (C) 2016-2020 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -36,8 +36,11 @@ type cmdDownload struct { channelMixin - Revision string `long:"revision"` + Revision string `long:"revision"` + Basename string `long:"basename"` + TargetDir string `long:"target-directory"` + CohortKey string `long:"cohort"` Positional struct { Snap remoteSnapName } `positional-args:"true" required:"true"` @@ -53,7 +56,14 @@ addCommand("download", shortDownloadHelp, longDownloadHelp, func() flags.Commander { return &cmdDownload{} }, channelDescs.also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. "revision": i18n.G("Download the given revision of a snap, to which you must have developer access"), + // TRANSLATORS: This should not start with a lowercase letter. + "cohort": i18n.G("Download from the given cohort"), + // TRANSLATORS: This should not start with a lowercase letter. + "basename": i18n.G("Use this basename for the snap and assertion files (defaults to _)"), + // TRANSLATORS: This should not start with a lowercase letter. + "target-directory": i18n.G("Download to this directory (defaults to the current directory)"), }), []argDesc{{ name: "", // TRANSLATORS: This should not start with a lowercase letter. @@ -87,7 +97,26 @@ return assertPath, err } +func printInstallHint(assertPath, snapPath string) { + // simplify paths + wd, _ := os.Getwd() + if p, err := filepath.Rel(wd, assertPath); err == nil { + assertPath = p + } + if p, err := filepath.Rel(wd, snapPath); err == nil { + snapPath = p + } + // add a hint what to do with the downloaded snap (LP:1676707) + fmt.Fprintf(Stdout, i18n.G(`Install the snap with: + snap ack %s + snap install %s +`), assertPath, snapPath) +} + func (x *cmdDownload) Execute(args []string) error { + if strings.ContainsRune(x.Basename, filepath.Separator) { + return fmt.Errorf(i18n.G("cannot specify a path in basename (use --target-dir for that)")) + } if err := x.setChannelFromCommandline(); err != nil { return err } @@ -103,6 +132,9 @@ if x.Channel != "" { return fmt.Errorf(i18n.G("cannot specify both channel and revision")) } + if x.CohortKey != "" { + return fmt.Errorf(i18n.G("cannot specify both cohort and revision")) + } var err error revision, err = snap.ParseRevision(x.Revision) if err != nil { @@ -119,10 +151,15 @@ fmt.Fprintf(Stdout, i18n.G("Fetching snap %q\n"), snapName) dlOpts := image.DownloadOptions{ - TargetDir: "", // cwd + TargetDir: x.TargetDir, + Basename: x.Basename, 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, revision, &dlOpts) + snapPath, snapInfo, _, err := tsto.DownloadSnap(snapName, dlOpts) if err != nil { return err } @@ -132,20 +169,7 @@ if err != nil { return err } - - // simplify paths - wd, _ := os.Getwd() - if p, err := filepath.Rel(wd, assertPath); err == nil { - assertPath = p - } - if p, err := filepath.Rel(wd, snapPath); err == nil { - snapPath = p - } - // add a hint what to do with the downloaded snap (LP:1676707) - fmt.Fprintf(Stdout, i18n.G(`Install the snap with: - snap ack %s - snap install %s -`), assertPath, snapPath) + printInstallHint(assertPath, snapPath) return nil } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_download_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_download_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_download_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_download_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,84 @@ +// -*- 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 ( + "os" + "path/filepath" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +// these only cover errors that happen before hitting the network, +// because we're not (yet!) mocking the tooling store + +func (s *SnapSuite) TestDownloadBadBasename(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{ + "download", "--basename=/foo", "a-snap", + }) + + c.Check(err, check.ErrorMatches, "cannot specify a path in basename .use --target-dir for that.") +} + +func (s *SnapSuite) TestDownloadBadChannelCombo(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{ + "download", "--beta", "--channel=foo", "a-snap", + }) + + c.Check(err, check.ErrorMatches, "Please specify a single channel") +} + +func (s *SnapSuite) TestDownloadCohortAndRevision(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{ + "download", "--cohort=what", "--revision=1234", "a-snap", + }) + + c.Check(err, check.ErrorMatches, "cannot specify both cohort and revision") +} + +func (s *SnapSuite) TestDownloadChannelAndRevision(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{ + "download", "--beta", "--revision=1234", "a-snap", + }) + + c.Check(err, check.ErrorMatches, "cannot specify both channel and revision") +} + +func (s *SnapSuite) TestPrintInstalHint(c *check.C) { + snap.PrintInstallHint("foo_1.assert", "foo_1.snap") + c.Check(s.Stdout(), check.Equals, `Install the snap with: + snap ack foo_1.assert + snap install foo_1.snap +`) + s.stdout.Reset() + + cwd, err := os.Getwd() + c.Assert(err, check.IsNil) + as := filepath.Join(cwd, "some-dir/foo_1.assert") + sn := filepath.Join(cwd, "some-dir/foo_1.snap") + snap.PrintInstallHint(as, sn) + c.Check(s.Stdout(), check.Equals, `Install the snap with: + snap ack some-dir/foo_1.assert + snap install some-dir/foo_1.snap +`) + +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_find.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_find.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_find.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_find.go 2020-06-05 13:13:49.000000000 +0000 @@ -38,7 +38,7 @@ var shortFindHelp = i18n.G("Find packages to install") var longFindHelp = i18n.G(` -The find command queries the store for available packages in the stable channel. +The find command queries the store for available packages. With the --private flag, which requires the user to be logged-in to the store (see 'snap help login'), it instead searches for private snaps that the user @@ -153,7 +153,7 @@ clientMixin Private bool `long:"private"` Narrow bool `long:"narrow"` - Section SectionName `long:"section" optional:"true" optional-value:"show-all-sections-please" default:"no-section-specified"` + Section SectionName `long:"section" optional:"true" optional-value:"show-all-sections-please" default:"no-section-specified" default-mask:"-"` Positional struct { Query string } `positional-args:"yes"` @@ -165,11 +165,11 @@ return &cmdFind{} }, colorDescs.also(map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. - "private": i18n.G("Search private snaps"), + "private": i18n.G("Search private snaps."), // TRANSLATORS: This should not start with a lowercase letter. - "narrow": i18n.G("Only search for snaps in “stable”"), + "narrow": i18n.G("Only search for snaps in “stable”."), // TRANSLATORS: This should not start with a lowercase letter. - "section": i18n.G("Restrict the search to a given section"), + "section": i18n.G("Restrict the search to a given section."), }), []argDesc{{ // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), @@ -224,9 +224,9 @@ } opts := &client.FindOptions{ - Private: x.Private, - Section: string(x.Section), Query: x.Positional.Query, + Section: string(x.Section), + Private: x.Private, } if !x.Narrow { diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_first_boot.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_first_boot.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_first_boot.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_first_boot.go 2020-06-05 13:13:49.000000000 +0000 @@ -29,7 +29,7 @@ func init() { cmd := addCommand("firstboot", - "Internal", + "Deprecated (hidden)", "The firstboot command is only retained for backwards compatibility.", func() flags.Commander { return &cmdInternalFirstBoot{} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_get_base_declaration.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_get_base_declaration.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_get_base_declaration.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_get_base_declaration.go 2020-06-05 13:13:49.000000000 +0000 @@ -26,15 +26,24 @@ ) type cmdGetBaseDeclaration struct { + get bool clientMixin } func init() { cmd := addDebugCommand("get-base-declaration", + "(internal) obtain the base declaration for all interfaces (deprecated)", + "(internal) obtain the base declaration for all interfaces (deprecated)", + func() flags.Commander { + return &cmdGetBaseDeclaration{} + }, nil, nil) + cmd.hidden = true + + cmd = addDebugCommand("base-declaration", "(internal) obtain the base declaration for all interfaces", "(internal) obtain the base declaration for all interfaces", func() flags.Commander { - return &cmdGetBaseDeclaration{} + return &cmdGetBaseDeclaration{get: true} }, nil, nil) cmd.hidden = true } @@ -46,7 +55,13 @@ var resp struct { BaseDeclaration string `json:"base-declaration"` } - if err := x.client.Debug("get-base-declaration", nil, &resp); err != nil { + var err error + if x.get { + err = x.client.DebugGet("base-declaration", &resp, nil) + } else { + err = x.client.Debug("get-base-declaration", nil, &resp) + } + if err != nil { return err } fmt.Fprintf(Stdout, "%s\n", resp.BaseDeclaration) diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_get_base_declaration_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_get_base_declaration_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_get_base_declaration_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_get_base_declaration_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -53,3 +53,28 @@ c.Check(s.Stdout(), check.Equals, "hello\n") c.Check(s.Stderr(), check.Equals, "") } + +func (s *SnapSuite) TestBaseDeclaration(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/debug") + c.Check(r.URL.RawQuery, check.Equals, "aspect=base-declaration") + data, err := ioutil.ReadAll(r.Body) + c.Check(err, check.IsNil) + c.Check(data, check.HasLen, 0) + fmt.Fprintln(w, `{"type": "sync", "result": {"base-declaration": "hello"}}`) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"debug", "base-declaration"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "hello\n") + c.Check(s.Stderr(), check.Equals, "") +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_get.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_get.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_get.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_get.go 2020-06-05 13:13:49.000000000 +0000 @@ -37,13 +37,12 @@ $ snap get snap-name username frank -If multiple option names are provided, a document is returned: +If multiple option names are provided, the corresponding values are returned: $ snap get snap-name username password - { - "username": "frank", - "password": "..." - } + Key Value + username frank + password ... Nested values may be retrieved via a dotted path: diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_handle_link.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_handle_link.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_handle_link.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_handle_link.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/snap/cmd_help.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_help.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_help.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_help.go 2020-06-05 13:13:49.000000000 +0000 @@ -22,6 +22,8 @@ import ( "bytes" "fmt" + "io" + "regexp" "strings" "unicode/utf8" @@ -46,8 +48,18 @@ // on which help is being requested (like "snap foo // --help", active is foo), or nil in the toplevel. if parser.Command.Active == nil { - // toplevel --help will get handled via ErrCommandRequired - return nil + // this means *either* a bare 'snap --help', + // *or* 'snap --help command' + // + // If we return nil in the first case go-flags + // will throw up an ErrCommandRequired on its + // own, but in the second case it'll go on to + // run the command, which is very unexpected. + // + // So we force the ErrCommandRequired here. + + // toplevel --help gets handled via ErrCommandRequired + return &flags.Error{Type: flags.ErrCommandRequired} } // not toplevel, so ask for regular help return &flags.Error{Type: flags.ErrHelp} @@ -69,7 +81,7 @@ Manpage bool `long:"man" hidden:"true"` Positional struct { // TODO: find a way to make Command tab-complete - Sub string `positional-arg-name:""` + Subs []string `positional-arg-name:""` } `positional-args:"yes"` parser *flags.Parser } @@ -88,10 +100,11 @@ cmd.parser = parser } -// manfixer is a hackish way to get the generated manpage into section 8 -// (go-flags doesn't have an option for this; I'll be proposing something -// there soon, but still waiting on some other PRs to make it through) +// manfixer is a hackish way to fix drawbacks in the generated manpage: +// - no way to get it into section 8 +// - duplicated TP lines that break older groff (e.g. 14.04), lp:1814767 type manfixer struct { + bytes.Buffer done bool } @@ -100,13 +113,20 @@ w.done = true if bytes.HasPrefix(buf, []byte(".TH snap 1 ")) { // io.Writer.Write must not modify the buffer, even temporarily - n, _ := Stdout.Write(buf[:9]) - Stdout.Write([]byte{'8'}) - m, err := Stdout.Write(buf[10:]) + n, _ := w.Buffer.Write(buf[:9]) + w.Buffer.Write([]byte{'8'}) + m, err := w.Buffer.Write(buf[10:]) return n + m + 1, err } } - return Stdout.Write(buf) + return w.Buffer.Write(buf) +} + +var tpRegexp = regexp.MustCompile(`(?m)(?:^\.TP\n)+`) + +func (w *manfixer) flush() { + str := tpRegexp.ReplaceAllLiteralString(w.Buffer.String(), ".TP\n") + io.Copy(Stdout, strings.NewReader(str)) } func (cmd cmdHelp) Execute(args []string) error { @@ -116,27 +136,36 @@ if cmd.Manpage { // you shouldn't try to to combine --man with --all nor a // subcommand, but --man is hidden so no real need to check. - cmd.parser.WriteManPage(&manfixer{}) + out := &manfixer{} + cmd.parser.WriteManPage(out) + out.flush() return nil } if cmd.All { - if cmd.Positional.Sub != "" { + if len(cmd.Positional.Subs) > 0 { return fmt.Errorf(i18n.G("help accepts a command, or '--all', but not both.")) } printLongHelp(cmd.parser) return nil } - if cmd.Positional.Sub != "" { - subcmd := cmd.parser.Find(cmd.Positional.Sub) + var subcmd = cmd.parser.Command + for _, subname := range cmd.Positional.Subs { + subcmd = subcmd.Find(subname) if subcmd == nil { - return fmt.Errorf(i18n.G("Unknown command %q. Try 'snap help'."), cmd.Positional.Sub) + sug := "snap help" + if x := cmd.parser.Command.Active; x != nil && x.Name != "help" { + sug = "snap help " + x.Name + } + // TRANSLATORS: %q is the command the user entered; %s is 'snap help' or 'snap help ' + return fmt.Errorf(i18n.G("unknown command %q, see '%s'."), subname, sug) } // this makes "snap help foo" work the same as "snap foo --help" cmd.parser.Command.Active = subcmd + } + if subcmd != cmd.parser.Command { return &flags.Error{Type: flags.ErrHelp} } - return &flags.Error{Type: flags.ErrCommandRequired} } @@ -171,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"), @@ -179,7 +208,7 @@ }, { Label: i18n.G("Permissions"), Description: i18n.G("manage permissions"), - Commands: []string{"interfaces", "interface", "connect", "disconnect"}, + Commands: []string{"connections", "interface", "connect", "disconnect"}, }, { Label: i18n.G("Snapshots"), Description: i18n.G("archives of snap data"), @@ -187,11 +216,11 @@ }, { Label: i18n.G("Other"), Description: i18n.G("miscellanea"), - Commands: []string{"version", "warnings", "okay"}, + Commands: []string{"version", "warnings", "okay", "ack", "known", "model", "create-cohort"}, }, { Label: i18n.G("Development"), Description: i18n.G("developer-oriented features"), - Commands: []string{"run", "pack", "try", "ack", "known", "download"}, + Commands: []string{"run", "pack", "try", "download", "prepare-image"}, }, } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_help_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_help_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_help_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_help_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -41,6 +41,7 @@ {"snap", "help"}, {"snap", "--help"}, {"snap", "-h"}, + {"snap", "--help", "install"}, } { s.ResetStdStreams() @@ -174,3 +175,24 @@ c.Check(s.Stdout(), check.Matches, `\.TH snap 8 (?s).*`) } + +func (s *SnapSuite) TestManpageNoDoubleTP(c *check.C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + os.Args = []string{"snap", "help", "--man"} + + err := snap.RunMain() + c.Assert(err, check.IsNil) + + c.Check(s.Stdout(), check.Not(check.Matches), `(?s).*(?m-s)^\.TP\n\.TP$(?s-m).*`) + +} + +func (s *SnapSuite) TestBadSub(c *check.C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + os.Args = []string{"snap", "debug", "brotato"} + + err := snap.RunMain() + c.Assert(err, check.ErrorMatches, `unknown command "brotato", see 'snap help debug'.`) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_info.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_info.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_info.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_info.go 2020-06-05 13:13:49.000000000 +0000 @@ -23,6 +23,7 @@ "fmt" "io" "path/filepath" + "strconv" "strings" "text/tabwriter" "time" @@ -34,9 +35,12 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/cmd" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snapfile" + "github.com/snapcore/snapd/snap/squashfs" "github.com/snapcore/snapd/strutil" ) @@ -73,106 +77,66 @@ }), nil) } -func norm(path string) string { - path = filepath.Clean(path) - if osutil.IsDirectory(path) { - path = path + "/" - } - - return path -} - -func maybePrintPrice(w io.Writer, snap *client.Snap, resInfo *client.ResultInfo) { - if resInfo == nil { +func (iw *infoWriter) maybePrintHealth() { + if iw.localSnap == nil { return } - price, currency, err := getPrice(snap.Prices, resInfo.SuggestedCurrency) - if err != nil { - return + health := iw.localSnap.Health + if health == nil { + if !iw.verbose { + return + } + health = &client.SnapHealth{ + Status: "unknown", + Message: "health has not been set", + } } - fmt.Fprintf(w, "price:\t%s\n", formatPrice(price, currency)) -} - -func maybePrintType(w io.Writer, t string) { - // XXX: using literals here until we reshuffle snap & client properly - // (and os->core rename happens, etc) - switch t { - case "", "app", "application": + if health.Status == "okay" && !iw.verbose { return - case "os": - t = "core" } - fmt.Fprintf(w, "type:\t%s\n", t) -} - -func maybePrintID(w io.Writer, snap *client.Snap) { - if snap.ID != "" { - fmt.Fprintf(w, "snap-id:\t%s\n", snap.ID) + 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) } -} - -func maybePrintBase(w io.Writer, base string, verbose bool) { - if verbose && base != "" { - fmt.Fprintf(w, "base:\t%s\n", base) + 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 tryDirect(w io.Writer, path string, verbose bool) bool { - path = norm(path) - - snapf, err := snap.Open(path) +func clientSnapFromPath(path string) (*client.Snap, error) { + snapf, err := snapfile.Open(path) if err != nil { - return false + return nil, err } - - var sha3_384 string - if verbose && !osutil.IsDirectory(path) { - var err error - sha3_384, _, err = asserts.SnapFileSHA3_384(path) - if err != nil { - return false - } - } - info, err := snap.ReadInfoFromSnapFile(snapf, nil) if err != nil { - return false + return nil, err } - fmt.Fprintf(w, "path:\t%q\n", path) - fmt.Fprintf(w, "name:\t%s\n", info.InstanceName()) - fmt.Fprintf(w, "summary:\t%s\n", formatSummary(info.Summary())) - - var notes *Notes - if verbose { - fmt.Fprintln(w, "notes:\t") - fmt.Fprintf(w, " confinement:\t%s\n", info.Confinement) - if info.Broken == "" { - fmt.Fprintln(w, " broken:\tfalse") - } else { - fmt.Fprintf(w, " broken:\ttrue (%s)\n", info.Broken) - } - } else { - notes = NotesFromInfo(info) - } - fmt.Fprintf(w, "version:\t%s %s\n", info.Version, notes) - maybePrintType(w, string(info.Type)) - maybePrintBase(w, info.Base, verbose) - if sha3_384 != "" { - fmt.Fprintf(w, "sha3-384:\t%s\n", sha3_384) + direct, err := cmd.ClientSnapFromSnapInfo(info) + if err != nil { + return nil, err } - return true + return direct, nil } -func coalesce(snaps ...*client.Snap) *client.Snap { - for _, s := range snaps { - if s != nil { - return s - } +func norm(path string) string { + path = filepath.Clean(path) + if osutil.IsDirectory(path) { + path = path + "/" } - return nil + + return path } // runesTrimRightSpace returns text, with any trailing whitespace dropped. @@ -195,9 +159,38 @@ return -1 } -// wrapLine wraps a line to fit into width, preserving the line's indent, and +// wrapLine wraps a line, assumed to be part of a block-style yaml +// string, to fit into termWidth, preserving the line's indent, and // writes it out prepending padding to each line. -func wrapLine(out io.Writer, text []rune, pad string, width int) error { +func wrapLine(out io.Writer, text []rune, pad string, termWidth int) error { + // discard any trailing whitespace + text = runesTrimRightSpace(text) + // establish the indent of the whole block + idx := 0 + for idx < len(text) && unicode.IsSpace(text[idx]) { + idx++ + } + indent := pad + string(text[:idx]) + text = text[idx:] + if len(indent) > termWidth/2 { + // If indent is too big there's not enough space for the actual + // text, in the pathological case the indent can even be bigger + // than the terminal which leads to lp:1828425. + // Rather than let that happen, give up. + indent = pad + " " + } + return wrapGeneric(out, text, indent, indent, termWidth) +} + +// wrapFlow wraps the text using yaml's flow style, allowing indent +// characters for the first line. +func wrapFlow(out io.Writer, text []rune, indent string, termWidth int) error { + return wrapGeneric(out, text, indent, " ", termWidth) +} + +// wrapGeneric wraps the given text to the given width, prefixing the +// first line with indent and the remaining lines with indent2 +func wrapGeneric(out io.Writer, text []rune, indent, indent2 string, termWidth int) error { // Note: this is _wrong_ for much of unicode (because the width of a rune on // the terminal is anything between 0 and 2, not always 1 as this code // assumes) but fixing that is Hard. Long story short, you can get close @@ -210,20 +203,15 @@ // This (and possibly printDescr below) should move to strutil once // we're happy with it getting wider (heh heh) use. - // discard any trailing whitespace - text = runesTrimRightSpace(text) + indentWidth := utf8.RuneCountInString(indent) + delta := indentWidth - utf8.RuneCountInString(indent2) + width := termWidth - indentWidth + // establish the indent of the whole block - idx := 0 - for idx < len(text) && unicode.IsSpace(text[idx]) { - idx++ - } - indent := pad + string(text[:idx]) - text = text[idx:] - width -= idx + utf8.RuneCountInString(pad) var err error for len(text) > width && err == nil { // find a good place to chop the text - idx = runesLastIndexSpace(text[:width+1]) + idx := runesLastIndexSpace(text[:width+1]) if idx < 0 { // there's no whitespace; just chop at line width idx = width @@ -234,6 +222,9 @@ idx++ } text = text[idx:] + width += delta + indent = indent2 + delta = 0 } if err != nil { return err @@ -242,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. // @@ -249,11 +254,11 @@ // - trim trailing whitespace // - word wrap at "max" chars preserving line indent // - keep \n intact and break there -func printDescr(w io.Writer, descr string, max int) error { +func printDescr(w io.Writer, descr string, termWidth int) error { var err error descr = strings.TrimRightFunc(descr, unicode.IsSpace) for _, line := range strings.Split(descr, "\n") { - err = wrapLine(w, []rune(line), " ", max) + err = wrapLine(w, []rune(line), " ", termWidth) if err != nil { break } @@ -261,37 +266,246 @@ return err } -func maybePrintCommands(w io.Writer, snapName string, allApps []client.AppInfo, n int) { - if len(allApps) == 0 { +type writeflusher interface { + io.Writer + Flush() error +} + +type infoWriter struct { + // fields that are set every iteration + theSnap *client.Snap + diskSnap *client.Snap + localSnap *client.Snap + remoteSnap *client.Snap + resInfo *client.ResultInfo + path string + // fields that don't change and so can be set once + writeflusher + esc *escapes + termWidth int + fmtTime func(time.Time) string + absTime bool + verbose bool +} + +func (iw *infoWriter) setupDiskSnap(path string, diskSnap *client.Snap) { + iw.localSnap, iw.remoteSnap, iw.resInfo = nil, nil, nil + iw.path = path + iw.diskSnap = diskSnap + iw.theSnap = diskSnap +} + +func (iw *infoWriter) setupSnap(localSnap, remoteSnap *client.Snap, resInfo *client.ResultInfo) { + iw.path, iw.diskSnap = "", nil + iw.localSnap = localSnap + iw.remoteSnap = remoteSnap + iw.resInfo = resInfo + if localSnap != nil { + iw.theSnap = localSnap + } else { + iw.theSnap = remoteSnap + } +} + +func (iw *infoWriter) maybePrintPrice() { + if iw.resInfo == nil { + return + } + price, currency, err := getPrice(iw.remoteSnap.Prices, iw.resInfo.SuggestedCurrency) + if err != nil { + return + } + fmt.Fprintf(iw, "price:\t%s\n", formatPrice(price, currency)) +} + +func (iw *infoWriter) maybePrintType() { + // XXX: using literals here until we reshuffle snap & client properly + // (and os->core rename happens, etc) + t := iw.theSnap.Type + switch t { + case "", "app", "application": return + case "os": + t = "core" + } + + fmt.Fprintf(iw, "type:\t%s\n", t) +} + +func (iw *infoWriter) maybePrintID() { + if iw.theSnap.ID != "" { + fmt.Fprintf(iw, "snap-id:\t%s\n", iw.theSnap.ID) } +} - commands := make([]string, 0, len(allApps)) - for _, app := range allApps { +func (iw *infoWriter) maybePrintTrackingChannel() { + if iw.localSnap == nil { + return + } + if iw.localSnap.TrackingChannel == "" { + return + } + fmt.Fprintf(iw, "tracking:\t%s\n", iw.localSnap.TrackingChannel) +} + +func (iw *infoWriter) maybePrintInstallDate() { + if iw.localSnap == nil { + return + } + if iw.localSnap.InstallDate.IsZero() { + return + } + fmt.Fprintf(iw, "refresh-date:\t%s\n", iw.fmtTime(iw.localSnap.InstallDate)) +} + +func (iw *infoWriter) maybePrintChinfo() { + if iw.diskSnap != nil { + return + } + chInfos := channelInfos{ + chantpl: "%s%s:\t%s %s%*s %*s %s\n", + releasedfmt: "2006-01-02", + esc: iw.esc, + } + if iw.absTime { + chInfos.releasedfmt = time.RFC3339 + } + if iw.remoteSnap != nil && iw.remoteSnap.Channels != nil && iw.remoteSnap.Tracks != nil { + iw.Flush() + chInfos.chantpl = "%s%s:\t%s\t%s\t%*s\t%*s\t%s\n" + chInfos.addFromRemote(iw.remoteSnap) + } + if iw.localSnap != nil { + chInfos.addFromLocal(iw.localSnap) + } + chInfos.dump(iw) +} + +func (iw *infoWriter) maybePrintBase() { + if iw.verbose && iw.theSnap.Base != "" { + fmt.Fprintf(iw, "base:\t%s\n", iw.theSnap.Base) + } +} + +func (iw *infoWriter) maybePrintPath() { + if iw.path != "" { + fmt.Fprintf(iw, "path:\t%q\n", iw.path) + } +} + +func (iw *infoWriter) printName() { + fmt.Fprintf(iw, "name:\t%s\n", iw.theSnap.Name) +} + +func (iw *infoWriter) printSummary() { + wrapFlow(iw, quotedIfNeeded(iw.theSnap.Summary), "summary:\t", iw.termWidth) +} + +func (iw *infoWriter) maybePrintStoreURL() { + storeURL := "" + // XXX: store-url for local snaps comes from aux data, but that gets + // updated only when the snap is refreshed, be smart and poke remote + // snap info if available + switch { + case iw.theSnap.StoreURL != "": + storeURL = iw.theSnap.StoreURL + case iw.remoteSnap != nil && iw.remoteSnap.StoreURL != "": + storeURL = iw.remoteSnap.StoreURL + } + if storeURL == "" { + return + } + fmt.Fprintf(iw, "store-url:\t%s\n", storeURL) +} + +func (iw *infoWriter) maybePrintPublisher() { + if iw.diskSnap != nil { + // snaps read from disk won't have a publisher + return + } + fmt.Fprintf(iw, "publisher:\t%s\n", longPublisher(iw.esc, iw.theSnap.Publisher)) +} + +func (iw *infoWriter) maybePrintStandaloneVersion() { + if iw.diskSnap == nil { + // snaps not read from disk will have version information shown elsewhere + return + } + version := iw.diskSnap.Version + if version == "" { + version = iw.esc.dash + } + // NotesFromRemote might be better called NotesFromNotInstalled but that's nasty + fmt.Fprintf(iw, "version:\t%s %s\n", version, NotesFromRemote(iw.diskSnap, nil)) +} + +func (iw *infoWriter) maybePrintBuildDate() { + if iw.diskSnap == nil { + return + } + if osutil.IsDirectory(iw.path) { + return + } + buildDate := squashfs.BuildDate(iw.path) + if buildDate.IsZero() { + return + } + fmt.Fprintf(iw, "build-date:\t%s\n", iw.fmtTime(buildDate)) +} + +func (iw *infoWriter) maybePrintContact() error { + contact := strings.TrimPrefix(iw.theSnap.Contact, "mailto:") + if contact == "" { + return nil + } + _, err := fmt.Fprintf(iw, "contact:\t%s\n", contact) + return err +} + +func (iw *infoWriter) printLicense() { + license := iw.theSnap.License + if license == "" { + license = "unset" + } + fmt.Fprintf(iw, "license:\t%s\n", license) +} + +func (iw *infoWriter) printDescr() { + fmt.Fprintln(iw, "description: |") + printDescr(iw, iw.theSnap.Description, iw.termWidth) +} + +func (iw *infoWriter) maybePrintCommands() { + if len(iw.theSnap.Apps) == 0 { + return + } + + commands := make([]string, 0, len(iw.theSnap.Apps)) + for _, app := range iw.theSnap.Apps { if app.IsService() { continue } - cmdStr := snap.JoinSnapApp(snapName, app.Name) + cmdStr := snap.JoinSnapApp(iw.theSnap.Name, app.Name) commands = append(commands, cmdStr) } if len(commands) == 0 { return } - fmt.Fprintf(w, "commands:\n") + fmt.Fprintf(iw, "commands:\n") for _, cmd := range commands { - fmt.Fprintf(w, " - %s\n", cmd) + fmt.Fprintf(iw, " - %s\n", cmd) } } -func maybePrintServices(w io.Writer, snapName string, allApps []client.AppInfo, n int) { - if len(allApps) == 0 { +func (iw *infoWriter) maybePrintServices() { + if len(iw.theSnap.Apps) == 0 { return } - services := make([]string, 0, len(allApps)) - for _, app := range allApps { + services := make([]string, 0, len(iw.theSnap.Apps)) + for _, app := range iw.theSnap.Apps { if !app.IsService() { continue } @@ -307,82 +521,159 @@ } else { enabled = "disabled" } - services = append(services, fmt.Sprintf(" %s:\t%s, %s, %s", snap.JoinSnapApp(snapName, app.Name), app.Daemon, enabled, active)) + services = append(services, fmt.Sprintf(" %s:\t%s, %s, %s", snap.JoinSnapApp(iw.theSnap.Name, app.Name), app.Daemon, enabled, active)) } if len(services) == 0 { return } - fmt.Fprintf(w, "services:\n") + fmt.Fprintf(iw, "services:\n") for _, svc := range services { - fmt.Fprintln(w, svc) + fmt.Fprintln(iw, svc) + } +} + +func (iw *infoWriter) maybePrintNotes() { + if !iw.verbose { + return + } + fmt.Fprintln(iw, "notes:\t") + fmt.Fprintf(iw, " private:\t%t\n", iw.theSnap.Private) + fmt.Fprintf(iw, " confinement:\t%s\n", iw.theSnap.Confinement) + if iw.localSnap == nil { + return } + jailMode := iw.localSnap.Confinement == client.DevModeConfinement && !iw.localSnap.DevMode + fmt.Fprintf(iw, " devmode:\t%t\n", iw.localSnap.DevMode) + fmt.Fprintf(iw, " jailmode:\t%t\n", jailMode) + fmt.Fprintf(iw, " trymode:\t%t\n", iw.localSnap.TryMode) + fmt.Fprintf(iw, " enabled:\t%t\n", iw.localSnap.Status == client.StatusActive) + if iw.localSnap.Broken == "" { + fmt.Fprintf(iw, " broken:\t%t\n", false) + } else { + fmt.Fprintf(iw, " broken:\t%t (%s)\n", true, iw.localSnap.Broken) + } + + fmt.Fprintf(iw, " ignore-validation:\t%t\n", iw.localSnap.IgnoreValidation) + return +} + +func (iw *infoWriter) maybePrintCohortKey() { + if !iw.verbose { + return + } + if iw.localSnap == nil { + return + } + coh := iw.localSnap.CohortKey + if coh == "" { + return + } + if isStdoutTTY { + // 15 is 1 + the length of "refresh-date: " + coh = strutil.ElliptLeft(iw.localSnap.CohortKey, iw.termWidth-15) + } + fmt.Fprintf(iw, "cohort:\t%s\n", coh) +} + +func (iw *infoWriter) maybePrintSum() { + if !iw.verbose { + return + } + if iw.diskSnap == nil { + // TODO: expose the sha via /v2/snaps and /v2/find + return + } + if osutil.IsDirectory(iw.path) { + // no sha3_384 of a directory :-) + return + } + sha3_384, _, _ := asserts.SnapFileSHA3_384(iw.path) + if sha3_384 == "" { + return + } + fmt.Fprintf(iw, "sha3-384:\t%s\n", sha3_384) } var channelRisks = []string{"stable", "candidate", "beta", "edge"} -// displayChannels displays channels and tracks in the right order -func (x *infoCmd) displayChannels(w io.Writer, chantpl string, esc *escapes, remote *client.Snap, revLen, sizeLen int) (maxRevLen, maxSizeLen int) { - fmt.Fprintln(w, "channels:") +type channelInfo struct { + indent, name, version, released, revision, size, notes string +} + +type channelInfos struct { + channels []*channelInfo + maxRevLen, maxSizeLen int + releasedfmt, chantpl string + needsHeader bool + esc *escapes +} - releasedfmt := "2006-01-02" - if x.AbsTime { - releasedfmt = time.RFC3339 +func (chInfos *channelInfos) add(indent, name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) { + chInfo := &channelInfo{ + indent: indent, + name: name, + version: version, + revision: fmt.Sprintf("(%s)", revision), + size: strutil.SizeToStr(size), + notes: notes.String(), + } + if !released.IsZero() { + chInfo.released = released.Format(chInfos.releasedfmt) + } + if len(chInfo.revision) > chInfos.maxRevLen { + chInfos.maxRevLen = len(chInfo.revision) } + if len(chInfo.size) > chInfos.maxSizeLen { + chInfos.maxSizeLen = len(chInfo.size) + } + chInfos.channels = append(chInfos.channels, chInfo) +} + +func (chInfos *channelInfos) addFromLocal(local *client.Snap) { + chInfos.add("", "installed", local.Version, local.Revision, time.Time{}, local.InstalledSize, NotesFromLocal(local)) +} - type chInfoT struct { - name, version, released, revision, size, notes string +func (chInfos *channelInfos) addOpenChannel(name, version string, revision snap.Revision, released time.Time, size int64, notes *Notes) { + chInfos.add(" ", name, version, revision, released, size, notes) +} + +func (chInfos *channelInfos) addClosedChannel(name string, trackHasOpenChannel bool) { + chInfo := &channelInfo{indent: " ", name: name} + if trackHasOpenChannel { + chInfo.version = chInfos.esc.uparrow + } else { + chInfo.version = chInfos.esc.dash } - var chInfos []*chInfoT - maxRevLen, maxSizeLen = revLen, sizeLen + chInfos.channels = append(chInfos.channels, chInfo) +} + +func (chInfos *channelInfos) addFromRemote(remote *client.Snap) { // order by tracks for _, tr := range remote.Tracks { trackHasOpenChannel := false for _, risk := range channelRisks { chName := fmt.Sprintf("%s/%s", tr, risk) ch, ok := remote.Channels[chName] - if tr == "latest" { - chName = risk - } - chInfo := chInfoT{name: chName} if ok { - chInfo.version = ch.Version - chInfo.revision = fmt.Sprintf("(%s)", ch.Revision) - if len(chInfo.revision) > maxRevLen { - maxRevLen = len(chInfo.revision) - } - chInfo.released = ch.ReleasedAt.Format(releasedfmt) - chInfo.size = strutil.SizeToStr(ch.Size) - if len(chInfo.size) > maxSizeLen { - maxSizeLen = len(chInfo.size) - } - chInfo.notes = NotesFromChannelSnapInfo(ch).String() + chInfos.addOpenChannel(chName, ch.Version, ch.Revision, ch.ReleasedAt, ch.Size, NotesFromChannelSnapInfo(ch)) trackHasOpenChannel = true } else { - if trackHasOpenChannel { - chInfo.version = esc.uparrow - } else { - chInfo.version = esc.dash - } + chInfos.addClosedChannel(chName, trackHasOpenChannel) } - chInfos = append(chInfos, &chInfo) } } - - for _, chInfo := range chInfos { - fmt.Fprintf(w, " "+chantpl, chInfo.name, chInfo.version, chInfo.released, maxRevLen, chInfo.revision, maxSizeLen, chInfo.size, chInfo.notes) - } - - return maxRevLen, maxSizeLen + chInfos.needsHeader = len(chInfos.channels) > 0 } -func formatSummary(raw string) string { - s, err := yaml.Marshal(raw) - if err != nil { - return fmt.Sprintf("cannot marshal summary: %s", err) +func (chInfos *channelInfos) dump(w io.Writer) { + if chInfos.needsHeader { + fmt.Fprintln(w, "channels:") + } + for _, c := range chInfos.channels { + fmt.Fprintf(w, chInfos.chantpl, c.indent, c.name, c.version, c.released, chInfos.maxRevLen, c.revision, chInfos.maxSizeLen, c.size, c.notes) } - return strings.TrimSpace(string(s)) } func (x *infoCmd) Execute([]string) error { @@ -395,6 +686,14 @@ esc := x.getEscapes() w := tabwriter.NewWriter(Stdout, 2, 2, 1, ' ', 0) + iw := &infoWriter{ + writeflusher: w, + esc: esc, + termWidth: termWidth, + verbose: x.Verbose, + fmtTime: x.fmtTime, + absTime: x.AbsTime, + } noneOK := true for i, snapName := range x.Positional.Snaps { @@ -407,17 +706,18 @@ continue } - if tryDirect(w, snapName, x.Verbose) { - noneOK = false - continue + if diskSnap, err := clientSnapFromPath(snapName); err == nil { + iw.setupDiskSnap(norm(snapName), diskSnap) + } else { + remoteSnap, resInfo, _ := x.client.FindOne(snap.InstanceSnap(snapName)) + localSnap, _, _ := x.client.Snap(snapName) + iw.setupSnap(localSnap, remoteSnap, resInfo) } - remote, resInfo, _ := x.client.FindOne(snapName) - local, _, _ := x.client.Snap(snapName) + // note diskSnap == nil, or localSnap == nil and remoteSnap == nil - both := coalesce(local, remote) - - if both == nil { + if iw.theSnap == nil { if len(x.Positional.Snaps) == 1 { + w.Flush() return fmt.Errorf("no snap found for %q", snapName) } @@ -426,80 +726,31 @@ } noneOK = false - fmt.Fprintf(w, "name:\t%s\n", both.Name) - fmt.Fprintf(w, "summary:\t%s\n", formatSummary(both.Summary)) - fmt.Fprintf(w, "publisher:\t%s\n", longPublisher(esc, both.Publisher)) - if both.Contact != "" { - fmt.Fprintf(w, "contact:\t%s\n", strings.TrimPrefix(both.Contact, "mailto:")) - } - license := both.License - if license == "" { - license = "unset" - } - fmt.Fprintf(w, "license:\t%s\n", license) - maybePrintPrice(w, remote, resInfo) - fmt.Fprintln(w, "description: |") - printDescr(w, both.Description, termWidth) - maybePrintCommands(w, snapName, both.Apps, termWidth) - maybePrintServices(w, snapName, both.Apps, termWidth) - - if x.Verbose { - fmt.Fprintln(w, "notes:\t") - fmt.Fprintf(w, " private:\t%t\n", both.Private) - fmt.Fprintf(w, " confinement:\t%s\n", both.Confinement) - } - - var notes *Notes - if local != nil { - if x.Verbose { - jailMode := local.Confinement == client.DevModeConfinement && !local.DevMode - fmt.Fprintf(w, " devmode:\t%t\n", local.DevMode) - fmt.Fprintf(w, " jailmode:\t%t\n", jailMode) - fmt.Fprintf(w, " trymode:\t%t\n", local.TryMode) - fmt.Fprintf(w, " enabled:\t%t\n", local.Status == client.StatusActive) - if local.Broken == "" { - fmt.Fprintf(w, " broken:\t%t\n", false) - } else { - fmt.Fprintf(w, " broken:\t%t (%s)\n", true, local.Broken) - } - - fmt.Fprintf(w, " ignore-validation:\t%t\n", local.IgnoreValidation) - } else { - notes = NotesFromLocal(local) - } - } + iw.maybePrintPath() + iw.printName() + iw.printSummary() + iw.maybePrintHealth() + iw.maybePrintPublisher() + iw.maybePrintStoreURL() + iw.maybePrintStandaloneVersion() + iw.maybePrintBuildDate() + iw.maybePrintContact() + iw.printLicense() + iw.maybePrintPrice() + iw.printDescr() + iw.maybePrintCommands() + iw.maybePrintServices() + iw.maybePrintNotes() // stops the notes etc trying to be aligned with channels - w.Flush() - maybePrintType(w, both.Type) - maybePrintBase(w, both.Base, x.Verbose) - maybePrintID(w, both) - var localRev, localSize string - var revLen, sizeLen int - if local != nil { - if local.TrackingChannel != "" { - fmt.Fprintf(w, "tracking:\t%s\n", local.TrackingChannel) - } - if !local.InstallDate.IsZero() { - fmt.Fprintf(w, "refresh-date:\t%s\n", x.fmtTime(local.InstallDate)) - } - localRev = fmt.Sprintf("(%s)", local.Revision) - revLen = len(localRev) - localSize = strutil.SizeToStr(local.InstalledSize) - sizeLen = len(localSize) - } - - chantpl := "%s:\t%s %s%*s %*s %s\n" - if remote != nil && remote.Channels != nil && remote.Tracks != nil { - chantpl = "%s:\t%s\t%s\t%*s\t%*s\t%s\n" - - w.Flush() - revLen, sizeLen = x.displayChannels(w, chantpl, esc, remote, revLen, sizeLen) - } - if local != nil { - fmt.Fprintf(w, chantpl, - "installed", local.Version, "", revLen, localRev, sizeLen, localSize, notes) - } - + iw.Flush() + iw.maybePrintType() + iw.maybePrintBase() + iw.maybePrintSum() + iw.maybePrintID() + iw.maybePrintCohortKey() + iw.maybePrintTrackingChannel() + iw.maybePrintInstallDate() + iw.maybePrintChinfo() } w.Flush() diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_info_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_info_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_info_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_info_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -22,13 +22,18 @@ import ( "bytes" "fmt" + "io/ioutil" "net/http" + "path/filepath" "time" "gopkg.in/check.v1" "github.com/snapcore/snapd/client" snap "github.com/snapcore/snapd/cmd/snap" + snaplib "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/snap/squashfs" ) var cmdAppInfos = []client.AppInfo{{Name: "app1"}, {Name: "app2"}} @@ -55,10 +60,17 @@ var _ = check.Suite(&infoSuite{}) +type flushBuffer struct{ bytes.Buffer } + +func (*flushBuffer) Flush() error { return nil } + func (s *infoSuite) TestMaybePrintServices(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) for _, infos := range [][]client.AppInfo{svcAppInfos, mixedAppInfos} { - var buf bytes.Buffer - snap.MaybePrintServices(&buf, "foo", infos, -1) + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Name: "foo", Apps: infos}) + snap.MaybePrintServices(iw) c.Check(buf.String(), check.Equals, `services: foo.svc1: simple, disabled, active @@ -68,18 +80,23 @@ } func (s *infoSuite) TestMaybePrintServicesNoServices(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) for _, infos := range [][]client.AppInfo{cmdAppInfos, nil} { - var buf bytes.Buffer - snap.MaybePrintServices(&buf, "foo", infos, -1) - + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Name: "foo", Apps: infos}) + snap.MaybePrintServices(iw) c.Check(buf.String(), check.Equals, "") } } func (s *infoSuite) TestMaybePrintCommands(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) for _, infos := range [][]client.AppInfo{cmdAppInfos, mixedAppInfos} { - var buf bytes.Buffer - snap.MaybePrintCommands(&buf, "foo", infos, -1) + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Name: "foo", Apps: infos}) + snap.MaybePrintCommands(iw) c.Check(buf.String(), check.Equals, `commands: - foo.app1 @@ -89,14 +106,342 @@ } func (s *infoSuite) TestMaybePrintCommandsNoCommands(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) for _, infos := range [][]client.AppInfo{svcAppInfos, nil} { - var buf bytes.Buffer - snap.MaybePrintCommands(&buf, "foo", infos, -1) + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Name: "foo", Apps: infos}) + snap.MaybePrintCommands(iw) c.Check(buf.String(), check.Equals, "") } } +func (infoSuite) TestPrintType(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + for from, to := range map[string]string{ + "": "", + "app": "", + "application": "", + "gadget": "type:\tgadget\n", + "core": "type:\tcore\n", + "os": "type:\tcore\n", + } { + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Type: from}) + snap.MaybePrintType(iw) + c.Check(buf.String(), check.Equals, to, check.Commentf("%q", from)) + } +} + +func (infoSuite) TestPrintSummary(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + for from, to := range map[string]string{ + "": `""`, // empty results in quoted empty + "foo": "foo", // plain text results in unquoted + "two words": "two words", // ...even when multi-word + "{": `"{"`, // but yaml-breaking is quoted + "very long text": "very long\n text", // too-long text gets split (TODO: split with tabbed indent to preserve alignment) + } { + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Summary: from}) + snap.PrintSummary(iw) + c.Check(buf.String(), check.Equals, "summary:\t"+to+"\n", check.Commentf("%q", from)) + } +} + +func (s *infoSuite) TestMaybePrintPublisher(c *check.C) { + acct := &snaplib.StoreAccount{ + Validation: "verified", + Username: "team-potato", + DisplayName: "Team Potato", + } + + type T struct { + diskSnap, localSnap *client.Snap + expected string + } + + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + for i, t := range []T{ + {&client.Snap{}, nil, ""}, // nothing output for on-disk snap + {nil, &client.Snap{}, "publisher:\t--\n"}, // from-snapd snap with no publisher is explicit + {nil, &client.Snap{Publisher: acct}, "publisher:\tTeam Potato*\n"}, + } { + buf.Reset() + if t.diskSnap == nil { + snap.SetupSnap(iw, t.localSnap, nil, nil) + } else { + snap.SetupDiskSnap(iw, "", t.diskSnap) + } + snap.MaybePrintPublisher(iw) + c.Check(buf.String(), check.Equals, t.expected, check.Commentf("%d", i)) + } +} + +func (s *infoSuite) TestMaybePrintNotes(c *check.C) { + type T struct { + localSnap, diskSnap *client.Snap + expected string + } + + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + for i, t := range []T{ + { + nil, + &client.Snap{Private: true, Confinement: "devmode"}, + "notes:\t\n" + + " private:\ttrue\n" + + " confinement:\tdevmode\n", + }, { + &client.Snap{Private: true, Confinement: "devmode"}, + nil, + "notes:\t\n" + + " private:\ttrue\n" + + " confinement:\tdevmode\n" + + " devmode:\tfalse\n" + + " jailmode:\ttrue\n" + + " trymode:\tfalse\n" + + " enabled:\tfalse\n" + + " broken:\tfalse\n" + + " ignore-validation:\tfalse\n", + }, { + &client.Snap{Private: true, Confinement: "devmode", Broken: "ouch"}, + nil, + "notes:\t\n" + + " private:\ttrue\n" + + " confinement:\tdevmode\n" + + " devmode:\tfalse\n" + + " jailmode:\ttrue\n" + + " trymode:\tfalse\n" + + " enabled:\tfalse\n" + + " broken:\ttrue (ouch)\n" + + " ignore-validation:\tfalse\n", + }, + } { + buf.Reset() + snap.SetVerbose(iw, false) + if t.diskSnap == nil { + snap.SetupSnap(iw, t.localSnap, nil, nil) + } else { + snap.SetupDiskSnap(iw, "", t.diskSnap) + } + snap.MaybePrintNotes(iw) + c.Check(buf.String(), check.Equals, "", check.Commentf("%d/false", i)) + + buf.Reset() + snap.SetVerbose(iw, true) + snap.MaybePrintNotes(iw) + c.Check(buf.String(), check.Equals, t.expected, check.Commentf("%d/true", i)) + } +} + +func (s *infoSuite) TestMaybePrintStandaloneVersion(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + + // no disk snap -> no version + snap.MaybePrintStandaloneVersion(iw) + c.Check(buf.String(), check.Equals, "") + + for version, expected := range map[string]string{ + "": "--", + "4.2": "4.2", + } { + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Version: version}) + snap.MaybePrintStandaloneVersion(iw) + c.Check(buf.String(), check.Equals, "version:\t"+expected+" -\n", check.Commentf("%q", version)) + + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Version: version, Confinement: "devmode"}) + snap.MaybePrintStandaloneVersion(iw) + c.Check(buf.String(), check.Equals, "version:\t"+expected+" devmode\n", check.Commentf("%q", version)) + } +} + +func (s *infoSuite) TestMaybePrintBuildDate(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + // some prep + dir := c.MkDir() + arbfile := filepath.Join(dir, "arb") + c.Assert(ioutil.WriteFile(arbfile, nil, 0600), check.IsNil) + filename := filepath.Join(c.MkDir(), "foo.snap") + diskSnap := squashfs.New(filename) + c.Assert(diskSnap.Build(dir, nil), check.IsNil) + buildDate := diskSnap.BuildDate().Format(time.Kitchen) + + // no disk snap -> no build date + snap.MaybePrintBuildDate(iw) + c.Check(buf.String(), check.Equals, "") + + // path is directory -> no build date + buf.Reset() + snap.SetupDiskSnap(iw, dir, &client.Snap{}) + snap.MaybePrintBuildDate(iw) + c.Check(buf.String(), check.Equals, "") + + // not actually a snap -> no build date + buf.Reset() + snap.SetupDiskSnap(iw, arbfile, &client.Snap{}) + snap.MaybePrintBuildDate(iw) + c.Check(buf.String(), check.Equals, "") + + // disk snap -> get build date + buf.Reset() + snap.SetupDiskSnap(iw, filename, &client.Snap{}) + snap.MaybePrintBuildDate(iw) + c.Check(buf.String(), check.Equals, "build-date:\t"+buildDate+"\n") +} + +func (s *infoSuite) TestMaybePrintSum(c *check.C) { + var buf flushBuffer + // some prep + dir := c.MkDir() + filename := filepath.Join(c.MkDir(), "foo.snap") + diskSnap := squashfs.New(filename) + c.Assert(diskSnap.Build(dir, nil), check.IsNil) + iw := snap.NewInfoWriter(&buf) + snap.SetVerbose(iw, true) + + // no disk snap -> no checksum + snap.MaybePrintSum(iw) + c.Check(buf.String(), check.Equals, "") + + // path is directory -> no checksum + buf.Reset() + snap.SetupDiskSnap(iw, dir, &client.Snap{}) + snap.MaybePrintSum(iw) + c.Check(buf.String(), check.Equals, "") + + // disk snap and verbose -> get checksum + buf.Reset() + snap.SetupDiskSnap(iw, filename, &client.Snap{}) + snap.MaybePrintSum(iw) + c.Check(buf.String(), check.Matches, "sha3-384:\t\\S+\n") + + // disk snap but not verbose -> no checksum + buf.Reset() + snap.SetVerbose(iw, false) + snap.MaybePrintSum(iw) + c.Check(buf.String(), check.Equals, "") +} + +func (s *infoSuite) TestMaybePrintContact(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + + for contact, expected := range map[string]string{ + "mailto:joe@example.com": "contact:\tjoe@example.com\n", + // gofmt 1.9 being silly + "foo": "contact:\tfoo\n", + "": "", + } { + buf.Reset() + snap.SetupDiskSnap(iw, "", &client.Snap{Contact: contact}) + snap.MaybePrintContact(iw) + c.Check(buf.String(), check.Equals, expected, check.Commentf("%q", contact)) + } +} + +func (s *infoSuite) TestMaybePrintBase(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + dSnap := &client.Snap{} + snap.SetupDiskSnap(iw, "", dSnap) + + // no verbose -> no base + snap.SetVerbose(iw, false) + snap.MaybePrintBase(iw) + c.Check(buf.String(), check.Equals, "") + buf.Reset() + + // no base -> no base :) + snap.SetVerbose(iw, true) + snap.MaybePrintBase(iw) + c.Check(buf.String(), check.Equals, "") + buf.Reset() + + // base + verbose -> base + dSnap.Base = "xyzzy" + snap.MaybePrintBase(iw) + c.Check(buf.String(), check.Equals, "base:\txyzzy\n") + buf.Reset() +} + +func (s *infoSuite) TestMaybePrintPath(c *check.C) { + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + dSnap := &client.Snap{} + + // no path -> no path + snap.SetupDiskSnap(iw, "", dSnap) + snap.MaybePrintPath(iw) + c.Check(buf.String(), check.Equals, "") + buf.Reset() + + // path -> path (quoted!) + snap.SetupDiskSnap(iw, "xyzzy", dSnap) + snap.MaybePrintPath(iw) + c.Check(buf.String(), check.Equals, "path:\t\"xyzzy\"\n") + buf.Reset() +} + +func (s *infoSuite) TestClientSnapFromPath(c *check.C) { + // minimal sanity check + fn := snaptest.MakeTestSnapWithFiles(c, ` +name: some-snap +version: 9 +`, nil) + dSnap, err := snap.ClientSnapFromPath(fn) + c.Assert(err, check.IsNil) + c.Check(dSnap.Version, check.Equals, "9") +} + +func (s *infoSuite) TestInfoPricedNarrowTerminal(c *check.C) { + defer snap.MockTermSize(func() (int, int) { return 44, 25 })() + + 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") + fmt.Fprintln(w, findPricedJSON) + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/hello") + fmt.Fprintln(w, "{}") + default: + c.Fatalf("expected to get 1 requests, now on %d (%v)", n+1, r) + } + + n++ + }) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "hello"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, ` +name: hello +summary: GNU Hello, the "hello world" + snap +publisher: Canonical* +license: Proprietary +price: 1.99GBP +description: | + GNU hello prints a friendly greeting. + This is part of the snapcraft tour at + https://snapcraft.io/ +snap-id: mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6 +`[1:]) + c.Check(s.Stderr(), check.Equals, "") +} + func (s *infoSuite) TestInfoPriced(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { @@ -131,6 +476,7 @@ c.Check(s.Stderr(), check.Equals, "") } +// only used for results on /v2/find const mockInfoJSON = ` { "type": "sync", @@ -195,6 +541,7 @@ "revision": "1", "status": "available", "summary": "The GNU Hello snap", + "store-url": "https://snapcraft.io/hello", "type": "app", "version": "2.10", "license": "MIT", @@ -250,6 +597,7 @@ c.Check(s.Stderr(), check.Equals, "") } +// only used for /v2/snaps/hello const mockInfoJSONOtherLicense = ` { "type": "sync", @@ -266,6 +614,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, @@ -336,8 +685,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: | @@ -346,8 +701,30 @@ 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, "") +} + +func (s *infoSuite) TestInfoNotFound(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n % 2 { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/find") + case 1: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/x") + } + w.WriteHeader(404) + fmt.Fprintln(w, `{"type":"error","status-code":404,"status":"Not Found","result":{"message":"No.","kind":"snap-not-found","value":"x"}}`) + + n++ + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"info", "--verbose", "/x"}) + c.Check(err, check.ErrorMatches, `no snap found for "/x"`) + c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") } @@ -411,6 +788,7 @@ c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap publisher: Canonical* +store-url: https://snapcraft.io/hello license: unset description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at @@ -436,6 +814,7 @@ c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap publisher: Canonical* +store-url: https://snapcraft.io/hello license: unset description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at @@ -461,6 +840,7 @@ c.Check(s.Stdout(), check.Equals, `name: hello summary: The GNU Hello snap publisher: Canonical✓ +store-url: https://snapcraft.io/hello license: unset description: | GNU hello prints a friendly greeting. This is part of the snapcraft tour at @@ -550,3 +930,294 @@ c.Check(buf.String(), check.Equals, v, check.Commentf("%q", k)) } } + +func (infoSuite) TestMaybePrintCohortKey(c *check.C) { + type T struct { + snap *client.Snap + verbose bool + expected string + } + + tests := []T{ + {snap: nil, verbose: false, expected: ""}, + {snap: nil, verbose: true, expected: ""}, + {snap: &client.Snap{}, verbose: false, expected: ""}, + {snap: &client.Snap{}, verbose: true, expected: ""}, + {snap: &client.Snap{CohortKey: "some-cohort-key"}, verbose: false, expected: ""}, + {snap: &client.Snap{CohortKey: "some-cohort-key"}, verbose: true, expected: "cohort:\t…-key\n"}, + } + + var buf flushBuffer + iw := snap.NewInfoWriter(&buf) + defer snap.MockIsStdoutTTY(true)() + + for i, t := range tests { + buf.Reset() + snap.SetupSnap(iw, t.snap, nil, nil) + snap.SetVerbose(iw, t.verbose) + snap.MaybePrintCohortKey(iw) + c.Check(buf.String(), check.Equals, t.expected, check.Commentf("tty:true/%d", i)) + } + // now the same but without a tty -> the last test should no longer ellipt + tests[len(tests)-1].expected = "cohort:\tsome-cohort-key\n" + snap.MockIsStdoutTTY(false) + for i, t := range tests { + buf.Reset() + snap.SetupSnap(iw, t.snap, nil, nil) + snap.SetVerbose(iw, t.verbose) + snap.MaybePrintCohortKey(iw) + c.Check(buf.String(), check.Equals, t.expected, check.Commentf("tty:false/%d", i)) + } +} + +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 + var buf bytes.Buffer + const s = "This is a paragraph indented with leading spaces that are encoded as multiple bytes. All hail EN SPACE." + snap.WrapFlow(&buf, []rune(s), "\u2002\u2002", 30) + c.Check(buf.String(), check.Equals, ` +  This is a paragraph indented + with leading spaces that are + encoded as multiple bytes. + All hail EN SPACE. +`[1:]) +} + +func (infoSuite) TestBug1828425(c *check.C) { + const s = `This is a description + that has + lines + too deeply + indented. +` + var buf bytes.Buffer + err := snap.PrintDescr(&buf, s, 30) + c.Assert(err, check.IsNil) + c.Check(buf.String(), check.Equals, ` This is a description + that has + lines + too deeply + 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* +store-url: https://snapcraft.io/hello +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, "") +} + +const mockInfoJSONWithStoreURL = ` +{ + "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", + "private": false, + "revision": "100", + "status": "available", + "store-url": "https://snapcraft.io/hello", + "summary": "The GNU Hello snap", + "type": "app", + "version": "2.10", + "license": "", + "tracking-channel": "beta" + } +} +` + +func (s *infoSuite) TestInfoStoreURL(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") + fmt.Fprintln(w, mockInfoJSONWithStoreURL) + 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"}) + 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 +summary: The GNU Hello snap +publisher: Canonical* +store-url: https://snapcraft.io/hello +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.37.4ubuntu0.1/cmd/snap/cmd_interfaces.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_interfaces.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_interfaces.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_interfaces.go 2020-06-05 13:13:49.000000000 +0000 @@ -22,6 +22,7 @@ import ( "fmt" + "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" "github.com/jessevdk/go-flags" @@ -53,10 +54,13 @@ Filters the complete output so only plugs and/or slots matching the provided details are listed. + +NOTE this command is deprecated and has been replaced with the 'connections' + command. `) func init() { - addCommand("interfaces", shortInterfacesHelp, longInterfacesHelp, func() flags.Commander { + cmd := addCommand("interfaces", shortInterfacesHelp, longInterfacesHelp, func() flags.Commander { return &cmdInterfaces{} }, map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. @@ -67,26 +71,35 @@ // TRANSLATORS: This should not start with a lowercase letter. desc: i18n.G("Constrain listing to a specific snap or snap:name"), }}) + cmd.hidden = true } +var interfacesDeprecationNotice = i18n.G("'snap interfaces' is deprecated; use 'snap connections'.") + func (x *cmdInterfaces) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs } - ifaces, err := x.client.Connections() + opts := client.ConnectionOptions{ + All: true, + Snap: x.Positionals.Query.Snap, + } + ifaces, err := x.client.Connections(&opts) if err != nil { return err } if len(ifaces.Plugs) == 0 && len(ifaces.Slots) == 0 { return fmt.Errorf(i18n.G("no interfaces found")) } + + defer fmt.Fprintln(Stderr, "\n"+fill(interfacesDeprecationNotice, 0)) + w := tabWriter() defer w.Flush() fmt.Fprintln(w, i18n.G("Slot\tPlug")) wantedSnap := x.Positionals.Query.Snap - for _, slot := range ifaces.Slots { if wantedSnap != "" { var ok bool diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_interfaces_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_interfaces_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_interfaces_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_interfaces_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -29,12 +29,13 @@ "github.com/snapcore/snapd/client" . "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/testutil" ) -func (s *SnapSuite) TestConnectionsZeroSlotsOnePlug(c *C) { +func (s *SnapSuite) TestInterfacesZeroSlotsOnePlug(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.Path, Equals, "/v2/connections") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) @@ -57,13 +58,13 @@ "Slot Plug\n" + "- keyboard-lights:capslock-led\n" c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) } -func (s *SnapSuite) TestConnectionsZeroPlugsOneSlot(c *C) { +func (s *SnapSuite) TestInterfacesZeroPlugsOneSlot(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.Path, Equals, "/v2/connections") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) @@ -88,13 +89,13 @@ "Slot Plug\n" + "canonical-pi2:pin-13 -\n" c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) } -func (s *SnapSuite) TestConnectionsOneSlotOnePlug(c *C) { +func (s *SnapSuite) TestInterfacesOneSlotOnePlug(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.Path, Equals, "/v2/connections") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) @@ -139,7 +140,7 @@ "Slot Plug\n" + "canonical-pi2:pin-13 keyboard-lights:capslock-led\n" c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) s.SetUpTest(c) // should be the same @@ -147,7 +148,7 @@ c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) s.SetUpTest(c) // and the same again @@ -155,13 +156,13 @@ c.Assert(err, IsNil) c.Assert(rest, DeepEquals, []string{}) c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) } -func (s *SnapSuite) TestConnectionsTwoPlugs(c *C) { +func (s *SnapSuite) TestInterfacesTwoPlugs(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.Path, Equals, "/v2/connections") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) @@ -196,13 +197,13 @@ "Slot Plug\n" + "canonical-pi2:pin-13 keyboard-lights:capslock-led,keyboard-lights:scrollock-led\n" c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) } -func (s *SnapSuite) TestConnectionsPlugsWithCommonName(c *C) { +func (s *SnapSuite) TestInterfacesPlugsWithCommonName(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.Path, Equals, "/v2/connections") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) @@ -263,13 +264,13 @@ "Slot Plug\n" + "canonical-pi2:network-listening paste-daemon,time-daemon\n" c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) } -func (s *SnapSuite) TestConnectionsOsSnapSlots(c *C) { +func (s *SnapSuite) TestInterfacesOsSnapSlots(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.Path, Equals, "/v2/connections") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) @@ -330,13 +331,13 @@ "Slot Plug\n" + ":network-listening paste-daemon,time-daemon\n" c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) } -func (s *SnapSuite) TestConnectionsTwoSlotsAndFiltering(c *C) { +func (s *SnapSuite) TestInterfacesTwoSlotsAndFiltering(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.Path, Equals, "/v2/connections") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) @@ -379,13 +380,13 @@ "Slot Plug\n" + "canonical-pi2:debug-console core\n" c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) } -func (s *SnapSuite) TestConnectionsOfSpecificSnap(c *C) { +func (s *SnapSuite) TestInterfacesOfSpecificSnap(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.Path, Equals, "/v2/connections") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) @@ -423,13 +424,13 @@ "wake-up-alarm:toggle -\n" + "wake-up-alarm:snooze -\n" c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) } -func (s *SnapSuite) TestConnectionsOfSystemNicknameSnap(c *C) { +func (s *SnapSuite) TestInterfacesOfSystemNicknameSnap(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.Path, Equals, "/v2/connections") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) @@ -466,7 +467,7 @@ "Slot Plug\n" + ":core-support core:core-support-plug\n" c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) s.ResetStdStreams() @@ -478,13 +479,13 @@ "Slot Plug\n" + ":core-support core:core-support-plug\n" c.Assert(s.Stdout(), Equals, expectedStdoutSystem) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) } -func (s *SnapSuite) TestConnectionsOfSpecificSnapAndSlot(c *C) { +func (s *SnapSuite) TestInterfacesOfSpecificSnapAndSlot(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.Path, Equals, "/v2/connections") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) @@ -521,13 +522,13 @@ "Slot Plug\n" + "wake-up-alarm:snooze -\n" c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) } -func (s *SnapSuite) TestConnectionsNothingAtAll(c *C) { +func (s *SnapSuite) TestInterfacesNothingAtAll(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.Path, Equals, "/v2/connections") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) @@ -545,10 +546,10 @@ c.Assert(s.Stderr(), Equals, "") } -func (s *SnapSuite) TestConnectionsOfSpecificType(c *C) { +func (s *SnapSuite) TestInterfacesOfSpecificType(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.Path, Equals, "/v2/connections") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) @@ -587,13 +588,13 @@ "wake-up-alarm:toggle -\n" + "wake-up-alarm:snooze -\n" c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) } -func (s *SnapSuite) TestConnectionsCompletion(c *C) { +func (s *SnapSuite) TestInterfacesCompletion(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { switch r.URL.Path { - case "/v2/interfaces": + case "/v2/connections": c.Assert(r.Method, Equals, "GET") EncodeResponseBody(c, w, map[string]interface{}{ "type": "sync", @@ -628,26 +629,26 @@ c.Assert(s.Stderr(), Equals, "") } -func (s *SnapSuite) TestConnectionsCoreNicknamedSystem(c *C) { +func (s *SnapSuite) TestInterfacesCoreNicknamedSystem(c *C) { s.checkConnectionsSystemCoreRemapping(c, "core", "system") } -func (s *SnapSuite) TestConnectionsSnapdNicknamedSystem(c *C) { +func (s *SnapSuite) TestInterfacesSnapdNicknamedSystem(c *C) { s.checkConnectionsSystemCoreRemapping(c, "snapd", "system") } -func (s *SnapSuite) TestConnectionsSnapdNicknamedCore(c *C) { +func (s *SnapSuite) TestInterfacesSnapdNicknamedCore(c *C) { s.checkConnectionsSystemCoreRemapping(c, "snapd", "core") } -func (s *SnapSuite) TestConnectionsCoreSnap(c *C) { +func (s *SnapSuite) TestInterfacesCoreSnap(c *C) { s.checkConnectionsSystemCoreRemapping(c, "core", "core") } func (s *SnapSuite) checkConnectionsSystemCoreRemapping(c *C, apiSnapName, cliSnapName string) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { c.Check(r.Method, Equals, "GET") - c.Check(r.URL.Path, Equals, "/v2/interfaces") + c.Check(r.URL.Path, Equals, "/v2/connections") body, err := ioutil.ReadAll(r.Body) c.Check(err, IsNil) c.Check(body, DeepEquals, []byte{}) @@ -670,5 +671,5 @@ "Slot Plug\n" + ":network -\n" c.Assert(s.Stdout(), Equals, expectedStdout) - c.Assert(s.Stderr(), Equals, "") + c.Assert(s.Stderr(), testutil.EqualsWrapped, InterfacesDeprecationNotice) } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_keys.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_keys.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_keys.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_keys.go 2020-06-05 13:13:49.000000000 +0000 @@ -47,6 +47,7 @@ "json": i18n.G("Output results in JSON format"), }, nil) cmd.hidden = true + cmd.completeHidden = true } // Key represents a key that can be used for signing assertions. diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_known.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_known.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_known.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_known.go 2020-06-05 13:13:49.000000000 +0000 @@ -23,12 +23,14 @@ "fmt" "strings" + "github.com/jessevdk/go-flags" + "golang.org/x/xerrors" + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/store" - - "github.com/jessevdk/go-flags" ) type cmdKnown struct { @@ -40,6 +42,7 @@ } `positional-args:"true" required:"true"` Remote bool `long:"remote"` + Direct bool `long:"direct"` } var shortKnownHelp = i18n.G("Show known assertions of the provided type") @@ -52,7 +55,12 @@ func init() { addCommand("known", shortKnownHelp, longKnownHelp, func() flags.Commander { return &cmdKnown{} - }, nil, []argDesc{ + }, map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "remote": i18n.G("Query the store for the assertion, via snapd if possible"), + // TRANSLATORS: This should not start with a lowercase letter. + "direct": i18n.G("Query the store for the assertion, without attempting to go via snapd"), + }, []argDesc{ { // TRANSLATORS: This needs to begin with < and end with > name: i18n.G(""), @@ -73,7 +81,7 @@ var user *auth.UserState // FIXME: set auth context - var authContext auth.AuthContext + var storeCtx store.DeviceAndAuthContext at := asserts.Type(typeName) if at == nil { @@ -84,7 +92,7 @@ return nil, fmt.Errorf("cannot query remote assertion: %v", err) } - sto := storeNew(nil, authContext) + sto := storeNew(nil, storeCtx) as, err := sto.Assertion(at, primaryKeys, user) if err != nil { return nil, err @@ -110,10 +118,21 @@ var assertions []asserts.Assertion var err error - if x.Remote { + switch { + case x.Remote && !x.Direct: + // --remote will query snapd + assertions, err = x.client.Known(string(x.KnownOptions.AssertTypeName), headers, &client.KnownOptions{Remote: true}) + // if snapd is unavailable automatically fallback + var connErr client.ConnectionError + if xerrors.As(err, &connErr) { + assertions, err = downloadAssertion(string(x.KnownOptions.AssertTypeName), headers) + } + case x.Direct: + // --direct implies remote assertions, err = downloadAssertion(string(x.KnownOptions.AssertTypeName), headers) - } else { - assertions, err = x.client.Known(string(x.KnownOptions.AssertTypeName), headers) + default: + // default is to look only local + assertions, err = x.client.Known(string(x.KnownOptions.AssertTypeName), headers, nil) } if err != nil { return err diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_known_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_known_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_known_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_known_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -28,7 +28,7 @@ "github.com/jessevdk/go-flags" "gopkg.in/check.v1" - "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/store" snap "github.com/snapcore/snapd/cmd/snap" @@ -50,16 +50,85 @@ AcLorsomethingthatlooksvaguelylikeasignature== ` -func (s *SnapSuite) TestKnownRemote(c *check.C) { +func (s *SnapSuite) TestKnownViaSnapd(c *check.C) { + n := 0 + expectedQuery := url.Values{ + "series": []string{"16"}, + "brand-id": []string{"canonical"}, + "model": []string{"pi99"}, + } + + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.URL.Path, check.Equals, "/v2/assertions/model") + c.Check(r.URL.Query(), check.DeepEquals, expectedQuery) + w.Header().Set("X-Ubuntu-Assertions-Count", "1") + fmt.Fprintln(w, mockModelAssertion) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + n++ + }) + + // first run "normal" + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "model", "series=16", "brand-id=canonical", "model=pi99"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, mockModelAssertion) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 1) + + // then with "--remote" + n = 0 + s.stdout.Reset() + expectedQuery["remote"] = []string{"true"} + rest, err = snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical", "model=pi99"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, mockModelAssertion) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestKnownRemoteViaSnapd(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.URL.Path, check.Equals, "/v2/assertions/model") + c.Check(r.URL.Query(), check.DeepEquals, url.Values{ + "series": []string{"16"}, + "brand-id": []string{"canonical"}, + "model": []string{"pi99"}, + "remote": []string{"true"}, + }) + w.Header().Set("X-Ubuntu-Assertions-Count", "1") + fmt.Fprintln(w, mockModelAssertion) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + n++ + }) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical", "model=pi99"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, mockModelAssertion) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestKnownRemoteDirect(c *check.C) { var server *httptest.Server - restorer := snap.MockStoreNew(func(cfg *store.Config, auth auth.AuthContext) *store.Store { + restorer := snap.MockStoreNew(func(cfg *store.Config, stoCtx store.DeviceAndAuthContext) *store.Store { if cfg == nil { cfg = store.DefaultConfig() } serverURL, _ := url.Parse(server.URL) cfg.AssertionsBaseURL = serverURL - return store.New(cfg, auth) + return store.New(cfg, stoCtx) }) defer restorer() @@ -78,7 +147,58 @@ n++ })) - rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical", "model=pi99"}) + // first test "--remote --direct" + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "--direct", "model", "series=16", "brand-id=canonical", "model=pi99"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, mockModelAssertion) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 1) + + // "--direct" behave the same as "--remote --direct" + s.stdout.Reset() + n = 0 + rest, err = snap.Parser(snap.Client()).ParseArgs([]string{"known", "--direct", "model", "series=16", "brand-id=canonical", "model=pi99"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, mockModelAssertion) + c.Check(s.Stderr(), check.Equals, "") + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestKnownRemoteAutoFallback(c *check.C) { + var server *httptest.Server + + restorer := snap.MockStoreNew(func(cfg *store.Config, stoCtx store.DeviceAndAuthContext) *store.Store { + if cfg == nil { + cfg = store.DefaultConfig() + } + serverURL, _ := url.Parse(server.URL) + cfg.AssertionsBaseURL = serverURL + return store.New(cfg, stoCtx) + }) + defer restorer() + + n := 0 + server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + c.Assert(r.URL.Path, check.Matches, ".*/assertions/.*") // sanity check request + switch n { + case 0: + c.Check(r.Method, check.Equals, "GET") + c.Check(r.URL.Path, check.Equals, "/api/v1/snaps/assertions/model/16/canonical/pi99") + fmt.Fprintln(w, mockModelAssertion) + default: + c.Fatalf("expected to get 1 requests, now on %d", n+1) + } + n++ + })) + + cli := snap.Client() + cli.Hijack(func(*http.Request) (*http.Response, error) { + return nil, client.ConnectionError{Err: fmt.Errorf("no snapd")} + }) + + rest, err := snap.Parser(cli).ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical", "model=pi99"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Equals, mockModelAssertion) @@ -86,7 +206,7 @@ } func (s *SnapSuite) TestKnownRemoteMissingPrimaryKey(c *check.C) { - _, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "model", "series=16", "brand-id=canonical"}) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"known", "--remote", "--direct", "model", "series=16", "brand-id=canonical"}) c.Assert(err, check.ErrorMatches, `cannot query remote assertion: must provide primary key: model`) } @@ -106,4 +226,5 @@ }) c.Check(snap.AssertTypeNameCompletion("v"), check.DeepEquals, []flags.Completion{{Item: "validation"}}) + c.Check(n, check.Equals, 1) } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_list.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_list.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_list.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_list.go 2020-06-05 13:13:49.000000000 +0000 @@ -30,7 +30,6 @@ "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" - "github.com/snapcore/snapd/strutil" ) var shortListHelp = i18n.G("List installed snaps") @@ -69,38 +68,20 @@ // snapd will give us and we want // "" (local snap) "-" -// risk risk -// track track (not yet returned by snapd) -// track/stable track +// latest/risk latest/risk // track/risk track/risk -// risk/branch risk/… // track/risk/branch track/risk/… +// anything else SISO func fmtChannel(ch string) string { if ch == "" { // "" -> "-" (local snap) return "-" } - idx := strings.IndexByte(ch, '/') - if idx < 0 { - // risk -> risk + if strings.Count(ch, "/") != 2 { return ch } - first, rest := ch[:idx], ch[idx+1:] - if rest == "stable" && first != "" { - // track/stable -> track - return first - } - if idx2 := strings.IndexByte(rest, '/'); idx2 >= 0 { - // track/risk/branch -> track/risk/… - return ch[:idx2+idx+2] + "…" - } - // so it's foo/bar -> either risk/branch, or track/risk. - if strutil.ListContains(channelRisks, first) { - // risk/branch -> risk/… - return first + "/…" - } - // track/risk -> track/risk - return ch + idx := strings.LastIndexByte(ch, '/') + return ch[:idx+1] + "…" } func (x *cmdList) Execute(args []string) error { diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_list_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_list_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_list_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_list_test.go 2020-06-05 13:13:49.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, "") } @@ -216,15 +227,11 @@ } for _, t := range []tableT{ {"", "-"}, - {"stable", "stable"}, - {"edge", "edge"}, - {"foo/stable", "foo"}, + {"latest/stable", "latest/stable"}, + {"foo/stable", "foo/stable"}, {"foo/edge", "foo/edge"}, - {"foo", "foo"}, {"foo/stable/bar", "foo/stable/…"}, {"foo/edge/bar", "foo/edge/…"}, - {"stable/bar", "stable/…"}, - {"edge/bar", "edge/…"}, } { c.Check(snap.FormatChannel(t.channel), check.Equals, t.expected, check.Commentf(t.channel)) } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_model.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_model.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_model.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_model.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,327 @@ +// -*- 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 { + clientMixin + 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(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 and model/model + display-name differently on just + // `snap model` w/o opts + if x.Serial || x.Verbose { + fmt.Fprintf(w, "brand-id:\t%s\n", brandIDHeader) + fmt.Fprintf(w, "model:\t%s\n", modelHeader) + } 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)) + + // 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.37.4ubuntu0.1/cmd/snap/cmd_model_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_model_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_model_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_model_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,486 @@ +// -*- 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) TestModelVerboseDisplayName(c *check.C) { + s.RedirectClientToTestServer( + makeHappyTestServerHandler( + c, + simpleHappyResponder(happyModelWithDisplayNameAssertionResponse), + 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 +display-name: Model Name +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.37.4ubuntu0.1/cmd/snap/cmd_pack.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_pack.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_pack.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_pack.go 2020-06-05 13:13:49.000000000 +0000 @@ -28,11 +28,15 @@ "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/pack" + + // for SanitizePlugsSlots + "github.com/snapcore/snapd/interfaces/builtin" ) type packCmd struct { CheckSkeleton bool `long:"check-skeleton"` Filename string `long:"filename"` + Compression string `long:"compression" hidden:"yes"` Positional struct { SnapDir string `positional-arg-name:""` TargetDir string `positional-arg-name:""` @@ -69,6 +73,8 @@ "check-skeleton": i18n.G("Validate snap-dir metadata only"), // TRANSLATORS: This should not start with a lowercase letter. "filename": i18n.G("Output to this filename"), + // TRANSLATORS: This should not start with a lowercase letter. + "compression": i18n.G("Compression to use (e.g. xz)"), }, nil) cmd.extra = func(cmd *flags.Command) { // TRANSLATORS: this describes the default filename for a snap, e.g. core_16-2.35.2_amd64.snap @@ -77,6 +83,10 @@ } func (x *packCmd) Execute([]string) error { + // plug/slot sanitization is disabled (no-op) by default at the package level for "snap" command, + // for "snap pack" however we want real validation. + snap.SanitizePlugsSlots = builtin.SanitizePlugsSlots + if x.Positional.TargetDir != "" && x.Filename != "" && filepath.IsAbs(x.Filename) { return fmt.Errorf(i18n.G("you can't specify an absolute filename while also specifying target dir.")) } @@ -89,14 +99,18 @@ } if x.CheckSkeleton { - err := pack.CheckSkeleton(x.Positional.SnapDir) + err := pack.CheckSkeleton(Stderr, x.Positional.SnapDir) if err == snap.ErrMissingPaths { return nil } return err } - snapPath, err := pack.Snap(x.Positional.SnapDir, x.Positional.TargetDir, x.Filename) + snapPath, err := pack.Snap(x.Positional.SnapDir, &pack.Options{ + TargetDir: x.Positional.TargetDir, + SnapName: x.Filename, + Compression: x.Compression, + }) if err != nil { // TRANSLATORS: the %q is the snap-dir (the first positional // argument to the command); the %v is an error diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_pack_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_pack_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_pack_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_pack_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -1,6 +1,7 @@ package main_test import ( + "fmt" "io/ioutil" "os" "path/filepath" @@ -71,6 +72,20 @@ c.Assert(err, check.ErrorMatches, `cannot validate snap "foo": application ("bar" common-id "org.foo.foo" must be unique, already used by application "foo"|"foo" common-id "org.foo.foo" must be unique, already used by application "bar")`) } +func (s *SnapSuite) TestPackCheckSkeletonWonkyInterfaces(c *check.C) { + snapYaml := ` +name: foo +version: 1.0.1 +slots: + kale: +` + snapDir := makeSnapDirForPack(c, snapYaml) + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--check-skeleton", snapDir}) + c.Assert(err, check.IsNil) + c.Check(s.stderr.String(), check.Equals, "snap \"foo\" has bad plugs or slots: kale (unknown interface \"kale\")\n") +} + func (s *SnapSuite) TestPackPacksFailsForMissingPaths(c *check.C) { _, r := logger.MockLogger() defer r() @@ -101,3 +116,27 @@ c.Assert(err, check.IsNil) c.Assert(matches, check.HasLen, 1) } + +func (s *SnapSuite) TestPackPacksASnapWithCompressionHappy(c *check.C) { + snapDir := makeSnapDirForPack(c, "name: hello\nversion: 1.0") + + for _, comp := range []string{"xz", "lzo"} { + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--compression", comp, snapDir, snapDir}) + c.Assert(err, check.IsNil) + + matches, err := filepath.Glob(snapDir + "/hello*.snap") + c.Assert(err, check.IsNil) + c.Assert(matches, check.HasLen, 1) + err = os.Remove(matches[0]) + c.Assert(err, check.IsNil) + } +} + +func (s *SnapSuite) TestPackPacksASnapWithCompressionUnhappy(c *check.C) { + snapDir := makeSnapDirForPack(c, "name: hello\nversion: 1.0") + + for _, comp := range []string{"gzip", "zstd", "silly"} { + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"pack", "--compression", comp, snapDir, snapDir}) + c.Assert(err, check.ErrorMatches, fmt.Sprintf(`cannot pack "/.*": cannot use compression %q`, comp)) + } +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_prefer_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_prefer_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_prefer_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_prefer_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/snap/cmd_prepare_image.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_prepare_image.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_prepare_image.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_prepare_image.go 2020-06-05 13:13:49.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,7 +20,7 @@ package main import ( - "path/filepath" + "strings" "github.com/jessevdk/go-flags" @@ -29,27 +29,41 @@ ) type cmdPrepareImage struct { + Classic bool `long:"classic"` + Architecture string `long:"arch"` + Positional struct { ModelAssertionFn string - Rootdir string + TargetDir string } `positional-args:"yes" required:"yes"` - ExtraSnaps []string `long:"extra-snaps"` - Channel string `long:"channel" default:"stable"` + Channel string `long:"channel"` + // TODO: introduce SnapWithChannel? + Snaps []string `long:"snap" value-name:"[=]"` + ExtraSnaps []string `long:"extra-snaps" hidden:"yes"` // DEPRECATED } func init() { - cmd := addCommand("prepare-image", - i18n.G("Prepare a core device image"), + addCommand("prepare-image", + i18n.G("Prepare a device image"), i18n.G(` -The prepare-image command performs some of the steps necessary for creating -core device images. -`), - func() flags.Commander { - return &cmdPrepareImage{} - }, map[string]string{ +The prepare-image command performs some of the steps necessary for +creating device images. + +For core images it is not invoked directly but usually via +ubuntu-image. + +For preparing classic images it supports a --classic mode`), + func() flags.Commander { return &cmdPrepareImage{} }, + map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "classic": i18n.G("Enable classic mode to prepare a classic model image"), // TRANSLATORS: This should not start with a lowercase letter. - "extra-snaps": i18n.G("Extra snaps to be installed"), + "arch": i18n.G("Specify an architecture for snaps for --classic when the model does not"), + // TRANSLATORS: This should not start with a lowercase letter. + "snap": i18n.G("Include the given snap from the store or a local file and/or specify the channel to track for the given snap"), + // TRANSLATORS: This should not start with a lowercase letter. + "extra-snaps": i18n.G("Extra snaps to be installed (DEPRECATED)"), // TRANSLATORS: This should not start with a lowercase letter. "channel": i18n.G("The channel to use"), }, []argDesc{ @@ -60,23 +74,44 @@ desc: i18n.G("The model assertion name"), }, { // TRANSLATORS: This needs to begin with < and end with > - name: i18n.G(""), + name: i18n.G(""), // TRANSLATORS: This should not start with a lowercase letter. - desc: i18n.G("The output directory"), + desc: i18n.G("The target directory"), }, }) - cmd.hidden = true } +var imagePrepare = image.Prepare + func (x *cmdPrepareImage) Execute(args []string) error { opts := &image.Options{ - ModelFile: x.Positional.ModelAssertionFn, + Snaps: x.ExtraSnaps, + ModelFile: x.Positional.ModelAssertionFn, + Channel: x.Channel, + Architecture: x.Architecture, + } + + snaps := make([]string, 0, len(x.Snaps)+len(x.ExtraSnaps)) + snapChannels := make(map[string]string) + for _, snapWChannel := range x.Snaps { + snapAndChannel := strings.SplitN(snapWChannel, "=", 2) + snaps = append(snaps, snapAndChannel[0]) + if len(snapAndChannel) == 2 { + snapChannels[snapAndChannel[0]] = snapAndChannel[1] + } + } - RootDir: filepath.Join(x.Positional.Rootdir, "image"), - GadgetUnpackDir: filepath.Join(x.Positional.Rootdir, "gadget"), - Channel: x.Channel, - Snaps: x.ExtraSnaps, + snaps = append(snaps, x.ExtraSnaps...) + + if len(snaps) != 0 { + opts.Snaps = snaps + } + if len(snapChannels) != 0 { + opts.SnapChannels = snapChannels } - return image.Prepare(opts) + opts.PrepareDir = x.Positional.TargetDir + opts.Classic = x.Classic + + return imagePrepare(opts) } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_prepare_image_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_prepare_image_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_prepare_image_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_prepare_image_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,115 @@ +// -*- 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" + + snap "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/image" +) + +type SnapPrepareImageSuite struct { + BaseSnapSuite +} + +var _ = Suite(&SnapPrepareImageSuite{}) + +func (s *SnapPrepareImageSuite) TestPrepareImageCore(c *C) { + var opts *image.Options + prep := func(o *image.Options) error { + opts = o + return nil + } + r := snap.MockImagePrepare(prep) + defer r() + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "model", "prepare-dir"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + + c.Check(opts, DeepEquals, &image.Options{ + ModelFile: "model", + PrepareDir: "prepare-dir", + }) +} + +func (s *SnapPrepareImageSuite) TestPrepareImageClassic(c *C) { + var opts *image.Options + prep := func(o *image.Options) error { + opts = o + return nil + } + r := snap.MockImagePrepare(prep) + defer r() + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "--classic", "model", "prepare-dir"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + + c.Check(opts, DeepEquals, &image.Options{ + Classic: true, + ModelFile: "model", + PrepareDir: "prepare-dir", + }) +} + +func (s *SnapPrepareImageSuite) TestPrepareImageClassicArch(c *C) { + var opts *image.Options + prep := func(o *image.Options) error { + opts = o + return nil + } + r := snap.MockImagePrepare(prep) + defer r() + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "--classic", "--arch", "i386", "model", "prepare-dir"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + + c.Check(opts, DeepEquals, &image.Options{ + Classic: true, + Architecture: "i386", + ModelFile: "model", + PrepareDir: "prepare-dir", + }) +} + +func (s *SnapPrepareImageSuite) TestPrepareImageExtraSnaps(c *C) { + var opts *image.Options + prep := func(o *image.Options) error { + opts = o + return nil + } + r := snap.MockImagePrepare(prep) + defer r() + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"prepare-image", "model", "prepare-dir", "--channel", "candidate", "--snap", "foo", "--snap", "bar=t/edge", "--snap", "local.snap", "--extra-snaps", "local2.snap", "--extra-snaps", "store-snap"}) + c.Assert(err, IsNil) + c.Assert(rest, DeepEquals, []string{}) + + c.Check(opts, DeepEquals, &image.Options{ + ModelFile: "model", + Channel: "candidate", + PrepareDir: "prepare-dir", + Snaps: []string{"foo", "bar", "local.snap", "local2.snap", "store-snap"}, + SnapChannels: map[string]string{"bar": "t/edge"}, + }) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_remodel.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_remodel.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_remodel.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_remodel.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,86 @@ +// -*- 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" + "io/ioutil" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +var ( + shortRemodelHelp = i18n.G("Remodel this device") + longRemodelHelp = i18n.G(` +The remodel command changes the model assertion of the device, either to a new +revision or a full new model. + +In the process it applies any implied changes to the device: new required +snaps, new kernel or gadget etc. +`) +) + +type cmdRemodel struct { + waitMixin + RemodelOptions struct { + NewModelFile flags.Filename + } `positional-args:"true" required:"true"` +} + +func init() { + cmd := addCommand("remodel", + shortRemodelHelp, + longRemodelHelp, + func() flags.Commander { + return &cmdRemodel{} + }, nil, []argDesc{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("New model file"), + }}) + cmd.hidden = true +} + +func (x *cmdRemodel) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + newModelFile := x.RemodelOptions.NewModelFile + modelData, err := ioutil.ReadFile(string(newModelFile)) + if err != nil { + return err + } + changeID, err := x.client.Remodel(modelData) + if err != nil { + return fmt.Errorf("cannot remodel: %v", err) + } + + if _, err := x.wait(changeID); err != nil { + if err == noWait { + return nil + } + return err + } + fmt.Fprintf(Stdout, i18n.G("New model %s set\n"), newModelFile) + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_remove_user.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_remove_user.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_remove_user.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_remove_user.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,75 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "fmt" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + + "github.com/jessevdk/go-flags" +) + +var shortRemoveUserHelp = i18n.G("Remove a local system user") +var longRemoveUserHelp = i18n.G(` +The remove-user command removes a local system user. +`) + +type cmdRemoveUser struct { + clientMixin + Positional struct { + Username string + } `positional-args:"yes"` +} + +func init() { + cmd := addCommand("remove-user", shortRemoveUserHelp, longRemoveUserHelp, func() flags.Commander { return &cmdRemoveUser{} }, + map[string]string{}, []argDesc{{ + // TRANSLATORS: This is a noun and it needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter + desc: i18n.G("The username to remove"), + }}) + cmd.hidden = true +} + +func (x *cmdRemoveUser) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + username := x.Positional.Username + + options := client.RemoveUserOptions{ + Username: username, + } + + removed, err := x.client.RemoveUser(&options) + if err != nil { + return err + } + if len(removed) != 1 { + return fmt.Errorf("internal error: RemoveUser returned unexpected number of removed users: %v", len(removed)) + } + fmt.Fprintf(Stdout, i18n.G("removed user %q\n"), removed[0].Username) + + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_remove_user_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_remove_user_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_remove_user_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_remove_user_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,109 @@ +// -*- 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 ( + "encoding/json" + "fmt" + "net/http" + + "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +var removeUserJsonFmtReplyHappy = `{ + "type": "sync", + "result": { + "removed": [{"username": %q}] + } +}` + +var removeUserJsonReplyTooMany = `{ + "type": "sync", + "result": { + "removed": [{"username": "too"}, {"username": "many"}] + } +}` + +var removeUserJsonReplyTooFew = `{ + "type": "sync", + "result": { + "removed": [] + } +}` + +func makeRemoveUserChecker(c *check.C, n *int, username string, fmtJsonReply string) func(w http.ResponseWriter, r *http.Request) { + f := func(w http.ResponseWriter, r *http.Request) { + switch *n { + case 0: + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/users") + var gotBody map[string]interface{} + dec := json.NewDecoder(r.Body) + err := dec.Decode(&gotBody) + c.Assert(err, check.IsNil) + + wantBody := map[string]interface{}{ + "username": username, + "action": "remove", + } + c.Check(gotBody, check.DeepEquals, wantBody) + + fmt.Fprint(w, fmtJsonReply) + default: + c.Fatalf("got too many requests (now on %d)", *n+1) + } + + *n++ + } + return f +} + +func (s *SnapSuite) TestRemoveUser(c *check.C) { + n := 0 + username := "karl" + s.RedirectClientToTestServer(makeRemoveUserChecker(c, &n, username, fmt.Sprintf(removeUserJsonFmtReplyHappy, username))) + + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove-user", "karl"}) + c.Assert(err, check.IsNil) + c.Check(rest, check.DeepEquals, []string{}) + c.Check(n, check.Equals, 1) + c.Assert(s.Stdout(), check.Equals, fmt.Sprintf("removed user %q\n", username)) + c.Assert(s.Stderr(), check.Equals, "") +} + +func (s *SnapSuite) TestRemoveUserUnhappyTooMany(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeRemoveUserChecker(c, &n, "karl", removeUserJsonReplyTooMany)) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove-user", "karl"}) + c.Assert(err, check.ErrorMatches, `internal error: RemoveUser returned unexpected number of removed users: 2`) + c.Check(n, check.Equals, 1) +} + +func (s *SnapSuite) TestRemoveUserUnhappyTooFew(c *check.C) { + n := 0 + s.RedirectClientToTestServer(makeRemoveUserChecker(c, &n, "karl", removeUserJsonReplyTooFew)) + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove-user", "karl"}) + c.Assert(err, check.ErrorMatches, `internal error: RemoveUser returned unexpected number of removed users: 0`) + c.Check(n, check.Equals, 1) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_repair_repairs.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_repair_repairs.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_repair_repairs.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_repair_repairs.go 2020-06-05 13:13:49.000000000 +0000 @@ -69,6 +69,10 @@ } func (x *cmdShowRepair) Execute(args []string) error { + if len(x.Positional.Repair) == 0 { + return fmt.Errorf("no given. Try 'snap repairs' to list all repairs or specify a specific repair id.") + } + return runSnapRepair("show", x.Positional.Repair) } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_repair_repairs_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_repair_repairs_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_repair_repairs_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_repair_repairs_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -20,7 +20,6 @@ package main_test import ( - "os" "path/filepath" . "gopkg.in/check.v1" @@ -32,10 +31,7 @@ ) func mockSnapRepair(c *C) *testutil.MockCmd { - coreLibExecDir := filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir) - err := os.MkdirAll(coreLibExecDir, 0755) - c.Assert(err, IsNil) - return testutil.MockCommand(c, filepath.Join(coreLibExecDir, "snap-repair"), "") + return testutil.MockCommand(c, filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir, "snap-repair"), "") } func (s *SnapSuite) TestSnapShowRepair(c *C) { @@ -52,6 +48,14 @@ }) } +func (s *SnapSuite) TestSnapShowRepairNoArgs(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"repair"}) + c.Assert(err, ErrorMatches, "no given. Try 'snap repairs' to list all repairs or specify a specific repair id.") +} + func (s *SnapSuite) TestSnapListRepairs(c *C) { restore := release.MockOnClassic(false) defer restore() diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_file_access.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_file_access.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_file_access.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_file_access.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,216 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main + +import ( + "fmt" + "os" + "path/filepath" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" +) + +type cmdRoutineFileAccess struct { + clientMixin + FileAccessOptions struct { + Snap installedSnapName + Path flags.Filename + } `positional-args:"true" required:"true"` +} + +var shortRoutineFileAccessHelp = i18n.G("Return information about file access by a snap") +var longRoutineFileAccessHelp = i18n.G(` +The file-access command returns information about a snap's file system access. + +This command is used by the xdg-document-portal service to identify +files that do not need to be proxied to provide access within +confinement. + +File paths are interpreted as host file system paths. The tool may +return false negatives (e.g. report that a file path is unreadable, +despite being readable under a different path). It also does not +check if file system permissions would render a file unreadable. +`) + +func init() { + addRoutineCommand("file-access", shortRoutineFileAccessHelp, longRoutineFileAccessHelp, func() flags.Commander { + return &cmdRoutineFileAccess{} + }, nil, []argDesc{ + { + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Snap name"), + }, + { + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("File path"), + }, + }) +} + +func (x *cmdRoutineFileAccess) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snapName := string(x.FileAccessOptions.Snap) + path := string(x.FileAccessOptions.Path) + + snap, _, err := x.client.Snap(snapName) + if err != nil { + return fmt.Errorf("cannot retrieve info for snap %q: %v", snapName, err) + } + + // Check whether the snap has home or removable-media plugs connected + connections, err := x.client.Connections(&client.ConnectionOptions{ + Snap: snap.Name, + }) + if err != nil { + return fmt.Errorf("cannot get connections for snap %q: %v", snap.Name, err) + } + var hasHome, hasRemovableMedia bool + for _, conn := range connections.Established { + if conn.Plug.Snap != snap.Name { + continue + } + switch conn.Interface { + case "home": + hasHome = true + case "removable-media": + hasRemovableMedia = true + } + } + + access, err := x.checkAccess(snap, hasHome, hasRemovableMedia, path) + if err != nil { + return err + } + fmt.Fprintln(Stdout, access) + return nil +} + +type FileAccess string + +const ( + FileAccessHidden FileAccess = "hidden" + FileAccessReadOnly FileAccess = "read-only" + FileAccessReadWrite FileAccess = "read-write" +) + +func splitPathAbs(path string) ([]string, error) { + // Abs also cleans the path, removing any ".." components + path, err := filepath.Abs(path) + if err != nil { + return nil, err + } + // Ignore the empty component before the first slash + return strings.Split(path, string(os.PathSeparator))[1:], nil +} + +func pathHasPrefix(path, prefix []string) bool { + if len(path) < len(prefix) { + return false + } + for i := range prefix { + if path[i] != prefix[i] { + return false + } + } + return true +} + +func (x *cmdRoutineFileAccess) checkAccess(snap *client.Snap, hasHome, hasRemovableMedia bool, path string) (FileAccess, error) { + // Classic confinement snaps run in the host system namespace, + // so can see everything. + if snap.Confinement == client.ClassicConfinement { + return FileAccessReadWrite, nil + } + + pathParts, err := splitPathAbs(path) + if err != nil { + return "", err + } + + // Snaps have access to $SNAP_DATA and $SNAP_COMMON + if pathHasPrefix(pathParts, []string{"var", "snap", snap.Name}) { + if len(pathParts) == 3 { + return FileAccessReadOnly, nil + } + switch pathParts[3] { + case "common", "current", snap.Revision.String(): + return FileAccessReadWrite, nil + default: + return FileAccessReadOnly, nil + } + } + + // Snaps with removable-media plugged can access removable + // media mount points. + if hasRemovableMedia { + if pathHasPrefix(pathParts, []string{"mnt"}) || pathHasPrefix(pathParts, []string{"media"}) || pathHasPrefix(pathParts, []string{"run", "media"}) { + return FileAccessReadWrite, nil + } + } + + usr, err := userCurrent() + if err != nil { + return "", fmt.Errorf("cannot get the current user: %v", err) + } + + home, err := splitPathAbs(usr.HomeDir) + if err != nil { + return "", err + } + if pathHasPrefix(pathParts, home) { + pathInHome := pathParts[len(home):] + // Snaps have access to $SNAP_USER_DATA and $SNAP_USER_COMMON + if pathHasPrefix(pathInHome, []string{"snap"}) { + if !pathHasPrefix(pathInHome, []string{"snap", snap.Name}) { + return FileAccessHidden, nil + } + if len(pathInHome) < 3 { + return FileAccessReadOnly, nil + } + switch pathInHome[2] { + case "common", "current", snap.Revision.String(): + return FileAccessReadWrite, nil + default: + return FileAccessReadOnly, nil + } + } + // If the home interface is connected, the snap has + // access to other files in home, except top-level dot + // files. + if hasHome { + if len(pathInHome) == 0 || !strings.HasPrefix(pathInHome[0], ".") { + return FileAccessReadWrite, nil + } + } + } + + return FileAccessHidden, nil +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_file_access_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_file_access_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_file_access_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_file_access_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,185 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2020 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package main_test + +import ( + "fmt" + "net/http" + "net/url" + "os/user" + "path/filepath" + "strings" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" +) + +type SnapRoutineFileAccessSuite struct { + BaseSnapSuite + + fakeHome string +} + +var _ = Suite(&SnapRoutineFileAccessSuite{}) + +func (s *SnapRoutineFileAccessSuite) SetUpTest(c *C) { + s.BaseSnapSuite.SetUpTest(c) + + s.fakeHome = c.MkDir() + u, err := user.Current() + c.Assert(err, IsNil) + s.AddCleanup(snap.MockUserCurrent(func() (*user.User, error) { + return &user.User{Uid: u.Uid, HomeDir: s.fakeHome}, nil + })) +} + +func (s *SnapRoutineFileAccessSuite) setUpClient(c *C, isClassic, hasHome, hasRemovableMedia bool) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/snaps/hello": + c.Check(r.Method, Equals, "GET") + // snap hello at revision 100 + response := mockInfoJSONNoLicense + if isClassic { + response = strings.Replace(response, `"confinement": "strict"`, `"confinement": "classic"`, 1) + } + fmt.Fprintln(w, response) + case "/v2/connections": + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, url.Values{ + "snap": []string{"hello"}, + }) + connections := []client.Connection{} + if hasHome { + connections = append(connections, client.Connection{ + Slot: client.SlotRef{ + Snap: "core", + Name: "home", + }, + Plug: client.PlugRef{ + Snap: "hello", + Name: "home", + }, + Interface: "home", + }) + } + if hasRemovableMedia { + connections = append(connections, client.Connection{ + Slot: client.SlotRef{ + Snap: "core", + Name: "removable-media", + }, + Plug: client.PlugRef{ + Snap: "hello", + Name: "removable-media", + }, + Interface: "removable-media", + }) + } + result := client.Connections{Established: connections} + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": result, + }) + default: + c.Fatalf("unexpected request: %v", r) + } + }) +} + +func (s *SnapRoutineFileAccessSuite) checkAccess(c *C, path, access string) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "file-access", "hello", path}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, access) + c.Check(s.Stderr(), Equals, "") + s.ResetStdStreams() +} + +func (s *SnapRoutineFileAccessSuite) checkBasicAccess(c *C) { + // Check access to SNAP_DATA and SNAP_COMMON + s.checkAccess(c, "/var/snap", "hidden\n") + s.checkAccess(c, "/var/snap/other-snap", "hidden\n") + s.checkAccess(c, "/var/snap/hello", "read-only\n") + s.checkAccess(c, "/var/snap/hello/common", "read-write\n") + s.checkAccess(c, "/var/snap/hello/current", "read-write\n") + s.checkAccess(c, "/var/snap/hello/100", "read-write\n") + s.checkAccess(c, "/var/snap/hello/99", "read-only\n") + + // Check access to SNAP_USER_DATA and SNAP_USER_COMMON + s.checkAccess(c, filepath.Join(s.fakeHome, "snap"), "hidden\n") + s.checkAccess(c, filepath.Join(s.fakeHome, "snap/other-snap"), "hidden\n") + s.checkAccess(c, filepath.Join(s.fakeHome, "snap/hello"), "read-only\n") + s.checkAccess(c, filepath.Join(s.fakeHome, "snap/hello/common"), "read-write\n") + s.checkAccess(c, filepath.Join(s.fakeHome, "snap/hello/current"), "read-write\n") + s.checkAccess(c, filepath.Join(s.fakeHome, "snap/hello/100"), "read-write\n") + s.checkAccess(c, filepath.Join(s.fakeHome, "snap/hello/99"), "read-only\n") +} + +func (s *SnapRoutineFileAccessSuite) TestAccessDefault(c *C) { + s.setUpClient(c, false, false, false) + s.checkBasicAccess(c) + + // No access to root + s.checkAccess(c, "/", "hidden\n") + s.checkAccess(c, "/usr/lib/libfoo.so", "hidden\n") + // No access to removable media + s.checkAccess(c, "/media/foo", "hidden\n") + // No access to home directory + s.checkAccess(c, s.fakeHome, "hidden\n") + s.checkAccess(c, filepath.Join(s.fakeHome, "Documents"), "hidden\n") +} + +func (s *SnapRoutineFileAccessSuite) TestAccessClassicConfinement(c *C) { + s.setUpClient(c, true, false, false) + + // Classic confinement snaps run in the host file system + // namespace, so have access to everything. + s.checkAccess(c, "/", "read-write\n") + s.checkAccess(c, "/usr/lib/libfoo.so", "read-write\n") + s.checkAccess(c, "/", "read-write\n") + s.checkAccess(c, s.fakeHome, "read-write\n") + s.checkAccess(c, filepath.Join(s.fakeHome, "snap/other-snap"), "read-write\n") +} + +func (s *SnapRoutineFileAccessSuite) TestAccessHomeInterface(c *C) { + s.setUpClient(c, false, true, false) + s.checkBasicAccess(c) + + // Access to non-hidden files in the home directory + s.checkAccess(c, s.fakeHome, "read-write\n") + s.checkAccess(c, filepath.Join(s.fakeHome, "Documents/foo.txt"), "read-write\n") + s.checkAccess(c, filepath.Join(s.fakeHome, "Documents/.hidden"), "read-write\n") + s.checkAccess(c, filepath.Join(s.fakeHome, ".config"), "hidden\n") +} + +func (s *SnapRoutineFileAccessSuite) TestAccessRemovableMedia(c *C) { + s.setUpClient(c, false, false, true) + s.checkBasicAccess(c) + + s.checkAccess(c, "/mnt", "read-write\n") + s.checkAccess(c, "/mnt/path/file.txt", "read-write\n") + s.checkAccess(c, "/media", "read-write\n") + s.checkAccess(c, "/media/path/file.txt", "read-write\n") + s.checkAccess(c, "/run/media", "read-write\n") + s.checkAccess(c, "/run/media/path/file.txt", "read-write\n") +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,35 @@ +// -*- 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/snapcore/snapd/i18n" +) + +type cmdRoutine struct{} + +var shortRoutineHelp = i18n.G("Run routine commands") +var longRoutineHelp = i18n.G(` +The routine command contains a selection of additional sub-commands. + +Routine commands are not intended to be directly invoked by the user. +Instead, they are intended to be called by other programs and produce +machine readable output. +`) diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_portal_info.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_portal_info.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_portal_info.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_portal_info.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,152 @@ +// -*- 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" + "path/filepath" + "text/template" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/sandbox/apparmor" + "github.com/snapcore/snapd/sandbox/cgroup" +) + +type cmdRoutinePortalInfo struct { + clientMixin + PortalInfoOptions struct { + Pid int + } `positional-args:"true" required:"true"` +} + +var shortRoutinePortalInfoHelp = i18n.G("Return information about a process") +var longRoutinePortalInfoHelp = i18n.G(` +The portal-info command returns information about a process in keyfile format. + +This command is used by the xdg-desktop-portal service to retrieve +information about snap confined processes. +`) + +func init() { + addRoutineCommand("portal-info", shortRoutinePortalInfoHelp, longRoutinePortalInfoHelp, func() flags.Commander { + return &cmdRoutinePortalInfo{} + }, nil, []argDesc{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("Process ID of confined app"), + }}) +} + +var ( + cgroupSnapNameFromPid = cgroup.SnapNameFromPid + apparmorSnapAppFromPid = apparmor.SnapAppFromPid +) + +func (x *cmdRoutinePortalInfo) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + snapName, err := cgroupSnapNameFromPid(x.PortalInfoOptions.Pid) + if err != nil { + return err + } + snap, _, err := x.client.Snap(snapName) + if err != nil { + return fmt.Errorf("cannot retrieve info for snap %q: %v", snapName, err) + } + + // Try to identify the application name from AppArmor + var app *client.AppInfo + if snapName, appName, _, err := apparmorSnapAppFromPid(x.PortalInfoOptions.Pid); err == nil && snapName == snap.Name && appName != "" { + for i := range snap.Apps { + if snap.Apps[i].Name == appName { + app = &snap.Apps[i] + break + } + } + } + // As a fallback, pick an app with a desktop file, favouring + // the app named identically to the snap. + if app == nil { + for i := range snap.Apps { + if snap.Apps[i].DesktopFile != "" && (app == nil || snap.Apps[i].Name == snap.Name) { + app = &snap.Apps[i] + } + } + } + + var desktopFile string + if app != nil { + desktopFile = filepath.Base(app.DesktopFile) + } + + // Determine whether the snap has access to the network status + // TODO: use direct API for asking about interface being connected if + // that becomes available + connections, err := x.client.Connections(&client.ConnectionOptions{ + Snap: snap.Name, + Interface: "network-status", + }) + if err != nil { + return fmt.Errorf("cannot get connections for snap %q: %v", snap.Name, err) + } + // XXX: on non-AppArmor systems, or systems where there is only a + // partial AppArmor support, the snap may still be able to access the + // network despite the 'network' interface being disconnected + var hasNetworkStatus bool + for _, conn := range connections.Established { + if conn.Plug.Snap == snap.Name && conn.Interface == "network-status" { + hasNetworkStatus = true + break + } + } + + const portalInfoTemplate = `[Snap Info] +InstanceName={{.Snap.Name}} +{{- if .App}} +AppName={{.App.Name}} +{{- end}} +{{- if .DesktopFile}} +DesktopFile={{.DesktopFile}} +{{- end}} +HasNetworkStatus={{.HasNetworkStatus}} +` + t := template.Must(template.New("portal-info").Parse(portalInfoTemplate)) + data := struct { + Snap *client.Snap + App *client.AppInfo + DesktopFile string + HasNetworkStatus bool + }{ + Snap: snap, + App: app, + DesktopFile: desktopFile, + HasNetworkStatus: hasNetworkStatus, + } + if err := t.Execute(Stdout, data); err != nil { + return fmt.Errorf("cannot render output template: %s", err) + } + return nil +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_portal_info_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_portal_info_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_routine_portal_info_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_routine_portal_info_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,188 @@ +// -*- 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 ( + "errors" + "fmt" + "net/http" + "net/url" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/client" + snap "github.com/snapcore/snapd/cmd/snap" +) + +// only used for /v2/snaps/hello +const mockInfoJSONWithApps = ` +{ + "type": "sync", + "status-code": 200, + "status": "OK", + "result": { + "id": "mVyGrEwiqSi5PugCwyH7WgpoQLemtTd6", + "title": "hello", + "summary": "GNU Hello, the \"hello world\" snap", + "description": "GNU hello prints a friendly greeting. This is part of the snapcraft tour at https://snapcraft.io/", + "installed-size": 98304, + "name": "hello", + "publisher": { + "id": "canonical", + "username": "canonical", + "display-name": "Canonical", + "validation": "verified" + }, + "developer": "canonical", + "status": "active", + "type": "app", + "version": "2.10", + "channel": "stable", + "tracking-channel": "stable", + "ignore-validation": false, + "revision": "38", + "confinement": "strict", + "private": false, + "devmode": false, + "jailmode": false, + "apps": [ + { + "snap": "hello", + "name": "hello", + "desktop-file": "/path/to/hello_hello.desktop" + }, + { + "snap": "hello", + "name": "universe", + "desktop-file": "/path/to/hello_universe.desktop" + } + ], + "contact": "mailto:snaps@canonical.com", + "mounted-from": "/var/lib/snapd/snaps/hello_38.snap", + "install-date": "2019-10-11T13:34:15.630955389+08:00" + } +} +` + +func (s *SnapSuite) TestPortalInfo(c *C) { + restore := snap.MockCgroupSnapNameFromPid(func(pid int) (string, error) { + c.Check(pid, Equals, 42) + return "hello", nil + }) + defer restore() + restore = snap.MockApparmorSnapAppFromPid(func(pid int) (string, string, string, error) { + c.Check(pid, Equals, 42) + return "hello", "universe", "", nil + }) + defer restore() + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/snaps/hello") + fmt.Fprintln(w, mockInfoJSONWithApps) + case 1: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, url.Values{ + "snap": []string{"hello"}, + "interface": []string{"network-status"}, + }) + result := client.Connections{ + Established: []client.Connection{ + { + Slot: client.SlotRef{ + Snap: "core", + Name: "network-status", + }, + Plug: client.PlugRef{ + Snap: "hello", + Name: "network-status", + }, + Interface: "network-status", + }, + }, + } + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": result, + }) + default: + c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r) + } + n++ + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "portal-info", "42"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, `[Snap Info] +InstanceName=hello +AppName=universe +DesktopFile=hello_universe.desktop +HasNetworkStatus=true +`) + c.Check(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestPortalInfoNoAppInfo(c *C) { + restore := snap.MockCgroupSnapNameFromPid(func(pid int) (string, error) { + c.Check(pid, Equals, 42) + return "hello", nil + }) + defer restore() + restore = snap.MockApparmorSnapAppFromPid(func(pid int) (string, string, string, error) { + c.Check(pid, Equals, 42) + return "", "", "", errors.New("no apparmor") + }) + defer restore() + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch n { + case 0: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/snaps/hello") + fmt.Fprintln(w, mockInfoJSONWithApps) + case 1: + c.Check(r.Method, Equals, "GET") + c.Check(r.URL.Path, Equals, "/v2/connections") + c.Check(r.URL.Query(), DeepEquals, url.Values{ + "snap": []string{"hello"}, + "interface": []string{"network-status"}, + }) + result := client.Connections{} + EncodeResponseBody(c, w, map[string]interface{}{ + "type": "sync", + "result": result, + }) + default: + c.Fatalf("expected to get 2 requests, now on %d (%v)", n+1, r) + } + n++ + }) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"routine", "portal-info", "42"}) + c.Assert(err, IsNil) + c.Check(s.Stdout(), Equals, `[Snap Info] +InstanceName=hello +AppName=hello +DesktopFile=hello_hello.desktop +HasNetworkStatus=false +`) + c.Check(s.Stderr(), Equals, "") +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_run.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_run.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_run.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_run.go 2020-06-05 13:13:49.000000000 +0000 @@ -44,7 +44,7 @@ "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/strace" - "github.com/snapcore/snapd/selinux" + "github.com/snapcore/snapd/sandbox/selinux" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapenv" "github.com/snapcore/snapd/strutil/shlex" @@ -112,6 +112,26 @@ }, nil) } +// isStopping returns true if the system is shutting down. +func isStopping() (bool, error) { + // Make sure, just in case, that systemd doesn't localize the output string. + env, err := osutil.OSEnvironment() + if err != nil { + return false, err + } + env["LC_MESSAGES"] = "C" + // Check if systemd is stopping (shutting down or rebooting). + cmd := exec.Command("systemctl", "is-system-running") + cmd.Env = env.ForExec() + stdout, err := cmd.Output() + // systemctl is-system-running returns non-zero for outcomes other than "running" + // As such, ignore any ExitError and just process the stdout buffer. + if _, ok := err.(*exec.ExitError); ok { + return string(stdout) == "stopping\n", nil + } + return false, err +} + func maybeWaitForSecurityProfileRegeneration(cli *client.Client) error { // check if the security profiles key has changed, if so, we need // to wait for snapd to re-generate all profiles @@ -125,6 +145,18 @@ logger.Debugf("SystemKeyMismatch returned an error: %v", err) } + // We have a mismatch but maybe it is only because systemd is shutting down + // and core or snapd were already unmounted and we failed to re-execute. + // For context see: https://bugs.launchpad.net/snapd/+bug/1871652 + stopping, err := isStopping() + if err != nil { + logger.Debugf("cannot check if system is stopping: %s", err) + } + if stopping { + logger.Debugf("ignoring system key mismatch during system shutdown/reboot") + return nil + } + // We have a mismatch, try to connect to snapd, once we can // connect we just continue because that usually means that // a new snapd is ready and has generated profiles. @@ -145,11 +177,13 @@ } } + logger.Debugf("system key mismatch detected, waiting for snapd to start responding...") + for i := 0; i < timeout; i++ { if _, err := cli.SysInfo(); err == nil { return nil } - // sleep a litte bit for good measure + // sleep a little bit for good measure time.Sleep(1 * time.Second) } @@ -313,6 +347,14 @@ } func createUserDataDirs(info *snap.Info) error { + // Adjust umask so that the created directories have the permissions we + // expect and are unaffected by the initial umask. While go runtime creates + // threads at will behind the scenes, the setting of umask applies to the + // entire process so it doesn't need any special handling to lock the + // executing goroutine to a single thread. + oldUmask := syscall.Umask(0) + defer syscall.Umask(oldUmask) + usr, err := userCurrent() if err != nil { return fmt.Errorf(i18n.G("cannot get the current user: %v"), err) @@ -448,13 +490,32 @@ var osReadlink = os.Readlink -func isReexeced() bool { +// snapdHelperPath return the path of a helper like "snap-confine" or +// "snap-exec" based on if snapd is re-execed or not +func snapdHelperPath(toolName string) (string, error) { exe, err := osReadlink("/proc/self/exe") if err != nil { - logger.Noticef("cannot read /proc/self/exe: %v", err) - return false + return "", fmt.Errorf("cannot read /proc/self/exe: %v", err) } - return strings.HasPrefix(exe, dirs.SnapMountDir) + // no re-exec + if !strings.HasPrefix(exe, dirs.SnapMountDir) { + return filepath.Join(dirs.DistroLibExecDir, toolName), nil + } + // The logic below only works if the last two path components + // are /usr/bin + // FIXME: use a snap warning? + if !strings.HasSuffix(exe, "/usr/bin/"+filepath.Base(exe)) { + logger.Noticef("(internal error): unexpected exe input in snapdHelperPath: %v", exe) + return filepath.Join(dirs.DistroLibExecDir, toolName), nil + } + // snapBase will be "/snap/{core,snapd}/$rev/" because + // the snap binary is always at $root/usr/bin/snap + snapBase := filepath.Clean(filepath.Join(filepath.Dir(exe), "..", "..")) + // Run snap-confine from the core/snapd snap. The tools in + // core/snapd snap are statically linked, or mostly + // statically, with the exception of libraries such as libudev + // and libc. + return filepath.Join(snapBase, dirs.CoreLibExecDir, toolName), nil } func migrateXauthority(info *snap.Info) (string, error) { @@ -685,9 +746,9 @@ return nil } -func (x *cmdRun) runCmdUnderGdb(origCmd, env []string) error { - env = append(env, "SNAP_CONFINE_RUN_UNDER_GDB=1") +type envForExecFunc func(extra map[string]string) []string +func (x *cmdRun) runCmdUnderGdb(origCmd []string, envForExec envForExecFunc) error { cmd := []string{"sudo", "-E", "gdb", "-ex=run", "-ex=catch exec", "-ex=continue", "--args"} cmd = append(cmd, origCmd...) @@ -695,11 +756,11 @@ gcmd.Stdin = os.Stdin gcmd.Stdout = os.Stdout gcmd.Stderr = os.Stderr - gcmd.Env = env + gcmd.Env = envForExec(map[string]string{"SNAP_CONFINE_RUN_UNDER_GDB": "1"}) return gcmd.Run() } -func (x *cmdRun) runCmdWithTraceExec(origCmd, env []string) error { +func (x *cmdRun) runCmdWithTraceExec(origCmd []string, envForExec envForExecFunc) error { // setup private tmp dir with strace fifo straceTmp, err := ioutil.TempDir("", "exec-trace") if err != nil { @@ -734,7 +795,7 @@ return err } // run - cmd.Env = env + cmd.Env = envForExec(nil) cmd.Stdin = Stdin cmd.Stdout = Stdout cmd.Stderr = Stderr @@ -754,7 +815,7 @@ return err } -func (x *cmdRun) runCmdUnderStrace(origCmd, env []string) error { +func (x *cmdRun) runCmdUnderStrace(origCmd []string, envForExec envForExecFunc) error { extraStraceOpts, raw, err := x.straceOpts() if err != nil { return err @@ -765,7 +826,7 @@ } // run with filter - cmd.Env = env + cmd.Env = envForExec(nil) cmd.Stdin = Stdin cmd.Stdout = Stdout stderr, err := cmd.StderrPipe() @@ -841,24 +902,10 @@ } func (x *cmdRun) runSnapConfine(info *snap.Info, securityTag, snapApp, hook string, args []string) error { - snapConfine := filepath.Join(dirs.DistroLibExecDir, "snap-confine") - // if we re-exec, we must run the snap-confine from the core/snapd snap - // as well, if they get out of sync, havoc will happen - if isReexeced() { - // exe is something like /snap/{snapd,core}/123/usr/bin/snap - exe, err := osReadlink("/proc/self/exe") - if err != nil { - return err - } - // snapBase will be "/snap/{core,snapd}/$rev/" because - // the snap binary is always at $root/usr/bin/snap - snapBase := filepath.Clean(filepath.Join(filepath.Dir(exe), "..", "..")) - // Run snap-confine from the core/snapd snap. That - // will work because snap-confine on the core/snapd snap is - // mostly statically linked (except libudev and libc) - snapConfine = filepath.Join(snapBase, dirs.CoreLibExecDir, "snap-confine") + snapConfine, err := snapdHelperPath("snap-confine") + if err != nil { + return err } - if !osutil.FileExists(snapConfine) { if hook != "" { logger.Noticef("WARNING: skipping running hook %q of snap %q: missing snap-confine", hook, info.InstanceName()) @@ -884,6 +931,11 @@ if info.NeedsClassic() { cmd = append(cmd, "--classic") } + + // this should never happen since we validate snaps with "base: none" and do not allow hooks/apps + if info.Base == "none" { + return fmt.Errorf(`cannot run hooks / applications with base "none"`) + } if info.Base != "" { cmd = append(cmd, "--base", info.Base) } @@ -895,14 +947,9 @@ if info.NeedsClassic() { // running with classic confinement, carefully pick snap-exec we // are going to use - if isReexeced() { - // same rule as when choosing the location of snap-confine - snapExecPath = filepath.Join(dirs.SnapMountDir, "core/current", - dirs.CoreLibExecDir, "snap-exec") - } else { - // there is no mount namespace where 'core' is the - // rootfs, hence we need to use distro's snap-exec - snapExecPath = filepath.Join(dirs.DistroLibExecDir, "snap-exec") + snapExecPath, err = snapdHelperPath("snap-exec") + if err != nil { + return err } } cmd = append(cmd, snapExecPath) @@ -925,19 +972,44 @@ cmd = append(cmd, snapApp) cmd = append(cmd, args...) - extraEnv := make(map[string]string) + env, err := osutil.OSEnvironment() + if err != nil { + return err + } + snapenv.ExtendEnvForRun(env, info) + if len(xauthPath) > 0 { - extraEnv["XAUTHORITY"] = xauthPath + // Environment is not nil here because it comes from + // osutil.OSEnvironment and that guarantees this + // property. + env["XAUTHORITY"] = xauthPath + } + + // on each run variant path this will be used once to get + // the environment plus additions in the right form + envForExec := func(extra map[string]string) []string { + for varName, value := range extra { + env[varName] = value + } + if !info.NeedsClassic() { + return env.ForExec() + } + // For a classic snap, environment variables that are + // usually stripped out by ld.so when starting a + // setuid process are presevered by being renamed by + // prepending PreservedUnsafePrefix -- which snap-exec + // will remove, restoring the variables to their + // original names. + return env.ForExecEscapeUnsafe(snapenv.PreservedUnsafePrefix) } - env := snapenv.ExecEnv(info, extraEnv) if x.TraceExec { - return x.runCmdWithTraceExec(cmd, env) + return x.runCmdWithTraceExec(cmd, envForExec) } else if x.Gdb { - return x.runCmdUnderGdb(cmd, env) + return x.runCmdUnderGdb(cmd, envForExec) } else if x.useStrace() { - return x.runCmdUnderStrace(cmd, env) + return x.runCmdUnderStrace(cmd, envForExec) } else { - return syscallExec(cmd[0], cmd, env) + return syscallExec(cmd[0], cmd, envForExec(nil)) } } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_run_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_run_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_run_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_run_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -34,7 +34,7 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/selinux" + "github.com/snapcore/snapd/sandbox/selinux" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" @@ -50,7 +50,40 @@ configure: `) -func (s *SnapSuite) TestInvalidParameters(c *check.C) { +var mockYamlBaseNone1 = []byte(`name: snapname1 +version: 1.0 +base: none +apps: + app: + command: run-app +`) + +var mockYamlBaseNone2 = []byte(`name: snapname2 +version: 1.0 +base: none +hooks: + configure: +`) + +type RunSuite struct { + fakeHome string + BaseSnapSuite +} + +var _ = check.Suite(&RunSuite{}) + +func (s *RunSuite) SetUpTest(c *check.C) { + s.BaseSnapSuite.SetUpTest(c) + s.fakeHome = c.MkDir() + + u, err := user.Current() + c.Assert(err, check.IsNil) + s.AddCleanup(snaprun.MockUserCurrent(func() (*user.User, error) { + return &user.User{Uid: u.Uid, HomeDir: s.fakeHome}, nil + })) +} + +func (s *RunSuite) TestInvalidParameters(c *check.C) { invalidParameters := []string{"run", "--hook=configure", "--command=command-name", "--", "snap-name"} _, err := snaprun.Parser(snaprun.Client()).ParseArgs(invalidParameters) c.Check(err, check.ErrorMatches, ".*you can only use one of --hook, --command, and --timer.*") @@ -76,7 +109,25 @@ c.Check(err, check.ErrorMatches, ".*too many arguments for hook \"configure\": bar.*") } -func (s *SnapSuite) TestSnapRunWhenMissingConfine(c *check.C) { +func (s *RunSuite) TestRunCmdWithBaseNone(c *check.C) { + defer mockSnapConfine(dirs.DistroLibExecDir)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYamlBaseNone1), &snap.SideInfo{ + Revision: snap.R("1"), + }) + snaptest.MockSnapCurrent(c, string(mockYamlBaseNone2), &snap.SideInfo{ + Revision: snap.R("1"), + }) + + _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname1.app", "--arg1", "arg2"}) + c.Assert(err, check.ErrorMatches, `cannot run hooks / applications with base \"none\"`) + + _, err = snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "--", "snapname2"}) + c.Assert(err, check.ErrorMatches, `cannot run hooks / applications with base \"none\"`) +} + +func (s *RunSuite) TestSnapRunWhenMissingConfine(c *check.C) { _, r := logger.MockLogger() defer r() @@ -105,9 +156,16 @@ c.Check(execs, check.IsNil) } -func (s *SnapSuite) TestSnapRunAppIntegration(c *check.C) { +func (s *RunSuite) TestSnapRunAppIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() + tmpdir := os.Getenv("TMPDIR") + if tmpdir == "" { + tmpdir = "/var/tmp" + os.Setenv("TMPDIR", tmpdir) + defer os.Unsetenv("TMPDIR") + } + // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R("x2"), @@ -136,11 +194,19 @@ filepath.Join(dirs.CoreLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") + c.Check(execEnv, testutil.Contains, fmt.Sprintf("TMPDIR=%s", tmpdir)) } -func (s *SnapSuite) TestSnapRunClassicAppIntegration(c *check.C) { +func (s *RunSuite) TestSnapRunClassicAppIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() + tmpdir := os.Getenv("TMPDIR") + if tmpdir == "" { + tmpdir = "/var/tmp" + os.Setenv("TMPDIR", tmpdir) + defer os.Unsetenv("TMPDIR") + } + // mock installed snap snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{ Revision: snap.R("x2"), @@ -169,10 +235,10 @@ filepath.Join(dirs.DistroLibExecDir, "snap-exec"), "snapname.app", "--arg1", "arg2"}) c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") - + c.Check(execEnv, testutil.Contains, fmt.Sprintf("SNAP_SAVED_TMPDIR=%s", tmpdir)) } -func (s *SnapSuite) TestSnapRunClassicAppIntegrationReexeced(c *check.C) { +func (s *RunSuite) TestSnapRunClassicAppIntegrationReexecedFromCore(c *check.C) { mountedCorePath := filepath.Join(dirs.SnapMountDir, "core/current") mountedCoreLibExecPath := filepath.Join(mountedCorePath, dirs.CoreLibExecDir) @@ -205,7 +271,40 @@ "snapname.app", "--arg1", "arg2"}) } -func (s *SnapSuite) TestSnapRunAppWithCommandIntegration(c *check.C) { +func (s *RunSuite) TestSnapRunClassicAppIntegrationReexecedFromSnapd(c *check.C) { + mountedSnapdPath := filepath.Join(dirs.SnapMountDir, "snapd/current") + mountedSnapdLibExecPath := filepath.Join(mountedSnapdPath, dirs.CoreLibExecDir) + + defer mockSnapConfine(mountedSnapdLibExecPath)() + + // mock installed snap + snaptest.MockSnapCurrent(c, string(mockYaml)+"confinement: classic\n", &snap.SideInfo{ + Revision: snap.R("x2"), + }) + + restore := snaprun.MockOsReadlink(func(name string) (string, error) { + // pretend 'snap' is reexeced from 'core' + return filepath.Join(mountedSnapdPath, "usr/bin/snap"), nil + }) + defer restore() + + execArgs := []string{} + restorer := snaprun.MockSyscallExec(func(arg0 string, args []string, envv []string) error { + execArgs = args + return nil + }) + defer restorer() + rest, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "snapname.app", "--arg1", "arg2"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{"snapname.app", "--arg1", "arg2"}) + c.Check(execArgs, check.DeepEquals, []string{ + filepath.Join(mountedSnapdLibExecPath, "snap-confine"), "--classic", + "snap.snapname.app", + filepath.Join(mountedSnapdLibExecPath, "snap-exec"), + "snapname.app", "--arg1", "arg2"}) +} + +func (s *RunSuite) TestSnapRunAppWithCommandIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap @@ -237,48 +336,36 @@ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") } -func (s *SnapSuite) TestSnapRunCreateDataDirs(c *check.C) { +func (s *RunSuite) TestSnapRunCreateDataDirs(c *check.C) { info, err := snap.InfoFromSnapYaml(mockYaml) c.Assert(err, check.IsNil) info.SideInfo.Revision = snap.R(42) - fakeHome := c.MkDir() - restorer := snaprun.MockUserCurrent(func() (*user.User, error) { - return &user.User{HomeDir: fakeHome}, nil - }) - defer restorer() - err = snaprun.CreateUserDataDirs(info) c.Assert(err, check.IsNil) - c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/42")), check.Equals, true) - c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname/common")), check.Equals, true) + c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname/42")), check.Equals, true) + c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname/common")), check.Equals, true) } -func (s *SnapSuite) TestParallelInstanceSnapRunCreateDataDirs(c *check.C) { +func (s *RunSuite) TestParallelInstanceSnapRunCreateDataDirs(c *check.C) { info, err := snap.InfoFromSnapYaml(mockYaml) c.Assert(err, check.IsNil) info.SideInfo.Revision = snap.R(42) info.InstanceKey = "foo" - fakeHome := c.MkDir() - restorer := snaprun.MockUserCurrent(func() (*user.User, error) { - return &user.User{HomeDir: fakeHome}, nil - }) - defer restorer() - err = snaprun.CreateUserDataDirs(info) c.Assert(err, check.IsNil) - c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname_foo/42")), check.Equals, true) - c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname_foo/common")), check.Equals, true) + c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname_foo/42")), check.Equals, true) + c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname_foo/common")), check.Equals, true) // mount point for snap instance mapping has been created - c.Check(osutil.FileExists(filepath.Join(fakeHome, "/snap/snapname")), check.Equals, true) + c.Check(osutil.FileExists(filepath.Join(s.fakeHome, "/snap/snapname")), check.Equals, true) // and it's empty inside - m, err := filepath.Glob(filepath.Join(fakeHome, "/snap/snapname/*")) + m, err := filepath.Glob(filepath.Join(s.fakeHome, "/snap/snapname/*")) c.Assert(err, check.IsNil) c.Assert(m, check.HasLen, 0) } -func (s *SnapSuite) TestSnapRunHookIntegration(c *check.C) { +func (s *RunSuite) TestSnapRunHookIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap @@ -310,7 +397,7 @@ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") } -func (s *SnapSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) { +func (s *RunSuite) TestSnapRunHookUnsetRevisionIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap @@ -342,7 +429,7 @@ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=42") } -func (s *SnapSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) { +func (s *RunSuite) TestSnapRunHookSpecificRevisionIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap @@ -378,7 +465,7 @@ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=41") } -func (s *SnapSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) { +func (s *RunSuite) TestSnapRunHookMissingRevisionIntegration(c *check.C) { // Only create revision 42 snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), @@ -396,13 +483,13 @@ c.Check(err, check.ErrorMatches, "cannot find .*") } -func (s *SnapSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) { +func (s *RunSuite) TestSnapRunHookInvalidRevisionIntegration(c *check.C) { _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--hook=configure", "-r=invalid", "--", "snapname"}) c.Assert(err, check.NotNil) c.Check(err, check.ErrorMatches, "invalid snap revision: \"invalid\"") } -func (s *SnapSuite) TestSnapRunHookMissingHookIntegration(c *check.C) { +func (s *RunSuite) TestSnapRunHookMissingHookIntegration(c *check.C) { // Only create revision 42 snaptest.MockSnapCurrent(c, string(mockYaml), &snap.SideInfo{ Revision: snap.R(42), @@ -421,22 +508,22 @@ c.Check(called, check.Equals, false) } -func (s *SnapSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) { +func (s *RunSuite) TestSnapRunErorsForUnknownRunArg(c *check.C) { _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--unknown", "--", "snapname.app", "--arg1", "arg2"}) c.Assert(err, check.ErrorMatches, "unknown flag `unknown'") } -func (s *SnapSuite) TestSnapRunErorsForMissingApp(c *check.C) { +func (s *RunSuite) TestSnapRunErorsForMissingApp(c *check.C) { _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--command=shell"}) c.Assert(err, check.ErrorMatches, "need the application to run as argument") } -func (s *SnapSuite) TestSnapRunErorrForUnavailableApp(c *check.C) { +func (s *RunSuite) TestSnapRunErorrForUnavailableApp(c *check.C) { _, err := snaprun.Parser(snaprun.Client()).ParseArgs([]string{"run", "--", "not-there"}) c.Assert(err, check.ErrorMatches, fmt.Sprintf("cannot find current revision for snap not-there: readlink %s/not-there/current: no such file or directory", dirs.SnapMountDir)) } -func (s *SnapSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) { +func (s *RunSuite) TestSnapRunSaneEnvironmentHandling(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap @@ -471,26 +558,39 @@ c.Check(execEnv, testutil.Contains, "SNAP_THE_WORLD=YES") } -func (s *SnapSuite) TestSnapRunIsReexeced(c *check.C) { +func (s *RunSuite) TestSnapRunSnapdHelperPath(c *check.C) { var osReadlinkResult string restore := snaprun.MockOsReadlink(func(name string) (string, error) { return osReadlinkResult, nil }) defer restore() + tool := "snap-confine" for _, t := range []struct { readlink string - expected bool + expected string }{ - {filepath.Join(dirs.SnapMountDir, dirs.CoreLibExecDir, "snapd"), true}, - {filepath.Join(dirs.DistroLibExecDir, "snapd"), false}, + { + filepath.Join(dirs.SnapMountDir, "core/current/usr/bin/snap"), + filepath.Join(dirs.SnapMountDir, "core/current", dirs.CoreLibExecDir, tool), + }, + { + filepath.Join(dirs.SnapMountDir, "snapd/current/usr/bin/snap"), + filepath.Join(dirs.SnapMountDir, "snapd/current", dirs.CoreLibExecDir, tool), + }, + { + filepath.Join("/usr/bin/snap"), + filepath.Join(dirs.DistroLibExecDir, tool), + }, } { osReadlinkResult = t.readlink - c.Check(snaprun.IsReexeced(), check.Equals, t.expected) + toolPath, err := snaprun.SnapdHelperPath(tool) + c.Assert(err, check.IsNil) + c.Check(toolPath, check.Equals, t.expected) } } -func (s *SnapSuite) TestSnapRunAppIntegrationFromCore(c *check.C) { +func (s *RunSuite) TestSnapRunAppIntegrationFromCore(c *check.C) { defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "core", "111", dirs.CoreLibExecDir))() // mock installed snap @@ -529,7 +629,7 @@ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") } -func (s *SnapSuite) TestSnapRunAppIntegrationFromSnapd(c *check.C) { +func (s *RunSuite) TestSnapRunAppIntegrationFromSnapd(c *check.C) { defer mockSnapConfine(filepath.Join(dirs.SnapMountDir, "snapd", "222", dirs.CoreLibExecDir))() // mock installed snap @@ -568,7 +668,7 @@ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") } -func (s *SnapSuite) TestSnapRunXauthorityMigration(c *check.C) { +func (s *RunSuite) TestSnapRunXauthorityMigration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() u, err := user.Current() @@ -644,7 +744,7 @@ return out } -func (s *SnapSuite) TestAntialiasHappy(c *check.C) { +func (s *RunSuite) TestAntialiasHappy(c *check.C) { c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil) inArgs := mkCompArgs("10", "alias", "alias", "bo-alias") @@ -672,7 +772,7 @@ }) } -func (s *SnapSuite) TestAntialiasBailsIfUnhappy(c *check.C) { +func (s *RunSuite) TestAntialiasBailsIfUnhappy(c *check.C) { // alias exists but args are somehow wonky c.Assert(os.MkdirAll(dirs.SnapBinariesDir, 0755), check.IsNil) c.Assert(os.Symlink("an-app", filepath.Join(dirs.SnapBinariesDir, "alias")), check.IsNil) @@ -701,7 +801,7 @@ } } -func (s *SnapSuite) TestSnapRunAppWithStraceIntegration(c *check.C) { +func (s *RunSuite) TestSnapRunAppWithStraceIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap @@ -781,7 +881,7 @@ c.Check(s.Stderr(), check.Equals, fmt.Sprintf(expectedFullFmt, dirs.SnapMountDir)) } -func (s *SnapSuite) TestSnapRunAppWithStraceOptions(c *check.C) { +func (s *RunSuite) TestSnapRunAppWithStraceOptions(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap @@ -822,7 +922,7 @@ }) } -func (s *SnapSuite) TestSnapRunShellIntegration(c *check.C) { +func (s *RunSuite) TestSnapRunShellIntegration(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap @@ -855,7 +955,7 @@ c.Check(execEnv, testutil.Contains, "SNAP_REVISION=x2") } -func (s *SnapSuite) TestSnapRunAppTimer(c *check.C) { +func (s *RunSuite) TestSnapRunAppTimer(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap @@ -910,7 +1010,7 @@ "snapname.app", "--arg1", "arg2"}) } -func (s *SnapSuite) TestRunCmdWithTraceExecUnhappy(c *check.C) { +func (s *RunSuite) TestRunCmdWithTraceExecUnhappy(c *check.C) { defer mockSnapConfine(dirs.DistroLibExecDir)() // mock installed snap @@ -933,7 +1033,7 @@ c.Check(s.Stderr(), check.Equals, "") } -func (s *SnapSuite) TestSnapRunRestoreSecurityContextHappy(c *check.C) { +func (s *RunSuite) TestSnapRunRestoreSecurityContextHappy(c *check.C) { logbuf, restorer := logger.MockLogger() defer restorer() @@ -944,12 +1044,6 @@ Revision: snap.R("x2"), }) - fakeHome := c.MkDir() - restorer = snaprun.MockUserCurrent(func() (*user.User, error) { - return &user.User{HomeDir: fakeHome}, nil - }) - defer restorer() - // redirect exec execCalled := 0 restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error { @@ -964,7 +1058,7 @@ enabled := false verify := true - snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir) + snapUserDir := filepath.Join(s.fakeHome, dirs.UserHomeSnapDir) restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) { c.Check(what, check.Equals, snapUserDir) @@ -1020,7 +1114,7 @@ c.Check(logbuf.String(), testutil.Contains, fmt.Sprintf("restoring default SELinux context of %s", snapUserDir)) } -func (s *SnapSuite) TestSnapRunRestoreSecurityContextFail(c *check.C) { +func (s *RunSuite) TestSnapRunRestoreSecurityContextFail(c *check.C) { logbuf, restorer := logger.MockLogger() defer restorer() @@ -1031,12 +1125,6 @@ Revision: snap.R("x2"), }) - fakeHome := c.MkDir() - restorer = snaprun.MockUserCurrent(func() (*user.User, error) { - return &user.User{HomeDir: fakeHome}, nil - }) - defer restorer() - // redirect exec execCalled := 0 restorer = snaprun.MockSyscallExec(func(_ string, args []string, envv []string) error { @@ -1052,7 +1140,7 @@ verifyErr := errors.New("verify failed") restoreErr := errors.New("restore failed") - snapUserDir := filepath.Join(fakeHome, dirs.UserHomeSnapDir) + snapUserDir := filepath.Join(s.fakeHome, dirs.UserHomeSnapDir) restorer = snaprun.MockSELinuxVerifyPathContext(func(what string) (bool, error) { c.Check(what, check.Equals, snapUserDir) @@ -1109,3 +1197,63 @@ c.Check(verifyCalls, check.Equals, 2) c.Check(restoreCalls, check.Equals, 1) } + +// systemctl is-system-running returns "running" in normal situations. +func (s *RunSuite) TestIsStoppingRunning(c *check.C) { + systemctl := testutil.MockCommand(c, "systemctl", ` +case "$1" in + is-system-running) + echo "running" + exit 0 + ;; +esac +`) + defer systemctl.Restore() + stop, err := snaprun.IsStopping() + c.Check(err, check.IsNil) + c.Check(stop, check.Equals, false) + c.Check(systemctl.Calls(), check.DeepEquals, [][]string{ + {"systemctl", "is-system-running"}, + }) +} + +// systemctl is-system-running returns "stopping" when the system is +// shutting down or rebooting. At the same time it returns a non-zero +// exit status. +func (s *RunSuite) TestIsStoppingStopping(c *check.C) { + systemctl := testutil.MockCommand(c, "systemctl", ` +case "$1" in + is-system-running) + echo "stopping" + exit 1 + ;; +esac +`) + defer systemctl.Restore() + stop, err := snaprun.IsStopping() + c.Check(err, check.IsNil) + c.Check(stop, check.Equals, true) + c.Check(systemctl.Calls(), check.DeepEquals, [][]string{ + {"systemctl", "is-system-running"}, + }) +} + +// systemctl is-system-running can often return "degraded" +// Let's make sure that is not confusing us. +func (s *RunSuite) TestIsStoppingDegraded(c *check.C) { + systemctl := testutil.MockCommand(c, "systemctl", ` +case "$1" in + is-system-running) + echo "degraded" + exit 1 + ;; +esac +`) + defer systemctl.Restore() + stop, err := snaprun.IsStopping() + c.Check(err, check.IsNil) + c.Check(stop, check.Equals, false) + c.Check(systemctl.Calls(), check.DeepEquals, [][]string{ + {"systemctl", "is-system-running"}, + }) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_services_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_services_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_services_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_services_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -23,6 +23,8 @@ "encoding/json" "fmt" "net/http" + "sort" + "strings" "time" "gopkg.in/check.v1" @@ -33,8 +35,6 @@ type appOpSuite struct { BaseSnapSuite - - restoreAll func() } var _ = check.Suite(&appOpSuite{}) @@ -42,16 +42,13 @@ func (s *appOpSuite) SetUpTest(c *check.C) { s.BaseSnapSuite.SetUpTest(c) - restoreClientRetry := client.MockDoRetry(time.Millisecond, 10*time.Millisecond) + restoreClientRetry := client.MockDoTimings(time.Millisecond, 100*time.Millisecond) restorePollTime := snap.MockPollTime(time.Millisecond) - s.restoreAll = func() { - restoreClientRetry() - restorePollTime() - } + s.AddCleanup(restoreClientRetry) + s.AddCleanup(restorePollTime) } func (s *appOpSuite) TearDownTest(c *check.C) { - s.restoreAll() s.BaseSnapSuite.TearDownTest(c) } @@ -222,6 +219,51 @@ c.Check(n, check.Equals, 1) } +func (s *appOpSuite) TestServiceCompletion(c *check.C) { + n := 0 + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(r.URL.Path, check.Equals, "/v2/apps") + c.Check(r.URL.Query(), check.HasLen, 1) + c.Check(r.URL.Query().Get("select"), check.Equals, "service") + c.Check(r.Method, check.Equals, "GET") + w.WriteHeader(200) + enc := json.NewEncoder(w) + enc.Encode(map[string]interface{}{ + "type": "sync", + "result": []map[string]interface{}{ + {"snap": "a-snap", "name": "foo", "daemon": "simple"}, + {"snap": "a-snap", "name": "bar", "daemon": "simple"}, + {"snap": "b-snap", "name": "baz", "daemon": "simple"}, + }, + "status": "OK", + "status-code": 200, + }) + + n++ + }) + + var comp = func(s string) string { + comps := snap.ServiceName("").Complete(s) + as := make([]string, len(comps)) + for i := range comps { + as[i] = comps[i].Item + } + sort.Strings(as) + return strings.Join(as, " ") + } + + c.Check(comp(""), check.Equals, "a-snap a-snap.bar a-snap.foo b-snap.baz") + c.Check(comp("a"), check.Equals, "a-snap a-snap.bar a-snap.foo") + c.Check(comp("a-snap"), check.Equals, "a-snap a-snap.bar a-snap.foo") + c.Check(comp("a-snap."), check.Equals, "a-snap.bar a-snap.foo") + c.Check(comp("a-snap.b"), check.Equals, "a-snap.bar") + c.Check(comp("b"), check.Equals, "b-snap.baz") + c.Check(comp("c"), check.Equals, "") + + // ensure that the fake server api was actually hit + c.Check(n, check.Equals, 7) +} + func (s *appOpSuite) TestAppStatusNoServices(c *check.C) { n := 0 s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_set.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_set.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_set.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_set.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/snap/cmd_set_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_set_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_set_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_set_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/snap/cmd_sign.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_sign.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_sign.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_sign.go 2020-06-05 13:13:49.000000000 +0000 @@ -38,6 +38,10 @@ `) type cmdSign struct { + Positional struct { + Filename flags.Filename + } `positional-args:"yes"` + KeyName keyName `short:"k" default:"default"` } @@ -47,8 +51,14 @@ }, map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "k": i18n.G("Name of the key to use, otherwise use the default key"), - }, nil) + }, []argDesc{{ + // TRANSLATORS: This needs to begin with < and end with > + name: i18n.G(""), + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("File to sign (defaults to stdin)"), + }}) cmd.hidden = true + cmd.completeHidden = true } func (x *cmdSign) Execute(args []string) error { @@ -56,7 +66,17 @@ return ErrExtraArgs } - statement, err := ioutil.ReadAll(Stdin) + useStdin := x.Positional.Filename == "" || x.Positional.Filename == "-" + + var ( + statement []byte + err error + ) + if !useStdin { + statement, err = ioutil.ReadFile(string(x.Positional.Filename)) + } else { + statement, err = ioutil.ReadAll(Stdin) + } if err != nil { return fmt.Errorf(i18n.G("cannot read assertion input: %v"), err) } @@ -64,7 +84,8 @@ keypairMgr := asserts.NewGPGKeypairManager() privKey, err := keypairMgr.GetByName(string(x.KeyName)) if err != nil { - return err + // TRANSLATORS: %q is the key name, %v the error message + return fmt.Errorf(i18n.G("cannot use %q key: %v"), x.KeyName, err) } signOpts := signtool.Options{ diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snap_op.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snap_op.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snap_op.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snap_op.go 2020-06-05 13:13:49.000000000 +0000 @@ -32,8 +32,11 @@ "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap/channel" + "github.com/snapcore/snapd/strutil" ) var ( @@ -111,6 +114,7 @@ waitMixin Revision string `long:"revision"` + Purge bool `long:"purge"` Positional struct { Snaps []installedSnapName `positional-arg-name:"" required:"1"` } `positional-args:"yes" required:"yes"` @@ -182,7 +186,7 @@ } func (x *cmdRemove) Execute([]string) error { - opts := &client.SnapOptions{Revision: x.Revision} + opts := &client.SnapOptions{Revision: x.Revision, Purge: x.Purge} if len(x.Positional.Snaps) == 1 { return x.removeOne(opts) } @@ -248,15 +252,62 @@ mx.Channel = ch.chName } - if !strings.Contains(mx.Channel, "/") && mx.Channel != "" && mx.Channel != "edge" && mx.Channel != "beta" && mx.Channel != "candidate" && mx.Channel != "stable" { - // shortcut to jump to a different track, e.g. - // snap install foo --channel=3.4 # implies 3.4/stable - mx.Channel += "/stable" + if mx.Channel != "" { + if _, err := channel.Parse(mx.Channel, ""); err != nil { + full, er := channel.Full(mx.Channel) + if er != nil { + // the parse error has more detailed info + return err + } + + // TODO: get escapes in here so we can bold the Warning + head := i18n.G("Warning:") + msg := i18n.G("Specifying a channel %q is relying on undefined behaviour. Interpreting it as %q for now, but this will be an error later.\n") + warn := fill(fmt.Sprintf(msg, mx.Channel, full), utf8.RuneCountInString(head)+1) // +1 for the space + fmt.Fprint(Stderr, head, " ", warn, "\n\n") + mx.Channel = full // so a malformed-but-eh channel will always be full, i.e. //stable// -> latest/stable + } } return nil } +// isSnapInPath checks whether the snap binaries dir (e.g. /snap/bin) +// is in $PATH. +// +// TODO: consider symlinks +func isSnapInPath() bool { + paths := filepath.SplitList(os.Getenv("PATH")) + for _, path := range paths { + if filepath.Clean(path) == dirs.SnapBinariesDir { + return true + } + } + return false +} + +func isSameRisk(tracking, current string) (bool, error) { + if tracking == current { + return true, nil + } + var trackingRisk, currentRisk string + if tracking != "" { + traCh, err := channel.Parse(tracking, "") + if err != nil { + return false, err + } + trackingRisk = traCh.Risk + } + if current != "" { + curCh, err := channel.Parse(current, "") + if err != nil { + return false, err + } + currentRisk = curCh.Risk + } + return trackingRisk == currentRisk, nil +} + // show what has been done func showDone(cli *client.Client, names []string, op string, opts *client.SnapOptions, esc *escapes) error { snaps, err := cli.List(names, nil) @@ -264,19 +315,33 @@ return err } + needsPathWarning := !isSnapInPath() for _, snap := range snaps { channelStr := "" - if snap.Channel != "" && snap.Channel != "stable" { - channelStr = fmt.Sprintf(" (%s)", snap.Channel) + if snap.Channel != "" { + ch, err := channel.Parse(snap.Channel, "") + if err != nil { + return err + } + if ch.Name != "stable" { + channelStr = fmt.Sprintf(" (%s)", ch.Name) + } } switch op { case "install": + if needsPathWarning { + head := i18n.G("Warning:") + warn := fill(fmt.Sprintf(i18n.G("%s was not found in your $PATH. If you've not restarted your session since you installed snapd, try doing that. Please see https://forum.snapcraft.io/t/9469 for more details."), dirs.SnapBinariesDir), utf8.RuneCountInString(head)+1) // +1 for the space + fmt.Fprint(Stderr, esc.bold, head, esc.end, " ", warn, "\n\n") + needsPathWarning = false + } + if opts != nil && opts.Classic && snap.Confinement != client.ClassicConfinement { // requested classic but the snap is not classic head := i18n.G("Warning:") // TRANSLATORS: the arg is a snap name (e.g. "some-snap") warn := fill(fmt.Sprintf(i18n.G("flag --classic ignored for strictly confined snap %s"), snap.Name), utf8.RuneCountInString(head)+1) // +1 for the space - fmt.Fprint(Stderr, head, " ", warn, "\n\n") + fmt.Fprint(Stderr, esc.bold, head, esc.end, " ", warn, "\n\n") } if snap.Publisher != nil { @@ -297,12 +362,40 @@ 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 != "" { - // 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) + } + } } } @@ -364,6 +457,7 @@ Name string `long:"name"` + Cohort string `long:"cohort"` Positional struct { Snaps []remoteSnapName `positional-arg-name:""` } `positional-args:"yes" required:"yes"` @@ -409,6 +503,7 @@ } } + // TODO: mention details of the install (e.g. like switch does) return showDone(x.client, []string{snapName}, "install", opts, x.getEscapes()) } @@ -483,6 +578,7 @@ Revision: x.Revision, Dangerous: dangerous, Unaliased: x.Unaliased, + CohortKey: x.Cohort, } x.setModes(opts) @@ -519,6 +615,8 @@ Amend bool `long:"amend"` Revision string `long:"revision"` + Cohort string `long:"cohort"` + LeaveCohort bool `long:"leave-cohort"` List bool `long:"list"` Time bool `long:"time"` IgnoreValidation bool `long:"ignore-validation"` @@ -573,6 +671,8 @@ return err } + // TODO: this doesn't really tell about all the things you + // could set while refreshing (something switch does) return showDone(x.client, []string{name}, "refresh", opts, x.getEscapes()) } @@ -686,6 +786,8 @@ Channel: x.Channel, IgnoreValidation: x.IgnoreValidation, Revision: x.Revision, + CohortKey: x.Cohort, + LeaveCohort: x.LeaveCohort, } x.setModes(opts) return x.refreshOne(names[0], opts) @@ -904,6 +1006,9 @@ waitMixin channelMixin + Cohort string `long:"cohort"` + LeaveCohort bool `long:"leave-cohort"` + Positional struct { Snap installedSnapName `positional-arg-name:"" required:"1"` } `positional-args:"yes" required:"yes"` @@ -913,14 +1018,28 @@ if err := x.setChannelFromCommandline(); err != nil { return err } - if x.Channel == "" { - return fmt.Errorf("missing --channel= parameter") - } name := string(x.Positional.Snap) channel := string(x.Channel) + + switchCohort := x.Cohort != "" + switchChannel := x.Channel != "" + + // 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")) + } + if !switchCohort && !x.LeaveCohort && !switchChannel { + return fmt.Errorf(i18n.G("nothing to switch; specify --channel (and/or one of --cohort/--leave-cohort)")) + } + opts := &client.SnapOptions{ - Channel: channel, + Channel: channel, + CohortKey: x.Cohort, + LeaveCohort: x.LeaveCohort, } changeID, err := x.client.Switch(name, opts) if err != nil { @@ -934,8 +1053,7 @@ return err } - fmt.Fprintf(Stdout, i18n.G("%q switched to the %q channel\n"), name, channel) - return nil + return showDone(x.client, []string{name}, "switch", opts, nil) } func init() { @@ -943,6 +1061,8 @@ waitDescs.also(map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "revision": i18n.G("Remove only the given revision"), + // TRANSLATORS: This should not start with a lowercase letter. + "purge": i18n.G("Remove the snap without saving a snapshot of its data"), }), nil) addCommand("install", shortInstallHelp, longInstallHelp, func() flags.Commander { return &cmdInstall{} }, colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(map[string]string{ @@ -956,6 +1076,8 @@ "unaliased": i18n.G("Install the given snap without enabling its automatic aliases"), // TRANSLATORS: This should not start with a lowercase letter. "name": i18n.G("Install the snap file under the given instance name"), + // TRANSLATORS: This should not start with a lowercase letter. + "cohort": i18n.G("Install the snap in the given cohort"), }), nil) addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} }, colorDescs.also(waitDescs).also(channelDescs).also(modeDescs).also(timeDescs).also(map[string]string{ @@ -969,6 +1091,10 @@ "time": i18n.G("Show auto refresh information but do not perform a refresh"), // TRANSLATORS: This should not start with a lowercase letter. "ignore-validation": i18n.G("Ignore validation by other snaps blocking the refresh"), + // TRANSLATORS: This should not start with a lowercase letter. + "cohort": i18n.G("Refresh the snap into the given cohort"), + // TRANSLATORS: This should not start with a lowercase letter. + "leave-cohort": i18n.G("Refresh the snap out of its cohort"), }), nil) addCommand("try", shortTryHelp, longTryHelp, func() flags.Commander { return &cmdTry{} }, waitDescs.also(modeDescs), nil) addCommand("enable", shortEnableHelp, longEnableHelp, func() flags.Commander { return &cmdEnable{} }, waitDescs, nil) @@ -977,5 +1103,10 @@ // TRANSLATORS: This should not start with a lowercase letter. "revision": i18n.G("Revert to the given revision"), }), nil) - addCommand("switch", shortSwitchHelp, longSwitchHelp, func() flags.Commander { return &cmdSwitch{} }, waitDescs.also(channelDescs), nil) + addCommand("switch", shortSwitchHelp, longSwitchHelp, func() flags.Commander { return &cmdSwitch{} }, waitDescs.also(channelDescs).also(map[string]string{ + // TRANSLATORS: This should not start with a lowercase letter. + "cohort": i18n.G("Switch the snap into the given cohort"), + // TRANSLATORS: This should not start with a lowercase letter. + "leave-cohort": i18n.G("Switch the snap out of its cohort"), + }), nil) } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snap_op_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snap_op_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snap_op_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snap_op_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -44,13 +44,14 @@ type snapOpTestServer struct { c *check.C - checker func(r *http.Request) - n int - total int - channel string - confinement string - rebooting bool - snap string + checker func(r *http.Request) + n int + total int + channel string + trackingChannel string + confinement string + rebooting bool + snap string } var _ = check.Suite(&SnapOpSuite{}) @@ -81,7 +82,7 @@ case 3: t.c.Check(r.Method, check.Equals, "GET") t.c.Check(r.URL.Path, check.Equals, "/v2/snaps") - fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "%s", "status": "active", "version": "1.0", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":42, "channel":"%s", "confinement": "%s"}]}\n`, t.snap, t.channel, t.confinement) + fmt.Fprintf(w, `{"type": "sync", "result": [{"name": "%s", "status": "active", "version": "1.0", "developer": "bar", "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "revision":42, "channel":"%s", "tracking-channel": "%s", "confinement": "%s"}]}\n`, t.snap, t.channel, t.trackingChannel, t.confinement) default: t.c.Fatalf("expected to get %d requests, now on %d", t.total, t.n+1) } @@ -99,7 +100,7 @@ func (s *SnapOpSuite) SetUpTest(c *check.C) { s.BaseSnapSuite.SetUpTest(c) - restoreClientRetry := client.MockDoRetry(time.Millisecond, 10*time.Millisecond) + restoreClientRetry := client.MockDoTimings(time.Millisecond, 100*time.Millisecond) restorePollTime := snap.MockPollTime(time.Millisecond) s.restoreAll = func() { restoreClientRetry() @@ -139,8 +140,6 @@ func (s *SnapOpSuite) TestWaitRecovers(c *check.C) { meter := &progresstest.Meter{} defer progress.MockMeter(meter)() - restore := snap.MockMaxGoneTime(time.Millisecond) - defer restore() nah := true s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { @@ -154,7 +153,7 @@ cli := snap.Client() chg, err := snap.Wait(cli, "x") // we got the change - c.Assert(chg, check.NotNil) + c.Check(chg, check.NotNil) c.Assert(err, check.IsNil) // but only after recovering @@ -190,6 +189,29 @@ 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": "install", + "channel": "candidate", + "cohort-key": "what", + }) + s.srv.channel = "candidate" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "candidate", "--cohort", "what", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(candidate\) 1.0 from Bar installed`) + 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) TestInstallNoPATH(c *check.C) { + // PATH restored by test tear down + os.Setenv("PATH", "/bin:/usr/bin:/sbin:/usr/sbin") + 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": "install", "channel": "candidate", }) @@ -201,7 +223,7 @@ c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(candidate\) 1.0 from Bar installed`) - c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stderr(), testutil.MatchesWrapped, `Warning: \S+/bin was not found in your \$PATH.*`) // ensure that the fake server api was actually hit c.Check(s.srv.n, check.Equals, s.srv.total) } @@ -211,7 +233,7 @@ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "install", - "channel": "3.4/stable", + "channel": "3.4", }) s.srv.channel = "3.4/stable" } @@ -232,16 +254,81 @@ c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ "action": "install", - "channel": "3.4/hotfix-1", + "channel": "3.4/stable/hotfix-1", }) - s.srv.channel = "3.4/hotfix-1" + s.srv.channel = "3.4/stable/hotfix-1" } s.RedirectClientToTestServer(s.srv.handle) - rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "3.4/hotfix-1", "foo"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "3.4/stable/hotfix-1", "foo"}) c.Assert(err, check.IsNil) c.Assert(rest, check.DeepEquals, []string{}) - c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/hotfix-1\) 1.0 from Bar installed`) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(3.4/stable/hotfix-1\) 1.0 from Bar installed`) + 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) TestInstallSameRiskInTrack(c *check.C) { + 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": "install", + "channel": "latest/stable", + }) + s.srv.channel = "stable" + s.srv.trackingChannel = "latest/stable" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "latest/stable", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "foo 1.0 from Bar installed\n") + 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) TestInstallSameRiskInDefaultTrack(c *check.C) { + 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": "install", + "channel": "stable", + }) + s.srv.channel = "18/stable" + s.srv.trackingChannel = "18/stable" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--stable", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, "foo (18/stable) 1.0 from Bar installed\n") + 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) TestInstallRiskChannelClosed(c *check.C) { + 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": "install", + "channel": "edge", + }) + s.srv.channel = "stable" + s.srv.trackingChannel = "edge" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel", "edge", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Equals, `foo 1.0 from Bar installed +Channel edge for foo is closed; temporarily forwarding to stable. +`) 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) @@ -362,11 +449,7 @@ }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=mytrack", "foo"}) - c.Assert(err, check.NotNil) - c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` -error: snap "foo" not available on channel "mytrack/stable" (see 'snap info - foo') -`) + c.Check(err, check.ErrorMatches, `snap "foo" not available on channel "mytrack" \(see 'snap info foo'\)`) c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") @@ -573,21 +656,11 @@ func (s *SnapOpSuite) TestInstallSnapRevisionNotAvailableInvalidChannel(c *check.C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, `{"type": "error", "result": {"message": "no snap revision on specified channel", "value": { - "snap-name": "foo", - "action": "install", - "architecture": "amd64", - "channel": "a/b/c/d", - "releases": [{"architecture": "amd64", "channel": "stable"}] -}, "kind": "snap-channel-not-available"}, "status-code": 404}`) + c.Fatal("unexpected call to server") }) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--channel=a/b/c/d", "foo"}) - c.Assert(err, check.NotNil) - c.Check(fmt.Sprintf("\nerror: %v\n", err), check.Equals, ` -error: requested channel "a/b/c/d" is not valid (see 'snap info foo' for valid - ones) -`) + c.Assert(err, check.ErrorMatches, "channel name has too many components: a/b/c/d") c.Check(s.Stdout(), check.Equals, "") c.Check(s.Stderr(), check.Equals, "") @@ -854,7 +927,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 @@ -1090,6 +1162,53 @@ c.Check(s.Stdout(), check.Matches, `(?sm).*foo \(beta\) 1.0 from Bar refreshed`) } +func (s *SnapOpSuite) TestRefreshOneSwitchCohort(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "cohort-key": "what", + }) + } + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--cohort=what", "foo"}) + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`) +} + +func (s *SnapOpSuite) TestRefreshOneLeaveCohort(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "leave-cohort": true, + }) + } + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--leave-cohort", "foo"}) + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo 1.0 from Bar refreshed`) +} + +func (s *SnapOpSuite) TestRefreshOneWithPinnedTrack(c *check.C) { + s.RedirectClientToTestServer(s.srv.handle) + s.srv.checker = func(r *http.Request) { + c.Check(r.Method, check.Equals, "POST") + c.Check(r.URL.Path, check.Equals, "/v2/snaps/foo") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "refresh", + "channel": "stable", + }) + s.srv.channel = "18/stable" + s.srv.trackingChannel = "18/stable" + } + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--stable", "foo"}) + c.Assert(err, check.IsNil) + c.Check(s.Stdout(), check.Equals, "foo (18/stable) 1.0 from Bar refreshed\n") +} + func (s *SnapOpSuite) TestRefreshOneClassic(c *check.C) { s.RedirectClientToTestServer(s.srv.handle) s.srv.checker = func(r *http.Request) { @@ -1166,6 +1285,25 @@ } +func (s *SnapOpSuite) TestRefreshOneChanDeprecated(c *check.C) { + var in, out string + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{"action": "refresh", "channel": out}) + fmt.Fprintln(w, `{"type": "error", "result": {"message": "snap not found", "value": "foo", "kind": "snap-not-found"}, "status-code": 404}`) + }) + + for in, out = range map[string]string{ + "/foo": "foo/stable", + "/stable": "latest/stable", + "///foo/stable//": "foo/stable", + } { + s.stderr.Reset() + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--channel=" + in, "one"}) + c.Assert(err, check.ErrorMatches, "snap \"one\" not found") + c.Check(s.Stderr(), testutil.EqualsWrapped, `Warning: Specifying a channel "`+in+`" is relying on undefined behaviour. Interpreting it as "`+out+`" for now, but this will be an error later.`) + } +} + func (s *SnapOpSuite) TestRefreshOneModeErr(c *check.C) { s.RedirectClientToTestServer(nil) _, err := snap.Parser(snap.Client()).ParseArgs([]string{"refresh", "--jailmode", "--devmode", "one"}) @@ -1455,6 +1593,26 @@ c.Check(s.srv.n, check.Equals, s.srv.total) } +func (s *SnapOpSuite) TestRemoveWithPurge(c *check.C) { + s.srv.total = 3 + 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": "remove", + "purge": true, + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"remove", "--purge", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*foo removed`) + 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) TestRemoveRevision(c *check.C) { s.srv.total = 3 s.srv.checker = func(r *http.Request) { @@ -1720,20 +1878,107 @@ } 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 = 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", + "cohort-key": "what", + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--cohort=what", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" switched to the "what" cohort`) + 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) TestSwitchHappyLeaveCohort(c *check.C) { + 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", + "leave-cohort": true, + }) + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--leave-cohort", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" left the cohort`) + 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) TestSwitchHappyChannelAndCohort(c *check.C) { + 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", + "cohort-key": "what", + "channel": "edge", + }) + s.srv.trackingChannel = "edge" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--cohort=what", "--edge", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" switched to the "edge" channel and the "what" cohort`) + 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) TestSwitchHappyChannelAndLeaveCohort(c *check.C) { + 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", + "leave-cohort": true, + "channel": "edge", + }) + s.srv.trackingChannel = "edge" + } + + s.RedirectClientToTestServer(s.srv.handle) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "--leave-cohort", "--edge", "foo"}) + c.Assert(err, check.IsNil) + c.Assert(rest, check.DeepEquals, []string{}) + c.Check(s.Stdout(), check.Matches, `(?sm).*"foo" left the cohort, and switched to the "edge" 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) @@ -1746,7 +1991,12 @@ func (s *SnapOpSuite) TestSwitchAlsoUnhappy(c *check.C) { _, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "foo"}) - c.Assert(err, check.ErrorMatches, `missing --channel= parameter`) + c.Assert(err, check.ErrorMatches, `nothing to switch.*`) +} + +func (s *SnapOpSuite) TestSwitchMoreUnhappy(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"switch", "foo", "--cohort=what", "--leave-cohort"}) + c.Assert(err, check.ErrorMatches, `cannot specify both --cohort and --leave-cohort`) } func (s *SnapOpSuite) TestSnapOpNetworkTimeoutError(c *check.C) { diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snapshot.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snapshot.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snapshot.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snapshot.go 2020-06-05 13:13:49.000000000 +0000 @@ -21,6 +21,8 @@ import ( "fmt" + "strconv" + "strings" "github.com/jessevdk/go-flags" @@ -30,7 +32,7 @@ ) func fmtSize(size int64) string { - return quantity.FormatAmount(uint64(size), -1) + return quantity.FormatAmount(uint64(size), -1) + "B" } var ( @@ -106,7 +108,14 @@ } func (x *savedCmd) Execute([]string) error { - setID := uint64(x.ID) + var setID uint64 + var err error + if x.ID != "" { + setID, err = x.ID.ToUint() + if err != nil { + return err + } + } snaps := installedSnapNames(x.Positional.Snaps) list, err := x.client.SnapshotSets(setID, snaps) if err != nil { @@ -133,12 +142,20 @@ i18n.G("Notes")) for _, sg := range list { for _, sh := range sg.Snapshots { - note := "-" + notes := []string{} + if sh.Auto { + notes = append(notes, "auto") + } if sh.Broken != "" { - note = "broken: " + sh.Broken + notes = append(notes, "broken: "+sh.Broken) } - size := quantity.FormatAmount(uint64(sh.Size), -1) + "B" - fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\n", sg.ID, sh.Snap, x.fmtDuration(sh.Time), sh.Version, sh.Revision, size, note) + note := "-" + if len(notes) > 0 { + note = strings.Join(notes, ", ") + } + size := fmtSize(sh.Size) + age := x.fmtDuration(sh.Time) + fmt.Fprintf(w, "%d\t%s\t%s\t%s\t%s\t%s\t%s\n", sg.ID, sh.Snap, age, sh.Version, sh.Revision, size, note) } } return nil @@ -170,7 +187,7 @@ y := &savedCmd{ clientMixin: x.clientMixin, durationMixin: x.durationMixin, - ID: snapshotID(setID), + ID: snapshotID(strconv.FormatUint(setID, 10)), } return y.Execute(nil) } @@ -184,7 +201,10 @@ } func (x *forgetCmd) Execute([]string) error { - setID := uint64(x.Positional.ID) + setID, err := x.Positional.ID.ToUint() + if err != nil { + return err + } snaps := installedSnapNames(x.Positional.Snaps) changeID, err := x.client.ForgetSnapshots(setID, snaps) if err != nil { @@ -200,9 +220,9 @@ if len(snaps) > 0 { // TRANSLATORS: the %s is a comma-separated list of quoted snap names - fmt.Fprintf(Stdout, i18n.NG("Snapshot #%d of snap %s forgotten.\n", "Snapshot #%d of snaps %s forgotten.\n", len(snaps)), x.Positional.ID, strutil.Quoted(snaps)) + fmt.Fprintf(Stdout, i18n.NG("Snapshot #%s of snap %s forgotten.\n", "Snapshot #%s of snaps %s forgotten.\n", len(snaps)), x.Positional.ID, strutil.Quoted(snaps)) } else { - fmt.Fprintf(Stdout, i18n.G("Snapshot #%d forgotten.\n"), x.Positional.ID) + fmt.Fprintf(Stdout, i18n.G("Snapshot #%s forgotten.\n"), x.Positional.ID) } return nil } @@ -217,7 +237,10 @@ } func (x *checkSnapshotCmd) Execute([]string) error { - setID := uint64(x.Positional.ID) + setID, err := x.Positional.ID.ToUint() + if err != nil { + return err + } snaps := installedSnapNames(x.Positional.Snaps) users := strutil.CommaSeparatedList(x.Users) changeID, err := x.client.CheckSnapshots(setID, snaps, users) @@ -235,10 +258,10 @@ // TODO: also mention the home archives that were actually checked if len(snaps) > 0 { // TRANSLATORS: the %s is a comma-separated list of quoted snap names - fmt.Fprintf(Stdout, i18n.G("Snapshot #%d of snaps %s verified successfully.\n"), + fmt.Fprintf(Stdout, i18n.G("Snapshot #%s of snaps %s verified successfully.\n"), x.Positional.ID, strutil.Quoted(snaps)) } else { - fmt.Fprintf(Stdout, i18n.G("Snapshot #%d verified successfully.\n"), x.Positional.ID) + fmt.Fprintf(Stdout, i18n.G("Snapshot #%s verified successfully.\n"), x.Positional.ID) } return nil } @@ -253,7 +276,10 @@ } func (x *restoreCmd) Execute([]string) error { - setID := uint64(x.Positional.ID) + setID, err := x.Positional.ID.ToUint() + if err != nil { + return err + } snaps := installedSnapNames(x.Positional.Snaps) users := strutil.CommaSeparatedList(x.Users) changeID, err := x.client.RestoreSnapshots(setID, snaps, users) @@ -271,10 +297,10 @@ // TODO: also mention the home archives that were actually restored if len(snaps) > 0 { // TRANSLATORS: the %s is a comma-separated list of quoted snap names - fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%d of snaps %s.\n"), + fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%s of snaps %s.\n"), x.Positional.ID, strutil.Quoted(snaps)) } else { - fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%d.\n"), x.Positional.ID) + fmt.Fprintf(Stdout, i18n.G("Restored snapshot #%s.\n"), x.Positional.ID) } return nil } @@ -310,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("Set id of snapshot to restore (see 'snap help saved')"), + }, { + name: "", + // TRANSLATORS: This should not start with a lowercase letter. + desc: i18n.G("The snap for which data will be restored"), + }, + }) 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, @@ -327,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.37.4ubuntu0.1/cmd/snap/cmd_snapshot_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snapshot_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_snapshot_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_snapshot_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -0,0 +1,114 @@ +// -*- 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" + "strings" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/cmd/snap" + "github.com/snapcore/snapd/testutil" +) + +var snapshotsTests = []getCmdArgs{{ + args: "restore x", + 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 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", +}, { + args: "saved", + stdout: "Set Snap Age Version Rev Size Notes\n1 htop .* 2 1168 1B -\n", +}, { + args: "forget x", + 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 snapshot set id: expected a non-negative integer argument \(see 'snap help saved'\)`, +}, { + args: "restore 1", + stdout: "Restored snapshot #1.\n", +}, { + args: "forget 2", + stdout: "Snapshot #2 forgotten.\n", +}, { + args: "forget 2 snap1 snap2", + stdout: "Snapshot #2 of snaps \"snap1\", \"snap2\" forgotten.\n", +}, { + args: "check-snapshot 4", + stdout: "Snapshot #4 verified successfully.\n", +}, { + args: "check-snapshot 4 snap1 snap2", + stdout: "Snapshot #4 of snaps \"snap1\", \"snap2\" verified successfully.\n", +}} + +func (s *SnapSuite) TestSnapSnaphotsTest(c *C) { + s.mockSnapshotsServer(c) + + restore := main.MockIsStdinTTY(true) + defer restore() + + for _, test := range snapshotsTests { + s.stdout.Truncate(0) + s.stderr.Truncate(0) + + c.Logf("Test: %s", test.args) + + _, err := main.Parser(main.Client()).ParseArgs(strings.Fields(test.args)) + if test.error != "" { + c.Check(err, ErrorMatches, test.error) + } else { + c.Check(err, IsNil) + c.Check(s.Stderr(), testutil.EqualsWrapped, test.stderr) + c.Check(s.Stdout(), testutil.MatchesWrapped, test.stdout) + } + } +} + +func (s *SnapSuite) mockSnapshotsServer(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case "/v2/snapshots": + if r.Method == "GET" { + // simulate a 1-month old snapshot + snapshotTime := time.Now().AddDate(0, -1, 0).Format(time.RFC3339) + if r.URL.Query().Get("set") == "3" { + fmt.Fprintf(w, `{"type":"sync","status-code":200,"status":"OK","result":[{"id":3,"snapshots":[{"set":3,"time":%q,"snap":"htop","revision":"1168","snap-id":"Z","auto":true,"epoch":{"read":[0],"write":[0]},"summary":"","version":"2","sha3-384":{"archive.tgz":""},"size":1}]}]}`, snapshotTime) + return + } + 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": + fmt.Fprintln(w, `{"type": "sync", "result": {"ready": true, "status": "Done", "data": {}}}`) + default: + c.Errorf("unexpected path %q", r.URL.Path) + } + }) +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_unalias_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_unalias_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_unalias_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_unalias_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/snap/cmd_unset.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_unset.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_unset.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_unset.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/snap/cmd_unset_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_unset_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_unset_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_unset_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/snap/cmd_userd.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_userd.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_userd.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_userd.go 2020-06-05 13:13:49.000000000 +0000 @@ -1,7 +1,8 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +// +build !darwin /* - * 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 +28,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 +54,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 +69,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.37.4ubuntu0.1/cmd/snap/cmd_userd_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_userd_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_userd_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_userd_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -1,7 +1,8 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +// +build !darwin /* - * 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 +21,9 @@ package main_test import ( + "fmt" + "net" + "net/http" "os" "strings" "syscall" @@ -28,7 +32,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 +42,7 @@ BaseSnapSuite testutil.DBusTest - restoreLogger func() + agentSocketPath string } var _ = Suite(&userdSuite{}) @@ -45,14 +51,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 +69,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 +123,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.37.4ubuntu0.1/cmd/snap/cmd_version.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_version.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_version.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_version.go 2020-06-05 13:13:49.000000000 +0000 @@ -68,6 +68,7 @@ if sv.KernelVersion != "" { fmt.Fprintf(w, "kernel\t%s\n", sv.KernelVersion) } + w.Flush() return nil diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_version_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_version_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_version_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_version_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -30,7 +30,7 @@ func (s *SnapSuite) TestVersionCommandOnClassic(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"on-classic":true,"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`) + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"on-classic":true,"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89","architecture":"ia64"}}`) }) restore := mockArgs("snap", "version") defer restore() @@ -45,7 +45,7 @@ func (s *SnapSuite) TestVersionCommandOnAllSnap(c *C) { s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { - fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89"}}`) + fmt.Fprintln(w, `{"type":"sync","status-code":200,"status":"OK","result":{"os-release":{"id":"ubuntu","version-id":"12.34"},"series":"56","version":"7.89","architecture":"powerpc","virtualization":"qemu"}}`) }) restore := mockArgs("snap", "--version") defer restore() diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_wait.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_wait.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_wait.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_wait.go 2020-06-05 13:13:49.000000000 +0000 @@ -22,7 +22,6 @@ import ( "encoding/json" "fmt" - "math/rand" "reflect" "time" @@ -43,7 +42,7 @@ func init() { addCommand("wait", "Wait for configuration", - "The wait command waits until a configration becomes true.", + "The wait command waits until a configuration becomes true.", func() flags.Commander { return &cmdWait{} }, nil, []argDesc{ @@ -117,21 +116,6 @@ snapName := string(x.Positional.Snap) confKey := x.Positional.Key - // This is fine because not providing a confKey is unsupported so this - // won't interfere with supported uses of `snap wait`. - if snapName == "godot" && confKey == "" { - switch rand.Intn(10) { - case 0: - fmt.Fprintln(Stdout, `The tears of the world are a constant quantity. -For each one who begins to weep somewhere else another stops. -The same is true of the laugh.`) - case 1: - fmt.Fprintln(Stdout, "Nothing happens. Nobody comes, nobody goes. It's awful.") - default: - fmt.Fprintln(Stdout, `"Let's go." "We can't." "Why not?" "We're waiting for Godot."`) - } - return nil - } if confKey == "" { return fmt.Errorf("the required argument `` was not provided") } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_warnings.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_warnings.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_warnings.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_warnings.go 2020-06-05 13:13:49.000000000 +0000 @@ -38,6 +38,7 @@ type cmdWarnings struct { clientMixin timeMixin + unicodeMixin All bool `long:"all"` Verbose bool `long:"verbose"` } @@ -64,7 +65,7 @@ `) func init() { - addCommand("warnings", shortWarningsHelp, longWarningsHelp, func() flags.Commander { return &cmdWarnings{} }, timeDescs.also(map[string]string{ + addCommand("warnings", shortWarningsHelp, longWarningsHelp, func() flags.Commander { return &cmdWarnings{} }, timeDescs.also(unicodeDescs).also(map[string]string{ // TRANSLATORS: This should not start with a lowercase letter. "all": i18n.G("Show all warnings"), // TRANSLATORS: This should not start with a lowercase letter. @@ -96,35 +97,36 @@ return err } + termWidth, _ := termSize() + if termWidth > 100 { + // any wider than this and it gets hard to read + termWidth = 100 + } + + esc := cmd.getEscapes() w := tabWriter() - if cmd.Verbose { - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", - i18n.G("First occurrence"), - i18n.G("Last occurrence"), - i18n.G("Expires after"), - i18n.G("Acknowledged"), - i18n.G("Repeats after"), - i18n.G("Warning")) - for _, warning := range warnings { - lastShown := "-" + for i, warning := range warnings { + if i > 0 { + fmt.Fprintln(w, "---") + } + if cmd.Verbose { + fmt.Fprintf(w, "first-occurrence:\t%s\n", cmd.fmtTime(warning.FirstAdded)) + } + fmt.Fprintf(w, "last-occurrence:\t%s\n", cmd.fmtTime(warning.LastAdded)) + if cmd.Verbose { + lastShown := esc.dash if !warning.LastShown.IsZero() { lastShown = cmd.fmtTime(warning.LastShown) } - fmt.Fprintf(w, "%s\t%s\t%s\t%s\t%s\t%s\n", - cmd.fmtTime(warning.FirstAdded), - cmd.fmtTime(warning.LastAdded), - quantity.FormatDuration(warning.ExpireAfter.Seconds()), - lastShown, - quantity.FormatDuration(warning.RepeatAfter.Seconds()), - warning.Message) - } - } else { - fmt.Fprintf(w, "%s\t%s\n", i18n.G("Last occurrence"), i18n.G("Warning")) - for _, warning := range warnings { - fmt.Fprintf(w, "%s\t%s\n", cmd.fmtTime(warning.LastAdded), warning.Message) + fmt.Fprintf(w, "acknowledged:\t%s\n", lastShown) + // TODO: cmd.fmtDuration() using timeutil.HumanDuration + fmt.Fprintf(w, "repeats-after:\t%s\n", quantity.FormatDuration(warning.RepeatAfter.Seconds())) + fmt.Fprintf(w, "expires-after:\t%s\n", quantity.FormatDuration(warning.ExpireAfter.Seconds())) } + fmt.Fprintln(w, "warning: |") + printDescr(w, warning.Message, termWidth) + w.Flush() } - w.Flush() return nil } @@ -136,7 +138,7 @@ last, err := lastWarningTimestamp() if err != nil { - return fmt.Errorf("no client-side warning timestamp found: %v", err) + return err } return cmd.client.Okay(last) @@ -191,8 +193,12 @@ if err != nil { return time.Time{}, fmt.Errorf("cannot determine real user: %v", err) } + f, err := os.Open(warnFilename(user.HomeDir)) if err != nil { + if os.IsNotExist(err) { + return time.Time{}, fmt.Errorf("you must have looked at the warnings before acknowledging them. Try 'snap warnings'.") + } return time.Time{}, fmt.Errorf("cannot open timestamp file: %v", err) } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_warnings_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_warnings_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_warnings_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_warnings_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -102,28 +102,44 @@ func (s *warningSuite) TestWarnings(c *check.C) { s.RedirectClientToTestServer(mkWarningsFakeHandler(c, twoWarnings)) - rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time", "--unicode=never"}) c.Assert(err, check.IsNil) c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, ` -Last occurrence Warning -2018-09-19T12:41:18Z hello world number one -2018-09-19T12:44:19Z hello world number two +last-occurrence: 2018-09-19T12:41:18Z +warning: | + hello world number one +--- +last-occurrence: 2018-09-19T12:44:19Z +warning: | + hello world number two `[1:]) } func (s *warningSuite) TestVerboseWarnings(c *check.C) { s.RedirectClientToTestServer(mkWarningsFakeHandler(c, twoWarnings)) - rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time", "--verbose"}) + rest, err := snap.Parser(snap.Client()).ParseArgs([]string{"warnings", "--abs-time", "--verbose", "--unicode=never"}) c.Assert(err, check.IsNil) c.Check(rest, check.HasLen, 0) c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, ` -First occurrence Last occurrence Expires after Acknowledged Repeats after Warning -2018-09-19T12:41:18Z 2018-09-19T12:41:18Z 28d0h - 1d00h hello world number one -2018-09-19T12:44:19Z 2018-09-19T12:44:19Z 28d0h - 1d00h hello world number two +first-occurrence: 2018-09-19T12:41:18Z +last-occurrence: 2018-09-19T12:41:18Z +acknowledged: -- +repeats-after: 1d00h +expires-after: 28d0h +warning: | + hello world number one +--- +first-occurrence: 2018-09-19T12:44:19Z +last-occurrence: 2018-09-19T12:44:19Z +acknowledged: -- +repeats-after: 1d00h +expires-after: 28d0h +warning: | + hello world number two `[1:]) } @@ -155,6 +171,13 @@ c.Check(s.Stderr(), check.Equals, "") c.Check(s.Stdout(), check.Equals, "") } + +func (s *warningSuite) TestOkayBeforeWarnings(c *check.C) { + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"okay"}) + c.Assert(err, check.ErrorMatches, "you must have looked at the warnings before acknowledging them. Try 'snap warnings'.") + c.Check(s.Stderr(), check.Equals, "") + c.Check(s.Stdout(), check.Equals, "") +} func (s *warningSuite) TestListWithWarnings(c *check.C) { var called bool diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/cmd_whoami_test.go snapd-2.45.1ubuntu0.2/cmd/snap/cmd_whoami_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/cmd_whoami_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/cmd_whoami_test.go 2020-06-05 13:13:49.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.37.4ubuntu0.1/cmd/snap/color.go snapd-2.45.1ubuntu0.2/cmd/snap/color.go --- snapd-2.37.4ubuntu0.1/cmd/snap/color.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/color.go 2020-06-05 13:13:49.000000000 +0000 @@ -30,14 +30,12 @@ "github.com/snapcore/snapd/snap" ) -type colorMixin struct { - Color string `long:"color" default:"auto" choice:"auto" choice:"never" choice:"always"` - Unicode string `long:"unicode" default:"auto" choice:"auto" choice:"never" choice:"always"` // do we want this hidden? +type unicodeMixin struct { + Unicode string `long:"unicode" default:"auto" choice:"auto" choice:"never" choice:"always"` } -func (mx colorMixin) getEscapes() *escapes { - esc := colorTable(mx.Color) - if canUnicode(mx.Unicode) { +func (ux unicodeMixin) addUnicodeChars(esc *escapes) { + if canUnicode(ux.Unicode) { esc.dash = "–" // that's an en dash (so yaml is happy) esc.uparrow = "↑" esc.tick = "✓" @@ -46,7 +44,22 @@ esc.uparrow = "^" esc.tick = "*" } +} +func (ux unicodeMixin) getEscapes() *escapes { + esc := &escapes{} + ux.addUnicodeChars(esc) + return esc +} + +type colorMixin struct { + Color string `long:"color" default:"auto" choice:"auto" choice:"never" choice:"always"` + unicodeMixin +} + +func (mx colorMixin) getEscapes() *escapes { + esc := colorTable(mx.Color) + mx.addUnicodeChars(&esc) return &esc } @@ -103,13 +116,18 @@ var colorDescs = mixinDescs{ // TRANSLATORS: This should not start with a lowercase letter. - "color": i18n.G("Use a little bit of color to highlight some things."), + "color": i18n.G("Use a little bit of color to highlight some things."), + "unicode": unicodeDescs["unicode"], +} + +var unicodeDescs = mixinDescs{ // TRANSLATORS: This should not start with a lowercase letter. "unicode": i18n.G("Use a little bit of Unicode to improve legibility."), } type escapes struct { green string + bold string end string tick, dash, uparrow string @@ -118,11 +136,13 @@ var ( color = escapes{ green: "\033[32m", + bold: "\033[1m", end: "\033[0m", } mono = escapes{ green: "\033[1m", + bold: "\033[1m", end: "\033[0m", } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/complete.go snapd-2.45.1ubuntu0.2/cmd/snap/complete.go --- snapd-2.37.4ubuntu0.1/cmd/snap/complete.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/complete.go 2020-06-05 13:13:49.000000000 +0000 @@ -31,6 +31,7 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/snap" ) @@ -105,8 +106,8 @@ return nil } snaps, _, err := mkClient().Find(&client.FindOptions{ - Prefix: true, Query: match, + Prefix: true, }) if err != nil { return nil @@ -296,7 +297,10 @@ parts := strings.SplitN(match, ":", 2) // Ask snapd about available interfaces. - ifaces, err := mkClient().Connections() + opts := client.ConnectionOptions{ + All: true, + } + ifaces, err := mkClient().Connections(&opts) if err != nil { return nil } @@ -435,17 +439,25 @@ return nil } - snaps := map[string]bool{} + snaps := map[string]int{} var ret []flags.Completion for _, app := range apps { if !app.IsService() { continue } - if !snaps[app.Snap] { - snaps[app.Snap] = true - ret = append(ret, flags.Completion{Item: app.Snap}) + name := snap.JoinSnapApp(app.Snap, app.Name) + if !strings.HasPrefix(name, match) { + continue + } + ret = append(ret, flags.Completion{Item: name}) + if len(match) <= len(app.Snap) { + snaps[app.Snap]++ + } + } + for snap, n := range snaps { + if n > 1 { + ret = append(ret, flags.Completion{Item: snap}) } - ret = append(ret, flags.Completion{Item: app.Snap + "." + app.Name}) } return ret @@ -475,7 +487,7 @@ return ret } -type snapshotID uint64 +type snapshotID string func (snapshotID) Complete(match string) []flags.Completion { shots, err := mkClient().SnapshotSets(0, nil) @@ -492,3 +504,11 @@ return ret } + +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 snapshot set id: expected a non-negative integer argument (see 'snap help saved')")) + } + return setID, nil +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/error.go snapd-2.45.1ubuntu0.2/cmd/snap/error.go --- snapd-2.37.4ubuntu0.1/cmd/snap/error.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/error.go 2020-06-05 13:13:49.000000000 +0000 @@ -35,13 +35,15 @@ "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" ) var errorPrefix = i18n.G("error: %v\n") -func termSize() (width, height int) { +var termSize = termSizeImpl + +func termSizeImpl() (width, height int) { if f, ok := Stdout.(*os.File); ok { width, height, _ = terminal.GetSize(int(f.Fd())) } @@ -237,16 +239,17 @@ 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 { + // XXX: this is no longer possible (should be caught before hitting the store), unless the state itself has an invalid channel // 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) @@ -255,7 +258,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 @@ -263,7 +266,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() @@ -296,7 +299,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 @@ -348,7 +351,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 @@ -377,7 +380,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) @@ -389,7 +392,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.37.4ubuntu0.1/cmd/snap/export_test.go snapd-2.45.1ubuntu0.2/cmd/snap/export_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/export_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/export_test.go 2020-06-05 13:13:49.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,14 +20,15 @@ package main import ( + "os" "os/user" "time" "github.com/jessevdk/go-flags" "github.com/snapcore/snapd/client" - "github.com/snapcore/snapd/overlord/auth" - "github.com/snapcore/snapd/selinux" + "github.com/snapcore/snapd/image" + "github.com/snapcore/snapd/sandbox/selinux" "github.com/snapcore/snapd/store" ) @@ -38,17 +39,18 @@ FirstNonOptionIsRun = firstNonOptionIsRun - CreateUserDataDirs = createUserDataDirs - ResolveApp = resolveApp - IsReexeced = isReexeced - MaybePrintServices = maybePrintServices - MaybePrintCommands = maybePrintCommands - SortByPath = sortByPath - AdviseCommand = adviseCommand - Antialias = antialias - FormatChannel = fmtChannel - PrintDescr = printDescr - TrueishJSON = trueishJSON + CreateUserDataDirs = createUserDataDirs + ResolveApp = resolveApp + SnapdHelperPath = snapdHelperPath + SortByPath = sortByPath + AdviseCommand = adviseCommand + Antialias = antialias + FormatChannel = fmtChannel + PrintDescr = printDescr + WrapFlow = wrapFlow + TrueishJSON = trueishJSON + CompletionHandler = completionHandler + MarkForNoCompletion = markForNoCompletion CanUnicode = canUnicode ColorTable = colorTable @@ -76,6 +78,59 @@ LintDesc = lintDesc FixupArg = fixupArg + + InterfacesDeprecationNotice = interfacesDeprecationNotice + + SignalNotify = signalNotify + + SortTimingsTasks = sortTimingsTasks + + PrintInstallHint = printInstallHint + + IsStopping = isStopping +) + +func HiddenCmd(descr string, completeHidden bool) *cmdInfo { + return &cmdInfo{ + shortHelp: descr, + hidden: true, + completeHidden: completeHidden, + } +} + +type ChangeTimings = changeTimings + +func NewInfoWriter(w writeflusher) *infoWriter { + return &infoWriter{ + writeflusher: w, + termWidth: 20, + esc: &escapes{dash: "--", tick: "*"}, + fmtTime: func(t time.Time) string { return t.Format(time.Kitchen) }, + } +} + +func SetVerbose(iw *infoWriter, verbose bool) { + iw.verbose = verbose +} + +var ( + ClientSnapFromPath = clientSnapFromPath + SetupDiskSnap = (*infoWriter).setupDiskSnap + SetupSnap = (*infoWriter).setupSnap + MaybePrintServices = (*infoWriter).maybePrintServices + MaybePrintCommands = (*infoWriter).maybePrintCommands + MaybePrintType = (*infoWriter).maybePrintType + PrintSummary = (*infoWriter).printSummary + MaybePrintPublisher = (*infoWriter).maybePrintPublisher + MaybePrintNotes = (*infoWriter).maybePrintNotes + MaybePrintStandaloneVersion = (*infoWriter).maybePrintStandaloneVersion + MaybePrintBuildDate = (*infoWriter).maybePrintBuildDate + MaybePrintContact = (*infoWriter).maybePrintContact + MaybePrintBase = (*infoWriter).maybePrintBase + MaybePrintPath = (*infoWriter).maybePrintPath + MaybePrintSum = (*infoWriter).maybePrintSum + MaybePrintCohortKey = (*infoWriter).maybePrintCohortKey + MaybePrintHealth = (*infoWriter).maybePrintHealth ) func MockPollTime(d time.Duration) (restore func()) { @@ -110,7 +165,7 @@ } } -func MockStoreNew(f func(*store.Config, auth.AuthContext) *store.Store) (restore func()) { +func MockStoreNew(f func(*store.Config, store.DeviceAndAuthContext) *store.Store) (restore func()) { storeNewOrig := storeNew storeNew = f return func() { @@ -211,7 +266,10 @@ } func ColorMixin(cmode, umode string) colorMixin { - return colorMixin{Color: cmode, Unicode: umode} + return colorMixin{ + Color: cmode, + unicodeMixin: unicodeMixin{Unicode: umode}, + } } func CmdAdviseSnap() *cmdAdviseSnap { @@ -241,3 +299,61 @@ selinuxRestoreContext = old } } + +func MockTermSize(newTermSize func() (int, int)) (restore func()) { + old := termSize + termSize = newTermSize + return func() { + termSize = old + } +} + +func MockImagePrepare(newImagePrepare func(*image.Options) error) (restore func()) { + old := imagePrepare + imagePrepare = newImagePrepare + return func() { + imagePrepare = old + } +} + +func MockSignalNotify(newSignalNotify func(sig ...os.Signal) (chan os.Signal, func())) (restore func()) { + old := signalNotify + signalNotify = newSignalNotify + return func() { + signalNotify = old + } +} + +type ServiceName = serviceName + +func MockApparmorSnapAppFromPid(f func(pid int) (string, string, string, error)) (restore func()) { + old := apparmorSnapAppFromPid + apparmorSnapAppFromPid = f + return func() { + apparmorSnapAppFromPid = old + } +} + +func MockCgroupSnapNameFromPid(f func(pid int) (string, error)) (restore func()) { + old := cgroupSnapNameFromPid + cgroupSnapNameFromPid = f + return func() { + cgroupSnapNameFromPid = old + } +} + +func MockSyscallUmount(f func(string, int) error) (restore func()) { + old := syscallUnmount + syscallUnmount = f + return func() { + syscallUnmount = old + } +} + +func MockIoutilTempDir(f func(string, string) (string, error)) (restore func()) { + old := ioutilTempDir + ioutilTempDir = f + return func() { + ioutilTempDir = old + } +} diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/main.go snapd-2.45.1ubuntu0.2/cmd/snap/main.go --- snapd-2.37.4ubuntu0.1/cmd/snap/main.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/main.go 2020-06-05 13:13:49.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2014-2015 Canonical Ltd + * Copyright (C) 2014-2020 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -37,24 +37,25 @@ "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/cmd" "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/httputil" "github.com/snapcore/snapd/i18n" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snapdenv" ) func init() { // set User-Agent for when 'snap' talks to the store directly (snap download etc...) - httputil.SetUserAgentFromVersion(cmd.Version, "snap") + snapdenv.SetUserAgentFromVersion(cmd.Version, nil, "snap") - if osutil.GetenvBool("SNAPD_DEBUG") || osutil.GetenvBool("SNAPPY_TESTING") { + if osutil.GetenvBool("SNAPD_DEBUG") || snapdenv.Testing() { // in tests or when debugging, enforce the "tidy" lint checks noticef = logger.Panicf } - // plug/slot sanitization not used nor possible from snap command, make it no-op + // plug/slot sanitization not used by snap commands (except for snap pack + // which re-sets it), make it no-op. snap.SanitizePlugsSlots = func(snapInfo *snap.Info) {} } @@ -88,10 +89,13 @@ name, shortHelp, longHelp string builder func() flags.Commander hidden bool - optDescs map[string]string - argDescs []argDesc - alias string - extra func(*flags.Command) + // completeHidden set to true forces completion even of + // a hidden command + completeHidden bool + optDescs map[string]string + argDescs []argDesc + alias string + extra func(*flags.Command) } // commands holds information about all non-debug commands. @@ -100,6 +104,9 @@ // debugCommands holds information about all debug commands. var debugCommands []*cmdInfo +// routineCommands holds information about all internal commands. +var routineCommands []*cmdInfo + // addCommand replaces parser.addCommand() in a way that is compatible with // re-constructing a pristine parser. func addCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo { @@ -131,6 +138,22 @@ return info } +// addRoutineCommand replaces parser.addCommand() in a way that is +// compatible with re-constructing a pristine parser. It is meant for +// adding "snap routine" commands. +func addRoutineCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo { + info := &cmdInfo{ + name: name, + shortHelp: shortHelp, + longHelp: longHelp, + builder: builder, + optDescs: optDescs, + argDescs: argDescs, + } + routineCommands = append(routineCommands, info) + return info +} + type parserSetter interface { setParser(*flags.Parser) } @@ -146,11 +169,7 @@ // decode the first rune instead of converting all of desc into []rune r, _ := utf8.DecodeRuneInString(desc) // note IsLower != !IsUpper for runes with no upper/lower. - // Also note that login.u.c. is the only exception we're allowing for - // now, but the list of exceptions could grow -- if it does, we might - // want to change it to check for urlish things instead of just - // login.u.c. - if unicode.IsLower(r) && !strings.HasPrefix(desc, "login.ubuntu.com") { + if unicode.IsLower(r) && !strings.HasPrefix(desc, "login.ubuntu.com") && !strings.HasPrefix(desc, cmdName) { noticef("description of %s's %q is lowercase: %q", cmdName, optName, desc) } } @@ -206,32 +225,35 @@ return false } -// Parser creates and populates a fresh parser. -// Since commands have local state a fresh parser is required to isolate tests -// from each other. -func Parser(cli *client.Client) *flags.Parser { - optionsData.Version = func() { - printVersions(cli) - panic(&exitStatus{0}) - } - flagopts := flags.Options(flags.PassDoubleDash) - if firstNonOptionIsRun() { - flagopts |= flags.PassAfterNonOption +// noCompletion marks command descriptions of commands that should not +// be completed +var noCompletion = make(map[string]bool) + +func markForNoCompletion(ci *cmdInfo) { + if ci.hidden && !ci.completeHidden { + if ci.shortHelp == "" { + logger.Panicf("%q missing short help", ci.name) + } + noCompletion[ci.shortHelp] = true } - parser := flags.NewParser(&optionsData, flagopts) - parser.ShortDescription = i18n.G("Tool to interact with snaps") - parser.LongDescription = longSnapDescription - // hide the unhelpful "[OPTIONS]" from help output - parser.Usage = "" - if version := parser.FindOptionByLongName("version"); version != nil { - version.Description = i18n.G("Print the version and exit") - version.Hidden = true +} + +// completionHandler filters out unwanted completions based on +// the noCompletion map before dumping them to stdout. +func completionHandler(comps []flags.Completion) { + for _, comp := range comps { + if noCompletion[comp.Description] { + continue + } + fmt.Fprintln(Stdout, comp.Item) } - // add --help like what go-flags would do for us, but hidden - addHelp(parser) +} - // Add all regular commands +func registerCommands(cli *client.Client, parser *flags.Parser, baseCmd *flags.Command, commands []*cmdInfo, checkUnique func(*cmdInfo)) { for _, c := range commands { + checkUnique(c) + markForNoCompletion(c) + obj := c.builder() if x, ok := obj.(clientSetter); ok { x.setClient(cli) @@ -240,7 +262,7 @@ x.setParser(parser) } - cmd, err := parser.AddCommand(c.name, c.shortHelp, strings.TrimSpace(c.longHelp), obj) + cmd, err := baseCmd.AddCommand(c.name, c.shortHelp, strings.TrimSpace(c.longHelp), obj) if err != nil { logger.Panicf("cannot add command %q: %v", c.name, err) } @@ -287,6 +309,45 @@ c.extra(cmd) } } +} + +// Parser creates and populates a fresh parser. +// Since commands have local state a fresh parser is required to isolate tests +// from each other. +func Parser(cli *client.Client) *flags.Parser { + optionsData.Version = func() { + printVersions(cli) + panic(&exitStatus{0}) + } + flagopts := flags.Options(flags.PassDoubleDash) + if firstNonOptionIsRun() { + flagopts |= flags.PassAfterNonOption + } + parser := flags.NewParser(&optionsData, flagopts) + parser.CompletionHandler = completionHandler + parser.ShortDescription = i18n.G("Tool to interact with snaps") + parser.LongDescription = longSnapDescription + // hide the unhelpful "[OPTIONS]" from help output + parser.Usage = "" + if version := parser.FindOptionByLongName("version"); version != nil { + version.Description = i18n.G("Print the version and exit") + version.Hidden = true + } + // add --help like what go-flags would do for us, but hidden + addHelp(parser) + + seen := make(map[string]bool, len(commands)+len(debugCommands)+len(routineCommands)) + checkUnique := func(ci *cmdInfo, kind string) { + if seen[ci.shortHelp] && ci.shortHelp != "Internal" && ci.shortHelp != "Deprecated (hidden)" { + logger.Panicf(`%scommand %q has an already employed description != "Internal"|"Deprecated (hidden)": %s`, kind, ci.name, ci.shortHelp) + } + seen[ci.shortHelp] = true + } + + // Add all regular commands + registerCommands(cli, parser, parser.Command, commands, func(ci *cmdInfo) { + checkUnique(ci, "") + }) // Add the debug command debugCommand, err := parser.AddCommand("debug", shortDebugHelp, longDebugHelp, &cmdDebug{}) debugCommand.Hidden = true @@ -294,51 +355,19 @@ logger.Panicf("cannot add command %q: %v", "debug", err) } // Add all the sub-commands of the debug command - for _, c := range debugCommands { - obj := c.builder() - if x, ok := obj.(clientSetter); ok { - x.setClient(cli) - } - cmd, err := debugCommand.AddCommand(c.name, c.shortHelp, strings.TrimSpace(c.longHelp), obj) - if err != nil { - logger.Panicf("cannot add debug command %q: %v", c.name, err) - } - cmd.Hidden = c.hidden - opts := cmd.Options() - if c.optDescs != nil && len(opts) != len(c.optDescs) { - logger.Panicf("wrong number of option descriptions for %s: expected %d, got %d", c.name, len(opts), len(c.optDescs)) - } - for _, opt := range opts { - name := opt.LongName - if name == "" { - name = string(opt.ShortName) - } - desc, ok := c.optDescs[name] - if !(c.optDescs == nil || ok) { - logger.Panicf("%s missing description for %s", c.name, name) - } - lintDesc(c.name, name, desc, opt.Description) - if desc != "" { - opt.Description = desc - } - } - - args := cmd.Args() - if c.argDescs != nil && len(args) != len(c.argDescs) { - logger.Panicf("wrong number of argument descriptions for %s: expected %d, got %d", c.name, len(args), len(c.argDescs)) - } - for i, arg := range args { - name, desc := arg.Name, "" - if c.argDescs != nil { - name = c.argDescs[i].name - desc = c.argDescs[i].desc - } - lintArg(c.name, name, desc, arg.Description) - name = fixupArg(name) - arg.Name = name - arg.Description = desc - } + registerCommands(cli, parser, debugCommand, debugCommands, func(ci *cmdInfo) { + checkUnique(ci, "debug ") + }) + // Add the internal command + routineCommand, err := parser.AddCommand("routine", shortRoutineHelp, longRoutineHelp, &cmdRoutine{}) + routineCommand.Hidden = true + if err != nil { + logger.Panicf("cannot add command %q: %v", "internal", err) } + // Add all the sub-commands of the routine command + registerCommands(cli, parser, routineCommand, routineCommands, func(ci *cmdInfo) { + checkUnique(ci, "routine ") + }) return parser } @@ -355,7 +384,12 @@ // Client returns a new client using ClientConfig as configuration. // commands should (in general) not use this, and instead use clientMixin. func mkClient() *client.Client { - cli := client.New(&ClientConfig) + cfg := &ClientConfig + // Set client user-agent when talking to the snapd daemon to the + // same value as when talking to the store. + cfg.UserAgent = snapdenv.UserAgent() + + cli := client.New(cfg) goos := runtime.GOOS if release.OnWSL { goos = "Windows Subsystem for Linux" @@ -478,7 +512,7 @@ func run() error { cli := mkClient() parser := Parser(cli) - _, err := parser.Parse() + xtra, err := parser.Parse() if err != nil { if e, ok := err.(*flags.Error); ok { switch e.Type { @@ -489,7 +523,16 @@ parser.WriteHelp(Stdout) return nil case flags.ErrUnknownCommand: - return fmt.Errorf(i18n.G(`unknown command %q, see 'snap help'`), os.Args[1]) + sub := os.Args[1] + sug := "snap help" + if len(xtra) > 0 { + sub = xtra[0] + if x := parser.Command.Active; x != nil && x.Name != "help" { + sug = "snap help " + x.Name + } + } + // TRANSLATORS: %q is the command the user entered; %s is 'snap help' or 'snap help ' + return fmt.Errorf(i18n.G("unknown command %q, see '%s'."), sub, sug) } } diff -Nru snapd-2.37.4ubuntu0.1/cmd/snap/main_test.go snapd-2.45.1ubuntu0.2/cmd/snap/main_test.go --- snapd-2.37.4ubuntu0.1/cmd/snap/main_test.go 2019-02-27 18:53:36.000000000 +0000 +++ snapd-2.45.1ubuntu0.2/cmd/snap/main_test.go 2020-06-05 13:13:49.000000000 +0000 @@ -31,16 +31,15 @@ "strings" "testing" - . "gopkg.in/check.v1" - + "github.com/jessevdk/go-flags" "golang.org/x/crypto/ssh/terminal" + . "gopkg.in/check.v1" "github.com/snapcore/snapd/cmd" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" - snapdsnap "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/testutil" snap "github.com/snapcore/snapd/cmd/snap" @@ -67,6 +66,12 @@ s.BaseTest.SetUpTest(c) dirs.SetRootDir(c.MkDir()) + path := os.Getenv("PATH") + s.AddCleanup(func() { + os.Setenv("PATH", path) + }) + os.Setenv("PATH", path+":"+dirs.SnapBinariesDir) + s.stdin = bytes.NewBuffer(nil) s.stdout = bytes.NewBuffer(nil) s.stderr = bytes.NewBuffer(nil) @@ -79,8 +84,6 @@ s.AuthFile = filepath.Join(c.MkDir(), "json") os.Setenv(TestAuthFileEnvKey, s.AuthFile) - s.AddCleanup(snapdsnap.MockSanitizePlugsSlots(func(snapInfo *snapdsnap.Info) {})) - s.AddCleanup(interfaces.MockSystemKey(` { "build-id": "7a94e9736c091b3984bd63f5aebfc883c4d859e0", @@ -261,7 +264,7 @@ defer restore() err := snap.RunMain() - c.Assert(err, ErrorMatches, `unknown command "unknowncmd", see 'snap help'`) + c.Assert(err, ErrorMatches, `unknown command "unknowncmd", see 'snap help'.`) } func (s *SnapSuite) TestResolveApp(c *C) { @@ -364,6 +367,10 @@ } c.Check(fn, PanicMatches, `option on "command" has no name`) log.Reset() + + snap.LintDesc("snap-advise", "from-apt", "snap-advise will run as a hook", "") + c.Check(log.String(), HasLen, 0) + log.Reset() } func (s *SnapSuite) TestLintArg(c *C) { @@ -398,3 +405,29 @@ // Trailing ">s" is fixed to just >. c.Check(snap.FixupArg("