diff -Nru snapd-2.53+21.10ubuntu1/asserts/asserts.go snapd-2.54.2+21.10/asserts/asserts.go --- snapd-2.53+21.10ubuntu1/asserts/asserts.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/asserts.go 2022-01-06 21:25:16.000000000 +0000 @@ -150,7 +150,8 @@ // 2: support for $SLOT()/$PLUG()/$MISSING // 3: support for on-store/on-brand/on-model device scope constraints // 4: support for plug-names/slot-names constraints - maxSupportedFormat[SnapDeclarationType.Name] = 4 + // 5: alt attr matcher usage (was unused before, has new behavior now) + maxSupportedFormat[SnapDeclarationType.Name] = 5 // 1: support to limit to device serials maxSupportedFormat[SystemUserType.Name] = 1 diff -Nru snapd-2.53+21.10ubuntu1/asserts/database.go snapd-2.54.2+21.10/asserts/database.go --- snapd-2.53+21.10ubuntu1/asserts/database.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/database.go 2022-01-06 21:25:16.000000000 +0000 @@ -102,6 +102,8 @@ Put(privKey PrivateKey) error // Get returns the private/public key pair with the given key id. Get(keyID string) (PrivateKey, error) + // Delete deletes the private/public key pair with the given key id. + Delete(keyID string) error } // DatabaseConfig for an assertion database. diff -Nru snapd-2.53+21.10ubuntu1/asserts/database_test.go snapd-2.54.2+21.10/asserts/database_test.go --- snapd-2.53+21.10ubuntu1/asserts/database_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/database_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -132,7 +132,8 @@ info, err := os.Stat(keyPath) c.Assert(err, IsNil) c.Check(info.Mode().Perm(), Equals, os.FileMode(0600)) // secret - // too white box? ok at least until we have more functionality + // too much "clear box" testing? ok at least until we have + // more functionality privKey, err := ioutil.ReadFile(keyPath) c.Assert(err, IsNil) diff -Nru snapd-2.53+21.10ubuntu1/asserts/export_test.go snapd-2.54.2+21.10/asserts/export_test.go --- snapd-2.53+21.10ubuntu1/asserts/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -271,6 +271,13 @@ } } +func GPGBatchYes() (restore func()) { + gpgBatchYes = true + return func() { + gpgBatchYes = false + } +} + // Headers helpers to test var ( ParseHeaders = parseHeaders diff -Nru snapd-2.53+21.10ubuntu1/asserts/extkeypairmgr.go snapd-2.54.2+21.10/asserts/extkeypairmgr.go --- snapd-2.53+21.10ubuntu1/asserts/extkeypairmgr.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/extkeypairmgr.go 2022-01-06 21:25:16.000000000 +0000 @@ -204,7 +204,11 @@ return &ExternalUnsupportedOpError{"cannot import private key into external keypair manager"} } -func (em *ExternalKeypairManager) Delete(keyName string) error { +func (em *ExternalKeypairManager) Delete(keyID string) error { + return &ExternalUnsupportedOpError{"no support to delete external keypair manager keys"} +} + +func (em *ExternalKeypairManager) DeleteByName(keyName string) error { return &ExternalUnsupportedOpError{"no support to delete external keypair manager keys"} } diff -Nru snapd-2.53+21.10ubuntu1/asserts/extkeypairmgr_test.go snapd-2.54.2+21.10/asserts/extkeypairmgr_test.go --- snapd-2.53+21.10ubuntu1/asserts/extkeypairmgr_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/extkeypairmgr_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -300,11 +300,21 @@ c.Check(err, ErrorMatches, `cannot get all external keypair manager key names:.*exit status 1.*`) } -func (s *extKeypairMgrSuite) TestDeleteUnsupported(c *C) { +func (s *extKeypairMgrSuite) TestDeleteByNameUnsupported(c *C) { kmgr, err := asserts.NewExternalKeypairManager("keymgr") c.Assert(err, IsNil) - err = kmgr.Delete("key") + err = kmgr.DeleteByName("key") + c.Check(err, ErrorMatches, `no support to delete external keypair manager keys`) + c.Check(err, FitsTypeOf, &asserts.ExternalUnsupportedOpError{}) + +} + +func (s *extKeypairMgrSuite) TestDelete(c *C) { + kmgr, err := asserts.NewExternalKeypairManager("keymgr") + c.Assert(err, IsNil) + + err = kmgr.Delete("key-id") c.Check(err, ErrorMatches, `no support to delete external keypair manager keys`) c.Check(err, FitsTypeOf, &asserts.ExternalUnsupportedOpError{}) diff -Nru snapd-2.53+21.10ubuntu1/asserts/fsentryutils.go snapd-2.54.2+21.10/asserts/fsentryutils.go --- snapd-2.53+21.10ubuntu1/asserts/fsentryutils.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/fsentryutils.go 2022-01-06 21:25:16.000000000 +0000 @@ -68,3 +68,8 @@ fpath := filepath.Join(top, filepath.Join(subpath...)) return ioutil.ReadFile(fpath) } + +func removeEntry(top string, subpath ...string) error { + fpath := filepath.Join(top, filepath.Join(subpath...)) + return os.Remove(fpath) +} diff -Nru snapd-2.53+21.10ubuntu1/asserts/fskeypairmgr.go snapd-2.54.2+21.10/asserts/fskeypairmgr.go --- snapd-2.53+21.10ubuntu1/asserts/fskeypairmgr.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/fskeypairmgr.go 2022-01-06 21:25:16.000000000 +0000 @@ -90,3 +90,17 @@ } return privKey, nil } + +func (fskm *filesystemKeypairManager) Delete(keyID string) error { + fskm.mu.RLock() + defer fskm.mu.RUnlock() + + err := removeEntry(fskm.top, keyID) + if err != nil { + if os.IsNotExist(err) { + return errKeypairNotFound + } + return err + } + return nil +} diff -Nru snapd-2.53+21.10ubuntu1/asserts/fskeypairmgr_test.go snapd-2.54.2+21.10/asserts/fskeypairmgr_test.go --- snapd-2.53+21.10ubuntu1/asserts/fskeypairmgr_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/fskeypairmgr_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -63,3 +63,33 @@ c.Assert(err, ErrorMatches, "assert storage root unexpectedly world-writable: .*") c.Check(bs, IsNil) } + +func (fsbss *fsKeypairMgrSuite) TestDelete(c *C) { + // ensure umask is clean when creating the DB dir + oldUmask := syscall.Umask(0) + defer syscall.Umask(oldUmask) + + topDir := filepath.Join(c.MkDir(), "asserts-db") + err := os.MkdirAll(topDir, 0775) + c.Assert(err, IsNil) + + keypairMgr, err := asserts.OpenFSKeypairManager(topDir) + c.Check(err, IsNil) + + pk1 := testPrivKey1 + keyID := pk1.PublicKey().ID() + err = keypairMgr.Put(pk1) + c.Assert(err, IsNil) + + _, err = keypairMgr.Get(keyID) + c.Assert(err, IsNil) + + err = keypairMgr.Delete(keyID) + c.Assert(err, IsNil) + + err = keypairMgr.Delete(keyID) + c.Check(err, ErrorMatches, "cannot find key pair") + + _, err = keypairMgr.Get(keyID) + c.Check(err, ErrorMatches, "cannot find key pair") +} diff -Nru snapd-2.53+21.10ubuntu1/asserts/gpgkeypairmgr.go snapd-2.54.2+21.10/asserts/gpgkeypairmgr.go --- snapd-2.53+21.10ubuntu1/asserts/gpgkeypairmgr.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/gpgkeypairmgr.go 2022-01-06 21:25:16.000000000 +0000 @@ -31,6 +31,7 @@ "golang.org/x/crypto/openpgp/packet" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/strutil" ) func ensureGPGHomeDirectory() (string, error) { @@ -73,6 +74,8 @@ return path, err } +var gpgBatchYes = false + func runGPGImpl(input []byte, args ...string) ([]byte, error) { homedir, err := ensureGPGHomeDirectory() if err != nil { @@ -92,6 +95,9 @@ } general := []string{"--homedir", homedir, "-q", "--no-auto-check-trustdb"} + if gpgBatchYes && strutil.ListContains(args, "--batch") { + general = append(general, "--yes") + } allArgs := append(general, args...) path, err := findGPGCommand() @@ -236,12 +242,20 @@ return fmt.Errorf("cannot import private key into GPG keyring") } -func (gkm *GPGKeypairManager) Get(keyID string) (PrivateKey, error) { +type gpgKeypairInfo struct { + privKey PrivateKey + fingerprint string +} + +func (gkm *GPGKeypairManager) findByID(keyID string) (*gpgKeypairInfo, error) { stop := errors.New("stop marker") - var hit PrivateKey + var hit *gpgKeypairInfo match := func(privk PrivateKey, fpr string, uid string) error { if privk.PublicKey().ID() == keyID { - hit = privk + hit = &gpgKeypairInfo{ + privKey: privk, + fingerprint: fpr, + } return stop } return nil @@ -256,6 +270,26 @@ return nil, fmt.Errorf("cannot find key %q in GPG keyring", keyID) } +func (gkm *GPGKeypairManager) Get(keyID string) (PrivateKey, error) { + keyInfo, err := gkm.findByID(keyID) + if err != nil { + return nil, err + } + return keyInfo.privKey, nil +} + +func (gkm *GPGKeypairManager) Delete(keyID string) error { + keyInfo, err := gkm.findByID(keyID) + if err != nil { + return err + } + _, err = gkm.gpg(nil, "--batch", "--delete-secret-and-public-key", "0x"+keyInfo.fingerprint) + if err != nil { + return err + } + return nil +} + func (gkm *GPGKeypairManager) sign(fingerprint string, content []byte) (*packet.Signature, error) { out, err := gkm.gpg(content, "--personal-digest-preferences", "SHA512", "--default-key", "0x"+fingerprint, "--detach-sign") if err != nil { @@ -276,11 +310,6 @@ return sig, nil } -type gpgKeypairInfo struct { - privKey PrivateKey - fingerprint string -} - func (gkm *GPGKeypairManager) findByName(name string) (*gpgKeypairInfo, error) { stop := errors.New("stop marker") var hit *gpgKeypairInfo @@ -353,8 +382,8 @@ return EncodePublicKey(keyInfo.privKey.PublicKey()) } -// Delete removes the named key pair from GnuPG's storage. -func (gkm *GPGKeypairManager) Delete(name string) error { +// DeleteByName removes the named key pair from GnuPG's storage. +func (gkm *GPGKeypairManager) DeleteByName(name string) error { keyInfo, err := gkm.findByName(name) if err != nil { return err diff -Nru snapd-2.53+21.10ubuntu1/asserts/gpgkeypairmgr_test.go snapd-2.54.2+21.10/asserts/gpgkeypairmgr_test.go --- snapd-2.53+21.10ubuntu1/asserts/gpgkeypairmgr_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/gpgkeypairmgr_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -338,3 +338,20 @@ c.Check(keys[0].ID, Equals, assertstest.DevKeyID) c.Check(keys[0].Name, Not(Equals), "") } + +func (gkms *gpgKeypairMgrSuite) TestDelete(c *C) { + defer asserts.GPGBatchYes()() + + keyID := assertstest.DevKeyID + _, err := gkms.keypairMgr.Get(keyID) + c.Assert(err, IsNil) + + err = gkms.keypairMgr.Delete(keyID) + c.Assert(err, IsNil) + + err = gkms.keypairMgr.Delete(keyID) + c.Check(err, ErrorMatches, `cannot find key.*`) + + _, err = gkms.keypairMgr.Get(keyID) + c.Check(err, ErrorMatches, `cannot find key.*`) +} diff -Nru snapd-2.53+21.10ubuntu1/asserts/ifacedecls.go snapd-2.54.2+21.10/asserts/ifacedecls.go --- snapd-2.53+21.10ubuntu1/asserts/ifacedecls.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/ifacedecls.go 2022-01-06 21:25:16.000000000 +0000 @@ -44,6 +44,8 @@ deviceScopeConstraintsFeature = "device-scope-constraints" // feature label for plug-names/slot-names constraints nameConstraintsFeature = "name-constraints" + // feature label for alt attribute matcher usage + altAttrMatcherFeature = "alt-attr-matcher" ) type attrMatcher interface { @@ -293,6 +295,9 @@ } func (matcher altAttrMatcher) feature(flabel string) bool { + if flabel == altAttrMatcherFeature { + return true + } for _, alt := range matcher.alts { if alt.feature(flabel) { return true @@ -302,6 +307,14 @@ } func (matcher altAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { + // if the value is a list apply the alternative matcher to each element + // like we do for other matchers + switch x := v.(type) { + case []interface{}: + return matchList(apath, matcher, x, ctx) + default: + } + var firstErr error for _, alt := range matcher.alts { err := alt.match(apath, v, ctx) diff -Nru snapd-2.53+21.10ubuntu1/asserts/ifacedecls_test.go snapd-2.54.2+21.10/asserts/ifacedecls_test.go --- snapd-2.53+21.10ubuntu1/asserts/ifacedecls_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/ifacedecls_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -283,6 +283,99 @@ c.Check(err, ErrorMatches, `no alternative for attribute "bar\.bar2" matches: attribute "bar\.bar2" value "BAR3" does not match \^\(BAR2\)\$`) } +func (s *attrConstraintsSuite) TestAlternativeMatchingStringList(c *C) { + toMatch := attrs(` +write: + - /var/tmp + - /var/lib/snapd/snapshots +`) + m, err := asserts.ParseHeaders([]byte(`attrs: + write: /var/(tmp|lib/snapd/snapshots)`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(toMatch, nil) + c.Check(err, IsNil) + + m, err = asserts.ParseHeaders([]byte(`attrs: + write: + - /var/tmp + - /var/lib/snapd/snapshots`)) + c.Assert(err, IsNil) + + cstrsLst, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrsLst.Check(toMatch, nil) + c.Check(err, IsNil) +} + +func (s *attrConstraintsSuite) TestAlternativeMatchingComplex(c *C) { + toMatch := attrs(` +mnt: [{what: "/dev/x*", where: "/foo/*", options: ["rw", "nodev"]}, {what: "/bar/*", where: "/baz/*", options: ["rw", "bind"]}] +`) + + m, err := asserts.ParseHeaders([]byte(`attrs: + mnt: + - + what: /(bar/|dev/x)\* + where: /(foo|baz)/\* + options: rw|bind|nodev`)) + c.Assert(err, IsNil) + + cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrs.Check(toMatch, nil) + c.Check(err, IsNil) + + m, err = asserts.ParseHeaders([]byte(`attrs: + mnt: + - + what: /dev/x\* + where: /foo/\* + options: + - nodev + - rw + - + what: /bar/\* + where: /baz/\* + options: + - rw + - bind`)) + c.Assert(err, IsNil) + + cstrsExtensive, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrsExtensive.Check(toMatch, nil) + c.Check(err, IsNil) + + // not matching case + m, err = asserts.ParseHeaders([]byte(`attrs: + mnt: + - + what: /dev/x\* + where: /foo/\* + options: + - rw + - + what: /bar/\* + where: /baz/\* + options: + - rw + - bind`)) + c.Assert(err, IsNil) + + cstrsExtensiveNoMatch, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) + c.Assert(err, IsNil) + + err = cstrsExtensiveNoMatch.Check(toMatch, nil) + c.Check(err, ErrorMatches, `no alternative for attribute "mnt\.0" matches: no alternative for attribute "mnt\.0.options\.1" matches:.*`) +} + func (s *attrConstraintsSuite) TestOtherScalars(c *C) { m, err := asserts.ParseHeaders([]byte(`attrs: foo: 1 diff -Nru snapd-2.53+21.10ubuntu1/asserts/memkeypairmgr.go snapd-2.54.2+21.10/asserts/memkeypairmgr.go --- snapd-2.53+21.10ubuntu1/asserts/memkeypairmgr.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/memkeypairmgr.go 2022-01-06 21:25:16.000000000 +0000 @@ -57,3 +57,15 @@ } return privKey, nil } + +func (mkm *memoryKeypairManager) Delete(keyID string) error { + mkm.mu.RLock() + defer mkm.mu.RUnlock() + + _, ok := mkm.pairs[keyID] + if !ok { + return errKeypairNotFound + } + delete(mkm.pairs, keyID) + return nil +} diff -Nru snapd-2.53+21.10ubuntu1/asserts/memkeypairmgr_test.go snapd-2.54.2+21.10/asserts/memkeypairmgr_test.go --- snapd-2.53+21.10ubuntu1/asserts/memkeypairmgr_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/memkeypairmgr_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -71,3 +71,22 @@ c.Check(got, IsNil) c.Check(err, ErrorMatches, "cannot find key pair") } + +func (mkms *memKeypairMgtSuite) TestDelete(c *C) { + pk1 := testPrivKey1 + keyID := pk1.PublicKey().ID() + err := mkms.keypairMgr.Put(pk1) + c.Assert(err, IsNil) + + _, err = mkms.keypairMgr.Get(keyID) + c.Assert(err, IsNil) + + err = mkms.keypairMgr.Delete(keyID) + c.Assert(err, IsNil) + + err = mkms.keypairMgr.Delete(keyID) + c.Check(err, ErrorMatches, "cannot find key pair") + + _, err = mkms.keypairMgr.Get(keyID) + c.Check(err, ErrorMatches, "cannot find key pair") +} diff -Nru snapd-2.53+21.10ubuntu1/asserts/snapasserts/validation_sets.go snapd-2.54.2+21.10/asserts/snapasserts/validation_sets.go --- snapd-2.53+21.10ubuntu1/asserts/snapasserts/validation_sets.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/snapasserts/validation_sets.go 2022-01-06 21:25:16.000000000 +0000 @@ -359,7 +359,7 @@ } // CheckInstalledSnaps checks installed snaps against the validation sets. -func (v *ValidationSets) CheckInstalledSnaps(snaps []*InstalledSnap) error { +func (v *ValidationSets) CheckInstalledSnaps(snaps []*InstalledSnap, ignoreValidation map[string]bool) error { installed := naming.NewSnapSet(nil) for _, sn := range snaps { installed.Add(sn) @@ -377,6 +377,10 @@ sn := installed.Lookup(rc) isInstalled := sn != nil + if isInstalled && ignoreValidation[rc.Name] { + continue + } + switch { case !isInstalled && (cstrs.presence == asserts.PresenceOptional || cstrs.presence == asserts.PresenceInvalid): // not installed, but optional or not required @@ -401,7 +405,11 @@ sets[rc.validationSetKey] = v.sets[rc.validationSetKey] } default: - // not installed but required + // not installed but required. + // note, not checking ignoreValidation here because it's not a viable scenario (it's not + // possible to have enforced validation set while not having the required snap at all - it + // is only possible to have it with a wrong revision, or installed while invalid, in both + // cases through --ignore-validation flag). if missing[rc.Name] == nil { missing[rc.Name] = make(map[string]bool) } diff -Nru snapd-2.53+21.10ubuntu1/asserts/snapasserts/validation_sets_test.go snapd-2.54.2+21.10/asserts/snapasserts/validation_sets_test.go --- snapd-2.53+21.10ubuntu1/asserts/snapasserts/validation_sets_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/snapasserts/validation_sets_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -272,7 +272,7 @@ snaps := []*snapasserts.InstalledSnap{ snapasserts.NewInstalledSnap("snap-a", "mysnapaaaaaaaaaaaaaaaaaaaaaaaaaa", snap.R(1)), } - err := valsets.CheckInstalledSnaps(snaps) + err := valsets.CheckInstalledSnaps(snaps, nil) c.Assert(err, IsNil) } @@ -552,7 +552,7 @@ } for i, tc := range tests { - err := valsets.CheckInstalledSnaps(tc.snaps) + err := valsets.CheckInstalledSnaps(tc.snaps, nil) if err == nil { c.Assert(tc.expectedInvalid, IsNil) c.Assert(tc.expectedMissing, IsNil) @@ -568,6 +568,59 @@ } } +func (s *validationSetsSuite) TestCheckInstalledSnapsIgnoreValidation(c *C) { + // require: snapB rev 3, snapC rev 2. + // invalid: snapA + vs := assertstest.FakeAssertion(map[string]interface{}{ + "type": "validation-set", + "authority-id": "acme", + "series": "16", + "account-id": "acme", + "name": "fooname", + "sequence": "1", + "snaps": []interface{}{ + map[string]interface{}{ + "name": "snap-a", + "id": "mysnapaaaaaaaaaaaaaaaaaaaaaaaaaa", + "presence": "invalid", + }, + map[string]interface{}{ + "name": "snap-b", + "id": "mysnapbbbbbbbbbbbbbbbbbbbbbbbbbb", + "revision": "3", + "presence": "required", + }, + map[string]interface{}{ + "name": "snap-c", + "id": "mysnapcccccccccccccccccccccccccc", + "revision": "2", + "presence": "optional", + }, + }, + }).(*asserts.ValidationSet) + + valsets := snapasserts.NewValidationSets() + c.Assert(valsets.Add(vs), IsNil) + + snapA := snapasserts.NewInstalledSnap("snap-a", "mysnapaaaaaaaaaaaaaaaaaaaaaaaaaa", snap.R(1)) + snapB := snapasserts.NewInstalledSnap("snap-b", "mysnapbbbbbbbbbbbbbbbbbbbbbbbbbb", snap.R(3)) + snapBinvRev := snapasserts.NewInstalledSnap("snap-b", "mysnapbbbbbbbbbbbbbbbbbbbbbbbbbb", snap.R(8)) + + // sanity check + c.Check(valsets.CheckInstalledSnaps([]*snapasserts.InstalledSnap{snapA, snapB}, nil), ErrorMatches, "validation sets assertions are not met:\n"+ + "- invalid snaps:\n"+ + " - snap-a \\(invalid for sets acme/fooname\\)") + // snapA is invalid but ignore-validation is set so it's ok + c.Check(valsets.CheckInstalledSnaps([]*snapasserts.InstalledSnap{snapA, snapB}, map[string]bool{"snap-a": true}), IsNil) + + // sanity check + c.Check(valsets.CheckInstalledSnaps([]*snapasserts.InstalledSnap{snapBinvRev}, nil), ErrorMatches, "validation sets assertions are not met:\n"+ + "- snaps at wrong revisions:\n"+ + " - snap-b \\(required at revision 3 by sets acme/fooname\\)") + // snapB is at the wrong revision, but ignore-validation is set so it's ok + c.Check(valsets.CheckInstalledSnaps([]*snapasserts.InstalledSnap{snapBinvRev}, map[string]bool{"snap-b": true}), IsNil) +} + func (s *validationSetsSuite) TestCheckInstalledSnapsErrorFormat(c *C) { vs1 := assertstest.FakeAssertion(map[string]interface{}{ "type": "validation-set", @@ -641,7 +694,7 @@ } for i, tc := range tests { - err := valsets.CheckInstalledSnaps(tc.snaps) + err := valsets.CheckInstalledSnaps(tc.snaps, nil) c.Assert(err, NotNil, Commentf("#%d", i)) c.Assert(err, ErrorMatches, tc.errorMsg, Commentf("#%d: ", i)) } diff -Nru snapd-2.53+21.10ubuntu1/asserts/snap_asserts.go snapd-2.54.2+21.10/asserts/snap_asserts.go --- snapd-2.53+21.10ubuntu1/asserts/snap_asserts.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/snap_asserts.go 2022-01-06 21:25:16.000000000 +0000 @@ -175,6 +175,9 @@ if rule.feature(nameConstraintsFeature) { setFormatNum(4) } + if rule.feature(altAttrMatcherFeature) { + setFormatNum(5) + } }) if err != nil { return 0, err @@ -194,6 +197,9 @@ if rule.feature(nameConstraintsFeature) { setFormatNum(4) } + if rule.feature(altAttrMatcherFeature) { + setFormatNum(5) + } }) if err != nil { return 0, err diff -Nru snapd-2.53+21.10ubuntu1/asserts/snap_asserts_test.go snapd-2.54.2+21.10/asserts/snap_asserts_test.go --- snapd-2.53+21.10ubuntu1/asserts/snap_asserts_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/snap_asserts_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -546,6 +546,24 @@ c.Check(fmtnum, Equals, 4) } } + + // alt matcher (so far unused) => format 5 + for _, sidePrefix := range []string{"plug", "slot"} { + headers = map[string]interface{}{ + sidePrefix + "s": map[string]interface{}{ + "interface5": map[string]interface{}{ + "allow-auto-connection": map[string]interface{}{ + sidePrefix + "-attributes": map[string]interface{}{ + "x": []interface{}{"alt1", "alt2"}, // alt matcher + }, + }, + }, + }, + } + fmtnum, err = asserts.SuggestFormat(asserts.SnapDeclarationType, headers, nil) + c.Assert(err, IsNil) + c.Check(fmtnum, Equals, 5) + } } func prereqDevAccount(c *C, storeDB assertstest.SignerDB, db *asserts.Database) { diff -Nru snapd-2.53+21.10ubuntu1/asserts/sysdb/staging.go snapd-2.54.2+21.10/asserts/sysdb/staging.go --- snapd-2.53+21.10ubuntu1/asserts/sysdb/staging.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/sysdb/staging.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withtestkeys || withstagingkeys // +build withtestkeys withstagingkeys /* diff -Nru snapd-2.53+21.10ubuntu1/asserts/sysdb/testkeys.go snapd-2.54.2+21.10/asserts/sysdb/testkeys.go --- snapd-2.53+21.10ubuntu1/asserts/sysdb/testkeys.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/asserts/sysdb/testkeys.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withtestkeys // +build withtestkeys /* diff -Nru snapd-2.53+21.10ubuntu1/boot/seal.go snapd-2.54.2+21.10/boot/seal.go --- snapd-2.53+21.10ubuntu1/boot/seal.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/boot/seal.go 2022-01-06 21:25:16.000000000 +0000 @@ -335,6 +335,11 @@ // resealKeyToModeenv reseals the existing encryption key to the // parameters specified in modeenv. +// It is *very intentional* that resealing takes the modeenv and only +// the modeenv as input. modeenv content is well defined and updated +// atomically. In particular we want to avoid resealing against +// transient/in-memory information with the risk that successive +// reseals during in-progress operations produce diverging outcomes. func resealKeyToModeenvImpl(rootdir string, modeenv *Modeenv, expectReseal bool) error { method, err := sealedKeysMethod(rootdir) if err == errNoSealedKeys { diff -Nru snapd-2.53+21.10ubuntu1/bootloader/assets/assetstesting.go snapd-2.54.2+21.10/bootloader/assets/assetstesting.go --- snapd-2.53+21.10ubuntu1/bootloader/assets/assetstesting.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/bootloader/assets/assetstesting.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withbootassetstesting // +build withbootassetstesting /* diff -Nru snapd-2.53+21.10ubuntu1/bootloader/assets/grub_cfg_asset.go snapd-2.54.2+21.10/bootloader/assets/grub_cfg_asset.go --- snapd-2.53+21.10ubuntu1/bootloader/assets/grub_cfg_asset.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/bootloader/assets/grub_cfg_asset.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2021 Canonical Ltd + * Copyright (C) 2022 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as diff -Nru snapd-2.53+21.10ubuntu1/bootloader/assets/grub_recovery_cfg_asset.go snapd-2.54.2+21.10/bootloader/assets/grub_recovery_cfg_asset.go --- snapd-2.53+21.10ubuntu1/bootloader/assets/grub_recovery_cfg_asset.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/bootloader/assets/grub_recovery_cfg_asset.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2021 Canonical Ltd + * Copyright (C) 2022 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as diff -Nru snapd-2.53+21.10ubuntu1/bootloader/export_test.go snapd-2.54.2+21.10/bootloader/export_test.go --- snapd-2.53+21.10ubuntu1/bootloader/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/bootloader/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -123,17 +123,41 @@ lkBootDisk := &disks.MockDiskMapping{ // mock the partition labels, since these structures won't have // filesystems, but they will have partition labels - PartitionLabelToPartUUID: map[string]string{ - "snapbootsel": "snapbootsel-partuuid", - "snapbootselbak": "snapbootselbak-partuuid", - "snaprecoverysel": "snaprecoverysel-partuuid", - "snaprecoveryselbak": "snaprecoveryselbak-partuuid", + Structure: []disks.Partition{ + { + PartitionLabel: "snapbootsel", + PartitionUUID: "snapbootsel-partuuid", + }, + { + PartitionLabel: "snapbootselbak", + PartitionUUID: "snapbootselbak-partuuid", + }, + { + PartitionLabel: "snaprecoverysel", + PartitionUUID: "snaprecoverysel-partuuid", + }, + { + PartitionLabel: "snaprecoveryselbak", + PartitionUUID: "snaprecoveryselbak-partuuid", + }, // for run mode kernel snaps - "boot_a": "boot-a-partuuid", - "boot_b": "boot-b-partuuid", + { + PartitionLabel: "boot_a", + PartitionUUID: "boot-a-partuuid", + }, + { + PartitionLabel: "boot_b", + PartitionUUID: "boot-b-partuuid", + }, // for recovery system kernel snaps - "boot_ra": "boot-ra-partuuid", - "boot_rb": "boot-rb-partuuid", + { + PartitionLabel: "boot_ra", + PartitionUUID: "boot-ra-partuuid", + }, + { + PartitionLabel: "boot_rb", + PartitionUUID: "boot-rb-partuuid", + }, }, DiskHasPartitions: true, DevNum: "lk-boot-disk-dev-num", @@ -144,7 +168,7 @@ } // mock the disk - r := disks.MockDeviceNameDisksToPartitionMapping(m) + r := disks.MockDeviceNameToDiskMapping(m) cleanups = append(cleanups, r) // now mock the kernel command line diff -Nru snapd-2.53+21.10ubuntu1/bootloader/withbootassettesting.go snapd-2.54.2+21.10/bootloader/withbootassettesting.go --- snapd-2.53+21.10ubuntu1/bootloader/withbootassettesting.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/bootloader/withbootassettesting.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withbootassetstesting // +build withbootassetstesting /* diff -Nru snapd-2.53+21.10ubuntu1/bootloader/withbootassettesting_test.go snapd-2.54.2+21.10/bootloader/withbootassettesting_test.go --- snapd-2.53+21.10ubuntu1/bootloader/withbootassettesting_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/bootloader/withbootassettesting_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withbootassetstesting // +build withbootassetstesting /* diff -Nru snapd-2.53+21.10ubuntu1/build-aux/snap/snapcraft.yaml snapd-2.54.2+21.10/build-aux/snap/snapcraft.yaml --- snapd-2.53+21.10ubuntu1/build-aux/snap/snapcraft.yaml 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/build-aux/snap/snapcraft.yaml 2022-01-06 21:25:16.000000000 +0000 @@ -30,16 +30,36 @@ plugin: nil source: . build-snaps: [go/1.13/stable] + # these packages are needed to call mkversion.sh in override-pull, all other + # dependencies are installed using apt-get build-dep + build-packages: + - git + - dpkg-dev override-pull: | snapcraftctl pull + # set version, this needs dpkg-parsechangelog (from dpkg-dev) and git + snapcraftctl set-version "$(./mkversion.sh --output-only)" + # Ensure that ./debian/ packaging which we are about to use + # matches the current `build-base` release. I.e. ubuntu-16.04 + # for build-base:core, etc. + ./generate-packaging-dir # 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: | + # TODO: when something like "craftctl get-version" is ready, then we can + # use that, but until then, we have to re-run mkversion.sh to check if the + # version number was set as "dirty" from the override-pull step + if sh -x ./mkversion.sh --output-only | grep "dirty"; then + mkdir -p $SNAPCRAFT_PART_INSTALL/usr/lib/snapd + ( + echo "dirty git tree during build detected:" + git status + git diff + ) > $SNAPCRAFT_PART_INSTALL/usr/lib/snapd/dirty-git-tree-info.txt + fi # 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 diff -Nru snapd-2.53+21.10ubuntu1/client/client.go snapd-2.54.2+21.10/client/client.go --- snapd-2.53+21.10ubuntu1/client/client.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/client/client.go 2022-01-06 21:25:16.000000000 +0000 @@ -23,6 +23,7 @@ "bytes" "context" "encoding/json" + "errors" "fmt" "io" "io/ioutil" @@ -175,10 +176,15 @@ return fmt.Sprintf("cannot build request: %v", e.error) } -type AuthorizationError struct{ error } +type AuthorizationError struct{ Err error } func (e AuthorizationError) Error() string { - return fmt.Sprintf("cannot add authorization: %v", e.error) + return fmt.Sprintf("cannot add authorization: %v", e.Err) +} + +func (e AuthorizationError) Is(target error) bool { + _, ok := target.(AuthorizationError) + return ok } type ConnectionError struct{ Err error } @@ -200,6 +206,17 @@ return e.Err } +type InternalClientError struct{ Err error } + +func (e InternalClientError) Error() string { + return fmt.Sprintf("internal error: %s", e.Err.Error()) +} + +func (e InternalClientError) Is(target error) bool { + _, ok := target.(InternalClientError) + return ok +} + // AllowInteractionHeader is the HTTP request header used to indicate // that the client is willing to allow interaction. const AllowInteractionHeader = "X-Allow-Interaction" @@ -267,7 +284,7 @@ func (client *Client) rawWithTimeout(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body io.Reader, opts *doOptions) (*http.Response, context.CancelFunc, error) { opts = ensureDoOpts(opts) if opts.Timeout <= 0 { - return nil, nil, fmt.Errorf("internal error: timeout not set in options for rawWithTimeout") + return nil, nil, InternalClientError{fmt.Errorf("timeout not set in options for rawWithTimeout")} } ctx, cancel := context.WithTimeout(ctx, opts.Timeout) @@ -349,13 +366,13 @@ client.checkMaintenanceJSON() var rsp *http.Response - var ctx context.Context = context.Background() + ctx := context.Background() if opts.Timeout <= 0 { // no timeout and retries rsp, err = client.raw(ctx, method, path, query, headers, body) } else { if opts.Retry <= 0 { - return 0, fmt.Errorf("internal error: retry setting %s invalid", opts.Retry) + return 0, InternalClientError{fmt.Errorf("retry setting %s invalid", opts.Retry)} } retry := time.NewTicker(opts.Retry) defer retry.Stop() @@ -371,7 +388,7 @@ if err == nil { defer cancel() } - if err == nil || method != "GET" { + if err == nil || shouldNotRetryError(err) || method != "GET" { break } select { @@ -396,6 +413,11 @@ return rsp.StatusCode, nil } +func shouldNotRetryError(err error) bool { + return errors.Is(err, AuthorizationError{}) || + errors.Is(err, InternalClientError{}) +} + func decodeInto(reader io.Reader, v interface{}) error { dec := json.NewDecoder(reader) if err := dec.Decode(v); err != nil { diff -Nru snapd-2.53+21.10ubuntu1/client/client_test.go snapd-2.54.2+21.10/client/client_test.go --- snapd-2.53+21.10ubuntu1/client/client_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/client/client_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -225,6 +225,26 @@ c.Assert(cs.doCalls < 1100, Equals, true, Commentf("got %v calls", cs.doCalls)) } +func (cs *clientSuite) TestClientOnlyRetryAppropriateErrors(c *C) { + reqBody := ioutil.NopCloser(strings.NewReader("")) + doOpts := &client.DoOptions{ + Retry: time.Millisecond, + Timeout: 1 * time.Minute, + } + + for _, t := range []struct{ error }{ + {client.InternalClientError{Err: fmt.Errorf("boom")}}, + {client.AuthorizationError{Err: fmt.Errorf("boom")}}, + } { + cs.doCalls = 0 + cs.err = t.error + + _, err := cs.cli.Do("GET", "/this", nil, reqBody, nil, doOpts) + c.Check(err, ErrorMatches, fmt.Sprintf(".*%s", t.error.Error())) + c.Assert(cs.doCalls, Equals, 1) + } +} + func (cs *clientSuite) TestClientUnderstandsStatusCode(c *C) { var v []int cs.status = 202 diff -Nru snapd-2.53+21.10ubuntu1/cmd/.clangd snapd-2.54.2+21.10/cmd/.clangd --- snapd-2.53+21.10ubuntu1/cmd/.clangd 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/.clangd 2022-01-06 21:25:16.000000000 +0000 @@ -1,2 +1,2 @@ CompileFlags: - Add: -I/usr/include/glib-2.0 -Wall -Wextra -Wmissing-prototypes -Wstrict-prototypes -Wno-missing-field-initializers -Wno-unused-parameter \ No newline at end of file + Add: [-I/usr/include/glib-2.0, -Wall, -Wextra, -Wmissing-prototypes, -Wstrict-prototypes, -Wno-missing-field-initializers, -Wno-unused-parameter] \ No newline at end of file diff -Nru snapd-2.53+21.10ubuntu1/cmd/libsnap-confine-private/cleanup-funcs.c snapd-2.54.2+21.10/cmd/libsnap-confine-private/cleanup-funcs.c --- snapd-2.53+21.10ubuntu1/cmd/libsnap-confine-private/cleanup-funcs.c 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/libsnap-confine-private/cleanup-funcs.c 2022-01-06 21:25:16.000000000 +0000 @@ -28,6 +28,14 @@ } } +void sc_cleanup_shallow_strv(const char ***ptr) +{ + if (ptr != NULL && *ptr != NULL) { + free(*ptr); + *ptr = NULL; + } +} + void sc_cleanup_file(FILE ** ptr) { if (ptr != NULL && *ptr != NULL) { diff -Nru snapd-2.53+21.10ubuntu1/cmd/libsnap-confine-private/cleanup-funcs.h snapd-2.54.2+21.10/cmd/libsnap-confine-private/cleanup-funcs.h --- snapd-2.53+21.10ubuntu1/cmd/libsnap-confine-private/cleanup-funcs.h 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/libsnap-confine-private/cleanup-funcs.h 2022-01-06 21:25:16.000000000 +0000 @@ -41,6 +41,16 @@ void sc_cleanup_string(char **ptr); /** + * Shallow free a dynamically allocated string vector. + * + * The strings in the vector will not be freed. + * 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_shallow_strv(const char ***ptr); + +/** * Close an open file. * * This function is designed to be used with SC_CLEANUP() macro. diff -Nru snapd-2.53+21.10ubuntu1/cmd/libsnap-confine-private/cleanup-funcs-test.c snapd-2.54.2+21.10/cmd/libsnap-confine-private/cleanup-funcs-test.c --- snapd-2.53+21.10ubuntu1/cmd/libsnap-confine-private/cleanup-funcs-test.c 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/libsnap-confine-private/cleanup-funcs-test.c 2022-01-06 21:25:16.000000000 +0000 @@ -142,6 +142,27 @@ g_assert_cmpint(fd, ==, -1); } +static void test_cleanup_shallow_strv(void) +{ + /* It is safe to use with a NULL pointer */ + sc_cleanup_shallow_strv(NULL); + + const char **argses = NULL; + /* It is ok of the pointer value is NULL */ + sc_cleanup_shallow_strv(&argses); + g_assert_null(argses); + + argses = calloc(10, sizeof(char *)); + g_assert_nonnull(argses); + /* Fill with bogus pointers so attempts to free them would segfault */ + for (int i = 0; i < 10; i++) { + argses[i] = (char *)0x100 + i; + } + sc_cleanup_shallow_strv(&argses); + g_assert_null(argses); + /* If we are alive at this point, most likely only the array was free'd */ +} + static void __attribute__((constructor)) init(void) { g_test_add_func("/cleanup/sanity", test_cleanup_sanity); @@ -150,4 +171,5 @@ 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); + g_test_add_func("/cleanup/shallow_strv", test_cleanup_shallow_strv); } diff -Nru snapd-2.53+21.10ubuntu1/cmd/libsnap-confine-private/device-cgroup-support.c snapd-2.54.2+21.10/cmd/libsnap-confine-private/device-cgroup-support.c --- snapd-2.53+21.10ubuntu1/cmd/libsnap-confine-private/device-cgroup-support.c 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/libsnap-confine-private/device-cgroup-support.c 2022-01-06 21:25:16.000000000 +0000 @@ -57,8 +57,8 @@ sc_cgroup_fds fds; } v1; struct { - int cgroup_fd; int devmap_fd; + int prog_fd; char *tag; struct rlimit old_limit; } v2; @@ -73,20 +73,36 @@ static int _sc_cgroup_v1_init(sc_device_cgroup *self, int flags) { self->v1.fds = sc_cgroup_fds_new(); + /* are we creating the group or just using whatever there is? */ + const bool from_existing = (flags & SC_DEVICE_CGROUP_FROM_EXISTING) != 0; /* initialize to something sane */ if (sc_udev_open_cgroup_v1(self->security_tag, flags, &self->v1.fds) < 0) { - if ((flags & SC_DEVICE_CGROUP_FROM_EXISTING) != 0) { + if (from_existing) { return -1; } die("cannot prepare cgroup v1 device hierarchy"); } - /* Deny device access by default. - * - * Write 'a' to devices.deny to remove all existing devices that were added - * in previous launcher invocations, then add the static and assigned - * devices. This ensures that at application launch the cgroup only has - * what is currently assigned. */ - sc_dprintf(self->v1.fds.devices_deny_fd, "a"); + /* Only deny devices if we are not using an existing group - + * if we deny devices for an existing group that we just opened, + * we risk denying access to a device that a currently running process + * is about to access and should legitimately have access to. + * A concrete example of this is when this function is used by snap-device-helper + * when a new udev device event is triggered and we are adding that device + * to the snap's device cgroup. At this point, a running application may be + * accessing other devices which it should have access to (such as /dev/null + * or one of the other common, default devices) we would deny access to that + * existing device by re-creating the allow list of devices every time. + * */ + if (!from_existing) { + /* starting a device cgroup from scratch, so deny device access by + * default. + * + * Write 'a' to devices.deny to remove all existing devices that were added + * in previous launcher invocations, then add the static and assigned + * devices. This ensures that at application launch the cgroup only has + * what is currently assigned. */ + sc_dprintf(self->v1.fds.devices_deny_fd, "a"); + } return 0; } @@ -132,11 +148,6 @@ */ typedef uint8_t sc_cgroup_v2_device_value; -static void _sc_cgroup_v2_attach_pid(sc_device_cgroup *self, pid_t pid) { - /* nothing to do here, the device controller is attached to the cgroup - * already, and we are part of it */ -} - #ifdef ENABLE_BPF static int load_devcgroup_prog(int map_fd) { /* Basic rules about registers: @@ -284,28 +295,29 @@ return old_limit; } +static bool _sc_is_snap_cgroup(const char *group) { + /* make a copy as basename may modify its input */ + char copy[PATH_MAX] = {0}; + strncpy(copy, group, sizeof(copy) - 1); + char *leaf = basename(copy); + if (!sc_startswith(leaf, "snap.")) { + return false; + } + if (!sc_endswith(leaf, ".service") && !sc_endswith(leaf, ".scope")) { + return false; + } + return true; +} + static int _sc_cgroup_v2_init_bpf(sc_device_cgroup *self, int flags) { self->v2.devmap_fd = -1; - self->v2.cgroup_fd = -1; - - char *own_group SC_CLEANUP(sc_cleanup_string) = sc_cgroup_v2_own_path_full(); - if (own_group == NULL) { - die("cannot obtain own group path"); - } + self->v2.prog_fd = -1; /* fix the memlock limit if needed, this affects creating maps */ self->v2.old_limit = _sc_cgroup_v2_adjust_memlock_limit(); const bool from_existing = (flags & SC_DEVICE_CGROUP_FROM_EXISTING) != 0; - char own_group_full[PATH_MAX] = {0}; - sc_must_snprintf(own_group_full, sizeof(own_group_full), "/sys/fs/cgroup/%s", own_group); - int cgroup_fd = open(own_group_full, O_PATH | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); - if (cgroup_fd < 0) { - die("cannot open own cgroup directory %s", own_group_full); - } - debug("cgroup %s opened at %d", own_group_full, cgroup_fd); - self->v2.tag = sc_strdup(self->security_tag); /* bpffs is unhappy about dots in the name, replace all with underscores */ for (char *c = strchr(self->v2.tag, '.'); c != NULL; c = strchr(c, '.')) { @@ -325,14 +337,19 @@ } /* and obtain a file descriptor to the map, also as root */ int devmap_fd = bpf_get_by_path(path); + /* keep a copy of errno in case it gets clobbered */ + int get_by_path_errno = errno; (void)sc_set_effective_identity(old); /* XXX: this should be more than enough keys */ const size_t max_entries = 500; if (devmap_fd < 0) { - if (errno != ENOENT) { + if (get_by_path_errno != ENOENT) { die("cannot get existing device map"); } if (from_existing) { + debug("device map not present, not creating one"); + /* restore the errno so that the caller sees ENOENT */ + errno = get_by_path_errno; /* there is no map, and we haven't been asked to setup a new cgroup */ return -1; } @@ -426,15 +443,12 @@ /* load and attach the BPF program as root */ (void)sc_set_effective_identity(sc_root_group_identity()); int prog_fd = load_devcgroup_prog(devmap_fd); - int attach = bpf_prog_attach(BPF_CGROUP_DEVICE, cgroup_fd, prog_fd); - if (attach < 0) { - die("cannot attach cgroup program"); - } (void)sc_set_effective_identity(old); + /* keep track of the program */ + self->v2.prog_fd = prog_fd; } self->v2.devmap_fd = devmap_fd; - self->v2.cgroup_fd = cgroup_fd; return 0; } @@ -447,7 +461,7 @@ /* the map is pinned to a per-snap-application file and referenced by the * program */ sc_cleanup_close(&self->v2.devmap_fd); - sc_cleanup_close(&self->v2.cgroup_fd); + sc_cleanup_close(&self->v2.prog_fd); } static void _sc_cgroup_v2_allow_bpf(sc_device_cgroup *self, int kind, int major, int minor) { @@ -474,6 +488,48 @@ die("cannot delete device map entry for key %c %u:%u", key.type, key.major, key.minor); } } + +static void _sc_cgroup_v2_attach_pid_bpf(sc_device_cgroup *self, pid_t pid) { + /* we are setting up device filtering for ourselves */ + if (pid != getpid()) { + die("internal error: cannot attach device cgroup to other process than current"); + } + if (self->v2.prog_fd == -1) { + die("internal error: BPF program not loaded"); + } + + char *own_group SC_CLEANUP(sc_cleanup_string) = sc_cgroup_v2_own_path_full(); + if (own_group == NULL) { + die("cannot obtain own group path"); + } + debug("process in cgroup %s", own_group); + + if (!_sc_is_snap_cgroup(own_group)) { + /* we cannot proceed to install a device filtering program when the + * process is not in a snap specific cgroup, as we would effectively + * lock down the group that can be shared with other processes or even + * the whole desktop session */ + die("%s is not a snap cgroup", own_group); + } + + char own_group_full_path[PATH_MAX] = {0}; + sc_must_snprintf(own_group_full_path, sizeof(own_group_full_path), "/sys/fs/cgroup/%s", own_group); + + int cgroup_fd SC_CLEANUP(sc_cleanup_close) = -1; + cgroup_fd = open(own_group_full_path, O_PATH | O_DIRECTORY | O_CLOEXEC | O_NOFOLLOW); + if (cgroup_fd < 0) { + die("cannot open own cgroup directory %s", own_group_full_path); + } + debug("cgroup %s opened at %d", own_group_full_path, cgroup_fd); + + /* attach the program to the cgroup */ + sc_identity old = sc_set_effective_identity(sc_root_group_identity()); + int attach = bpf_prog_attach(BPF_CGROUP_DEVICE, cgroup_fd, self->v2.prog_fd); + (void)sc_set_effective_identity(old); + if (attach < 0) { + die("cannot attach cgroup program"); + } +} #endif /* ENABLE_BPF */ static void _sc_cgroup_v2_close(sc_device_cgroup *self) { @@ -496,6 +552,14 @@ #else die("device cgroup v2 is not enabled"); #endif +} + +static void _sc_cgroup_v2_attach_pid(sc_device_cgroup *self, pid_t pid) { +#ifdef ENABLE_BPF + _sc_cgroup_v2_attach_pid_bpf(self, pid); +#else + die("device cgroup v2 is not enabled"); +#endif } static int _sc_cgroup_v2_init(sc_device_cgroup *self, int flags) { diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/cmd_delete_key.go snapd-2.54.2+21.10/cmd/snap/cmd_delete_key.go --- snapd-2.53+21.10ubuntu1/cmd/snap/cmd_delete_key.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/cmd_delete_key.go 2022-01-06 21:25:16.000000000 +0000 @@ -62,7 +62,7 @@ if err != nil { return err } - err = keypairMgr.Delete(string(x.Positional.KeyName)) + err = keypairMgr.DeleteByName(string(x.Positional.KeyName)) if _, ok := err.(*asserts.ExternalUnsupportedOpError); ok { return fmt.Errorf(i18n.G("cannot delete external keypair manager key via snap command, use the appropriate external procedure")) } diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/cmd_disconnect.go snapd-2.54.2+21.10/cmd/snap/cmd_disconnect.go --- snapd-2.53+21.10ubuntu1/cmd/snap/cmd_disconnect.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/cmd_disconnect.go 2022-01-06 21:25:16.000000000 +0000 @@ -73,8 +73,8 @@ return ErrExtraArgs } - offer := x.Positionals.Offer.SnapAndName - use := x.Positionals.Use.SnapAndName + offer := x.Positionals.Offer.SnapAndNameStrict + use := x.Positionals.Use.SnapAndNameStrict // snap disconnect : // snap disconnect @@ -82,9 +82,6 @@ // Swap Offer and Use around offer, use = use, offer } - if use.Name == "" { - return fmt.Errorf("please provide the plug or slot name to disconnect from snap %q", use.Snap) - } opts := &client.DisconnectOptions{Forget: x.Forget} id, err := x.client.Disconnect(offer.Snap, offer.Name, use.Snap, use.Name, opts) diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/cmd_disconnect_test.go snapd-2.54.2+21.10/cmd/snap/cmd_disconnect_test.go --- snapd-2.53+21.10ubuntu1/cmd/snap/cmd_disconnect_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/cmd_disconnect_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -209,7 +209,7 @@ c.Fatalf("expected nothing to reach the server") }) rest, err := Parser(Client()).ParseArgs([]string{"disconnect", "consumer"}) - c.Assert(err, ErrorMatches, `please provide the plug or slot name to disconnect from snap "consumer"`) + c.Assert(err, ErrorMatches, `invalid value: "consumer" \(want snap:name or :name\)`) c.Assert(rest, DeepEquals, []string{"consumer"}) c.Assert(s.Stdout(), Equals, "") c.Assert(s.Stderr(), Equals, "") diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/cmd_list.go snapd-2.54.2+21.10/cmd/snap/cmd_list.go --- snapd-2.53+21.10ubuntu1/cmd/snap/cmd_list.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/cmd_list.go 2022-01-06 21:25:16.000000000 +0000 @@ -84,6 +84,14 @@ return ch[:idx+1] + "…" } +func fmtVersion(v string) string { + if v == "" { + // most likely a broken snap, leave a placeholder + return "-" + } + return v +} + func (x *cmdList) Execute(args []string) error { if len(args) > 0 { return ErrExtraArgs @@ -116,7 +124,7 @@ // doing it this way because otherwise it's a sea of %s\t%s\t%s line := []string{ snap.Name, - snap.Version, + fmtVersion(snap.Version), snap.Revision.String(), fmtChannel(snap.TrackingChannel), shortPublisher(esc, snap.Publisher), diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/cmd_list_test.go snapd-2.54.2+21.10/cmd/snap/cmd_list_test.go --- snapd-2.53+21.10ubuntu1/cmd/snap/cmd_list_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/cmd_list_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -202,6 +202,8 @@ ,{"name": "dm1", "status": "active", "version": "5", "revision":1, "devmode": true, "confinement": "devmode"} ,{"name": "dm2", "status": "active", "version": "5", "revision":1, "devmode": true, "confinement": "strict"} ,{"name": "cf1", "status": "active", "version": "6", "revision":2, "confinement": "devmode", "jailmode": true} +,{"name": "br1", "status": "active", "version": "", "revision":2, "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "confinement": "strict", "broken": "snap is broken"} +,{"name": "dbr1", "status": "", "version": "", "revision":2, "publisher": {"id": "bar-id", "username": "bar", "display-name": "Bar", "validation": "unproven"}, "confinement": "strict", "broken": "snap is broken"} ]}`) default: c.Fatalf("expected to get 1 requests, now on %d", n+1) @@ -217,6 +219,8 @@ c.Check(s.Stdout(), check.Matches, `(?ms).*^dm1 +.* +devmode$`) c.Check(s.Stdout(), check.Matches, `(?ms).*^dm2 +.* +devmode$`) c.Check(s.Stdout(), check.Matches, `(?ms).*^cf1 +.* +jailmode$`) + c.Check(s.Stdout(), check.Matches, `(?ms).*^br1 +- +2 +- +bar +broken$`) + c.Check(s.Stdout(), check.Matches, `(?ms).*^dbr1 +- +2 +- +bar +disabled,broken$`) c.Check(s.Stderr(), check.Equals, "") } diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/cmd_run.go snapd-2.54.2+21.10/cmd/snap/cmd_run.go --- snapd-2.53+21.10ubuntu1/cmd/snap/cmd_run.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/cmd_run.go 2022-01-06 21:25:16.000000000 +0000 @@ -329,9 +329,9 @@ return info, err } -func createOrUpdateUserDataSymlink(info *snap.Info, usr *user.User) error { +func createOrUpdateUserDataSymlink(info *snap.Info, usr *user.User, opts *dirs.SnapDirOptions) error { // 'current' symlink for user data (SNAP_USER_DATA) - userData := info.UserDataDir(usr.HomeDir) + userData := info.UserDataDir(usr.HomeDir, opts) wantedSymlinkValue := filepath.Base(userData) currentActiveSymlink := filepath.Join(userData, "..", "current") @@ -373,7 +373,11 @@ return nil } -func createUserDataDirs(info *snap.Info) error { +func createUserDataDirs(info *snap.Info, opts *dirs.SnapDirOptions) error { + if opts == nil { + opts = &dirs.SnapDirOptions{} + } + // 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 @@ -387,16 +391,20 @@ return fmt.Errorf(i18n.G("cannot get the current user: %v"), err) } + snapDir := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) + if err := os.MkdirAll(snapDir, 0700); err != nil { + return fmt.Errorf(i18n.G("cannot create snap home dir: %w"), err) + } // see snapenv.User - instanceUserData := info.UserDataDir(usr.HomeDir) - instanceCommonUserData := info.UserCommonDataDir(usr.HomeDir) + instanceUserData := info.UserDataDir(usr.HomeDir, opts) + instanceCommonUserData := info.UserCommonDataDir(usr.HomeDir, opts) createDirs := []string{instanceUserData, instanceCommonUserData} if info.InstanceKey != "" { // parallel instance snaps get additional mapping in their mount // namespace, namely /home/joe/snap/foo_bar -> // /home/joe/snap/foo, make sure that the mount point exists and // is owned by the user - snapUserDir := snap.UserSnapDir(usr.HomeDir, info.SnapName()) + snapUserDir := snap.UserSnapDir(usr.HomeDir, info.SnapName(), opts) createDirs = append(createDirs, snapUserDir) } for _, d := range createDirs { @@ -406,17 +414,17 @@ } } - if err := createOrUpdateUserDataSymlink(info, usr); err != nil { + if err := createOrUpdateUserDataSymlink(info, usr, opts); err != nil { return err } - return maybeRestoreSecurityContext(usr) + return maybeRestoreSecurityContext(usr, opts) } // maybeRestoreSecurityContext attempts to restore security context of ~/snap on // systems where it's applicable -func maybeRestoreSecurityContext(usr *user.User) error { - snapUserHome := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) +func maybeRestoreSecurityContext(usr *user.User, opts *dirs.SnapDirOptions) error { + snapUserHome := snap.SnapDir(usr.HomeDir, opts) enabled, err := selinuxIsEnabled() if err != nil { return fmt.Errorf("cannot determine SELinux status: %v", err) @@ -1015,7 +1023,8 @@ return fmt.Errorf(i18n.G("missing snap-confine: try updating your core/snapd package")) } - if err := createUserDataDirs(info); err != nil { + opts := getSnapDirOptions() + if err := createUserDataDirs(info, opts); err != nil { logger.Noticef("WARNING: cannot create user data directory: %s", err) } @@ -1096,7 +1105,7 @@ if err != nil { return err } - snapenv.ExtendEnvForRun(env, info) + snapenv.ExtendEnvForRun(env, info, opts) if len(xauthPath) > 0 { // Environment is not nil here because it comes from @@ -1210,5 +1219,11 @@ } } +func getSnapDirOptions() *dirs.SnapDirOptions { + return &dirs.SnapDirOptions{ + HiddenSnapDataDir: features.HiddenSnapDataHomeDir.IsEnabled(), + } +} + var cgroupCreateTransientScopeForTracking = cgroup.CreateTransientScopeForTracking var cgroupConfirmSystemdServiceTracking = cgroup.ConfirmSystemdServiceTracking diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/cmd_run_test.go snapd-2.54.2+21.10/cmd/snap/cmd_run_test.go --- snapd-2.53+21.10ubuntu1/cmd/snap/cmd_run_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/cmd_run_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -38,6 +38,7 @@ "github.com/snapcore/snapd/features" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/osutil/strace" "github.com/snapcore/snapd/progress" "github.com/snapcore/snapd/progress/progresstest" "github.com/snapcore/snapd/sandbox/cgroup" @@ -478,14 +479,29 @@ } func (s *RunSuite) TestSnapRunCreateDataDirs(c *check.C) { + for _, t := range []struct { + snapDir string + opts *dirs.SnapDirOptions + }{ + {snapDir: dirs.UserHomeSnapDir}, + {snapDir: dirs.UserHomeSnapDir, opts: &dirs.SnapDirOptions{}}, + {snapDir: dirs.HiddenSnapDataHomeDir, opts: &dirs.SnapDirOptions{HiddenSnapDataDir: true}}, + } { + s.testSnapRunCreateDataDirs(c, t.snapDir, t.opts) + c.Assert(os.RemoveAll(s.fakeHome), check.IsNil) + s.fakeHome = c.MkDir() + } +} + +func (s *RunSuite) testSnapRunCreateDataDirs(c *check.C, snapDir string, opts *dirs.SnapDirOptions) { info, err := snap.InfoFromSnapYaml(mockYaml) c.Assert(err, check.IsNil) info.SideInfo.Revision = snap.R(42) - err = snaprun.CreateUserDataDirs(info) + err = snaprun.CreateUserDataDirs(info, opts) c.Assert(err, check.IsNil) - 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) + c.Check(osutil.FileExists(filepath.Join(s.fakeHome, snapDir, "snapname/42")), check.Equals, true) + c.Check(osutil.FileExists(filepath.Join(s.fakeHome, snapDir, "snapname/common")), check.Equals, true) } func (s *RunSuite) TestParallelInstanceSnapRunCreateDataDirs(c *check.C) { @@ -494,7 +510,7 @@ info.SideInfo.Revision = snap.R(42) info.InstanceKey = "foo" - err = snaprun.CreateUserDataDirs(info) + err = snaprun.CreateUserDataDirs(info, nil) c.Assert(err, check.IsNil) 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) @@ -993,7 +1009,7 @@ filepath.Join(straceCmd.BinDir(), "strace"), "-u", user.Username, "-f", - "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", + "-e", strace.ExcludedSyscalls, filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.app", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), @@ -1016,7 +1032,7 @@ filepath.Join(straceCmd.BinDir(), "strace"), "-u", user.Username, "-f", - "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", + "-e", strace.ExcludedSyscalls, filepath.Join(dirs.DistroLibExecDir, "snap-confine"), "snap.snapname.app", filepath.Join(dirs.CoreLibExecDir, "snap-exec"), @@ -1063,7 +1079,7 @@ filepath.Join(straceCmd.BinDir(), "strace"), "-u", user.Username, "-f", - "-e", "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep", + "-e", strace.ExcludedSyscalls, "-tt", "-o", "file with spaces", @@ -1819,3 +1835,20 @@ c.Check(meter.Finishes, check.Equals, 1) c.Check(meter.Labels, check.DeepEquals, []string{"please wait..."}) } + +func (s *RunSuite) TestCreateSnapDirPermissions(c *check.C) { + usr, err := user.Current() + c.Assert(err, check.IsNil) + + usr.HomeDir = s.fakeHome + snaprun.MockUserCurrent(func() (*user.User, error) { + return usr, nil + }) + + info := &snap.Info{SuggestedName: "some-snap"} + c.Assert(snaprun.CreateUserDataDirs(info, nil), check.IsNil) + + fi, err := os.Stat(filepath.Join(s.fakeHome, dirs.UserHomeSnapDir)) + c.Assert(err, check.IsNil) + c.Assert(fi.Mode()&os.ModePerm, check.Equals, os.FileMode(0700)) +} diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/cmd_snap_op.go snapd-2.54.2+21.10/cmd/snap/cmd_snap_op.go --- snapd-2.53+21.10ubuntu1/cmd/snap/cmd_snap_op.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/cmd_snap_op.go 2022-01-06 21:25:16.000000000 +0000 @@ -474,9 +474,10 @@ Name string `long:"name"` - Cohort string `long:"cohort"` - IgnoreRunning bool `long:"ignore-running" hidden:"yes"` - Positional struct { + Cohort string `long:"cohort"` + IgnoreValidation bool `long:"ignore-validation"` + IgnoreRunning bool `long:"ignore-running" hidden:"yes"` + Positional struct { Snaps []remoteSnapName `positional-arg-name:""` } `positional-args:"yes" required:"yes"` } @@ -592,12 +593,13 @@ dangerous := x.Dangerous || x.ForceDangerous opts := &client.SnapOptions{ - Channel: x.Channel, - Revision: x.Revision, - Dangerous: dangerous, - Unaliased: x.Unaliased, - CohortKey: x.Cohort, - IgnoreRunning: x.IgnoreRunning, + Channel: x.Channel, + Revision: x.Revision, + Dangerous: dangerous, + Unaliased: x.Unaliased, + CohortKey: x.Cohort, + IgnoreValidation: x.IgnoreValidation, + IgnoreRunning: x.IgnoreRunning, } x.setModes(opts) @@ -618,6 +620,9 @@ if x.asksForMode() || x.asksForChannel() { return errors.New(i18n.G("a single snap name is needed to specify mode or channel flags")) } + if x.IgnoreValidation { + return errors.New(i18n.G("a single snap name must be specified when ignoring validation")) + } if x.Name != "" { return errors.New(i18n.G("cannot use instance name when installing multiple snaps")) @@ -1107,6 +1112,8 @@ // TRANSLATORS: This should not start with a lowercase letter. "cohort": i18n.G("Install the snap in the given cohort"), // TRANSLATORS: This should not start with a lowercase letter. + "ignore-validation": i18n.G("Ignore validation by other snaps blocking the installation"), + // TRANSLATORS: This should not start with a lowercase letter. "ignore-running": i18n.G("Ignore running hooks or applications blocking the installation"), }), nil) addCommand("refresh", shortRefreshHelp, longRefreshHelp, func() flags.Commander { return &cmdRefresh{} }, diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/cmd_snap_op_test.go snapd-2.54.2+21.10/cmd/snap/cmd_snap_op_test.go --- snapd-2.53+21.10ubuntu1/cmd/snap/cmd_snap_op_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/cmd_snap_op_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1627,6 +1627,26 @@ c.Check(s.srv.n, check.Equals, s.srv.total) } +func (s *SnapOpSuite) TestInstallOneIgnoreValidation(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/one") + c.Check(DecodedRequestBody(c, r), check.DeepEquals, map[string]interface{}{ + "action": "install", + "ignore-validation": true, + }) + } + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--ignore-validation", "one"}) + c.Assert(err, check.IsNil) +} + +func (s *SnapOpSuite) TestInstallManyIgnoreValidation(c *check.C) { + s.RedirectClientToTestServer(nil) + _, err := snap.Parser(snap.Client()).ParseArgs([]string{"install", "--ignore-validation", "one", "two"}) + c.Assert(err, check.ErrorMatches, `a single snap name must be specified when ignoring validation`) +} + func (s *SnapOpSuite) TestEnable(c *check.C) { s.srv.total = 3 s.srv.checker = func(r *http.Request) { diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/cmd_userd.go snapd-2.54.2+21.10/cmd/snap/cmd_userd.go --- snapd-2.53+21.10ubuntu1/cmd/snap/cmd_userd.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/cmd_userd.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !darwin // +build !darwin /* @@ -24,13 +25,12 @@ "fmt" "os" "os/signal" - "path/filepath" "syscall" "github.com/jessevdk/go-flags" - "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snapdtool" "github.com/snapcore/snapd/usersession/agent" "github.com/snapcore/snapd/usersession/autostart" @@ -64,14 +64,7 @@ var osChmod = os.Chmod -func maybeFixupUsrSnapPermissions() error { - usr, err := userCurrent() - if err != nil { - return err - } - - usrSnapDir := filepath.Join(usr.HomeDir, dirs.UserHomeSnapDir) - +func maybeFixupUsrSnapPermissions(usrSnapDir string) error { // restrict the user's "snap dir", i.e. /home/$USER/snap, to be private with // permissions o0700 so that other users cannot read the data there, some // snaps such as chromium etc may store secrets inside this directory @@ -95,13 +88,19 @@ } if x.Autostart { + // get the user's snap dir ($HOME/snap or $HOME/.snap/data) + usrSnapDir, err := getUserSnapDir() + if err != nil { + return err + } + // autostart is called when starting the graphical session, use that as // an opportunity to fix ~/snap permission bits - if err := maybeFixupUsrSnapPermissions(); err != nil { + if err := maybeFixupUsrSnapPermissions(usrSnapDir); err != nil { fmt.Fprintf(Stderr, "failure fixing ~/snap permissions: %v\n", err) } - return x.runAutostart() + return x.runAutostart(usrSnapDir) } if x.Agent { @@ -154,8 +153,8 @@ return agent.Stop() } -func (x *cmdUserd) runAutostart() error { - if err := autostart.AutostartSessionApps(); err != nil { +func (x *cmdUserd) runAutostart(usrSnapDir string) error { + if err := autostart.AutostartSessionApps(usrSnapDir); err != nil { return fmt.Errorf("autostart failed for the following apps:\n%v", err) } return nil @@ -167,3 +166,13 @@ stop = func() { signal.Stop(ch) } return ch, stop } + +func getUserSnapDir() (string, error) { + usr, err := userCurrent() + if err != nil { + return "", err + } + + opts := getSnapDirOptions() + return snap.SnapDir(usr.HomeDir, opts), nil +} diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/cmd_userd_test.go snapd-2.54.2+21.10/cmd/snap/cmd_userd_test.go --- snapd-2.53+21.10ubuntu1/cmd/snap/cmd_userd_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/cmd_userd_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !darwin // +build !darwin /* diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/cmd_version_other.go snapd-2.54.2+21.10/cmd/snap/cmd_version_other.go --- snapd-2.53+21.10ubuntu1/cmd/snap/cmd_version_other.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/cmd_version_other.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !linux // +build !linux /* diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/cmd_warnings_test.go snapd-2.54.2+21.10/cmd/snap/cmd_warnings_test.go --- snapd-2.53+21.10ubuntu1/cmd/snap/cmd_warnings_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/cmd_warnings_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -222,7 +222,7 @@ c.Check(rest, check.HasLen, 0) c.Check(s.Stdout(), check.Equals, ` Name Version Rev Tracking Publisher Notes - unset - - disabled + - unset - - disabled `[1:]) c.Check(s.Stderr(), check.Equals, "WARNING: There are 2 new warnings. See 'snap warnings'.\n") diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/complete.go snapd-2.54.2+21.10/cmd/snap/complete.go --- snapd-2.53+21.10ubuntu1/cmd/snap/complete.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/complete.go 2022-01-06 21:25:16.000000000 +0000 @@ -202,7 +202,7 @@ } type disconnectSlotOrPlugSpec struct { - SnapAndName + SnapAndNameStrict } func (dps disconnectSlotOrPlugSpec) Complete(match string) []flags.Completion { @@ -217,7 +217,7 @@ } type disconnectSlotSpec struct { - SnapAndName + SnapAndNameStrict } // TODO: look at what the previous arg is, and filter accordingly diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/interfaces_common.go snapd-2.54.2+21.10/cmd/snap/interfaces_common.go --- snapd-2.53+21.10ubuntu1/cmd/snap/interfaces_common.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/interfaces_common.go 2022-01-06 21:25:16.000000000 +0000 @@ -32,7 +32,12 @@ Name string } -// UnmarshalFlag unmarshals snap and plug or slot name. +// UnmarshalFlag unmarshals the snap and plug or slot name. The following +// combinations are allowed: +// * : +// * +// * : +// Every other combination results in an error. func (sn *SnapAndName) UnmarshalFlag(value string) error { parts := strings.Split(value, ":") sn.Snap = "" @@ -53,3 +58,29 @@ } return nil } + +// SnapAndNameStrict holds a plug or slot name and, optionally, a snap name. +// The following combinations are allowed: +// * : +// * : +// Every other combination results in an error. +type SnapAndNameStrict struct { + SnapAndName +} + +// UnmarshalFlag unmarshals the snap and plug or slot name. The following +// combinations are allowed: +// * : +// * : +// Every other combination results in an error. +func (sn *SnapAndNameStrict) UnmarshalFlag(value string) error { + sn.Snap, sn.Name = "", "" + + parts := strings.Split(value, ":") + if len(parts) != 2 || parts[1] == "" { + return fmt.Errorf(i18n.G("invalid value: %q (want snap:name or :name)"), value) + } + + sn.Snap, sn.Name = parts[0], parts[1] + return nil +} diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/interfaces_common_test.go snapd-2.54.2+21.10/cmd/snap/interfaces_common_test.go --- snapd-2.53+21.10ubuntu1/cmd/snap/interfaces_common_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/interfaces_common_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -54,3 +54,33 @@ c.Check(sn.Name, Equals, "") } } + +func (s *SnapAndNameSuite) TestUnmarshalFlagStrict(c *C) { + var sn SnapAndNameStrict + + // Typical + err := sn.UnmarshalFlag("snap:name") + c.Assert(err, IsNil) + c.Check(sn.Snap, Equals, "snap") + c.Check(sn.Name, Equals, "name") + + // Core snap + err = sn.UnmarshalFlag(":name") + c.Assert(err, IsNil) + c.Check(sn.Snap, Equals, "") + c.Check(sn.Name, Equals, "name") + + // Invalid + for _, input := range []string{ + "snap:", // Empty name, should be spelled as "snap" + ":", // Both snap and name empty, makes no sense + "snap:name:more", // Name containing :, probably a typo + "", // Empty input + "snap", // Name empty unsupported for strict + } { + err = sn.UnmarshalFlag(input) + c.Assert(err, ErrorMatches, `invalid value: ".*" \(want snap:name or :name\)`) + c.Check(sn.Snap, Equals, "") + c.Check(sn.Name, Equals, "") + } +} diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/keymgr.go snapd-2.54.2+21.10/cmd/snap/keymgr.go --- snapd-2.53+21.10ubuntu1/cmd/snap/keymgr.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/keymgr.go 2022-01-06 21:25:16.000000000 +0000 @@ -36,7 +36,7 @@ GetByName(keyNname string) (asserts.PrivateKey, error) Export(keyName string) ([]byte, error) List() ([]asserts.ExternalKeyInfo, error) - Delete(keyName string) error + DeleteByName(keyName string) error } func getKeypairManager() (KeypairManager, error) { diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap/last.go snapd-2.54.2+21.10/cmd/snap/last.go --- snapd-2.53+21.10ubuntu1/cmd/snap/last.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap/last.go 2022-01-06 21:25:16.000000000 +0000 @@ -25,6 +25,7 @@ "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/i18n" + "github.com/snapcore/snapd/strutil" ) type changeIDMixin struct { @@ -72,7 +73,17 @@ kind = kind[:l] } // our internal change types use "-snap" postfix but let user skip it and use short form. - if kind == "refresh" || kind == "install" || kind == "remove" || kind == "connect" || kind == "disconnect" || kind == "configure" || kind == "try" { + shortForms := []string{ + // see api_snaps.go:snapInstructionDispTable + "install", "refresh", "remove", "revert", "enable", "disable", "switch", + // see api_interfaces.go:changeInterfaces + "connect", "disconnect", + // see api_snap_conf.go:setSnapConf + "configure", + // see api_sideload_n_try.go:trySnap + "try", + } + if strutil.ListContains(shortForms, kind) { kind += "-snap" } changes, err := queryChanges(cli, &client.ChangesOptions{Selector: client.ChangesAll}) diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/cmd_initramfs_mounts.go snapd-2.54.2+21.10/cmd/snap-bootstrap/cmd_initramfs_mounts.go --- snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/cmd_initramfs_mounts.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-bootstrap/cmd_initramfs_mounts.go 2022-01-06 21:25:16.000000000 +0000 @@ -86,6 +86,10 @@ secbootLockSealedKeys func() error bootFindPartitionUUIDForBootedKernelDisk = boot.FindPartitionUUIDForBootedKernelDisk + + mountReadOnlyOptions = &systemdMountOptions{ + ReadOnly: true, + } ) func stampedAction(stamp string, action func() error) error { @@ -1309,7 +1313,7 @@ dir := snapTypeToMountDir[essentialSnap.EssentialType] // TODO:UC20: we need to cross-check the kernel path with snapd_recovery_kernel used by grub - if err := doSystemdMount(essentialSnap.Path, filepath.Join(boot.InitramfsRunMntDir, dir), nil); err != nil { + if err := doSystemdMount(essentialSnap.Path, filepath.Join(boot.InitramfsRunMntDir, dir), mountReadOnlyOptions); err != nil { return nil, nil, err } } @@ -1557,7 +1561,7 @@ if sn, ok := mounts[typ]; ok { dir := snapTypeToMountDir[typ] snapPath := filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), sn.Filename()) - if err := doSystemdMount(snapPath, filepath.Join(boot.InitramfsRunMntDir, dir), nil); err != nil { + if err := doSystemdMount(snapPath, filepath.Join(boot.InitramfsRunMntDir, dir), mountReadOnlyOptions); err != nil { return err } } @@ -1570,8 +1574,7 @@ if err != nil { return fmt.Errorf("cannot load metadata and verify snapd snap: %v", err) } - - return doSystemdMount(essSnaps[0].Path, filepath.Join(boot.InitramfsRunMntDir, "snapd"), nil) + return doSystemdMount(essSnaps[0].Path, filepath.Join(boot.InitramfsRunMntDir, "snapd"), mountReadOnlyOptions) } return nil diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go snapd-2.54.2+21.10/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go --- snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-bootstrap/cmd_initramfs_mounts_nosecboot.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build nosecboot // +build nosecboot /* diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go snapd-2.54.2+21.10/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go --- snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-bootstrap/cmd_initramfs_mounts_secboot.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go snapd-2.54.2+21.10/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go --- snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-bootstrap/cmd_initramfs_mounts_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -92,35 +92,68 @@ needsNoSuidDiskMountOpts = &main.SystemdMountOptions{ NoSuid: true, } + snapMountOpts = &main.SystemdMountOptions{ + ReadOnly: true, + } + + seedPart = disks.Partition{ + FilesystemLabel: "ubuntu-seed", + PartitionUUID: "ubuntu-seed-partuuid", + } + + bootPart = disks.Partition{ + FilesystemLabel: "ubuntu-boot", + PartitionUUID: "ubuntu-boot-partuuid", + } + + savePart = disks.Partition{ + FilesystemLabel: "ubuntu-save", + PartitionUUID: "ubuntu-save-partuuid", + } + + dataPart = disks.Partition{ + FilesystemLabel: "ubuntu-data", + PartitionUUID: "ubuntu-data-partuuid", + } + + saveEncPart = disks.Partition{ + FilesystemLabel: "ubuntu-save-enc", + PartitionUUID: "ubuntu-save-enc-partuuid", + } + + dataEncPart = disks.Partition{ + FilesystemLabel: "ubuntu-data-enc", + PartitionUUID: "ubuntu-data-enc-partuuid", + } // a boot disk without ubuntu-save defaultBootDisk = &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-boot": "ubuntu-boot-partuuid", - "ubuntu-seed": "ubuntu-seed-partuuid", - "ubuntu-data": "ubuntu-data-partuuid", + Structure: []disks.Partition{ + seedPart, + bootPart, + dataPart, }, DiskHasPartitions: true, DevNum: "default", } defaultBootWithSaveDisk = &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-boot": "ubuntu-boot-partuuid", - "ubuntu-seed": "ubuntu-seed-partuuid", - "ubuntu-data": "ubuntu-data-partuuid", - "ubuntu-save": "ubuntu-save-partuuid", + Structure: []disks.Partition{ + seedPart, + bootPart, + dataPart, + savePart, }, DiskHasPartitions: true, DevNum: "default-with-save", } defaultEncBootDisk = &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-boot": "ubuntu-boot-partuuid", - "ubuntu-seed": "ubuntu-seed-partuuid", - "ubuntu-data-enc": "ubuntu-data-enc-partuuid", - "ubuntu-save-enc": "ubuntu-save-enc-partuuid", + Structure: []disks.Partition{ + bootPart, + seedPart, + dataEncPart, + saveEncPart, }, DiskHasPartitions: true, DevNum: "defaultEncDev", @@ -471,6 +504,7 @@ } mnt.what = filepath.Join(s.seedDir, "snaps", name+"_1.snap") mnt.where = filepath.Join(boot.InitramfsRunMntDir, dir) + mnt.opts = snapMountOpts return mnt } @@ -489,6 +523,7 @@ mnt.what = filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), sn.Filename()) mnt.where = filepath.Join(boot.InitramfsRunMntDir, dir) + mnt.opts = snapMountOpts return mnt } @@ -1217,6 +1252,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.kernel.Filename()), @@ -1224,6 +1260,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.core20.Filename()), @@ -1231,6 +1268,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", "tmpfs", @@ -1385,6 +1423,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.kernel.Filename()), @@ -1392,6 +1431,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.core20.Filename()), @@ -1399,6 +1439,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", "tmpfs", @@ -1531,6 +1572,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.kernel.Filename()), @@ -1538,6 +1580,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(s.seedDir, "snaps", s.core20.Filename()), @@ -1545,6 +1588,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", "tmpfs", @@ -1716,6 +1760,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), s.kernel.Filename()), @@ -1723,6 +1768,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, }) } @@ -1840,6 +1886,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, { "systemd-mount", filepath.Join(dirs.SnapBlobDirUnder(boot.InitramfsWritableDir), s.kernel.Filename()), @@ -1847,6 +1894,7 @@ "--no-pager", "--no-ask-password", "--fsck=no", + "--options=ro", }, }) } @@ -2086,10 +2134,10 @@ s.mockProcCmdlineContent(c, "snapd_recovery_mode=run") defaultEncNoSaveBootDisk := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-boot": "ubuntu-boot-partuuid", - "ubuntu-seed": "ubuntu-seed-partuuid", - "ubuntu-data-enc": "ubuntu-data-enc-partuuid", + Structure: []disks.Partition{ + seedPart, + bootPart, + dataEncPart, // missing ubuntu-save }, DiskHasPartitions: true, @@ -3542,10 +3590,11 @@ defer bootloader.Force(nil) defaultEncDiskNoBoot := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-seed": "ubuntu-seed-partuuid", - "ubuntu-data-enc": "ubuntu-data-enc-partuuid", - "ubuntu-save-enc": "ubuntu-save-enc-partuuid", + Structure: []disks.Partition{ + seedPart, + // missing ubuntu-boot + dataEncPart, + saveEncPart, }, DiskHasPartitions: true, DevNum: "defaultEncDevNoBoot", @@ -3698,10 +3747,11 @@ defer bootloader.Force(nil) defaultEncDiskNoBoot := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-seed": "ubuntu-seed-partuuid", - "ubuntu-data-enc": "ubuntu-data-enc-partuuid", - "ubuntu-save-enc": "ubuntu-save-enc-partuuid", + Structure: []disks.Partition{ + seedPart, + // missing ubuntu-boot + dataEncPart, + saveEncPart, }, DiskHasPartitions: true, DevNum: "defaultEncDevNoBoot", @@ -4059,10 +4109,10 @@ // no ubuntu-data on the disk at all mockDiskNoData := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-boot": "ubuntu-boot-partuuid", - "ubuntu-seed": "ubuntu-seed-partuuid", - "ubuntu-save": "ubuntu-save-partuuid", + Structure: []disks.Partition{ + seedPart, + bootPart, + savePart, }, DiskHasPartitions: true, DevNum: "noDataUnenc", @@ -4239,12 +4289,12 @@ // no ubuntu-data on the disk at all mockDiskDataUnencSaveEnc := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-boot": "ubuntu-boot-partuuid", - "ubuntu-seed": "ubuntu-seed-partuuid", + Structure: []disks.Partition{ + seedPart, + bootPart, // ubuntu-data is unencrypted but ubuntu-save is encrypted - "ubuntu-data": "ubuntu-data-partuuid", - "ubuntu-save-enc": "ubuntu-save-enc-partuuid", + dataPart, + saveEncPart, }, DiskHasPartitions: true, DevNum: "dataUnencSaveEnc", @@ -4372,12 +4422,12 @@ defer bootloader.Force(nil) mockDiskDataUnencSaveEnc := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-boot": "ubuntu-boot-partuuid", - "ubuntu-seed": "ubuntu-seed-partuuid", - "ubuntu-data-enc": "ubuntu-data-enc-partuuid", + Structure: []disks.Partition{ + seedPart, + bootPart, // ubuntu-data is encrypted but ubuntu-save is not - "ubuntu-save": "ubuntu-save-partuuid", + savePart, + dataEncPart, }, DiskHasPartitions: true, DevNum: "dataUnencSaveEnc", @@ -4627,12 +4677,12 @@ bootloader.Force(bloader) defer bootloader.Force(nil) - // no ubuntu-data on the disk at all mockDiskNoData := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-boot": "ubuntu-boot-partuuid", - "ubuntu-seed": "ubuntu-seed-partuuid", - "ubuntu-save-enc": "ubuntu-save-enc-partuuid", + Structure: []disks.Partition{ + seedPart, + bootPart, + // no ubuntu-data on the disk at all + saveEncPart, }, DiskHasPartitions: true, DevNum: "defaultEncDev", @@ -5193,21 +5243,33 @@ defer bootloader.Force(nil) mockDisk := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-seed": "ubuntu-seed-partuuid", - "ubuntu-boot": "ubuntu-boot-partuuid", - "ubuntu-data-enc": "ubuntu-data-enc-partuuid", - "ubuntu-save-enc": "ubuntu-save-enc-partuuid", + Structure: []disks.Partition{ + seedPart, + bootPart, + saveEncPart, + dataEncPart, }, DiskHasPartitions: true, DevNum: "bootDev", } attackerDisk := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-seed": "ubuntu-seed-attacker-partuuid", - "ubuntu-boot": "ubuntu-boot-attacker-partuuid", - "ubuntu-data-enc": "ubuntu-data-enc-attacker-partuuid", - "ubuntu-save-enc": "ubuntu-save-enc-attacker-partuuid", + Structure: []disks.Partition{ + { + FilesystemLabel: "ubuntu-seed", + PartitionUUID: "ubuntu-seed-attacker-partuuid", + }, + { + FilesystemLabel: "ubuntu-boot", + PartitionUUID: "ubuntu-boot-attacker-partuuid", + }, + { + FilesystemLabel: "ubuntu-save-enc", + PartitionUUID: "ubuntu-save-enc-attacker-partuuid", + }, + { + FilesystemLabel: "ubuntu-data-enc", + PartitionUUID: "ubuntu-data-enc-attacker-partuuid", + }, }, DiskHasPartitions: true, DevNum: "attackerDev", @@ -5332,8 +5394,8 @@ mockDiskMapping := map[disks.Mountpoint]*disks.MockDiskMapping{ {Mountpoint: boot.InitramfsUbuntuSeedDir}: { - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-seed": "ubuntu-seed-partuuid", + Structure: []disks.Partition{ + seedPart, }, DiskHasPartitions: true, }, @@ -5366,9 +5428,7 @@ // also add the ubuntu-data and ubuntu-save fs labels to the // disk referenced by the ubuntu-seed partition disk := mockDiskMapping[disks.Mountpoint{Mountpoint: boot.InitramfsUbuntuSeedDir}] - disk.FilesystemLabelToPartUUID["ubuntu-boot"] = "ubuntu-boot-partuuid" - disk.FilesystemLabelToPartUUID["ubuntu-data"] = "ubuntu-data-partuuid" - disk.FilesystemLabelToPartUUID["ubuntu-save"] = "ubuntu-save-partuuid" + disk.Structure = append(disk.Structure, bootPart, savePart, dataPart) // and also add the /run/mnt/host/ubuntu-{boot,data,save} mountpoints // for cross-checking after mounting diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/initramfs_systemd_mount.go snapd-2.54.2+21.10/cmd/snap-bootstrap/initramfs_systemd_mount.go --- snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/initramfs_systemd_mount.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-bootstrap/initramfs_systemd_mount.go 2022-01-06 21:25:16.000000000 +0000 @@ -72,6 +72,8 @@ NoSuid bool // Bind indicates a bind mount Bind bool + // Read-only mount + ReadOnly bool } // doSystemdMount will mount "what" at "where" using systemd-mount(1) with @@ -135,6 +137,9 @@ if opts.Bind { options = append(options, "bind") } + if opts.ReadOnly { + options = append(options, "ro") + } if len(options) > 0 { args = append(args, "--options="+strings.Join(options, ",")) } diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/initramfs_systemd_mount_test.go snapd-2.54.2+21.10/cmd/snap-bootstrap/initramfs_systemd_mount_test.go --- snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/initramfs_systemd_mount_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-bootstrap/initramfs_systemd_mount_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -22,6 +22,7 @@ import ( "fmt" "path/filepath" + "strings" "time" . "gopkg.in/check.v1" @@ -161,6 +162,16 @@ isMountedReturns: []bool{true}, comment: "happy nosuid+bind", }, + { + what: "/run/mnt/data/some.snap", + where: "/run/mnt/base", + opts: &main.SystemdMountOptions{ + ReadOnly: true, + }, + timeNowTimes: []time.Time{testStart, testStart}, + isMountedReturns: []bool{true}, + comment: "happy ro", + }, } for _, t := range tt { @@ -221,6 +232,8 @@ } else { c.Assert(err, IsNil) + c.Assert(len(cmd.Calls()), Equals, 1) + call := cmd.Calls()[0] args := []string{ "systemd-mount", t.what, t.where, "--no-pager", "--no-ask-password", } @@ -235,15 +248,30 @@ if opts.NoWait { args = append(args, "--no-block") } - if opts.Bind && opts.NoSuid { - args = append(args, "--options=nosuid,bind") - } else if opts.NoSuid { - args = append(args, "--options=nosuid") - } else if opts.Bind { - args = append(args, "--options=bind") + c.Assert(call[:len(args)], DeepEquals, args) + foundNoSuid := false + foundBind := false + foundReadOnly := false + if len(call) != len(args) { + c.Assert(len(call), Equals, len(args)+1) + c.Assert(strings.HasPrefix(call[len(args)], "--options="), Equals, true) + for _, opt := range strings.Split(strings.TrimPrefix(call[len(args)], "--options="), ",") { + switch opt { + case "nosuid": + foundNoSuid = true + case "bind": + foundBind = true + case "ro": + foundReadOnly = true + default: + c.Logf("Option '%s' unexpected", opt) + c.Fail() + } + } } - - c.Assert(cmd.Calls(), DeepEquals, [][]string{args}) + c.Assert(foundNoSuid, Equals, opts.NoSuid) + c.Assert(foundBind, Equals, opts.Bind) + c.Assert(foundReadOnly, Equals, opts.ReadOnly) // check that the overrides are present if opts.Ephemeral is false, // or check the overrides are not present if opts.Ephemeral is true diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/README.md snapd-2.54.2+21.10/cmd/snap-bootstrap/README.md --- snapd-2.53+21.10ubuntu1/cmd/snap-bootstrap/README.md 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-bootstrap/README.md 2022-01-06 21:25:16.000000000 +0000 @@ -2,6 +2,8 @@ Welcome to the world of the initramfs of UC20! +## Short intro + _snap-bootstrap_ is the main executable that is run during the initramfs stage of UC20. It has several responsibilities: 1. Mounting some partitions from the disk that UC20 is installed to. This includes ubuntu-data, ubuntu-boot, ubuntu-seed, and if present, ubuntu-save (ubuntu-save is optional on unencrypted devices). @@ -12,10 +14,64 @@ 1. _snap-bootstrap_ on kernel and base snap upgrades will also handle updating bootloader environment variables to implement A/B or try-boot functionality. 1. _snap-bootstrap_ then finally may do some additional setup of the root filesystem such as copying some default files for ephemeral system modes such as recover. -## Degraded recover mode +## In depth walkthrough + +_snap-bootstrap_ operates differently depending on snapd_recovery_mode, so each mode is considered separately below. + +Note that while snap-bootstrap contains the largest chunk of the logic for the initramfs, there are additional steps that need to be considered. These take over after snap-bootstrap has exited successfully and they're required to fully complete the initramfs operations beyond snap-bootstrap. Ideally, these additional steps will be moved into snap-bootstrap at some point, where they can be more fully tested and documented. But for now, take a look at the unit files in the initrd for "the-modeenv" and "the-tool" to follow what happens after snap-bootstrap is done. + +Additionally, note that in all modes where there is a TPM available, we must lock access to the keys before exiting snap-bootstrap. This is implemented specifically with `secboot.LockSealedKeys`. This is regardless of whether or not the system is encrypted or not. + +### Install mode + +Install mode has the following steps: + +1. The first step of the initramfs-mounts command is always to measure the "epoch" of the secboot version that snap-bootstrap is compiled with to the TPM (if one exists). This is for maximum security and to prevent a newer epoch of secboot from being vulnerable to prior versions. +1. The next step is to pick the first partition to mount as securely as possible. With EFI systems, we query an EFI variable used to indicate the Partition UUID of the disk which the kernel was booted off. We then use that Partition UUID to identify the partition which should be mounted as ubuntu-seed (since on grub amd64 systems, the kernel is initially booted by mounting the squashfs in grub and then booting the kernel.efi inside the mounted squashfs). If there is no such EFI variable, we fall back to just using the label to choose which partition to mount. Although we do have snap-bootstrap ordered to run after udev has fully settled via `After=systemd-udev-settle.service` in the unit file, sometimes we still don't have that Partition UUID device node available in /dev/ by the time we are executing, so we wait in a loop for the device node to appear before giving up. +1. After having identified which partition is ubuntu-seed, we mount it at /run/mnt/ubuntu-seed. +1. Next, we will load the "recovery system seed", which is the set of snaps associated with this recovery system, this includes the base snap, the kernel snap, the snapd snap and the gadget snap. These snaps are verified to match their assertions via hashing. +1. Next we do another measurement to the TPM (if available) of the model assertion from the recovery system we loaded. +1. After having verified that the recovery system seed snaps are valid and that the model assertion is correct, we will then mount these snaps at /run/mnt/base, /run/mnt/kernel, and /run/mnt/snapd (the gadget is not mounted at this time). +1. Next, we create a tmpfs mount at /run/mnt/data, which will be the root filesystem we pivot_root into at the end of the initramfs. +1. Next, we will "configure" the target system root filesystem using the gadget snap itself, this will handle things like "early snap config" and cloud-init config, etc. that need to be applied before we fully boot to userspace. +1. Next, we will write out a modeenv file to the root filesystem based on the model assertion and the recovery system seed snaps that will be read by snapd in userspace when we get there. +1. Finally, the last step of all modes is to expose any boot flags. There is currently only one boot flag and this is used during install mode to allow factory-specific behavior in the install-device hook, stopping re-execution if the device is re-installed in the field and re-enters install mode again. A boot flag is set by a bootloader environment variable which is then put into a file in /run for userspace to measure. See https://ubuntu.com/core/docs/uc20/installation-process for full details of how this can be used from an image building standpoint, and see the implementation of `boot.InitramfsExposeBootFlagsForSystem` for how this works at a low-level for a snapd/Ubuntu Core developer. + +### Run mode + +1. The first step of the initramfs-mounts command is always to measure the "epoch" of the secboot version that snap-bootstrap is compiled with to the TPM (if one exists). This is for maximum security and to prevent a newer epoch of secboot from being vulnerable to prior versions. +1. The next step is to pick the first partition to mount as securely as possible. With EFI systems, we query an EFI variable used to indicate the Partition UUID of the disk which the kernel was booted off of. We then use that Partition UUID to identify the partition which should be mounted as ubuntu-boot. This is because in run mode (for amd64 grub systems at least), we will boot using the kernel.efi file from the ubuntu-boot partition, as opposed to recover and install modes which use the kernel snap from ubuntu-seed. If there is no such EFI variable, we fall back to just using the label instead to choose which partition to mount. Although we do have snap-bootstrap ordered to run after udev has fully settled via `After=systemd-udev-settle.service` in the unit file, sometimes we still don't have that Partition UUID device node available in /dev/ by the time we are executing, so we wait in a loop for the device node to appear before giving up. +1. After having identified which partition is ubuntu-boot, we mount it at /run/mnt/ubuntu-boot. +1. Using the disk we found ubuntu-boot on as a reference, we will pick the partition with label "ubuntu-seed" and mount this partition at /run/mnt/ubuntu-seed. +1. Next we will measure the model assertion to the TPM as well. +1. Next, we will try to unlock the ubuntu-data partition (if it is encrypted) using the sealed-key which exists on ubuntu-boot. After unlocking (or just finding the unencrypted version if encryption is not being used), we will mount it at /run/mnt/data. +1. If ubuntu-data was encrypted, then we will proceed to attempt to unlock an ubuntu-save partition from the same disk, and mount it at /run/mnt/ubuntu-save. If ubuntu-data was not encrypted, then we will try to mount an unencrypted ubuntu-save at /run/mnt/ubuntu-save, but in the unencrypted case we do not require ubuntu-save to be present so it is not a fatal error if we do not find ubuntu-save in the unencrypted case. +1. After having mounted all of the relevant partitions, we will perform a double check that the mount points /run/mnt/ubuntu-{save,data} come from the same disk. For extra paranoia, we will also validate that ubuntu-data and ubuntu-save, if they were encrypted, were unlocked with the same key pairing. +1. Next we read the modeenv from the data partition, and based on the modeenv, we decide what snaps to mount. On all boots into run mode the base and kernel snap must be identified and mounted. Note that for run mode, we find the snaps to mount for this purpose through `boot.InitramfsRunModeSelectSnapsToMount` which handles kernel / base snap updates and will return the "try" snap if there is a new snap being tried on this boot. +1. If this boot is the first ever boot into run mode, we will also mount the snapd snap by reading and validating the recovery system seed from ubuntu-seed and mounting the snapd snap at /run/mnt/snap. +1. Finally, the last step of all modes is to expose the boot flags that were put into the boot environment for userspace to measure. This is done via `boot.InitramfsExposeBootFlagsForSystem` + +### Recover mode + +The first 8 steps for recover mode are shared exactly with install mode, so they are not repeated here, but see the steps 1-8 for install mode, then we continue: + +9. The next thing we check is whether we are inside the recovery environment to actually do recover mode, or if we are simply validating that the recovery system we are booting into is valid. We do this by inspecting bootloader environment variables via `boot.InitramfsIsTryingRecoverySystem`. +10. In the case that we are trying a recovery system, we will ensure that the next reboot will transition us back to run mode. Additionally, if we are in an inconsistent state, such as there being no agreement on the state of the tried recover system, for example, we will reboot and attempt to go back to run mode and give up on recover mode. +11. If we are either not trying a recovery system, or we are in a consistent state and are trying a recovery system, then we enter the following magical state machine. This state diagram essentially allows recover mode to be extra robust against failure modes, such as having a partition disappear, keys not being able to unlock some partitions, etc. This is referred to as "degraded mode". Specifically, if we don't use all the _happy paths_ then we are in a "degraded" recover mode as opposed to being in a normal recover mode. For the case where we are trying a recovery system, none of the fallback paths are allowed to be taken and will immediately exit the state machine and the state machine is marked as being in "degraded mode". -When booting into recover mode, _snap-bootstrap_ has some additional logic setup to try and be as robust as possible. This logic is fairly complicated and best explained in the following state diagram showing the states and transitions that _snap-bootstrap_ operates in during recover mode, which has been called degraded mode. ![](/cmd/snap-bootstrap/degraded-recover-mode.svg) -The above state diagram was made with https://app.diagrams.net/ and can be imported by opening the SVG file in this directory there. \ No newline at end of file + +The above state diagram was made with https://app.diagrams.net/ and can be imported by opening the SVG file in this directory there. + +12. After exiting the state machine (in all cases), we will again consider if we are trying a recovery system. If we are, we will inspect if the state machine degraded at all (meaning that the "happy path" for unlocking disks and mounting partitions was not fully executed and we had to use an alternative option at least one time). If the state machine outputs a degraded state, we mark the recovery system as a failure and go back to run mode. Once back in run mode, the tasks that requested the recovery system to be created will fail and be undone and the snap change will be in failed state. If it was successful, we mark it as successful and reboot to run mode. This is the last step for all situations related to trying a recovery system. +13. Next, we will write out a file called `degraded.json` that contains details on whether the state machine output was in a degraded state or not. This may affect some choices userspace snapd makes when we get there. +14. If the state machine exited in a state that was at least sufficiently usable, such that we can trust the data partition unlocked and mounted, we will then copy some files from the data partition to our tmpfs root filesystem. These could include authentication files, such as ssh keys, networking configuration, and other miscellaneous files like the clock sync file for systems without a battery powered RTC. If we didn't trust the data partition, then "safe" defaults will be used instead. This is to prevent a situation wherein we don't "trust" the data partition enough (but perhaps we did trust ubuntu-save when unlocking it) to copy authentication files over, but then we leave console-conf in such a state where it could allow an attacker to create their own new account and then exfiltrate secret data from the trusted ubuntu-save. +15. Next, we will write out a modeenv file to the root filesystem based on the model assertion and the recovery system seed snaps that will be read by snapd in userspace when we get there. +16. Penultimately, we will ensure that if the system is rebooted at all after this point, the system will be automatically transitioned back to run mode without further input. +17. Finally, the last step of all modes is to expose the boot flags that were put into the boot environment for userspace to measure. This is done via `boot.InitramfsExposeBootFlagsForSystem` + +### Classic mode + +This mode may eventually be developed to support using the same initramfs + kernel on Ubuntu Classic (i.e. Server or Desktop) as is currently used on Ubuntu Core 20+. This feature is still in the design stage. diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-confine/mount-support.c snapd-2.54.2+21.10/cmd/snap-confine/mount-support.c --- snapd-2.53+21.10ubuntu1/cmd/snap-confine/mount-support.c 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-confine/mount-support.c 2022-01-06 21:25:16.000000000 +0000 @@ -494,7 +494,7 @@ // code changes the nvidia code assumes it has access to the existing // pre-pivot filesystem. if (config->distro == SC_DISTRO_CLASSIC) { - sc_mount_nvidia_driver(scratch_dir); + sc_mount_nvidia_driver(scratch_dir, config->base_snap_name); } // XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX // pivot_root diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-confine/mount-support-nvidia.c snapd-2.54.2+21.10/cmd/snap-confine/mount-support-nvidia.c --- snapd-2.53+21.10ubuntu1/cmd/snap-confine/mount-support-nvidia.c 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-confine/mount-support-nvidia.c 2022-01-06 21:25:16.000000000 +0000 @@ -81,19 +81,10 @@ // FIXME: this doesn't yet work with libGLX and libglvnd redirector // FIXME: this still doesn't work with the 361 driver static const char *nvidia_globs[] = { - "libEGL.so*", "libEGL_nvidia.so*", - "libGL.so*", - "libOpenGL.so*", - "libGLESv1_CM.so*", "libGLESv1_CM_nvidia.so*", - "libGLESv2.so*", "libGLESv2_nvidia.so*", - "libGLX_indirect.so*", "libGLX_nvidia.so*", - "libGLX.so*", - "libGLdispatch.so*", - "libGLU.so*", "libXvMCNVIDIA.so*", "libXvMCNVIDIA_dynamic.so*", "libnvidia-cfg.so*", @@ -162,6 +153,21 @@ static const size_t nvidia_globs_len = sizeof nvidia_globs / sizeof *nvidia_globs; +static const char *glvnd_globs[] = { + "libEGL.so*", + "libGL.so*", + "libOpenGL.so*", + "libGLESv1_CM.so*", + "libGLESv2.so*", + "libGLX_indirect.so*", + "libGLX.so*", + "libGLdispatch.so*", + "libGLU.so*", +}; + +static const size_t glvnd_globs_len = + sizeof glvnd_globs / sizeof *glvnd_globs; + #endif // defined(NVIDIA_BIARCH) || defined(NVIDIA_MULTIARCH) // Populate libgl_dir with a symlink farm to files matching glob_list. @@ -351,7 +357,7 @@ // // In non GLVND cases we just copy across the exposed libGLs and NVIDIA // libraries from wherever we find, and clobbering is also harmless. -static void sc_mount_nvidia_driver_biarch(const char *rootfs_dir) +static void sc_mount_nvidia_driver_biarch(const char *rootfs_dir, const char **globs, size_t globs_len) { const char *native_sources[] = { @@ -374,14 +380,14 @@ // Primary arch sc_mkdir_and_mount_and_glob_files(rootfs_dir, native_sources, native_sources_len, - SC_LIBGL_DIR, nvidia_globs, - nvidia_globs_len); + SC_LIBGL_DIR, globs, + globs_len); #if UINTPTR_MAX == 0xffffffffffffffff // Alternative 32-bit support sc_mkdir_and_mount_and_glob_files(rootfs_dir, lib32_sources, lib32_sources_len, SC_LIBGL32_DIR, - nvidia_globs, nvidia_globs_len); + globs, globs_len); #endif } @@ -501,7 +507,7 @@ return 0; } -static void sc_mount_nvidia_driver_multiarch(const char *rootfs_dir) +static void sc_mount_nvidia_driver_multiarch(const char *rootfs_dir, const char **globs, size_t globs_len) { const char *native_libdir = NATIVE_LIBDIR "/" HOST_ARCH_TRIPLET; const char *lib32_libdir = NATIVE_LIBDIR "/" HOST_ARCH32_TRIPLET; @@ -519,8 +525,8 @@ sc_mkdir_and_mount_and_glob_files(rootfs_dir, native_sources, native_sources_len, - SC_LIBGL_DIR, nvidia_globs, - nvidia_globs_len); + SC_LIBGL_DIR, globs, + globs_len); // Alternative 32-bit support if ((strlen(HOST_ARCH32_TRIPLET) > 0) && @@ -536,8 +542,8 @@ lib32_sources, lib32_sources_len, SC_LIBGL32_DIR, - nvidia_globs, - nvidia_globs_len); + globs, + globs_len); } } else { // Attempt mount of both the native and 32-bit variants of the driver if they exist @@ -576,7 +582,7 @@ egl_vendor_globs_len); } -void sc_mount_nvidia_driver(const char *rootfs_dir) +void sc_mount_nvidia_driver(const char *rootfs_dir, const char *base_snap_name) { /* If NVIDIA module isn't loaded, don't attempt to mount the drivers */ if (access(SC_NVIDIA_DRIVER_VERSION_FILE, F_OK) != 0) { @@ -593,11 +599,37 @@ die("cannot change ownership of " SC_LIB); } (void)sc_set_effective_identity(old); + +#if defined(NVIDIA_BIARCH) || defined(NVIDIA_MULTIARCH) + /* We include the globs for the glvnd libraries for old snaps + * based on core, Ubuntu 16.04 did not include glvnd itself. + * + * While there is no guarantee that the host system's glvnd + * libGL will be compatible (as it is built with the host + * system's glibc), the Mesa libGL included with the snap will + * definitely not be compatible (as it expects to find the Mesa + * implementation of the GLX extension).. + */ + const char **globs = nvidia_globs; + size_t globs_len = nvidia_globs_len; + const char **full_globs SC_CLEANUP(sc_cleanup_shallow_strv) = NULL; + if (sc_streq(base_snap_name, "core")) { + full_globs = malloc(sizeof nvidia_globs + sizeof glvnd_globs); + if (full_globs == NULL) { + die("cannot allocate globs array"); + } + memcpy(full_globs, nvidia_globs, sizeof nvidia_globs); + memcpy(&full_globs[nvidia_globs_len], glvnd_globs, sizeof glvnd_globs); + globs = full_globs; + globs_len = nvidia_globs_len + glvnd_globs_len; + } +#endif + #ifdef NVIDIA_MULTIARCH - sc_mount_nvidia_driver_multiarch(rootfs_dir); + sc_mount_nvidia_driver_multiarch(rootfs_dir, globs, globs_len); #endif // ifdef NVIDIA_MULTIARCH #ifdef NVIDIA_BIARCH - sc_mount_nvidia_driver_biarch(rootfs_dir); + sc_mount_nvidia_driver_biarch(rootfs_dir, globs, globs_len); #endif // ifdef NVIDIA_BIARCH // Common for both driver mechanisms diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-confine/mount-support-nvidia.h snapd-2.54.2+21.10/cmd/snap-confine/mount-support-nvidia.h --- snapd-2.53+21.10ubuntu1/cmd/snap-confine/mount-support-nvidia.h 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-confine/mount-support-nvidia.h 2022-01-06 21:25:16.000000000 +0000 @@ -43,6 +43,6 @@ * /usr/lib directory on the classic filesystem. After the pivot_root() call * those symlinks rely on the /var/lib/snapd/hostfs directory as a "gateway". **/ -void sc_mount_nvidia_driver(const char *rootfs_dir); +void sc_mount_nvidia_driver(const char *rootfs_dir, const char *base_snap_name); #endif diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-confine/snap-confine.apparmor.in snapd-2.54.2+21.10/cmd/snap-confine/snap-confine.apparmor.in --- snapd-2.53+21.10ubuntu1/cmd/snap-confine/snap-confine.apparmor.in 2021-10-08 07:29:19.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-confine/snap-confine.apparmor.in 2022-01-06 21:25:16.000000000 +0000 @@ -371,6 +371,10 @@ @{HOME}/ r, @{HOME}/snap/{,*/,*/*/} rw, + # experimental + @{HOME}/.snap/ rw, + @{HOME}/.snap/data/{,*/,*/*/} rw, + # Special case for *classic* snaps that are used by users with existing dirs # in /var/lib/. Like jenkins, postgresql, mysql, puppet, ... # (see https://forum.snapcraft.io/t/9717) diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-confine/udev-support.c snapd-2.54.2+21.10/cmd/snap-confine/udev-support.c --- snapd-2.53+21.10ubuntu1/cmd/snap-confine/udev-support.c 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-confine/udev-support.c 2022-01-06 21:25:16.000000000 +0000 @@ -237,26 +237,26 @@ * when the binary itself is build with recent enough toolchain (eg. gcc & * binutils on Ubuntu 20.04) */ -static int (*__sc_udev_device_has_current_tag)(struct udev_device *udev_device, - const char *tag) = NULL; +static int (*__sc_udev_device_has_current_tag)(struct udev_device * udev_device, + const char *tag) = NULL; static void setup_current_tags_support(void) { - void *lib = dlopen("libudev.so.1", RTLD_NOW); - if (lib == NULL) { - debug("cannot load libudev.so.1: %s", dlerror()); - /* bit unexpected as we use the library from the host and it's stable */ - return; - } - /* check whether we have the symbol introduced in systemd v247 to inspect - * the CURRENT_TAGS property */ - void *sym = dlsym(lib, "udev_device_has_current_tag"); - if (sym == NULL) { - debug("cannot find current tags symbol: %s", dlerror()); - /* symbol is not found in the library version */ - return; - } - debug("libudev has current tags support"); - __sc_udev_device_has_current_tag = sym; + void *lib = dlopen("libudev.so.1", RTLD_NOW); + if (lib == NULL) { + debug("cannot load libudev.so.1: %s", dlerror()); + /* bit unexpected as we use the library from the host and it's stable */ + return; + } + /* check whether we have the symbol introduced in systemd v247 to inspect + * the CURRENT_TAGS property */ + void *sym = dlsym(lib, "udev_device_has_current_tag"); + if (sym == NULL) { + debug("cannot find current tags symbol: %s", dlerror()); + /* symbol is not found in the library version */ + return; + } + debug("libudev has current tags support"); + __sc_udev_device_has_current_tag = sym; } void sc_setup_device_cgroup(const char *security_tag) @@ -306,13 +306,9 @@ return; } - /* Note that -1 is the neutral value for a file descriptor. - * The cleanup function associated with this variable closes - * descriptors other than -1. */ - sc_device_cgroup *cgroup SC_CLEANUP(sc_device_cgroup_cleanup) = - sc_device_cgroup_new(security_tag, 0); - /* Setup the device group access control list */ - sc_udev_setup_acls_common(cgroup); + /* cgroup wrapper is lazily initialized when devices are actually + * assigned */ + sc_device_cgroup *cgroup SC_CLEANUP(sc_device_cgroup_cleanup) = NULL; for (struct udev_list_entry * entry = assigned; entry != NULL; entry = udev_list_entry_get_next(entry)) { const char *path = udev_list_entry_get_name(entry); @@ -338,7 +334,8 @@ * previously created/setup but should no longer be setup due * to interface disconnection, etc. */ if (__sc_udev_device_has_current_tag != NULL) { - if (__sc_udev_device_has_current_tag(device, udev_tag) <= 0) { + if (__sc_udev_device_has_current_tag(device, udev_tag) + <= 0) { debug("device %s has no matching current tag", path); udev_device_unref(device); @@ -347,11 +344,24 @@ debug("device %s has matching current tag", path); } + if (cgroup == NULL) { + /* initialize cgroup wrapper only when we are sure that there are + * devices assigned to this snap */ + cgroup = sc_device_cgroup_new(security_tag, 0); + /* Setup the device group access control list */ + sc_udev_setup_acls_common(cgroup); + } sc_udev_allow_assigned_device(cgroup, device); udev_device_unref(device); } - /* Move ourselves to the device cgroup */ - sc_device_cgroup_attach_pid(cgroup, getpid()); - debug("associated snap application process %i with device cgroup %s", - getpid(), security_tag); + if (cgroup != NULL) { + /* Move ourselves to the device cgroup */ + sc_device_cgroup_attach_pid(cgroup, getpid()); + debug + ("associated snap application process %i with device cgroup %s", + getpid(), security_tag); + } else { + debug("no devices tagged with %s, skipping device cgroup setup", + udev_tag); + } } diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-device-helper/snap-device-helper.c snapd-2.54.2+21.10/cmd/snap-device-helper/snap-device-helper.c --- snapd-2.53+21.10ubuntu1/cmd/snap-device-helper/snap-device-helper.c 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-device-helper/snap-device-helper.c 2022-01-06 21:25:16.000000000 +0000 @@ -15,6 +15,7 @@ * */ #include +#include #include #include #include @@ -175,11 +176,26 @@ char fullsubsystem[PATH_MAX] = {0}; sc_must_snprintf(sysdevsubsystem, sizeof(sysdevsubsystem), "%s/sys/%s/subsystem", sysroot, devpath); if (readlink(sysdevsubsystem, fullsubsystem, sizeof(fullsubsystem)) < 0) { - die("cannot read symlink %s", sysdevsubsystem); - } - char *subsystem = basename(fullsubsystem); - if (sc_streq(subsystem, "block")) { - devtype = S_IFBLK; + if (errno == ENOENT && sc_streq(action, "remove")) { + // on removal the devices are going away, so it is possible that the + // symlink is already gone, in which case try guessing the type like + // the old shell-based snap-device-helper did: + // + // > char devices are .../nvme/nvme* but block devices are + // > .../nvme/nvme*/nvme*n* and .../nvme/nvme*/nvme*n*p* so if have a + // > device that has nvme/nvme*/nvme*n* in it, treat it as a block + // > device + if ((fnmatch("*/block/*", devpath, 0) == 0) || (fnmatch("*/nvme/nvme*/nvme*n*", devpath, 0) == 0)) { + devtype = S_IFBLK; + } + } else { + die("cannot read symlink %s", sysdevsubsystem); + } + } else { + char *subsystem = basename(fullsubsystem); + if (sc_streq(subsystem, "block")) { + devtype = S_IFBLK; + } } sc_device_cgroup *cgroup = sc_device_cgroup_new(security_tag, SC_DEVICE_CGROUP_FROM_EXISTING); if (!cgroup) { diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-device-helper/snap-device-helper-test.c snapd-2.54.2+21.10/cmd/snap-device-helper/snap-device-helper-test.c --- snapd-2.53+21.10ubuntu1/cmd/snap-device-helper/snap-device-helper-test.c 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-device-helper/snap-device-helper-test.c 2022-01-06 21:25:16.000000000 +0000 @@ -282,6 +282,94 @@ } } +static void test_sdh_action_remove_fallback_devtype(sdh_test_fixture *fixture, gconstpointer test_data) { + /* check that fallback guessing of device type if applied during remove action */ + mkdir_in_sysroot(fixture, "/sys/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/nvme0n1"); + mkdir_in_sysroot(fixture, "/sys/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/nvme0n1p1"); + mkdir_in_sysroot(fixture, "/sys/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/ng0n1"); + mkdir_in_sysroot(fixture, "/sys/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/hwmon0"); + mkdir_in_sysroot(fixture, "/sys/devices/foo/block/sda/sda4"); + mkdir_in_sysroot(fixture, "/sys//devices/pnp0/00:04/tty/ttyS0"); + + struct { + const char *dev; + const char *majmin; + int expected_maj; + int expected_min; + int expected_type; + } tcs[] = { + /* these device paths match the fallback pattern of block devices */ + { + .dev = "/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/nvme0n1", + .majmin = "259:0", + .expected_maj = 259, + .expected_min = 0, + .expected_type = S_IFBLK, + }, + { + .dev = "/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/nvme0n1p1", + .majmin = "259:1", + .expected_maj = 259, + .expected_min = 1, + .expected_type = S_IFBLK, + }, + { + .dev = "/devices/foo/block/sda/sda4", + .majmin = "8:0", + .expected_maj = 8, + .expected_min = 0, + .expected_type = S_IFBLK, + }, + /* these are treated as char devices */ + { + .dev = "/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0", + .majmin = "242:0", + .expected_maj = 242, + .expected_min = 0, + .expected_type = S_IFCHR, + }, + { + .dev = "/devices/pci0000:00/0000:00:01.1/0000:01:00.0/nvme/nvme0/hwmon0", + .majmin = "241:0", + .expected_maj = 241, + .expected_min = 0, + .expected_type = S_IFCHR, + }, + { + .dev = "/devices/pnp0/00:04/tty/ttyS0", + .majmin = "4:64", + .expected_maj = 4, + .expected_min = 64, + .expected_type = S_IFCHR, + }, + }; + + int bogus = 0; + + for (size_t i = 0; i < sizeof(tcs) / sizeof(tcs[0]); i++) { + mocks_reset(); + /* make cgroup_device_new return a non-NULL */ + mocks.new_ret = &bogus; + + struct sdh_invocation inv_block = { + .action = "remove", + .tagname = "snap_foo_bar", + .devpath = tcs[i].dev, + .majmin = tcs[i].majmin, + }; + int ret = snap_device_helper_run(&inv_block); + g_assert_cmpint(ret, ==, 0); + g_assert_cmpint(mocks.cgorup_new_calls, ==, 1); + g_assert_cmpint(mocks.cgroup_allow_calls, ==, 0); + g_assert_cmpint(mocks.cgroup_deny_calls, ==, 1); + g_assert_cmpint(mocks.device_major, ==, tcs[i].expected_maj); + g_assert_cmpint(mocks.device_minor, ==, tcs[i].expected_min); + g_assert_cmpint(mocks.device_type, ==, tcs[i].expected_type); + g_assert_cmpint(mocks.new_flags, !=, 0); + g_assert_cmpint(mocks.new_flags, ==, SC_DEVICE_CGROUP_FROM_EXISTING); + } +} + static void run_sdh_die(const char *action, const char *tagname, const char *devpath, const char *majmin, const char *msg) { struct sdh_invocation inv = { @@ -355,12 +443,18 @@ "ERROR: unknown action \"badaction\"\n"); } -static void test_sdh_err_nosymlink(sdh_test_fixture *fixture, gconstpointer test_data) { +static void test_sdh_err_nosymlink_block(sdh_test_fixture *fixture, gconstpointer test_data) { // missing symlink run_sdh_die("add", "snap_foo_bar", "/devices/foo/block/sda/sda4", "8:4", "cannot read symlink */sys//devices/foo/block/sda/sda4/subsystem*\n"); } +static void test_sdh_err_nosymlink_char(sdh_test_fixture *fixture, gconstpointer test_data) { + // missing symlink + run_sdh_die("add", "snap_foo_bar", "/devices/pnp0/00:04/tty/ttyS0", "4:64", + "cannot read symlink */sys//devices/pnp0/00:04/tty/ttyS0/subsystem*\n"); +} + static void test_sdh_err_funtag1(sdh_test_fixture *fixture, gconstpointer test_data) { run_sdh_die("add", "snap___bar", "/devices/foo/block/sda/sda4", "8:4", "security tag \"snap._.bar\" for snap \"_\" is not valid\n"); @@ -427,6 +521,7 @@ _test_add("/snap-device-helper/add", &add_data, test_sdh_action); _test_add("/snap-device-helper/change", &change_data, test_sdh_action); _test_add("/snap-device-helper/remove", &remove_data, test_sdh_action); + _test_add("/snap-device-helper/remove_fallback", NULL, test_sdh_action_remove_fallback_devtype); _test_add("/snap-device-helper/err/no-appname", NULL, test_sdh_err_noappname); _test_add("/snap-device-helper/err/bad-appname", NULL, test_sdh_err_badappname); @@ -436,7 +531,8 @@ _test_add("/snap-device-helper/err/wrong-devmajorminor_late1", NULL, test_sdh_err_wrongdevmajorminor_late1); _test_add("/snap-device-helper/err/wrong-devmajorminor_late2", NULL, test_sdh_err_wrongdevmajorminor_late2); _test_add("/snap-device-helper/err/bad-action", NULL, test_sdh_err_badaction); - _test_add("/snap-device-helper/err/no-symlink", NULL, test_sdh_err_nosymlink); + _test_add("/snap-device-helper/err/no-symlink-block", NULL, test_sdh_err_nosymlink_block); + _test_add("/snap-device-helper/err/no-symlink-char", NULL, test_sdh_err_nosymlink_char); _test_add("/snap-device-helper/err/funtag1", NULL, test_sdh_err_funtag1); _test_add("/snap-device-helper/err/funtag2", NULL, test_sdh_err_funtag2); _test_add("/snap-device-helper/err/funtag3", NULL, test_sdh_err_funtag3); diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-failure/cmd_snapd.go snapd-2.54.2+21.10/cmd/snap-failure/cmd_snapd.go --- snapd-2.53+21.10ubuntu1/cmd/snap-failure/cmd_snapd.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-failure/cmd_snapd.go 2022-01-06 21:25:16.000000000 +0000 @@ -126,6 +126,11 @@ // system, either a remodel or a plain snapd installation, call // the snapd from the core snap snapdPath = filepath.Join(dirs.SnapMountDir, "core", "current", "/usr/lib/snapd/snapd") + if !osutil.FileExists(snapdPath) { + // it is possible that the core snap is not installed at + // all, in which case we should try the snapd snap + snapdPath = filepath.Join(dirs.SnapMountDir, "snapd", "current", "/usr/lib/snapd/snapd") + } prevRev = "0" case nil: // the snapd snap was installed before, use the previous revision diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-failure/cmd_snapd_test.go snapd-2.54.2+21.10/cmd/snap-failure/cmd_snapd_test.go --- snapd-2.53+21.10ubuntu1/cmd/snap-failure/cmd_snapd_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-failure/cmd_snapd_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -265,6 +265,41 @@ }) } +func (r *failureSuite) TestCallPrevSnapdFromSnapdWhenNoCore(c *C) { + origArgs := os.Args + defer func() { os.Args = origArgs }() + + // only one entry in sequence + writeSeqFile(c, "snapd", snap.R(123), []*snap.SideInfo{ + {Revision: snap.R(123)}, + }) + + // sanity + c.Assert(filepath.Join(dirs.SnapMountDir, "core", "current", "/usr/lib/snapd/snapd"), testutil.FileAbsent) + // mock snapd in the core snap + snapdCmd := testutil.MockCommand(c, filepath.Join(dirs.SnapMountDir, "snapd", "current", "/usr/lib/snapd/snapd"), + `test "$SNAPD_REVERT_TO_REV" = "0"`) + defer snapdCmd.Restore() + + systemctlCmd := testutil.MockCommand(c, "systemctl", "") + defer systemctlCmd.Restore() + + os.Args = []string{"snap-failure", "snapd"} + err := failure.Run() + c.Check(err, IsNil) + c.Check(r.Stderr(), HasLen, 0) + + c.Check(snapdCmd.Calls(), DeepEquals, [][]string{ + {"snapd"}, + }) + c.Check(systemctlCmd.Calls(), DeepEquals, [][]string{ + {"systemctl", "stop", "snapd.socket"}, + {"systemctl", "is-failed", "snapd.socket", "snapd.service"}, + {"systemctl", "reset-failed", "snapd.socket", "snapd.service"}, + {"systemctl", "restart", "snapd.socket"}, + }) +} + func (r *failureSuite) TestCallPrevSnapdFail(c *C) { origArgs := os.Args defer func() { os.Args = origArgs }() diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-mgmt/snap-mgmt.sh.in snapd-2.54.2+21.10/cmd/snap-mgmt/snap-mgmt.sh.in --- snapd-2.53+21.10ubuntu1/cmd/snap-mgmt/snap-mgmt.sh.in 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-mgmt/snap-mgmt.sh.in 2022-01-06 21:25:16.000000000 +0000 @@ -45,7 +45,10 @@ # Undo any bind mounts to ${SNAP_MOUNT_DIR} or /var/snap done by parallel # installs or LP:#1668659 for mp in "$SNAP_MOUNT_DIR" /var/snap; do - if grep -q " $mp $mp" /proc/self/mountinfo; then + # btrfs bind mounts actually include subvolume in the filesystem-path + # https://www.mail-archive.com/linux-btrfs@vger.kernel.org/msg51810.html + if grep -q " $mp $mp " /proc/self/mountinfo || + grep -q -e "\(/.*\)$mp $mp .* btrfs .*\(subvol=\1\)\(,.*\)\?\$" /proc/self/mountinfo ; then umount -l "$mp" || true fi done @@ -110,6 +113,7 @@ fi # modules rm -f "/etc/modules-load.d/snap.${snap}.conf" + rm -f "/etc/modprobe.d/snap.${snap}.conf" # timer and socket units find /etc/systemd/system -name "snap.${snap}.*.timer" -o -name "snap.${snap}.*.socket" | while read -r f; do systemctl_stop "$(basename "$f")" diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-preseed/preseed_linux.go snapd-2.54.2+21.10/cmd/snap-preseed/preseed_linux.go --- snapd-2.53+21.10ubuntu1/cmd/snap-preseed/preseed_linux.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-preseed/preseed_linux.go 2022-01-06 21:25:16.000000000 +0000 @@ -158,16 +158,16 @@ // The function must be called after syscall.Chroot(..). func chooseTargetSnapdVersion() (*targetSnapdInfo, error) { // read snapd version from the mounted core/snapd snap - infoPath := filepath.Join(snapdMountPath, dirs.CoreLibExecDir, "info") - verFromSnap, err := snapdtool.SnapdVersionFromInfoFile(infoPath) + snapdInfoDir := filepath.Join(snapdMountPath, dirs.CoreLibExecDir) + verFromSnap, _, err := snapdtool.SnapdVersionFromInfoFile(snapdInfoDir) if err != nil { return nil, err } // read snapd version from the main fs under chroot (snapd from the deb); // assumes running under chroot already. - infoPath = filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir, "info") - verFromDeb, err := snapdtool.SnapdVersionFromInfoFile(infoPath) + hostInfoDir := filepath.Join(dirs.GlobalRootDir, dirs.CoreLibExecDir) + verFromDeb, _, err := snapdtool.SnapdVersionFromInfoFile(hostInfoDir) if err != nil { return nil, err } diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-preseed/preseed_other.go snapd-2.54.2+21.10/cmd/snap-preseed/preseed_other.go --- snapd-2.53+21.10ubuntu1/cmd/snap-preseed/preseed_other.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-preseed/preseed_other.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !linux // +build !linux /* diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-repair/staging.go snapd-2.54.2+21.10/cmd/snap-repair/staging.go --- snapd-2.53+21.10ubuntu1/cmd/snap-repair/staging.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-repair/staging.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withtestkeys || withstagingkeys // +build withtestkeys withstagingkeys /* diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-repair/testkeys.go snapd-2.54.2+21.10/cmd/snap-repair/testkeys.go --- snapd-2.53+21.10ubuntu1/cmd/snap-repair/testkeys.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-repair/testkeys.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build withtestkeys // +build withtestkeys /* diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/main.go snapd-2.54.2+21.10/cmd/snap-seccomp/main.go --- snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/main.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-seccomp/main.go 2022-01-06 21:25:16.000000000 +0000 @@ -128,6 +128,10 @@ //#define SCMP_ARCH_S390X ARCH_BAD //#endif // +//#ifndef SCMP_ARCH_RISCV64 +//#define SCMP_ARCH_RISCV64 ARCH_BAD +//#endif +// //#ifndef SECCOMP_RET_LOG //#define SECCOMP_RET_LOG 0x7ffc0000U //#endif @@ -185,8 +189,7 @@ "strings" "syscall" - // FIXME: we want github.com/seccomp/libseccomp-golang but that will not work with trusty because libseccomp-golang checks for the seccomp version and errors if it find one < 2.2.0 - "github.com/mvo5/libseccomp-golang" + "github.com/seccomp/libseccomp-golang" "github.com/snapcore/snapd/arch" "github.com/snapcore/snapd/osutil" @@ -452,7 +455,7 @@ case "s390x": return seccomp.ArchS390X } - panic(fmt.Sprintf("cannot map dpkg arch %q to a seccomp arch", dpkgArch)) + return extraDpkgArchToScmpArch(dpkgArch) } // important for unit testing diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/main_nonriscv64.go snapd-2.54.2+21.10/cmd/snap-seccomp/main_nonriscv64.go --- snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/main_nonriscv64.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-seccomp/main_nonriscv64.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,38 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +//go:build !riscv64 +// +build !riscv64 + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public 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/seccomp/libseccomp-golang" +) + +// this extraDpkgArchToScmpArch does not have riscv64 constant, when +// building on non-riscv64 archtictures with an old seccomp library. +// once all distros upgrade to the new seccomp library we can drop +// this and riscv64 specific files and fold things back into +// DpkgArchToScmpArch() without this function +func extraDpkgArchToScmpArch(dpkgArch string) seccomp.ScmpArch { + panic(fmt.Sprintf("cannot map dpkg arch %q to a seccomp arch", dpkgArch)) +} diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/main_ppc64le.go snapd-2.54.2+21.10/cmd/snap-seccomp/main_ppc64le.go --- snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/main_ppc64le.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-seccomp/main_ppc64le.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,5 +1,6 @@ // -*- Mode: Go; indent-tabs-mode: t -*- // +//go:build ppc64le && go1.7 && !go1.8 // +build ppc64le,go1.7,!go1.8 /* diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/main_riscv64.go snapd-2.54.2+21.10/cmd/snap-seccomp/main_riscv64.go --- snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/main_riscv64.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-seccomp/main_riscv64.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,42 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +// +//go:build riscv64 +// +build riscv64 + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public 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/seccomp/libseccomp-golang" +) + +// this extraDpkgArchToScmpArch uses riscv64 constant, when building +// on riscv64 architecture which requires newer snapshot of libseccomp +// library. Once all distros have newer libseccomp golang library, +// this portion can be just folded into the DpkgArchToScmArch() +// function to be compiled on all architecutres. +func extraDpkgArchToScmpArch(dpkgArch string) seccomp.ScmpArch { + switch dpkgArch { + case "riscv64": + return seccomp.ArchRISCV64 + } + panic(fmt.Sprintf("cannot map dpkg arch %q to a seccomp arch", dpkgArch)) +} diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/main_test.go snapd-2.54.2+21.10/cmd/snap-seccomp/main_test.go --- snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/main_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-seccomp/main_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -30,7 +30,7 @@ "strings" "testing" - "github.com/mvo5/libseccomp-golang" + "github.com/seccomp/libseccomp-golang" . "gopkg.in/check.v1" "github.com/snapcore/snapd/arch" diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/old_seccomp.go snapd-2.54.2+21.10/cmd/snap-seccomp/old_seccomp.go --- snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/old_seccomp.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-seccomp/old_seccomp.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,31 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- +//go:build oldseccomp +// +build oldseccomp + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public 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 + +// On 14.04 we need to use forked libseccomp-golang, as recent +// upstream libseccomp-golang does not support building against +// libseecomp 2.1.1. This is patched in via packaging patch. But to +// continue vendoring the modules in go.mod any golang file must still +// reference the old forked libseccomp-golang. Which is here. This +// file and import can be safely removed, once 14.04 build support of +// master is deemed to never be required again. +import "github.com/mvo5/libseccomp-golang" diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/syscalls/syscalls.go snapd-2.54.2+21.10/cmd/snap-seccomp/syscalls/syscalls.go --- snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/syscalls/syscalls.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-seccomp/syscalls/syscalls.go 2022-01-06 21:25:16.000000000 +0000 @@ -292,6 +292,7 @@ "preadv2", "prlimit64", "process_madvise", + "process_mrelease", "process_vm_readv", "process_vm_writev", "prof", diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/versioninfo.go snapd-2.54.2+21.10/cmd/snap-seccomp/versioninfo.go --- snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/versioninfo.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-seccomp/versioninfo.go 2022-01-06 21:25:16.000000000 +0000 @@ -25,7 +25,7 @@ "os" "strings" - "github.com/mvo5/libseccomp-golang" + "github.com/seccomp/libseccomp-golang" "github.com/snapcore/snapd/cmd/snap-seccomp/syscalls" "github.com/snapcore/snapd/osutil" diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/versioninfo_test.go snapd-2.54.2+21.10/cmd/snap-seccomp/versioninfo_test.go --- snapd-2.53+21.10ubuntu1/cmd/snap-seccomp/versioninfo_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-seccomp/versioninfo_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -23,7 +23,7 @@ "fmt" "strings" - "github.com/mvo5/libseccomp-golang" + "github.com/seccomp/libseccomp-golang" . "gopkg.in/check.v1" main "github.com/snapcore/snapd/cmd/snap-seccomp" diff -Nru snapd-2.53+21.10ubuntu1/cmd/snap-update-ns/bootstrap_ppc64le.go snapd-2.54.2+21.10/cmd/snap-update-ns/bootstrap_ppc64le.go --- snapd-2.53+21.10ubuntu1/cmd/snap-update-ns/bootstrap_ppc64le.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/cmd/snap-update-ns/bootstrap_ppc64le.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,5 +1,6 @@ // -*- Mode: Go; indent-tabs-mode: t -*- // +//go:build ppc64le && go1.7 && !go1.8 // +build ppc64le,go1.7,!go1.8 /* diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_apps_test.go snapd-2.54.2+21.10/daemon/api_apps_test.go --- snapd-2.53+21.10ubuntu1/daemon/api_apps_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_apps_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -181,6 +181,7 @@ for _, name := range svcNames { s.SysctlBufs = append(s.SysctlBufs, []byte(fmt.Sprintf(` Id=snap.%s.service +Names=snap.%[1]s.service Type=simple ActiveState=active UnitFileState=enabled @@ -269,6 +270,7 @@ for _, name := range svcNames { s.SysctlBufs = append(s.SysctlBufs, []byte(fmt.Sprintf(` Id=snap.%s.service +Names=snap.%[1]s.service Type=simple ActiveState=active UnitFileState=enabled diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_base_test.go snapd-2.54.2+21.10/daemon/api_base_test.go --- snapd-2.53+21.10ubuntu1/daemon/api_base_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_base_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -625,7 +625,7 @@ func (s *apiBaseSuite) asyncReq(c *check.C, req *http.Request, u *auth.UserState) *daemon.RespJSON { rsp := s.jsonReq(c, req, u) - c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeAsync, check.Commentf("expected async resp: %#v", rsp)) + c.Assert(rsp.Type, check.Equals, daemon.ResponseTypeAsync, check.Commentf("expected async resp: %#v, result %v", rsp, rsp.Result)) return rsp } diff -Nru snapd-2.53+21.10ubuntu1/daemon/api.go snapd-2.54.2+21.10/daemon/api.go --- snapd-2.53+21.10ubuntu1/daemon/api.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api.go 2022-01-06 21:25:16.000000000 +0000 @@ -21,7 +21,6 @@ import ( "fmt" - "mime/multipart" "net/http" "strconv" "strings" @@ -133,6 +132,7 @@ var ( snapstateInstall = snapstate.Install snapstateInstallPath = snapstate.InstallPath + snapstateInstallPathMany = snapstate.InstallPathMany snapstateRefreshCandidates = snapstate.RefreshCandidates snapstateTryPath = snapstate.TryPath snapstateUpdate = snapstate.Update @@ -143,7 +143,8 @@ snapstateRevertToRevision = snapstate.RevertToRevision snapstateSwitch = snapstate.Switch - assertstateRefreshSnapAssertions = assertstate.RefreshSnapAssertions + assertstateRefreshSnapAssertions = assertstate.RefreshSnapAssertions + assertstateRestoreValidationSetsTracking = assertstate.RestoreValidationSetsTracking ) func ensureStateSoonImpl(st *state.State) { @@ -163,12 +164,12 @@ return chg } -func isTrue(form *multipart.Form, key string) bool { - value := form.Value[key] - if len(value) == 0 { +func isTrue(form *Form, key string) bool { + values := form.Values[key] + if len(values) == 0 { return false } - b, err := strconv.ParseBool(value[0]) + b, err := strconv.ParseBool(values[0]) if err != nil { return false } diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_model.go snapd-2.54.2+21.10/daemon/api_model.go --- snapd-2.53+21.10ubuntu1/daemon/api_model.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_model.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2019 Canonical Ltd + * Copyright (C) 2021 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -32,9 +32,11 @@ var ( serialModelCmd = &Command{ - Path: "/v2/model/serial", - GET: getSerial, - ReadAccess: openAccess{}, + Path: "/v2/model/serial", + GET: getSerial, + POST: postSerial, + ReadAccess: openAccess{}, + WriteAccess: rootAccess{}, } modelCmd = &Command{ Path: "/v2/model", @@ -165,3 +167,45 @@ return AssertResponse([]asserts.Assertion{serial}, false) } + +type postSerialData struct { + Action string `json:"action"` + NoRegistrationUntilReboot bool `json:"no-registration-until-reboot"` +} + +var devicestateDeviceManagerUnregister = (*devicestate.DeviceManager).Unregister + +func postSerial(c *Command, r *http.Request, _ *auth.UserState) Response { + var postData postSerialData + + decoder := json.NewDecoder(r.Body) + if err := decoder.Decode(&postData); err != nil { + return BadRequest("cannot decode serial action data from request body: %v", err) + } + if decoder.More() { + return BadRequest("spurious content after serial action") + } + switch postData.Action { + case "forget": + case "": + return BadRequest("missing serial action") + default: + return BadRequest("unsupported serial action %q", postData.Action) + } + + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + devmgr := c.d.overlord.DeviceManager() + + unregOpts := &devicestate.UnregisterOptions{ + NoRegistrationUntilReboot: postData.NoRegistrationUntilReboot, + } + err := devicestateDeviceManagerUnregister(devmgr, unregOpts) + if err != nil { + return InternalError("forgetting serial failed: %v", err) + } + + return SyncResponse(nil) +} diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_model_test.go snapd-2.54.2+21.10/daemon/api_model_test.go --- snapd-2.53+21.10ubuntu1/daemon/api_model_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_model_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2019-2020 Canonical Ltd + * Copyright (C) 2019-2021 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -22,6 +22,7 @@ import ( "bytes" "encoding/json" + "errors" "io" "net/http" "net/http/httptest" @@ -376,3 +377,63 @@ c.Assert(devKey, check.FitsTypeOf, "") c.Assert(devKey.(string), check.Equals, string(encDevKey)) } + +func (s *userSuite) TestPostSerialBadAction(c *check.C) { + buf := bytes.NewBufferString(`{"action":"what"}`) + req, err := http.NewRequest("POST", "/v2/model/serial", buf) + c.Assert(err, check.IsNil) + + rspe := s.errorReq(c, req, nil) + c.Check(rspe, check.DeepEquals, daemon.BadRequest(`unsupported serial action "what"`)) +} + +func (s *userSuite) TestPostSerialForget(c *check.C) { + unregister := 0 + defer daemon.MockDevicestateDeviceManagerUnregister(func(mgr *devicestate.DeviceManager, opts *devicestate.UnregisterOptions) error { + unregister++ + c.Check(mgr, check.NotNil) + c.Check(opts.NoRegistrationUntilReboot, check.Equals, false) + return nil + })() + + buf := bytes.NewBufferString(`{"action":"forget"}`) + req, err := http.NewRequest("POST", "/v2/model/serial", buf) + c.Assert(err, check.IsNil) + + rsp := s.syncReq(c, req, nil) + c.Check(rsp.Result, check.IsNil) + + c.Check(unregister, check.Equals, 1) +} + +func (s *userSuite) TestPostSerialForgetNoRegistrationUntilReboot(c *check.C) { + unregister := 0 + defer daemon.MockDevicestateDeviceManagerUnregister(func(mgr *devicestate.DeviceManager, opts *devicestate.UnregisterOptions) error { + unregister++ + c.Check(mgr, check.NotNil) + c.Check(opts.NoRegistrationUntilReboot, check.Equals, true) + return nil + })() + + buf := bytes.NewBufferString(`{"action":"forget", "no-registration-until-reboot": true}`) + req, err := http.NewRequest("POST", "/v2/model/serial", buf) + c.Assert(err, check.IsNil) + + rsp := s.syncReq(c, req, nil) + c.Check(rsp.Result, check.IsNil) + + c.Check(unregister, check.Equals, 1) +} + +func (s *userSuite) TestPostSerialForgetError(c *check.C) { + defer daemon.MockDevicestateDeviceManagerUnregister(func(mgr *devicestate.DeviceManager, opts *devicestate.UnregisterOptions) error { + return errors.New("boom") + })() + + buf := bytes.NewBufferString(`{"action":"forget"}`) + req, err := http.NewRequest("POST", "/v2/model/serial", buf) + c.Assert(err, check.IsNil) + + rspe := s.errorReq(c, req, nil) + c.Check(rspe, check.DeepEquals, daemon.InternalError(`forgetting serial failed: boom`)) +} diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_sideload_n_try.go snapd-2.54.2+21.10/daemon/api_sideload_n_try.go --- snapd-2.53+21.10ubuntu1/daemon/api_sideload_n_try.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_sideload_n_try.go 2022-01-06 21:25:16.000000000 +0000 @@ -20,6 +20,9 @@ package daemon import ( + "bytes" + "context" + "errors" "fmt" "io" "io/ioutil" @@ -32,6 +35,7 @@ "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/overlord/assertstate" "github.com/snapcore/snapd/overlord/auth" @@ -39,9 +43,85 @@ "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapfile" + "github.com/snapcore/snapd/strutil" ) -const maxReadBuflen = 1024 * 1024 +// Form is a multipart form that holds file and non-file parts +type Form struct { + // Values holds non-file parts keyed by their "name" parameter (from the + // part's Content-Disposition header). + Values map[string][]string + + // FileRefs holds file parts keyed by their "name" parameter (from the + // part's Content-Disposition header). Each reference contains a filename + // (the "filename" parameter) and the path to a file with the part's contents. + FileRefs map[string][]*FileReference +} + +type FileReference struct { + Filename string + TmpPath string +} + +func (f *Form) RemoveAllExcept(paths []string) { + for _, refs := range f.FileRefs { + for _, ref := range refs { + if strutil.ListContains(paths, ref.TmpPath) { + continue + } + + if err := os.Remove(ref.TmpPath); err != nil { + logger.Noticef("cannot remove temporary file: %v", err) + } + } + } +} + +type uploadedSnap struct { + // filename is the original name/path of the snap file. + filename string + // tmpPath is the location where the temp snap file is stored. + tmpPath string + // instanceName is optional and can only be set if only one snap was uploaded. + instanceName string +} + +// GetSnapFiles returns the original name and temp path for each snap file in +// the form. Optionally, it might include a requested instance name, but only +// if the was only one file in the form. +func (f *Form) GetSnapFiles() ([]*uploadedSnap, *apiError) { + if len(f.FileRefs["snap"]) == 0 { + return nil, BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`) + } + + refs := f.FileRefs["snap"] + if len(refs) == 1 && len(f.Values["snap-path"]) > 0 { + uploaded := &uploadedSnap{ + filename: f.Values["snap-path"][0], + tmpPath: refs[0].TmpPath, + } + + if len(f.Values["name"]) > 0 { + uploaded.instanceName = f.Values["name"][0] + } + return []*uploadedSnap{uploaded}, nil + } + + snapFiles := make([]*uploadedSnap, len(refs)) + for i, ref := range refs { + snapFiles[i] = &uploadedSnap{ + filename: ref.Filename, + tmpPath: ref.TmpPath, + } + } + + return snapFiles, nil +} + +type sideloadFlags struct { + snapstate.Flags + dangerousOK bool +} func sideloadOrTrySnap(c *Command, body io.ReadCloser, boundary string, user *auth.UserState) Response { route := c.d.router.Get(stateChangeCmd.Path) @@ -50,161 +130,288 @@ } // POSTs to sideload snaps must be a multipart/form-data file upload. - form, err := multipart.NewReader(body, boundary).ReadForm(maxReadBuflen) - if err != nil { - return BadRequest("cannot read POST form: %v", err) + mpReader := multipart.NewReader(body, boundary) + form, errRsp := readForm(mpReader) + if errRsp != nil { + return errRsp } - dangerousOK := isTrue(form, "dangerous") + // we are in charge of the temp files, until they're handed off to the change + var pathsToNotRemove []string + defer func() { + form.RemoveAllExcept(pathsToNotRemove) + }() + flags, err := modeFlags(isTrue(form, "devmode"), isTrue(form, "jailmode"), isTrue(form, "classic")) if err != nil { return BadRequest(err.Error()) } - if len(form.Value["action"]) > 0 && form.Value["action"][0] == "try" { - if len(form.Value["snap-path"]) == 0 { + if len(form.Values["action"]) > 0 && form.Values["action"][0] == "try" { + if len(form.Values["snap-path"]) == 0 { return BadRequest("need 'snap-path' value in form") } - return trySnap(c.d.overlord.State(), form.Value["snap-path"][0], flags) + return trySnap(c.d.overlord.State(), form.Values["snap-path"][0], flags) } - flags.RemoveSnapPath = true + flags.RemoveSnapPath = true flags.Unaliased = isTrue(form, "unaliased") flags.IgnoreRunning = isTrue(form, "ignore-running") - systemRestartImmediate := isTrue(form, "system-restart-immediate") - // find the file for the "snap" form field - var snapBody multipart.File - var origPath string -out: - for name, fheaders := range form.File { - if name != "snap" { - continue - } - for _, fheader := range fheaders { - snapBody, err = fheader.Open() - origPath = fheader.Filename - if err != nil { - return BadRequest(`cannot open uploaded "snap" file: %v`, err) - } - defer snapBody.Close() + sideloadFlags := sideloadFlags{ + Flags: flags, + dangerousOK: isTrue(form, "dangerous"), + } - break out - } + snapFiles, errRsp := form.GetSnapFiles() + if errRsp != nil { + return errRsp } - defer form.RemoveAll() - if snapBody == nil { - return BadRequest(`cannot find "snap" file field in provided multipart/form-data payload`) + st := c.d.overlord.State() + st.Lock() + defer st.Unlock() + + var chg *state.Change + if len(snapFiles) > 1 { + chg, errRsp = sideloadManySnaps(st, snapFiles, sideloadFlags, user) + } else { + chg, errRsp = sideloadSnap(st, snapFiles[0], sideloadFlags) + } + if errRsp != nil { + return errRsp } - // we are in charge of the tempfile life cycle until we hand it off to the change - changeTriggered := false - // if you change this prefix, look for it in the tests - // also see localInstallCleanup in snapstate/snapmgr.go - tmpf, err := ioutil.TempFile(dirs.SnapBlobDir, dirs.LocalInstallBlobTempPrefix) - if err != nil { - return InternalError("cannot create temporary file: %v", err) + chg.Set("system-restart-immediate", isTrue(form, "system-restart-immediate")) + + ensureStateSoon(st) + + // the handoff is only done when the unlock succeeds (instead of panicking) + // but this is good enough + pathsToNotRemove = make([]string, len(snapFiles)) + for i, snapFile := range snapFiles { + pathsToNotRemove[i] = snapFile.tmpPath } - tempPath := tmpf.Name() + return AsyncResponse(nil, chg.ID()) +} - defer func() { - if !changeTriggered { - os.Remove(tempPath) +func sideloadManySnaps(st *state.State, snapFiles []*uploadedSnap, flags sideloadFlags, user *auth.UserState) (*state.Change, *apiError) { + sideInfos := make([]*snap.SideInfo, len(snapFiles)) + names := make([]string, len(snapFiles)) + tempPaths := make([]string, len(snapFiles)) + origPaths := make([]string, len(snapFiles)) + + for i, snapFile := range snapFiles { + si, apiError := readSideInfo(st, snapFile.tmpPath, snapFile.filename, flags) + if apiError != nil { + return nil, apiError } - }() - if _, err := io.Copy(tmpf, snapBody); err != nil { - return InternalError("cannot copy request into temporary file: %v", err) + sideInfos[i] = si + names[i] = si.RealName + tempPaths[i] = snapFile.tmpPath + origPaths[i] = snapFile.filename } - tmpf.Sync() - if len(form.Value["snap-path"]) > 0 { - origPath = form.Value["snap-path"][0] + var userID int + if user != nil { + userID = user.ID } - var instanceName string + tss, err := snapstateInstallPathMany(context.TODO(), st, sideInfos, tempPaths, userID, &flags.Flags) + if err != nil { + return nil, errToResponse(err, tempPaths, InternalError, "cannot install snap files: %v") + } + + msg := fmt.Sprintf(i18n.G("Install snaps %s from files %s"), strutil.Quoted(names), strutil.Quoted(origPaths)) + chg := newChange(st, "install-snap", msg, tss, names) + chg.Set("api-data", map[string][]string{"snap-names": names}) + + return chg, nil +} - if len(form.Value["name"]) > 0 { +func sideloadSnap(st *state.State, snapFile *uploadedSnap, flags sideloadFlags) (*state.Change, *apiError) { + var instanceName string + if snapFile.instanceName != "" { // caller has specified desired instance name - instanceName = form.Value["name"][0] + instanceName = snapFile.instanceName if err := snap.ValidateInstanceName(instanceName); err != nil { - return BadRequest(err.Error()) + return nil, BadRequest(err.Error()) } } - st := c.d.overlord.State() - st.Lock() - defer st.Unlock() + sideInfo, apiErr := readSideInfo(st, snapFile.tmpPath, snapFile.filename, flags) + if apiErr != nil { + return nil, apiErr + } + + if instanceName != "" { + requestedSnapName := snap.InstanceSnap(instanceName) + if requestedSnapName != sideInfo.RealName { + return nil, BadRequest(fmt.Sprintf("instance name %q does not match snap name %q", instanceName, sideInfo.RealName)) + } + } else { + instanceName = sideInfo.RealName + } + + tset, _, err := snapstateInstallPath(st, sideInfo, snapFile.tmpPath, instanceName, "", flags.Flags) + if err != nil { + return nil, errToResponse(err, []string{sideInfo.RealName}, InternalError, "cannot install snap file: %v") + } + + msg := fmt.Sprintf(i18n.G("Install %q snap from file %q"), instanceName, snapFile.filename) + chg := newChange(st, "install-snap", msg, []*state.TaskSet{tset}, []string{instanceName}) + chg.Set("api-data", map[string]string{"snap-name": instanceName}) + + return chg, nil +} - var snapName string +func readSideInfo(st *state.State, tempPath string, origPath string, flags sideloadFlags) (*snap.SideInfo, *apiError) { var sideInfo *snap.SideInfo - if !dangerousOK { + if !flags.dangerousOK { si, err := snapasserts.DeriveSideInfo(tempPath, assertstate.DB(st)) switch { case err == nil: - snapName = si.RealName sideInfo = si case asserts.IsNotFound(err): // with devmode we try to find assertions but it's ok // if they are not there (implies --dangerous) - if !isTrue(form, "devmode") { + if !flags.DevMode { msg := "cannot find signatures with metadata for snap" if origPath != "" { msg = fmt.Sprintf("%s %q", msg, origPath) } - return BadRequest(msg) + return nil, BadRequest(msg) } // TODO: set a warning if devmode default: - return BadRequest(err.Error()) + return nil, BadRequest(err.Error()) } } - if snapName == "" { + if sideInfo == nil { // potentially dangerous but dangerous or devmode params were set info, err := unsafeReadSnapInfo(tempPath) if err != nil { - return BadRequest("cannot read snap file: %v", err) + return nil, BadRequest("cannot read snap file: %v", err) } - snapName = info.SnapName() - sideInfo = &snap.SideInfo{RealName: snapName} + sideInfo = &snap.SideInfo{RealName: info.SnapName()} } + return sideInfo, nil +} - if instanceName != "" { - requestedSnapName := snap.InstanceSnap(instanceName) - if requestedSnapName != snapName { - return BadRequest(fmt.Sprintf("instance name %q does not match snap name %q", instanceName, snapName)) +// maxReadBuflen is the maximum buffer size for reading the non-file parts in the snap upload form +const maxReadBuflen = 1024 * 1024 + +// readForm returns a Form populated with values (for non-file parts) and file headers (for file +// parts). The file headers contain the original file name and a path to the persisted file in +// dirs.SnapDirBlob. If an error occurs and a non-nil Response is returned, an attempt is made +// to remove temp files. +func readForm(reader *multipart.Reader) (_ *Form, apiErr *apiError) { + availMemory := int64(maxReadBuflen) + form := &Form{ + Values: make(map[string][]string), + FileRefs: make(map[string][]*FileReference), + } + + // clean up if we're failing the request + defer func() { + if apiErr != nil { + form.RemoveAllExcept(nil) + } + }() + + for { + part, err := reader.NextPart() + if err != nil { + if errors.Is(err, io.EOF) { + break + } + return nil, BadRequest("cannot read POST form: %v", err) + } + + name := part.FormName() + if name == "" { + continue + } + + filename := part.FileName() + if filename == "" { + // non-file parts are kept in memory + buf := &bytes.Buffer{} + + // copy one byte more than the max so we know if it exceeds the limit + n, err := io.CopyN(buf, part, availMemory+1) + if err != nil && !errors.Is(err, io.EOF) { + return nil, BadRequest("cannot read form data: %v", err) + } + + availMemory -= n + if availMemory < 0 { + return nil, BadRequest("cannot read form data: exceeds memory limit") + } + + form.Values[name] = append(form.Values[name], buf.String()) + continue + } + + tmpPath, err := writeToTempFile(part) + + // add it to the form even if err != nil, so it gets deleted + ref := &FileReference{TmpPath: tmpPath, Filename: filename} + form.FileRefs[name] = append(form.FileRefs[name], ref) + + if err != nil { + return nil, InternalError(err.Error()) } - } else { - instanceName = snapName } - msg := fmt.Sprintf(i18n.G("Install %q snap from file"), instanceName) - if origPath != "" { - msg = fmt.Sprintf(i18n.G("Install %q snap from file %q"), instanceName, origPath) + // sync the parent directory where the files were written to + if len(form.FileRefs) > 0 { + dir, err := os.Open(dirs.SnapBlobDir) + if err != nil { + return nil, InternalError("cannot open parent dir of temp files: %v", err) + } + defer func() { + if cerr := dir.Close(); apiErr == nil && cerr != nil { + apiErr = InternalError("cannot close parent dir of temp files: %v", cerr) + } + }() + + if err := dir.Sync(); err != nil { + return nil, InternalError("cannot sync parent dir of temp files: %v", err) + } } - tset, _, err := snapstateInstallPath(st, sideInfo, tempPath, instanceName, "", flags) + return form, nil +} + +// writeToTempFile writes the contents of reader to a temp file and returns +// its path. If the path is not empty then a file was written and it's the +// caller's responsibility to clean it up (even if the error is non-nil). +func writeToTempFile(reader io.Reader) (path string, err error) { + tmpf, err := ioutil.TempFile(dirs.SnapBlobDir, dirs.LocalInstallBlobTempPrefix) if err != nil { - return errToResponse(err, []string{snapName}, InternalError, "cannot install snap file: %v") + return "", fmt.Errorf("cannot create temp file for form data file part: %v", err) } + defer func() { + if cerr := tmpf.Close(); err == nil && cerr != nil { + err = fmt.Errorf("cannot close temp file: %v", cerr) + } + }() - chg := newChange(st, "install-snap", msg, []*state.TaskSet{tset}, []string{instanceName}) - if systemRestartImmediate { - chg.Set("system-restart-immediate", true) + // TODO: limit the file part size by wrapping it w/ http.MaxBytesReader + if _, err = io.Copy(tmpf, reader); err != nil { + return tmpf.Name(), fmt.Errorf("cannot write file part: %v", err) } - chg.Set("api-data", map[string]string{"snap-name": instanceName}) - - ensureStateSoon(st) - // only when the unlock succeeds (as opposed to panicing) is the handoff done - // but this is good enough - changeTriggered = true + if err := tmpf.Sync(); err != nil { + return tmpf.Name(), fmt.Errorf("cannot sync file: %v", err) + } - return AsyncResponse(nil, chg.ID()) + return tmpf.Name(), nil } func trySnap(st *state.State, trydir string, flags snapstate.Flags) Response { diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_sideload_n_try_test.go snapd-2.54.2+21.10/daemon/api_sideload_n_try_test.go --- snapd-2.53+21.10ubuntu1/daemon/api_sideload_n_try_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_sideload_n_try_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -22,12 +22,16 @@ import ( "bytes" "context" + "crypto" + "crypto/rand" + "errors" "fmt" "io/ioutil" "net/http" "os" "path/filepath" "regexp" + "strconv" "time" "gopkg.in/check.v1" @@ -42,6 +46,7 @@ "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/sandbox" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/testutil" ) @@ -508,6 +513,418 @@ c.Check(systemRestartImmediate, check.Equals, true) } +func (s *sideloadSuite) TestFormdataIsWrittenToCorrectTmpLocation(c *check.C) { + oldTempDir := os.Getenv("TMPDIR") + defer func() { + c.Assert(os.Setenv("TMPDIR", oldTempDir), check.IsNil) + }() + tmpDir := c.MkDir() + c.Assert(os.Setenv("TMPDIR", tmpDir), check.IsNil) + + head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} + chgSummary, _ := s.sideloadCheck(c, sideLoadBodyWithoutDevMode, head, "local", snapstate.Flags{RemoveSnapPath: true}) + c.Check(chgSummary, check.Equals, `Install "local" snap from file "a/b/local.snap"`) + + files, err := ioutil.ReadDir(tmpDir) + c.Assert(err, check.IsNil) + c.Assert(files, check.HasLen, 0) + + matches, err := filepath.Glob(filepath.Join(dirs.SnapBlobDir, dirs.LocalInstallBlobTempPrefix+"*")) + c.Assert(err, check.IsNil) + c.Assert(matches, check.HasLen, 1) + + c.Assert(err, check.IsNil) + c.Assert(matches[0], testutil.FileEquals, "xyzzy") +} + +func (s *sideloadSuite) TestSideloadExceedMemoryLimit(c *check.C) { + s.daemonWithOverlordMockAndStore() + + // check that there's a memory limit for the sum of the parts, not just each + bufs := make([][]byte, 2) + var body string + + for i := range bufs { + bufs[i] = make([]byte, daemon.MaxReadBuflen/2+1) + _, err := rand.Read(bufs[i]) + c.Assert(err, check.IsNil) + + body += "--foo\r\n" + + "Content-Disposition: form-data; name=\"stuff\"\r\n" + + "\r\n" + + string(bufs[i]) + + "\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=foo") + + apiErr := s.errorReq(c, req, nil) + c.Check(apiErr.Message, check.Equals, `cannot read form data: exceeds memory limit`) +} + +func (s *sideloadSuite) TestSideloadUsePreciselyAllMemory(c *check.C) { + s.daemonWithOverlordMockAndStore() + + buf := make([]byte, daemon.MaxReadBuflen) + _, err := rand.Read(buf) + c.Assert(err, check.IsNil) + + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + string(buf) + + "\r\n" + + "----hello--\r\n" + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + + // using the maximum memory doesn't cause the failure (not having a snap file does) + apiErr := s.errorReq(c, req, nil) + c.Check(apiErr.Message, check.Equals, `cannot find "snap" file field in provided multipart/form-data payload`) +} + +func (s *sideloadSuite) TestSideloadCleanUpTempFilesIfRequestFailed(c *check.C) { + s.daemonWithOverlordMockAndStore() + + // write file parts + body := "----hello--\r\n" + for _, name := range []string{"one", "two"} { + body += fmt.Sprintf( + "Content-Disposition: form-data; name=\"snap\"; filename=\"%s\"\r\n"+ + "\r\n"+ + "xyzzy\r\n", name) + } + + // make the request fail + buf := make([]byte, daemon.MaxReadBuflen+1) + _, err := rand.Read(buf) + c.Assert(err, check.IsNil) + + body += "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + string(buf) + + "\r\n" + + "----hello--\r\n" + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + + apiErr := s.errorReq(c, req, nil) + c.Check(apiErr, check.NotNil) + matches, err := filepath.Glob(filepath.Join(dirs.SnapBlobDir, "*")) + c.Assert(err, check.IsNil) + c.Check(matches, check.HasLen, 0) +} + +func (s *sideloadSuite) TestSideloadCleanUpUnusedTempSnapFiles(c *check.C) { + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + + "Content-Disposition: form-data; name=\"snap\"; filename=\"one\"\r\n" + + "\r\n" + + "xyzzy\r\n" + + "----hello--\r\n" + + // only files with the name 'snap' are used + "Content-Disposition: form-data; name=\"not-snap\"; filename=\"two\"\r\n" + + "\r\n" + + "bla\r\n" + + "----hello--\r\n" + + head := map[string]string{"Content-Type": "multipart/thing; boundary=--hello--"} + chgSummary, _ := s.sideloadCheck(c, body, head, "local", snapstate.Flags{RemoveSnapPath: true, DevMode: true}) + c.Check(chgSummary, check.Equals, `Install "local" snap from file "one"`) + + matches, err := filepath.Glob(filepath.Join(dirs.SnapBlobDir, dirs.LocalInstallBlobTempPrefix+"*")) + c.Assert(err, check.IsNil) + // only the file passed into the change (the request's first file) remains + c.Check(matches, check.HasLen, 1) +} + +func (s *sideloadSuite) TestSideloadManySnaps(c *check.C) { + d := s.daemonWithFakeSnapManager(c) + expectedFlags := &snapstate.Flags{RemoveSnapPath: true, DevMode: true} + + restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, paths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) { + c.Check(flags, check.DeepEquals, expectedFlags) + c.Check(userID, check.Not(check.Equals), 0) + + var tss []*state.TaskSet + for i, path := range paths { + si := infos[i] + c.Check(path, testutil.FileEquals, si.RealName) + + ts := state.NewTaskSet(s.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", si.RealName))) + tss = append(tss, ts) + } + + return tss, nil + }) + defer restore() + + snaps := []string{"one", "two"} + var i int + readRest := daemon.MockUnsafeReadSnapInfo(func(string) (*snap.Info, error) { + info := &snap.Info{SuggestedName: snaps[i]} + i++ + return info, nil + }) + defer readRest() + + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + prefixed := make([]string, len(snaps)) + for i, snap := range snaps { + prefixed[i] = "file-" + snap + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"" + prefixed[i] + "\"\r\n" + + "\r\n" + + snap + "\r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + s.asUserAuth(c, req) + rsp := s.asyncReq(c, req, s.authUser) + + st := d.Overlord().State() + st.Lock() + defer st.Unlock() + + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Install snaps %s from files %s`, strutil.Quoted(snaps), strutil.Quoted(prefixed))) + + var data map[string][]string + c.Assert(chg.Get("api-data", &data), check.IsNil) + c.Check(data["snap-names"], check.DeepEquals, snaps) +} + +func (s *sideloadSuite) TestSideloadManyFailInstallPathMany(c *check.C) { + s.daemon(c) + restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, paths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) { + return nil, errors.New("expected") + }) + defer restore() + + readRest := daemon.MockUnsafeReadSnapInfo(func(string) (*snap.Info, error) { + return &snap.Info{SuggestedName: "name"}, nil + }) + defer readRest() + + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + for _, snap := range []string{"one", "two"} { + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"file-" + snap + "\"\r\n" + + "\r\n" + + "xyzzy \r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + apiErr := s.errorReq(c, req, nil) + + c.Check(apiErr.JSON().Status, check.Equals, 500) + c.Check(apiErr.Message, check.Equals, `cannot install snap files: expected`) +} + +func (s *sideloadSuite) TestSideloadManyFailUnsafeReadInfo(c *check.C) { + s.daemon(c) + restore := daemon.MockUnsafeReadSnapInfo(func(string) (*snap.Info, error) { + return nil, errors.New("expected") + }) + defer restore() + + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + for _, snap := range []string{"one", "two"} { + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"file-" + snap + "\"\r\n" + + "\r\n" + + "xyzzy \r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + apiErr := s.errorReq(c, req, nil) + + c.Check(apiErr.JSON().Status, check.Equals, 400) + c.Check(apiErr.Message, check.Equals, `cannot read snap file: expected`) +} + +func (s *sideloadSuite) TestSideloadManySnapsDevmode(c *check.C) { + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"devmode\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + + s.errReadInfo(c, body) +} + +func (s *sideloadSuite) TestSideloadManySnapsDangerous(c *check.C) { + body := "----hello--\r\n" + + "Content-Disposition: form-data; name=\"dangerous\"\r\n" + + "\r\n" + + "true\r\n" + + "----hello--\r\n" + + s.errReadInfo(c, body) +} + +func (s *sideloadSuite) errReadInfo(c *check.C, body string) { + s.daemon(c) + + for _, snap := range []string{"one", "two"} { + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"" + snap + "\"\r\n" + + "\r\n" + + snap + "\r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + rsp := s.errorReq(c, req, nil) + + c.Assert(rsp.Status, check.Equals, 400) + // gets as far as reading the file to get the SideInfo + c.Assert(rsp.Message, check.Matches, "cannot read snap file:.*") +} + +func (s *sideloadSuite) TestSideloadManySnapsAsserted(c *check.C) { + d := s.daemonWithOverlordMockAndStore() + st := d.Overlord().State() + snaps := []string{"one", "two"} + s.mockAssertions(c, st, snaps) + + body := "----hello--\r\n" + expectedFlags := snapstate.Flags{RemoveSnapPath: true} + s.testSideloadManySnaps(c, st, body, snaps, expectedFlags) +} + +func (s *sideloadSuite) TestSideloadManySnapsOneNotAsserted(c *check.C) { + d := s.daemonWithOverlordMockAndStore() + st := d.Overlord().State() + snaps := []string{"one", "two"} + s.mockAssertions(c, st, []string{"one"}) + + body := "----hello--\r\n" + + fileSnaps := make([]string, len(snaps)) + for i, snap := range snaps { + fileSnaps[i] = "file-" + snap + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"" + fileSnaps[i] + "\"\r\n" + + "\r\n" + + snap + "\r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + rsp := s.errorReq(c, req, nil) + + c.Check(rsp.Status, check.Equals, 400) + c.Check(rsp.Message, check.Matches, "cannot find signatures with metadata for snap \"file-two\"") +} + +func (s *sideloadSuite) mockAssertions(c *check.C, st *state.State, snaps []string) { + for _, snap := range snaps { + hash := crypto.SHA3_384.New() + data := []byte(snap) + hash.Write(data) + digest := hash.Sum(nil) + + base64Digest, err := asserts.EncodeDigest(crypto.SHA3_384, digest) + c.Assert(err, check.IsNil) + dev1Acct := assertstest.NewAccount(s.StoreSigning, "devel1", nil, "") + snapDecl, err := s.StoreSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-id": snap + "-id", + "snap-name": snap, + "publisher-id": dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + snapRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{ + "snap-sha3-384": base64Digest, + "snap-size": strconv.Itoa(len(data)), + "snap-id": snap + "-id", + "snap-revision": "41", + "developer-id": dev1Acct.AccountID(), + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, check.IsNil) + + st.Lock() + assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey(""), dev1Acct, snapDecl, snapRev) + st.Unlock() + } +} + +func (s *sideloadSuite) testSideloadManySnaps(c *check.C, st *state.State, body string, snaps []string, expectedFlags snapstate.Flags) { + restore := daemon.MockSnapstateInstallPathMany(func(_ context.Context, s *state.State, infos []*snap.SideInfo, paths []string, userID int, flags *snapstate.Flags) ([]*state.TaskSet, error) { + c.Check(*flags, check.DeepEquals, expectedFlags) + + var tss []*state.TaskSet + for i, si := range infos { + c.Check(si, check.DeepEquals, &snap.SideInfo{ + RealName: snaps[i], + SnapID: snaps[i] + "-id", + Revision: snap.R(41), + }) + + ts := state.NewTaskSet(s.NewTask("fake-install-snap", fmt.Sprintf("Doing a fake install of %q", si.RealName))) + tss = append(tss, ts) + } + + return tss, nil + }) + defer restore() + + fileSnaps := make([]string, len(snaps)) + for i, snap := range snaps { + fileSnaps[i] = "file-" + snap + body += "Content-Disposition: form-data; name=\"snap\"; filename=\"" + fileSnaps[i] + "\"\r\n" + + "\r\n" + + snap + "\r\n" + + "----hello--\r\n" + } + + req, err := http.NewRequest("POST", "/v2/snaps", bytes.NewBufferString(body)) + c.Assert(err, check.IsNil) + req.Header.Set("Content-Type", "multipart/thing; boundary=--hello--") + rsp := s.asyncReq(c, req, nil) + + c.Check(rsp.Status, check.Equals, 202) + st.Lock() + defer st.Unlock() + chg := st.Change(rsp.Change) + c.Assert(chg, check.NotNil) + c.Check(chg.Summary(), check.Equals, fmt.Sprintf(`Install snaps %s from files %s`, strutil.Quoted(snaps), strutil.Quoted(fileSnaps))) +} + type trySuite struct { apiBaseSuite } diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_snaps.go snapd-2.54.2+21.10/daemon/api_snaps.go --- snapd-2.53+21.10ubuntu1/daemon/api_snaps.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_snaps.go 2022-01-06 21:25:16.000000000 +0000 @@ -607,6 +607,11 @@ // TODO: use a per-request context updated, tasksets, err := snapstateUpdateMany(context.TODO(), st, inst.Snaps, inst.userID, nil) if err != nil { + if opts.IsRefreshOfAllSnaps { + if err := assertstateRestoreValidationSetsTracking(st); err != nil && !errors.Is(err, state.ErrNoState) { + return nil, err + } + } return nil, err } diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_snaps_test.go snapd-2.54.2+21.10/daemon/api_snaps_test.go --- snapd-2.53+21.10ubuntu1/daemon/api_snaps_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_snaps_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -596,6 +596,35 @@ c.Check(refreshSnapAssertions, check.Equals, true) } +func (s *snapsSuite) TestRefreshAllRestoresValidationSets(c *check.C) { + refreshSnapAssertions := false + var refreshAssertionsOpts *assertstate.RefreshAssertionsOptions + defer daemon.MockAssertstateRefreshSnapAssertions(func(s *state.State, userID int, opts *assertstate.RefreshAssertionsOptions) error { + refreshSnapAssertions = true + refreshAssertionsOpts = opts + return nil + })() + + defer daemon.MockAssertstateRestoreValidationSetsTracking(func(s *state.State) error { + return nil + })() + + defer daemon.MockSnapstateUpdateMany(func(_ context.Context, s *state.State, names []string, userID int, flags *snapstate.Flags) ([]string, []*state.TaskSet, error) { + return nil, nil, fmt.Errorf("boom") + })() + + d := s.daemon(c) + inst := &daemon.SnapInstruction{Action: "refresh"} + st := d.Overlord().State() + st.Lock() + _, err := inst.DispatchForMany()(inst, st) + st.Unlock() + c.Assert(err, check.ErrorMatches, "boom") + c.Check(refreshSnapAssertions, check.Equals, true) + c.Assert(refreshAssertionsOpts, check.NotNil) + c.Check(refreshAssertionsOpts.IsRefreshOfAllSnaps, check.Equals, true) +} + func (s *snapsSuite) TestRefreshMany(c *check.C) { refreshSnapAssertions := false var refreshAssertionsOpts *assertstate.RefreshAssertionsOptions @@ -752,48 +781,58 @@ s.SysctlBufs = [][]byte{ []byte(`Type=simple Id=snap.foo.svc1.service +Names=snap.foo.svc1.service ActiveState=fumbling UnitFileState=enabled `), []byte(`Type=forking Id=snap.foo.svc2.service +Names=snap.foo.svc2.service ActiveState=active UnitFileState=disabled `), []byte(`Type=oneshot Id=snap.foo.svc3.service +Names=snap.foo.svc3.service ActiveState=reloading UnitFileState=static `), []byte(`Type=notify Id=snap.foo.svc4.service +Names=snap.foo.svc4.service ActiveState=inactive UnitFileState=potatoes `), []byte(`Type=simple Id=snap.foo.svc5.service +Names=snap.foo.svc5.service ActiveState=inactive UnitFileState=static `), []byte(`Id=snap.foo.svc5.timer +Names=snap.foo.svc5.timer ActiveState=active UnitFileState=enabled `), []byte(`Type=simple Id=snap.foo.svc6.service +Names=snap.foo.svc6.service ActiveState=inactive UnitFileState=static `), []byte(`Id=snap.foo.svc6.sock.socket +Names=snap.foo.svc6.sock.socket ActiveState=active UnitFileState=enabled `), []byte(`Type=simple Id=snap.foo.svc7.service +Names=snap.foo.svc7.service ActiveState=inactive UnitFileState=static `), []byte(`Id=snap.foo.svc7.other-sock.socket +Names=snap.foo.svc7.other-sock.socket ActiveState=inactive UnitFileState=enabled `), @@ -1150,8 +1189,7 @@ } func (s *snapsSuite) TestPostSnapSystemRestartImmediate(c *check.C) { - checkOpts := func(opts *snapstate.RevisionOptions) {} - _, systemRestartImmediate := s.testPostSnap(c, `"system-restart-immediate": true`, checkOpts) + _, systemRestartImmediate := s.testPostSnap(c, `"system-restart-immediate": true`, nil) c.Check(systemRestartImmediate, check.Equals, true) } @@ -1168,7 +1206,9 @@ checked := false defer daemon.MockSnapstateInstall(func(ctx context.Context, s *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags) (*state.TaskSet, error) { - checkOpts(opts) + if checkOpts != nil { + checkOpts(opts) + } checked = true t := s.NewTask("fake-install-snap", "Doing a fake install") return state.NewTaskSet(t), nil diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_systems_test.go snapd-2.54.2+21.10/daemon/api_systems_test.go --- snapd-2.53+21.10ubuntu1/daemon/api_systems_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_systems_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -42,7 +42,7 @@ "github.com/snapcore/snapd/overlord/assertstate/assertstatetest" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/hookstate" - "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/seed" "github.com/snapcore/snapd/seed/seedtest" "github.com/snapcore/snapd/snap" @@ -439,8 +439,8 @@ d := s.daemon(c) st := d.Overlord().State() st.Lock() - // devicemgr needs boot id to request a reboot - st.VerifyReboot("boot-id-0") + // make things look like a reboot + restart.ReplaceBootID(st, "boot-id-1") // device model assertstatetest.AddMany(st, s.StoreSigning.StoreAccountKey("")) assertstatetest.AddMany(st, s.Brands.AccountsAndKeys("my-brand")...) @@ -505,7 +505,7 @@ // daemon is not started, only check whether reboot was scheduled as expected // reboot flag - c.Check(d.RequestedRestart(), check.Equals, state.RestartSystemNow, check.Commentf(tc.comment)) + c.Check(d.RequestedRestart(), check.Equals, restart.RestartSystemNow, check.Commentf(tc.comment)) // slow reboot schedule c.Check(cmd.Calls(), check.DeepEquals, [][]string{ {"shutdown", "-r", "+10", "reboot scheduled to update the system"}, diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_test.go snapd-2.54.2+21.10/daemon/api_test.go --- snapd-2.53+21.10ubuntu1/daemon/api_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -21,7 +21,6 @@ import ( "fmt" - "mime/multipart" "net/http" "gopkg.in/check.v1" @@ -115,14 +114,14 @@ } func (s *apiSuite) TestIsTrue(c *check.C) { - form := &multipart.Form{} + form := &daemon.Form{} c.Check(daemon.IsTrue(form, "foo"), check.Equals, false) for _, f := range []string{"", "false", "0", "False", "f", "try"} { - form.Value = map[string][]string{"foo": {f}} + form.Values = map[string][]string{"foo": {f}} c.Check(daemon.IsTrue(form, "foo"), check.Equals, false, check.Commentf("expected %q to be false", f)) } for _, t := range []string{"true", "1", "True", "t"} { - form.Value = map[string][]string{"foo": {t}} + form.Values = map[string][]string{"foo": {t}} c.Check(daemon.IsTrue(form, "foo"), check.Equals, true, check.Commentf("expected %q to be true", t)) } } diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_users.go snapd-2.54.2+21.10/daemon/api_users.go --- snapd-2.53+21.10ubuntu1/daemon/api_users.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_users.go 2022-01-06 21:25:16.000000000 +0000 @@ -408,6 +408,12 @@ return "", nil, fmt.Errorf("cannot create user for %q: no ssh keys found", email) } + // Amend information where the key came from to ensure it can + // be update/replaced later + for i, k := range v.SSHKeys { + v.SSHKeys[i] = fmt.Sprintf(`%s # snapd {"origin":"store","email":%q}`, k, email) + } + gecos := fmt.Sprintf("%s,%s", email, v.OpenIDIdentifier) opts := &osutil.AddUserOptions{ SSHKeys: v.SSHKeys, diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_users_test.go snapd-2.54.2+21.10/daemon/api_users_test.go --- snapd-2.53+21.10ubuntu1/daemon/api_users_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_users_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -491,7 +491,10 @@ } defer daemon.MockOsutilAddUser(func(username string, opts *osutil.AddUserOptions) error { c.Check(username, check.Equals, expectedUsername) - c.Check(opts.SSHKeys, check.DeepEquals, []string{"ssh1", "ssh2"}) + c.Check(opts.SSHKeys, check.DeepEquals, []string{ + `ssh1 # snapd {"origin":"store","email":"popper@lse.ac.uk"}`, + `ssh2 # snapd {"origin":"store","email":"popper@lse.ac.uk"}`, + }) c.Check(opts.Gecos, check.Equals, "popper@lse.ac.uk,xxyyzz") c.Check(opts.Sudoer, check.Equals, false) return nil @@ -501,7 +504,10 @@ var expected interface{} expectedItem := daemon.UserResponseData{ Username: expectedUsername, - SSHKeys: []string{"ssh1", "ssh2"}, + SSHKeys: []string{ + `ssh1 # snapd {"origin":"store","email":"popper@lse.ac.uk"}`, + `ssh2 # snapd {"origin":"store","email":"popper@lse.ac.uk"}`, + }, } if oldWay { diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_validate.go snapd-2.54.2+21.10/daemon/api_validate.go --- snapd-2.53+21.10ubuntu1/daemon/api_validate.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_validate.go 2022-01-06 21:25:16.000000000 +0000 @@ -104,7 +104,7 @@ } sort.Strings(names) - snaps, err := snapstate.InstalledSnaps(st) + snaps, _, err := snapstate.InstalledSnaps(st) if err != nil { return InternalError(err.Error()) } @@ -120,7 +120,8 @@ if err != nil { return InternalError("cannot get assertion for validation set tracking %s/%s/%d: %v", tr.AccountID, tr.Name, sequence, err) } - validErr := checkInstalledSnaps(sets, snaps) + // do not pass ignore validation map, we don't want to ignore validation and show invalid ones. + validErr := checkInstalledSnaps(sets, snaps, nil) modeStr, err := modeString(tr.Mode) if err != nil { return InternalError(err.Error()) @@ -138,8 +139,8 @@ return SyncResponse(results) } -var checkInstalledSnaps = func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap) error { - return vsets.CheckInstalledSnaps(snaps) +var checkInstalledSnaps = func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + return vsets.CheckInstalledSnaps(snaps, ignoreValidation) } func getValidationSet(c *Command, r *http.Request, user *auth.UserState) Response { @@ -200,12 +201,12 @@ if err != nil { return InternalError(err.Error()) } - snaps, err := snapstate.InstalledSnaps(st) + snaps, _, err := snapstate.InstalledSnaps(st) if err != nil { return InternalError(err.Error()) } - validErr := checkInstalledSnaps(sets, snaps) + validErr := checkInstalledSnaps(sets, snaps, nil) res := validationSetResult{ AccountID: tr.AccountID, Name: tr.Name, @@ -261,41 +262,34 @@ } } -var validationSetAssertionForMonitor = assertstate.ValidationSetAssertionForMonitor +var assertstateMonitorValidationSet = assertstate.MonitorValidationSet +var assertstateEnforceValidationSet = assertstate.EnforceValidationSet // updateValidationSet handles snap validate --monitor and --enforce accountId/name[=sequence]. func updateValidationSet(st *state.State, accountID, name string, reqMode string, sequence int, user *auth.UserState) Response { var mode assertstate.ValidationSetMode - // TODO: only monitor mode for now, add enforce. switch reqMode { case "monitor": mode = assertstate.Monitor + case "enforce": + mode = assertstate.Enforce default: return BadRequest("invalid mode %q", reqMode) } - tr := assertstate.ValidationSetTracking{ - AccountID: accountID, - Name: name, - Mode: mode, - // note, Sequence may be 0, meaning not pinned. - PinnedAt: sequence, - } - userID := 0 if user != nil { userID = user.ID } - pinned := sequence > 0 - opts := assertstate.ResolveOptions{AllowLocalFallback: true} - as, local, err := validationSetAssertionForMonitor(st, accountID, name, sequence, pinned, userID, &opts) + + if mode == assertstate.Enforce { + return enforceValidationSet(st, accountID, name, sequence, userID) + } + + err := assertstateMonitorValidationSet(st, accountID, name, sequence, userID) if err != nil { return BadRequest("cannot get validation set assertion for %v: %v", assertstate.ValidationSetKey(accountID, name), err) } - tr.Current = as.Sequence() - tr.LocalOnly = local - - assertstate.UpdateValidationSet(st, &tr) return SyncResponse(nil) } @@ -311,7 +305,9 @@ if err != nil { return InternalError("accessing validation sets failed: %v", err) } - assertstate.DeleteValidationSet(st, accountID, name) + if err := assertstate.ForgetValidationSet(st, accountID, name); err != nil { + return BadRequest("cannot forget validation set for %v: %v", assertstate.ValidationSetKey(accountID, name), err) + } return SyncResponse(nil) } @@ -339,6 +335,7 @@ if err != nil { return nil, err } + vset := as.(*asserts.ValidationSet) return vset, nil } @@ -361,12 +358,12 @@ if err := sets.Add(vset); err != nil { return InternalError(err.Error()) } - snaps, err := snapstate.InstalledSnaps(st) + snaps, _, err := snapstate.InstalledSnaps(st) if err != nil { return InternalError(err.Error()) } - validErr := checkInstalledSnaps(sets, snaps) + validErr := checkInstalledSnaps(sets, snaps, nil) res := validationSetResult{ AccountID: vset.AccountID(), Name: vset.Name(), @@ -395,3 +392,17 @@ return as, nil } + +func enforceValidationSet(st *state.State, accountID, name string, sequence, userID int) Response { + snaps, ignoreValidation, err := snapstate.InstalledSnaps(st) + if err != nil { + return InternalError(err.Error()) + } + if err := assertstateEnforceValidationSet(st, accountID, name, sequence, userID, snaps, ignoreValidation); err != nil { + // XXX: provide more specific error kinds? This would probably require + // assertstate.ValidationSetAssertionForEnforce tuning too. + return BadRequest("cannot enforce validation set: %v", err) + } + + return SyncResponse(nil) +} diff -Nru snapd-2.53+21.10ubuntu1/daemon/api_validate_test.go snapd-2.54.2+21.10/daemon/api_validate_test.go --- snapd-2.53+21.10ubuntu1/daemon/api_validate_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/api_validate_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -37,9 +37,9 @@ "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" - "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/naming" + "github.com/snapcore/snapd/testutil" ) var _ = check.Suite(&apiValidationSetsSuite{}) @@ -365,7 +365,7 @@ return as, nil } - restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap) error { + restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { c.Assert(vsets, check.NotNil) sort.Sort(byName(snaps)) c.Assert(snaps, check.DeepEquals, []*snapasserts.InstalledSnap{ @@ -378,6 +378,7 @@ Revision: snap.R(4), }, }) + c.Assert(ignoreValidation, check.IsNil) // nil indicates successful validation return nil }) @@ -420,7 +421,7 @@ c.Assert(err, check.IsNil) return as, nil } - restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap) error { + restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { return &snapasserts.ValidationSetsValidationError{} }) defer restore() @@ -491,7 +492,7 @@ return as, nil } - restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap) error { + restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { c.Assert(vsets, check.NotNil) sort.Sort(byName(snaps)) c.Assert(snaps, check.DeepEquals, []*snapasserts.InstalledSnap{ @@ -500,6 +501,7 @@ Revision: snap.R(33), }, }) + c.Assert(ignoreValidation, check.IsNil) // nil indicates successful validation return nil }) @@ -540,7 +542,7 @@ Type: assertType, } } - restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap) error { + restore := daemon.MockCheckInstalledSnaps(func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { // nil indicates successful validation return nil }) @@ -597,196 +599,28 @@ } func (s *apiValidationSetsSuite) TestApplyValidationSetMonitorModePinnedLocalOnly(c *check.C) { - restore := daemon.MockValidationSetAssertionForMonitor(func(st *state.State, accountID, name string, sequence int, pinned bool, userID int, opts *assertstate.ResolveOptions) (*asserts.ValidationSet, bool, error) { + var called int + restore := daemon.MockAssertstateMonitorValidationSet(func(st *state.State, accountID, name string, sequence, userID int) error { c.Assert(accountID, check.Equals, s.dev1acct.AccountID()) c.Assert(name, check.Equals, "bar") c.Assert(sequence, check.Equals, 99) - c.Assert(pinned, check.Equals, true) - c.Assert(opts, check.NotNil) - c.Check(opts.AllowLocalFallback, check.Equals, true) - - db := assertstate.DB(st) - headers, err := asserts.HeadersFromPrimaryKey(asserts.ValidationSetType, []string{release.Series, accountID, name, fmt.Sprintf("%d", sequence)}) - c.Assert(err, check.IsNil) - // validation set assertion available locally - vs, err := db.Find(asserts.ValidationSetType, headers) - c.Assert(err, check.IsNil) - return vs.(*asserts.ValidationSet), true, nil - }) - defer restore() - - st := s.d.Overlord().State() - - st.Lock() - vs := s.mockAssert(c, "bar", "99") - // add validation set assertion to the local db - assertstatetest.AddMany(st, s.dev1acct, s.acct1Key, vs) - st.Unlock() - - body := `{"action":"apply","mode":"monitor", "sequence":99}` - req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body)) - c.Assert(err, check.IsNil) - - rsp := s.syncReq(c, req, nil) - c.Assert(rsp.Status, check.Equals, 200) - - var tr assertstate.ValidationSetTracking - - // verify tracking information - st.Lock() - err = assertstate.GetValidationSet(st, s.dev1acct.AccountID(), "bar", &tr) - st.Unlock() - c.Assert(err, check.IsNil) - c.Check(tr, check.DeepEquals, assertstate.ValidationSetTracking{ - Mode: assertstate.Monitor, - AccountID: s.dev1acct.AccountID(), - Name: "bar", - PinnedAt: 99, - Current: 99, - LocalOnly: true, - }) -} - -func (s *apiValidationSetsSuite) TestApplyValidationSetMonitorModePinnedUnresolved(c *check.C) { - restore := daemon.MockValidationSetAssertionForMonitor(func(st *state.State, accountID, name string, sequence int, pinned bool, userID int, opts *assertstate.ResolveOptions) (*asserts.ValidationSet, bool, error) { - c.Assert(accountID, check.Equals, s.dev1acct.AccountID()) - c.Assert(name, check.Equals, "bar") - c.Assert(sequence, check.Equals, 99) - c.Assert(pinned, check.Equals, true) - - snaps := []interface{}{map[string]interface{}{ - "id": "yOqKhntON3vR7kwEbVPsILm7bUViPDzz", - "name": "snap-b", - "presence": "required", - "revision": "1", - }} - headers := map[string]interface{}{ - "authority-id": s.dev1acct.AccountID(), - "account-id": s.dev1acct.AccountID(), - "name": "bar", - "series": "16", - "sequence": "99", - "revision": "5", - "timestamp": "2030-11-06T09:16:26Z", - "snaps": snaps, - } - // validation set assertion coming from the store - vs, err := s.dev1Signing.Sign(asserts.ValidationSetType, headers, nil, "") - c.Assert(err, check.IsNil) - return vs.(*asserts.ValidationSet), false, nil + called++ + return nil }) defer restore() - st := s.d.Overlord().State() - - st.Lock() - assertstatetest.AddMany(st, s.dev1acct, s.acct1Key) - st.Unlock() - body := `{"action":"apply","mode":"monitor", "sequence":99}` req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body)) c.Assert(err, check.IsNil) rsp := s.syncReq(c, req, nil) c.Assert(rsp.Status, check.Equals, 200) - - var tr assertstate.ValidationSetTracking - - // verify tracking information - st.Lock() - err = assertstate.GetValidationSet(st, s.dev1acct.AccountID(), "bar", &tr) - st.Unlock() - c.Assert(err, check.IsNil) - c.Check(tr, check.DeepEquals, assertstate.ValidationSetTracking{ - Mode: assertstate.Monitor, - AccountID: s.dev1acct.AccountID(), - Name: "bar", - PinnedAt: 99, - Current: 99, - }) -} - -func (s *apiValidationSetsSuite) TestApplyValidationSetMonitorModeUnpinnedRefreshed(c *check.C) { - snaps := []interface{}{map[string]interface{}{ - "id": "yOqKhntON3vR7kwEbVPsILm7bUViPDzz", - "name": "snap-b", - "presence": "required", - "revision": "1", - }} - - restore := daemon.MockValidationSetAssertionForMonitor(func(st *state.State, accountID, name string, sequence int, pinned bool, userID int, opts *assertstate.ResolveOptions) (*asserts.ValidationSet, bool, error) { - c.Assert(accountID, check.Equals, s.dev1acct.AccountID()) - c.Assert(name, check.Equals, "bar") - c.Assert(sequence, check.Equals, 0) - c.Assert(pinned, check.Equals, false) - - // new sequence - headers := map[string]interface{}{ - "authority-id": s.dev1acct.AccountID(), - "account-id": s.dev1acct.AccountID(), - "name": "bar", - "series": "16", - "sequence": "2", - "revision": "1", - "timestamp": "2030-11-06T09:16:26Z", - "snaps": snaps, - } - // updated validation set assertion coming from the store - vs, err := s.dev1Signing.Sign(asserts.ValidationSetType, headers, nil, "") - c.Assert(err, check.IsNil) - return vs.(*asserts.ValidationSet), false, nil - }) - defer restore() - - st := s.d.Overlord().State() - - st.Lock() - assertstatetest.AddMany(st, s.dev1acct, s.acct1Key) - st.Unlock() - - headers := map[string]interface{}{ - "authority-id": s.dev1acct.AccountID(), - "account-id": s.dev1acct.AccountID(), - "name": "bar", - "series": "16", - "sequence": "1", - "revision": "1", - "timestamp": "2030-11-06T09:16:26Z", - "snaps": snaps, - } - vs, err := s.dev1Signing.Sign(asserts.ValidationSetType, headers, nil, "") - c.Assert(err, check.IsNil) - - st.Lock() - // add validation set assertion to the local db - c.Assert(assertstate.Add(st, vs), check.IsNil) - st.Unlock() - - body := `{"action":"apply","mode":"monitor"}` - req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body)) - c.Assert(err, check.IsNil) - - rsp := s.syncReq(c, req, nil) - c.Assert(rsp.Status, check.Equals, 200) - - var tr assertstate.ValidationSetTracking - - // verify tracking information - st.Lock() - err = assertstate.GetValidationSet(st, s.dev1acct.AccountID(), "bar", &tr) - st.Unlock() - c.Assert(err, check.IsNil) - c.Check(tr, check.DeepEquals, assertstate.ValidationSetTracking{ - Mode: assertstate.Monitor, - AccountID: s.dev1acct.AccountID(), - Name: "bar", - Current: 2, - }) + c.Check(called, check.Equals, 1) } func (s *apiValidationSetsSuite) TestApplyValidationSetMonitorModeError(c *check.C) { - restore := daemon.MockValidationSetAssertionForMonitor(func(st *state.State, accountID, name string, sequence int, pinned bool, userID int, opts *assertstate.ResolveOptions) (*asserts.ValidationSet, bool, error) { - return nil, false, fmt.Errorf("boom") + restore := daemon.MockAssertstateMonitorValidationSet(func(st *state.State, accountID, name string, sequence, userID int) error { + return fmt.Errorf("boom") }) defer restore() @@ -887,13 +721,6 @@ message: `invalid mode "bad"`, status: 400, }, - // XXX: enable when enforcing is implemented. - { - validationSet: "foo/bar", - mode: "enforce", - message: `invalid mode "enforce"`, - status: 400, - }, { validationSet: "foo/bar", sequence: "-1", @@ -926,3 +753,142 @@ c.Check(rspe.Status, check.Equals, 400) c.Check(rspe.Message, check.Matches, `unsupported action "baz"`) } + +func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceMode(c *check.C) { + var called int + restore := daemon.MockAssertstateEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + c.Check(ignoreValidation, check.HasLen, 0) + c.Assert(accountID, check.Equals, s.dev1acct.AccountID()) + c.Assert(name, check.Equals, "bar") + c.Assert(sequence, check.Equals, 0) + c.Check(userID, check.Equals, 0) + called++ + return nil + }) + defer restore() + + st := s.d.Overlord().State() + st.Lock() + defer st.Unlock() + + snapstate.Set(st, "snap-b", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(1), SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}, + Current: snap.R(1), + }) + + assertstatetest.AddMany(st, s.dev1acct, s.acct1Key) + + st.Unlock() + defer st.Lock() + body := `{"action":"apply","mode":"enforce"}` + req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body)) + c.Assert(err, check.IsNil) + + rsp := s.syncReq(c, req, nil) + c.Assert(rsp.Status, check.Equals, 200) + c.Check(called, check.Equals, 1) +} + +func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceModeIgnoreValidationOK(c *check.C) { + var called int + restore := daemon.MockAssertstateEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + c.Check(ignoreValidation, check.DeepEquals, map[string]bool{"snap-b": true}) + c.Check(snaps, testutil.DeepUnsortedMatches, []*snapasserts.InstalledSnap{ + snapasserts.NewInstalledSnap("snap-b", "yOqKhntON3vR7kwEbVPsILm7bUViPDzz", snap.R("1"))}) + c.Assert(accountID, check.Equals, s.dev1acct.AccountID()) + c.Assert(name, check.Equals, "bar") + c.Assert(sequence, check.Equals, 0) + c.Check(userID, check.Equals, 0) + called++ + return nil + }) + defer restore() + + st := s.d.Overlord().State() + st.Lock() + defer st.Unlock() + + snapstate.Set(st, "snap-b", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(1), SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}, + Current: snap.R(1), + Flags: snapstate.Flags{IgnoreValidation: true}, + }) + + assertstatetest.AddMany(st, s.dev1acct, s.acct1Key) + + st.Unlock() + defer st.Lock() + body := `{"action":"apply","mode":"enforce"}` + req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body)) + c.Assert(err, check.IsNil) + + rsp := s.syncReq(c, req, nil) + c.Assert(rsp.Status, check.Equals, 200) + c.Check(called, check.Equals, 1) +} + +func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceModeSpecificSequence(c *check.C) { + var called int + restore := daemon.MockAssertstateEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + c.Assert(accountID, check.Equals, s.dev1acct.AccountID()) + c.Assert(name, check.Equals, "bar") + c.Assert(sequence, check.Equals, 5) + c.Check(userID, check.Equals, 0) + called++ + return nil + }) + defer restore() + + st := s.d.Overlord().State() + st.Lock() + defer st.Unlock() + + snapstate.Set(st, "snap-b", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(1), SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}, + Current: snap.R(1), + }) + + assertstatetest.AddMany(st, s.dev1acct, s.acct1Key) + + st.Unlock() + defer st.Lock() + body := `{"action":"apply","mode":"enforce","sequence":5}` + req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body)) + c.Assert(err, check.IsNil) + + rsp := s.syncReq(c, req, nil) + c.Assert(rsp.Status, check.Equals, 200) + c.Check(called, check.Equals, 1) +} + +func (s *apiValidationSetsSuite) TestApplyValidationSetEnforceModeError(c *check.C) { + restore := daemon.MockAssertstateEnforceValidationSet(func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + return fmt.Errorf("boom") + }) + defer restore() + + st := s.d.Overlord().State() + st.Lock() + defer st.Unlock() + + snapstate.Set(st, "snap-b", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{{RealName: "snap-b", Revision: snap.R(1), SnapID: "yOqKhntON3vR7kwEbVPsILm7bUViPDzz"}}, + Current: snap.R(1), + }) + + assertstatetest.AddMany(st, s.dev1acct, s.acct1Key) + + st.Unlock() + defer st.Lock() + body := `{"action":"apply","mode":"enforce"}` + req, err := http.NewRequest("POST", fmt.Sprintf("/v2/validation-sets/%s/bar", s.dev1acct.AccountID()), strings.NewReader(body)) + c.Assert(err, check.IsNil) + + rspe := s.errorReq(c, req, nil) + c.Assert(rspe.Status, check.Equals, 400) + c.Check(string(rspe.Message), check.Equals, "cannot enforce validation set: boom") +} diff -Nru snapd-2.53+21.10ubuntu1/daemon/daemon.go snapd-2.54.2+21.10/daemon/daemon.go --- snapd-2.53+21.10ubuntu1/daemon/daemon.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/daemon.go 2022-01-06 21:25:16.000000000 +0000 @@ -23,6 +23,7 @@ "bytes" "context" "encoding/json" + "errors" "fmt" "net" "net/http" @@ -43,6 +44,7 @@ "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord" "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/standby" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snapdenv" @@ -76,7 +78,7 @@ standbyOpinions *standby.StandbyOpinions // set to what kind of restart was requested if any - requestedRestart state.RestartType + requestedRestart restart.RestartType // set to remember that we need to exit the daemon in a way that // prevents systemd from restarting it restartSocket bool @@ -160,7 +162,9 @@ if srsp, ok := rsp.(StructuredResponse); ok { rjson := srsp.JSON() - _, rst := st.Restarting() + st.Lock() + _, rst := restart.Pending(st) + st.Unlock() rjson.addMaintenanceFromRestartType(rst) if rjson.Type != ResponseTypeError { @@ -351,8 +355,8 @@ // before serving actual connections remove the maintenance.json file as we // are no longer down for maintenance, this state most closely corresponds - // to state.RestartUnset - if err := d.updateMaintenanceFile(state.RestartUnset); err != nil { + // to restart.RestartUnset + if err := d.updateMaintenanceFile(restart.RestartUnset); err != nil { return err } @@ -383,7 +387,7 @@ } // HandleRestart implements overlord.RestartBehavior. -func (d *Daemon) HandleRestart(t state.RestartType) { +func (d *Daemon) HandleRestart(t restart.RestartType) { d.mu.Lock() defer d.mu.Unlock() @@ -395,27 +399,27 @@ // die when asked to restart (systemd should get us back up!) etc switch t { - case state.RestartDaemon: + case restart.RestartDaemon: // save the restart kind to write out a maintenance.json in a bit d.requestedRestart = t - case state.RestartSystem, state.RestartSystemNow: + case restart.RestartSystem, restart.RestartSystemNow: // try to schedule a fallback slow reboot already here // in case we get stuck shutting down // save the restart kind to write out a maintenance.json in a bit scheduleFallback(rebootReboot) d.requestedRestart = t - case state.RestartSystemHaltNow: + case restart.RestartSystemHaltNow: scheduleFallback(rebootHalt) d.requestedRestart = t - case state.RestartSystemPoweroffNow: + case restart.RestartSystemPoweroffNow: scheduleFallback(rebootPoweroff) d.requestedRestart = t - case state.RestartSocket: + case restart.RestartSocket: // save the restart kind to write out a maintenance.json in a bit d.requestedRestart = t d.restartSocket = true - case state.StopDaemon: + case restart.StopDaemon: logger.Noticef("stopping snapd as requested") default: logger.Noticef("internal error: restart handler called with unknown restart type: %v", t) @@ -431,9 +435,9 @@ rebootMaxTentatives = 3 ) -func (d *Daemon) updateMaintenanceFile(rst state.RestartType) error { +func (d *Daemon) updateMaintenanceFile(rst restart.RestartType) error { // for unset restart, just remove the maintenance.json file - if rst == state.RestartUnset { + if rst == restart.RestartUnset { err := os.Remove(dirs.SnapdMaintenanceFile) // only return err if the error was something other than the file not // existing @@ -458,7 +462,7 @@ if d.expectedRebootDidNotHappen { // make the reboot retry immediate immediateReboot := true - return d.doReboot(sigCh, state.RestartSystem, immediateReboot, rebootRetryWaitTimeout) + return d.doReboot(sigCh, restart.RestartSystem, immediateReboot, rebootRetryWaitTimeout) } if d.overlord == nil { return fmt.Errorf("internal error: no Overlord") @@ -473,12 +477,12 @@ // shutdown or not as a consequence of this request needsFullShutdown := false switch d.requestedRestart { - case state.RestartSystem, state.RestartSystemNow, state.RestartSystemHaltNow, state.RestartSystemPoweroffNow: + case restart.RestartSystem, restart.RestartSystemNow, restart.RestartSystemHaltNow, restart.RestartSystemPoweroffNow: needsFullShutdown = true } immediateShutdown := false switch d.requestedRestart { - case state.RestartSystemNow, state.RestartSystemHaltNow, state.RestartSystemPoweroffNow: + case restart.RestartSystemNow, restart.RestartSystemHaltNow, restart.RestartSystemPoweroffNow: immediateShutdown = true } restartSocket := d.restartSocket @@ -498,7 +502,10 @@ // stop running hooks first // and do it more gracefully if we are restarting hookMgr := d.overlord.HookManager() - if ok, _ := d.state.Restarting(); ok { + d.state.Lock() + ok, _ := restart.Pending(d.state) + d.state.Unlock() + if ok { logger.Noticef("gracefully waiting for running hooks") hookMgr.GracefullyWaitRunningHooks() logger.Noticef("done waiting for running hooks") @@ -599,16 +606,16 @@ return rebootDelay, nil } -func (d *Daemon) doReboot(sigCh chan<- os.Signal, rst state.RestartType, immediate bool, waitTimeout time.Duration) error { +func (d *Daemon) doReboot(sigCh chan<- os.Signal, rst restart.RestartType, immediate bool, waitTimeout time.Duration) error { rebootDelay, err := d.rebootDelay(immediate) if err != nil { return err } action := rebootReboot switch rst { - case state.RestartSystemHaltNow: + case restart.RestartSystemHaltNow: action = rebootHalt - case state.RestartSystemPoweroffNow: + case restart.RestartSystemPoweroffNow: action = rebootPoweroff } // ask for shutdown and wait for it to happen. @@ -701,6 +708,8 @@ return nil } +var errExpectedReboot = errors.New("expected reboot did not happen") + // RebootDidNotHappen implements part of overlord.RestartBehavior. func (d *Daemon) RebootDidNotHappen(st *state.State) error { var nTentative int @@ -712,7 +721,7 @@ if nTentative > rebootMaxTentatives { // giving up, proceed normally, some in-progress refresh // might get rolled back!! - st.ClearReboot() + restart.ClearReboot(st) clearReboot(st) logger.Noticef("snapd was restarted while a system restart was expected, snapd retried to schedule and waited again for a system restart %d times and is giving up", rebootMaxTentatives) return nil @@ -720,14 +729,14 @@ st.Set("daemon-system-restart-tentative", nTentative) d.state = st logger.Noticef("snapd was restarted while a system restart was expected, snapd will try to schedule and wait for a system restart again (tenative %d/%d)", nTentative, rebootMaxTentatives) - return state.ErrExpectedReboot + return errExpectedReboot } // New Daemon func New() (*Daemon, error) { d := &Daemon{} ovld, err := overlord.New(d) - if err == state.ErrExpectedReboot { + if err == errExpectedReboot { // we proceed without overlord until we reach Stop // where we will schedule and wait again for a system restart. // ATM we cannot do that in New because we need to satisfy diff -Nru snapd-2.53+21.10ubuntu1/daemon/daemon_test.go snapd-2.54.2+21.10/daemon/daemon_test.go --- snapd-2.53+21.10ubuntu1/daemon/daemon_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/daemon_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -44,6 +44,7 @@ "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" "github.com/snapcore/snapd/overlord/ifacestate" "github.com/snapcore/snapd/overlord/patch" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/standby" "github.com/snapcore/snapd/overlord/state" @@ -233,44 +234,47 @@ c.Check(rst.Maintenance, check.IsNil) tests := []struct { - rst state.RestartType + rst restart.RestartType kind client.ErrorKind msg string op string }{ { - rst: state.RestartSystem, + rst: restart.RestartSystem, kind: client.ErrorKindSystemRestart, msg: "system is restarting", op: "reboot", }, { - rst: state.RestartSystemNow, + rst: restart.RestartSystemNow, kind: client.ErrorKindSystemRestart, msg: "system is restarting", op: "reboot", }, { - rst: state.RestartDaemon, + rst: restart.RestartDaemon, kind: client.ErrorKindDaemonRestart, msg: "daemon is restarting", }, { - rst: state.RestartSystemHaltNow, + rst: restart.RestartSystemHaltNow, kind: client.ErrorKindSystemRestart, msg: "system is halting", op: "halt", }, { - rst: state.RestartSystemPoweroffNow, + rst: restart.RestartSystemPoweroffNow, kind: client.ErrorKindSystemRestart, msg: "system is powering off", op: "poweroff", }, { - rst: state.RestartSocket, + rst: restart.RestartSocket, kind: client.ErrorKindDaemonRestart, msg: "daemon is stopping to wait for socket activation", }, } for _, t := range tests { - state.MockRestarting(d.overlord.State(), t.rst) + st := d.overlord.State() + st.Lock() + restart.MockPending(st, t.rst) + st.Unlock() rec = httptest.NewRecorder() cmd.ServeHTTP(rec, req) c.Check(rec.Code, check.Equals, 200) @@ -693,12 +697,15 @@ <-snapdDone <-snapDone - d.overlord.State().RequestRestart(state.RestartDaemon) + st := d.overlord.State() + st.Lock() + restart.Request(st, restart.RestartDaemon) + st.Unlock() select { case <-d.Dying(): case <-time.After(2 * time.Second): - c.Fatal("RequestRestart -> overlord -> Kill chain didn't work") + c.Fatal("restart.Request -> daemon -> Kill chain didn't work") } d.Stop(nil) @@ -897,7 +904,7 @@ } } -func (s *daemonSuite) testRestartSystemWiring(c *check.C, prep func(d *Daemon), restart func(*state.State, state.RestartType), restartKind state.RestartType, wait time.Duration) { +func (s *daemonSuite) testRestartSystemWiring(c *check.C, prep func(d *Daemon), doRestart func(*state.State, restart.RestartType), restartKind restart.RestartType, wait time.Duration) { d := newTestDaemon(c) // mark as already seeded s.markSeeded(d) @@ -927,10 +934,10 @@ expectedAction := rebootReboot expectedOp := "reboot" - if restartKind == state.RestartSystemHaltNow { + if restartKind == restart.RestartSystemHaltNow { expectedAction = rebootHalt expectedOp = "halt" - } else if restartKind == state.RestartSystemPoweroffNow { + } else if restartKind == restart.RestartSystemPoweroffNow { expectedAction = rebootPoweroff expectedOp = "poweroff" } @@ -970,19 +977,19 @@ <-snapDone st.Lock() - restart(st, restartKind) + doRestart(st, restartKind) st.Unlock() defer func() { d.mu.Lock() - d.requestedRestart = state.RestartUnset + d.requestedRestart = restart.RestartUnset d.mu.Unlock() }() select { case <-d.Dying(): case <-time.After(2 * time.Second): - c.Fatal("RequestRestart -> overlord -> Kill chain didn't work") + c.Fatal("restart.Request -> daemon -> Kill chain didn't work") } d.mu.Lock() @@ -1036,19 +1043,19 @@ } func (s *daemonSuite) TestRestartSystemGracefulWiring(c *check.C) { - s.testRestartSystemWiring(c, nil, (*state.State).RequestRestart, state.RestartSystem, 1*time.Minute) + s.testRestartSystemWiring(c, nil, restart.Request, restart.RestartSystem, 1*time.Minute) } func (s *daemonSuite) TestRestartSystemImmediateWiring(c *check.C) { - s.testRestartSystemWiring(c, nil, (*state.State).RequestRestart, state.RestartSystemNow, 0) + s.testRestartSystemWiring(c, nil, restart.Request, restart.RestartSystemNow, 0) } func (s *daemonSuite) TestRestartSystemHaltImmediateWiring(c *check.C) { - s.testRestartSystemWiring(c, nil, (*state.State).RequestRestart, state.RestartSystemHaltNow, 0) + s.testRestartSystemWiring(c, nil, restart.Request, restart.RestartSystemHaltNow, 0) } func (s *daemonSuite) TestRestartSystemPoweroffImmediateWiring(c *check.C) { - s.testRestartSystemWiring(c, nil, (*state.State).RequestRestart, state.RestartSystemPoweroffNow, 0) + s.testRestartSystemWiring(c, nil, restart.Request, restart.RestartSystemPoweroffNow, 0) } type rstManager struct { @@ -1058,7 +1065,7 @@ func (m *rstManager) Ensure() error { m.st.Lock() defer m.st.Unlock() - m.st.RequestRestart(state.RestartSystemNow) + restart.Request(m.st, restart.RestartSystemNow) return nil } @@ -1072,14 +1079,14 @@ } func (s *daemonSuite) TestRestartSystemFromEnsure(c *check.C) { - // Test that calling RequestRestart from inside the first + // Test that calling restart.Request from inside the first // Ensure loop works. wm := &witnessManager{} prep := func(d *Daemon) { st := d.overlord.State() hm := d.overlord.HookManager() - o := overlord.MockWithStateAndRestartHandler(st, d.HandleRestart) + o := overlord.MockWithState(st) d.overlord = o o.AddManager(hm) rm := &rstManager{st: st} @@ -1087,9 +1094,9 @@ o.AddManager(wm) } - nop := func(*state.State, state.RestartType) {} + nop := func(*state.State, restart.RestartType) {} - s.testRestartSystemWiring(c, prep, nop, state.RestartSystemNow, 0) + s.testRestartSystemWiring(c, prep, nop, restart.RestartSystemNow, 0) c.Check(wm.ensureCalled, check.Equals, 1) } @@ -1168,7 +1175,7 @@ st := d.overlord.State() st.Lock() - st.RequestRestart(state.RestartSystem) + restart.Request(st, restart.RestartSystem) st.Unlock() ch := make(chan os.Signal, 2) @@ -1201,7 +1208,7 @@ st := d.overlord.State() st.Lock() - st.RequestRestart(state.RestartSystem) + restart.Request(st, restart.RestartSystem) st.Unlock() sigCh := make(chan os.Signal, 2) diff -Nru snapd-2.53+21.10ubuntu1/daemon/export_api_model_test.go snapd-2.54.2+21.10/daemon/export_api_model_test.go --- snapd-2.53+21.10ubuntu1/daemon/export_api_model_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/export_api_model_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2020 Canonical Ltd + * Copyright (C) 2021 Canonical Ltd * * This 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,6 +21,7 @@ import ( "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/state" ) @@ -32,6 +33,14 @@ } } +func MockDevicestateDeviceManagerUnregister(mock func(*devicestate.DeviceManager, *devicestate.UnregisterOptions) error) (restore func()) { + oldDevicestateDeviceManagerUnregister := devicestateDeviceManagerUnregister + devicestateDeviceManagerUnregister = mock + return func() { + devicestateDeviceManagerUnregister = oldDevicestateDeviceManagerUnregister + } +} + type ( PostModelData = postModelData ModelAssertJSON = modelAssertJSON diff -Nru snapd-2.53+21.10ubuntu1/daemon/export_api_snaps_test.go snapd-2.54.2+21.10/daemon/export_api_snaps_test.go --- snapd-2.53+21.10ubuntu1/daemon/export_api_snaps_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/export_api_snaps_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -21,6 +21,7 @@ import ( "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" ) @@ -31,3 +32,11 @@ var ( MapLocal = mapLocal ) + +func MockAssertstateRestoreValidationSetsTracking(f func(*state.State) error) (restore func()) { + old := assertstateRestoreValidationSetsTracking + assertstateRestoreValidationSetsTracking = f + return func() { + assertstateRestoreValidationSetsTracking = old + } +} diff -Nru snapd-2.53+21.10ubuntu1/daemon/export_api_validate_test.go snapd-2.54.2+21.10/daemon/export_api_validate_test.go --- snapd-2.53+21.10ubuntu1/daemon/export_api_validate_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/export_api_validate_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -20,9 +20,7 @@ package daemon import ( - "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/snapasserts" - "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/state" ) @@ -30,7 +28,7 @@ ValidationSetResult = validationSetResult ) -func MockCheckInstalledSnaps(f func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap) error) func() { +func MockCheckInstalledSnaps(f func(vsets *snapasserts.ValidationSets, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error) func() { old := checkInstalledSnaps checkInstalledSnaps = f return func() { @@ -38,10 +36,18 @@ } } -func MockValidationSetAssertionForMonitor(f func(st *state.State, accountID, name string, sequence int, pinned bool, userID int, opts *assertstate.ResolveOptions) (*asserts.ValidationSet, bool, error)) func() { - old := validationSetAssertionForMonitor - validationSetAssertionForMonitor = f +func MockAssertstateMonitorValidationSet(f func(st *state.State, accountID, name string, sequence int, userID int) error) func() { + old := assertstateMonitorValidationSet + assertstateMonitorValidationSet = f return func() { - validationSetAssertionForMonitor = old + assertstateMonitorValidationSet = old + } +} + +func MockAssertstateEnforceValidationSet(f func(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error) func() { + old := assertstateEnforceValidationSet + assertstateEnforceValidationSet = f + return func() { + assertstateEnforceValidationSet = old } } diff -Nru snapd-2.53+21.10ubuntu1/daemon/export_test.go snapd-2.54.2+21.10/daemon/export_test.go --- snapd-2.53+21.10ubuntu1/daemon/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -28,6 +28,7 @@ "github.com/snapcore/snapd/overlord" "github.com/snapcore/snapd/overlord/assertstate" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" @@ -60,7 +61,7 @@ return d.overlord } -func (d *Daemon) RequestedRestart() state.RestartType { +func (d *Daemon) RequestedRestart() restart.RestartType { return d.requestedRestart } @@ -194,6 +195,14 @@ } } +func MockSnapstateInstallPathMany(f func(context.Context, *state.State, []*snap.SideInfo, []string, int, *snapstate.Flags) ([]*state.TaskSet, error)) func() { + old := snapstateInstallPathMany + snapstateInstallPathMany = f + return func() { + snapstateInstallPathMany = old + } +} + type ( RespJSON = respJSON FileResponse = fileResponse @@ -228,4 +237,6 @@ MakeErrorResponder = makeErrorResponder ErrToResponse = errToResponse + + MaxReadBuflen = maxReadBuflen ) diff -Nru snapd-2.53+21.10ubuntu1/daemon/response.go snapd-2.54.2+21.10/daemon/response.go --- snapd-2.53+21.10ubuntu1/daemon/response.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/response.go 2022-01-06 21:25:16.000000000 +0000 @@ -33,6 +33,7 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapshotstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" @@ -88,42 +89,42 @@ return r } -func maintenanceForRestartType(rst state.RestartType) *errorResult { +func maintenanceForRestartType(rst restart.RestartType) *errorResult { e := &errorResult{} switch rst { - case state.RestartSystem, state.RestartSystemNow: + case restart.RestartSystem, restart.RestartSystemNow: e.Kind = client.ErrorKindSystemRestart e.Message = systemRestartMsg e.Value = map[string]interface{}{ "op": "reboot", } - case state.RestartSystemHaltNow: + case restart.RestartSystemHaltNow: e.Kind = client.ErrorKindSystemRestart e.Message = systemHaltMsg e.Value = map[string]interface{}{ "op": "halt", } - case state.RestartSystemPoweroffNow: + case restart.RestartSystemPoweroffNow: e.Kind = client.ErrorKindSystemRestart e.Message = systemPoweroffMsg e.Value = map[string]interface{}{ "op": "poweroff", } - case state.RestartDaemon: + case restart.RestartDaemon: e.Kind = client.ErrorKindDaemonRestart e.Message = daemonRestartMsg - case state.RestartSocket: + case restart.RestartSocket: e.Kind = client.ErrorKindDaemonRestart e.Message = socketRestartMsg - case state.RestartUnset: + case restart.RestartUnset: // shouldn't happen, maintenance for unset type should just be nil panic("internal error: cannot marshal maintenance for RestartUnset") } return e } -func (r *respJSON) addMaintenanceFromRestartType(rst state.RestartType) { - if rst == state.RestartUnset { +func (r *respJSON) addMaintenanceFromRestartType(rst restart.RestartType) { + if rst == restart.RestartUnset { // nothing to do return } diff -Nru snapd-2.53+21.10ubuntu1/daemon/ucrednet.go snapd-2.54.2+21.10/daemon/ucrednet.go --- snapd-2.53+21.10ubuntu1/daemon/ucrednet.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/daemon/ucrednet.go 2022-01-06 21:25:16.000000000 +0000 @@ -116,14 +116,18 @@ var unet *ucrednet if ucon, ok := con.(*net.UnixConn); ok { - f, err := ucon.File() + syscallConn, err := ucon.SyscallConn() if err != nil { return nil, err } - // File() is a dup(); needs closing - defer f.Close() - ucred, err := getUcred(int(f.Fd()), sys.SOL_SOCKET, sys.SO_PEERCRED) + var ucred *sys.Ucred + scErr := syscallConn.Control(func(fd uintptr) { + ucred, err = getUcred(int(fd), sys.SOL_SOCKET, sys.SO_PEERCRED) + }) + if scErr != nil { + return nil, scErr + } if err != nil { return nil, err } diff -Nru snapd-2.53+21.10ubuntu1/data/env/Makefile snapd-2.54.2+21.10/data/env/Makefile --- snapd-2.53+21.10ubuntu1/data/env/Makefile 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/data/env/Makefile 2022-01-06 21:25:16.000000000 +0000 @@ -15,22 +15,35 @@ SNAP_MOUNT_DIR := /snap ENVD := /etc/profile.d +DATADIR ?= /usr/share %.sh: %.sh.in sed < $< > $@ \ s:@SNAP_MOUNT_DIR@:${SNAP_MOUNT_DIR}:g -GENERATED = snapd.sh +%.fish: %.fish.in + sed < $< > $@ \ + s:@SNAP_MOUNT_DIR@:${SNAP_MOUNT_DIR}:g +GENERATED_SH = snapd.sh +GENERATED_FISH = snapd.fish +GENERATED = ${GENERATED_SH} ${GENERATED_FISH} all: ${GENERATED} .PHONY: all -install: ${GENERATED} +install-sh: ${GENERATED_SH} # NOTE: old (e.g. 14.04) GNU coreutils doesn't -D with -t install -d -m 0755 ${DESTDIR}/${ENVD} install -m 0644 -t ${DESTDIR}/${ENVD} $^ -.PHONY: install + +# fish uses a separate directory to store vendor configuration files +install-fish: ${GENERATED_FISH} + install -d -m 0755 ${DESTDIR}/${DATADIR}/fish/vendor_conf.d + install -m 0644 -t ${DESTDIR}/${DATADIR}/fish/vendor_conf.d $^ + +install: install-sh install-fish +.PHONY: install install-sh install-fish clean: $(RM) ${GENERATED} diff -Nru snapd-2.53+21.10ubuntu1/data/env/snapd.fish.in snapd-2.54.2+21.10/data/env/snapd.fish.in --- snapd-2.53+21.10ubuntu1/data/env/snapd.fish.in 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/data/env/snapd.fish.in 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,11 @@ +# Expand $PATH to include the directory where snappy applications go. +set -u snap_bin_path "@SNAP_MOUNT_DIR@/bin" +fish_add_path -aP $snap_bin_path + +# Desktop files (used by desktop environments within both X11 and Wayland) are +# looked for in XDG_DATA_DIRS; make sure it includes the relevant directory for +# snappy applications' desktop files. +set -u snap_xdg_path /var/lib/snapd/desktop +if ! contains $snap_xdg_path $XDG_DATA_DIRS + set XDG_DATA_DIRS $XDG_DATA_DIRS $snap_xdg_path +end diff -Nru snapd-2.53+21.10ubuntu1/data/selinux/snappy.te snapd-2.54.2+21.10/data/selinux/snappy.te --- snapd-2.53+21.10ubuntu1/data/selinux/snappy.te 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/data/selinux/snappy.te 2022-01-06 21:25:16.000000000 +0000 @@ -335,6 +335,26 @@ allow snappy_t journalctl_t:process sigkill; ') +# snapd may talk to systemd-timesyncd over dbus +optional_policy(` + gen_require(` + type systemd_timedated_t; + class dbus send_msg; + ') + allow snappy_t systemd_timedated_t:dbus send_msg; + allow systemd_timedated_t snappy_t:dbus send_msg; +') +# or on some systems same dbus API may be provided by timedatex +# RHEL7: there is no timedatex.if +ifndef(`distro_rhel7',` + timedatex_dbus_chat(snappy_t) +') + +# kernel-module-load interface may inspect or write files under /etc/modprobe.d +optional_policy(` + modutils_manage_module_config(snappy_t) +') + # only pops up in cloud images where cloud-init.target is incorrectly labeled allow snappy_t init_var_run_t:lnk_file read; @@ -417,6 +437,10 @@ allow snappy_t snappy_cli_t:process { getpgid sigkill }; allow snappy_t unconfined_service_t:process { getpgid sigkill }; +# Snapd invokes systemd-detect-virt, which may make poke /proc/xen/, but does +# not transition to a separate type and has no interface policy +kernel_read_xen_state(snappy_t) + ######################################## # # snap-update-ns, snap-dicsard-ns local policy @@ -591,6 +615,7 @@ # libudev udev_manage_pid_dirs(snappy_confine_t) +udev_read_db(snappy_confine_t) # basic access to system info in /proc kernel_read_system_state(snappy_confine_t) diff -Nru snapd-2.53+21.10ubuntu1/dbusutil/dbustest/dbustest.go snapd-2.54.2+21.10/dbusutil/dbustest/dbustest.go --- snapd-2.53+21.10ubuntu1/dbusutil/dbustest/dbustest.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/dbusutil/dbustest/dbustest.go 2022-01-06 21:25:16.000000000 +0000 @@ -23,7 +23,7 @@ "bytes" "encoding/binary" "fmt" - "sync" + "sync/atomic" "github.com/godbus/dbus" ) @@ -34,10 +34,10 @@ // DBusHandlerFunc is the type of handler function for interacting with test DBus. // // The handler is called for each message that arrives to the bus from the test -// client. The handler can respond by returning zero or more messages. -// Typically one message is returned (method response or error). Additional -// messages can be returned to represent signals emitted during message -// handling. +// client. The handler can respond by returning zero or more messages. Typically +// one message is returned (method response or error). Additional messages can +// be returned to represent signals emitted during message handling. The counter +// n aids in testing a sequence of messages that is expected. // // The handler is not called for internal messages related to DBus itself. type DBusHandlerFunc func(msg *dbus.Message, n int) ([]*dbus.Message, error) @@ -64,42 +64,41 @@ type testDBusStream struct { handler DBusHandlerFunc - m sync.Mutex - readable sync.Cond + outputBuf bytes.Buffer - outputBuf, inputBuf bytes.Buffer - closed bool - authDone bool - n int + output chan []byte + closeRequest chan struct{} + + closed atomic.Value + authDone bool + n int } -func (s *testDBusStream) decodeRequest() { - // s.m is locked +func (s *testDBusStream) decodeRequest(req []byte) { + buf := bytes.NewBuffer(req) if !s.authDone { // Before authentication is done process the text protocol anticipating // the TEST authentication used by NewDBusTestConn call below. - msg := s.inputBuf.String() - s.inputBuf.Reset() + msg := buf.String() switch msg { case "\x00": // initial NUL byte, ignore case "AUTH\r\n": - s.outputBuf.WriteString("REJECTED TEST\r\n") + s.output <- []byte("REJECTED TEST\r\n") case "AUTH TEST TEST\r\n": - s.outputBuf.WriteString("OK test://\r\n") + s.output <- []byte("OK test://\r\n") case "CANCEL\r\n": - s.outputBuf.WriteString("REJECTED\r\n") + s.output <- []byte("REJECTED\r\n") case "BEGIN\r\n": s.authDone = true default: panic(fmt.Errorf("unrecognized authentication message %q", msg)) } - s.readable.Signal() return } // After authentication the buffer must contain marshaled DBus messages. - msgIn, err := dbus.DecodeMessage(&s.inputBuf) + msgIn, err := dbus.DecodeMessage(buf) if err != nil { panic(fmt.Errorf("cannot decode incoming message: %v", err)) } @@ -149,54 +148,65 @@ func (s *testDBusStream) sendMsg(msg *dbus.Message) { // TODO: handle big endian if we ever get big endian machines again. - if err := msg.EncodeTo(&s.outputBuf, binary.LittleEndian); err != nil { + var buf bytes.Buffer + if err := msg.EncodeTo(&buf, binary.LittleEndian); err != nil { panic(fmt.Errorf("cannot encode outgoing message: %v", err)) } - // s.m is locked - s.readable.Signal() + s.output <- buf.Bytes() } func (s *testDBusStream) Read(p []byte) (n int, err error) { - s.m.Lock() - defer s.m.Unlock() - - // When the buffer is empty block until more data arrives. DBus - // continuously blocks on reading and premature empty read is treated as an - // EOF, terminating the message flow. - if s.outputBuf.Len() == 0 { - s.readable.Wait() - } - - if s.closed { - return 0, fmt.Errorf("stream is closed") + for { + // When the buffer is empty block until more data arrives. DBus + // continuously blocks on reading and premature empty read is treated as an + // EOF, terminating the message flow. + if s.closed.Load().(bool) { + return 0, fmt.Errorf("stream is closed") + } + if s.outputBuf.Len() > 0 { + return s.outputBuf.Read(p) + } + select { + case data := <-s.output: + // just accumulate the data in the output buffer + s.outputBuf.Write(data) + case <-s.closeRequest: + s.closed.Store(true) + } } - return s.outputBuf.Read(p) } func (s *testDBusStream) Write(p []byte) (n int, err error) { - s.m.Lock() - defer s.m.Unlock() - - if s.closed { - return 0, fmt.Errorf("stream is closed") + for { + select { + case <-s.closeRequest: + s.closed.Store(true) + default: + if s.closed.Load().(bool) { + return 0, fmt.Errorf("stream is closed") + } + s.decodeRequest(p) + return len(p), nil + } } - - n, err = s.inputBuf.Write(p) - s.decodeRequest() - return n, err } func (s *testDBusStream) Close() error { - s.m.Lock() - defer s.m.Unlock() - s.closed = true - s.readable.Signal() + s.closeRequest <- struct{}{} return nil } +func (s *testDBusStream) InjectMessage(msg *dbus.Message) { + s.sendMsg(msg) +} + func newTestDBusStream(handler DBusHandlerFunc) *testDBusStream { - s := &testDBusStream{handler: handler} - s.readable.L = &s.m + s := &testDBusStream{ + handler: handler, + output: make(chan []byte, 1), + closeRequest: make(chan struct{}, 1), + } + s.closed.Store(false) return s } @@ -211,23 +221,33 @@ return []byte(""), dbus.AuthOk } -// Connection returns a DBus connection for writing unit tests. -// -// The handler function is called for each message sent to the bus. It can -// return any number of messages to send in response. The counter aids in -// testing a sequence of messages that is expected. -func Connection(handler DBusHandlerFunc) (*dbus.Conn, error) { - conn, err := dbus.NewConn(newTestDBusStream(handler)) +type InjectMessageFunc func(msg *dbus.Message) + +// InjectableConnection returns a DBus connection for writing unit tests and a +// function that can be used to inject messages that will be received by the +// test client. +func InjectableConnection(handler DBusHandlerFunc) (*dbus.Conn, InjectMessageFunc, error) { + testDBusStream := newTestDBusStream(handler) + conn, err := dbus.NewConn(testDBusStream) if err != nil { - return nil, err + return nil, nil, err } if err = conn.Auth([]dbus.Auth{&testAuth{}}); err != nil { _ = conn.Close() - return nil, err + return nil, nil, err } if err = conn.Hello(); err != nil { _ = conn.Close() - return nil, err + return nil, nil, err } - return conn, nil + return conn, testDBusStream.InjectMessage, nil +} + +// Connection returns a DBus connection for writing unit tests. +// +// The handler function is called for each message sent to the bus. It can +// return any number of messages to send in response. +func Connection(handler DBusHandlerFunc) (*dbus.Conn, error) { + conn, _, err := InjectableConnection(handler) + return conn, err } diff -Nru snapd-2.53+21.10ubuntu1/debian/changelog snapd-2.54.2+21.10/debian/changelog --- snapd-2.53+21.10ubuntu1/debian/changelog 2021-10-08 07:29:37.000000000 +0000 +++ snapd-2.54.2+21.10/debian/changelog 2022-01-06 21:25:16.000000000 +0000 @@ -1,13 +1,469 @@ -snapd (2.53+21.10ubuntu1) impish; urgency=medium +snapd (2.54.2+21.10) impish; urgency=medium - * Cherry pick https://github.com/snapcore/snapd/pull/10898 to - fix apparmor profile for snap-confine on s390x,ppc64el,risvc64 - This fixes the snapd.seeded.service failures reported in - LP: #1944004 + * New upstream release, LP: #1955137 + - tests: exclude interfaces-kernel-module load on arm + - tests: ensure that test-snapd-kernel-module-load is + removed + - tests: do not test microk8s-smoke on arm + - tests/core/failover: replace boot-state with snap debug boot-vars + - tests: use snap info|awk to extract tracking channel + - tests: fix remodel-kernel test when running on external devices + - .github/workflows/test.yaml: also check internal snapd version for + cleanliness + - packaging/ubuntu-16.04/rules: eliminate seccomp modification + - bootloader/assets/grub_*cfg_asset.go: update Copyright + - build-aux/snap/snapcraft.yaml: adjust comment about get-version + - .github/workflows/test.yaml: add check in github actions for dirty + snapd snaps + - build-aux/snap/snapcraft.yaml: use build-packages, don't fail + dirty builds + - data/selinux: allow poking /proc/xen - -- Michael Vogt Fri, 08 Oct 2021 09:29:37 +0200 + -- Ian Johnson Thu, 06 Jan 2022 15:25:16 -0600 -snapd (2.53+21.10) impish; urgency=medium +snapd (2.54.1) xenial; urgency=medium + + * New upstream release, LP: #1955137 + - buid-aux: set version before calling ./generate-packaging-dir + This fixes the "dirty" suffix in the auto-generated version + + -- Michael Vogt Mon, 20 Dec 2021 10:06:09 +0100 + +snapd (2.54) xenial; urgency=medium + + * New upstream release, LP: #1955137 + - interfaces/builtin/opengl.go: add boot_vga sys/devices file + - o/configstate/configcore: add tmpfs.size option + - tests: moving to manual opensuse 15.2 + - cmd/snap-device-helper: bring back the device type identification + behavior, but for remove action fallback only + - cmd/snap-failure: use snapd from the snapd snap if core is not + present + - tests/core/failover: enable the test on core18 + - o/devicestate: ensure proper order when remodel does a simple + switch-snap-channel + - builtin/interfaces: add shared memory interface + - overlord: extend kernel/base success and failover with bootenv + checks + - o/snapstate: check disk space w/o store if possible + - snap-bootstrap: Mount snaps read only + - gadget/install: do not re-create partitions using OnDiskVolume + after deletion + - many: fix formatting w/ latest go version + - devicestate,timeutil: improve logging of NTP sync + - tests/main/security-device-cgroups-helper: more debugs + - cmd/snap: print a placeholder for version of broken snaps + - o/snapstate: mock system with classic confinement support + - cmd: Fixup .clangd to use correct syntax + - tests: run spread tests in fedora-35 + - data/selinux: allow snapd to access /etc/modprobe.d + - mount-control: step 2 + - daemon: add multiple snap sideload to API + - tests/lib/pkgdb: install dbus-user-session during prepare, drop + dbus-x11 + - systemd: provide more detailed errors for unimplemented method in + emulation mode + - tests: avoid checking TRUST_TEST_KEYS on restore on remodel-base + test + - tests: retry umounting /var/lib/snapd/seed on uc20 on fsck-on-boot + test + - o/snapstate: add hide/expose snap data to backend + - interfaces: kernel-module-load + - snap: add support for `snap watch + --last={revert,enable,disable,switch}` + - tests/main/security-udev-input-subsystem: drop info from udev + - tests/core/kernel-and-base-single-reboot-failover, + tests/lib/fakestore: verify failover scenario + - tests/main/security-device-cgroups-helper: collect some debug info + when the test fails + - tests/nested/manual/core20-remodel: wait for device to have a + serial before starting a remodel + - tests/main/generic-unregister: test re-registration if not blocked + - o/snapstate, assertsate: validation sets/undo on partial failure + - tests: ensure snapd can be downloaded as a module + - snapdtool, many: support additional key/value flags in info file + - data/env: improve fish shell env setup + - usersession/client: provide a way for client to send messages to a + subset of users + - tests: verify that simultaneous refresh of kernel and base + triggers a single reboot only + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - asserts: change behavior of alternative attribute matcher + - configcore: relax validation rules for hostname + - cmd/snap-confine: do not include libglvnd libraries from the host + system + - overlord, tests: add managers and a spread test for UC20 to UC22 + remodel + - HACKING.md: adjust again for building the snapd snap + - systemd: add support for systemd unit alias names + - o/snapstate: add InstallPathMany + - gadget: allow EnsureLayoutCompatibility to ensure disk has all + laid out structsnow reject/fail: + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider (#11111) + - interfaces/interfaces/scsi_generic: add interface for scsi generic + de… (#10936) + - osutil/disks/mockdisk.go: add MockDevicePathToDiskMapping + - interfaces/microstack-support: set controlsDeviceCgroup to true + - network-setup-control: add netplan generate D-Bus rules + - interface/builtin/log_observe: allow to access /dev/kmsg + - .github/workflows/test.yaml: restore failing of spread tests on + errors (nested) + - gadget: tweaks to DiskStructureDeviceTraits + expand test cases + - tests/lib/nested.sh: allow tests to use their own core18 in extra- + snaps-path + - interfaces/browser-support: Update rules for Edge + - o/devicestate: during remodel first check pending download tasks + for snaps + - polkit: add a package to validate polkit policy files + - HACKING.md: document building the snapd snap and splicing it into + the core snap + - interfaces/udev: fix installing snaps inside lxd in 21.10 + - o/snapstate: refactor disk space checks + - tests: add (strict) microk8s smoke test + - osutil/strace: try to enable strace on more arches + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - tests/main/snapd-reexec-snapd-snap: improve debugging + - daemon: write formdata file parts to snaps dir + - systemd: add support for .target units + - tests: run snap-disconnect on uc16 + - many: add experimental setting to allow using ~/.snap/data instead + of ~/snap + - overlord/snapstate: perform a single reboot when updating boot + base and kernel + - kernel/fde: add DeviceUnlockKernelHookDeviceMapperBackResolver, + use w/ disks pkg + - o/devicestate: introduce DeviceManager.Unregister + - interfaces: allow receiving PropertiesChanged on the mpris plug + - tests: new tool used to retrieve data from mongo db + - daemon: amend ssh keys coming from the store + - tests: Include the tools from snapd-testing-tools project in + "$TESTSTOOLS" + - tests: new workflow step used to report spread error to mongodb + - interfaces/builtin/dsp: update proc files for ambarella flavor + - gadget: replace ondisk implementation with disks package, refactor + part calcs + - tests: Revert "tests: disable flaky uc18 tests until systemd is + fixed" + - Revert: "many: Vendor apparmor-3.0.3 into the snapd snap" + - asserts: rename "white box" to "clear box" (woke checker) + - many: Vendor apparmor-3.0.3 into the snapd snap + - tests: reorganize the debug-each on the spread.yaml + - packaging: sync with downstream packaging in Fedora and openSUSE + - tests: disable flaky uc18 tests until systemd is fixed + - data/env: provide profile setup for fish shell + - tests: use ubuntu-image 1.11 from stable channel + - gadget/gadget.go: include disk schema in the disk device volume + traits too + - tests/main/security-device-cgroups-strict-enforced: extend the + comments + - README.md: point at bugs.launchpad.net/snapd instead of snappy + project + - osutil/disks: introduce RegisterDeviceMapperBackResolver + use for + crypt-luks2 + - packaging: make postrm script robust against `rm` failures + - tests: print extra debug on auto-refresh-gating test failure + - o/assertstate, api: move enforcing/monitoring from api to + assertstate, save history + - tests: skip the test-snapd-timedate-control-consumer.date to avoid + NTP sync error + - gadget/install: use disks functions to implement deviceFromRole, + also rename + - tests: the `lxd` test is failing right now on 21.10 + - o/snapstate: account for deleted revs when undoing install + - interfaces/builtin/block_devices: allow blkid to print block + device attributes + - gadget: include size + sector-size in DiskVolumeDeviceTraits + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - o/snapstate/handlers: propagate read errors on "copy-snap-data" + - osutil/disks: add more fields to Partition, populate them during + discovery + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - o/snapstate: remove repeated test assertions + - tests: skip `snap advise-command` test if the store is overloaded + - cmd: create ~/snap dir with 0700 perms + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - github: leave a comment documenting reasons for pipefail + - github: enable pipefail when running spread + - osutil/disks: add DiskFromPartitionDeviceNode + - gadget, many: add model param to Update() + - cmd/snap-seccomp: add riscv64 support + - o/snapstate: maintain a RevertStatus map in SnapState + - tests: enable lxd tests on impish system + - tests: (partially) revert the memory limits PR#r10241 + - o/assertstate: functions for handling validation sets tracking + history + - tests: some improvements for the spread log parser + - interfaces/network-manager-observe: Update for libnm / dart + clients + - tests: add ntp related debug around "auto-refresh" test + - boot: expand on the fact that reseal taking modeenv is very + intentional + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - data/selinux: update the policy to allow snapd to talk to + org.freedesktop.timedate1 + - o/snapstate: keep old revision if install doesn't add new one + - overlord/state: add a unit test for a kernel+base refresh like + sequence + - desktop, usersession: observe notifications + - osutil/disks: add AllPhysicalDisks() + - timeutil,deviceutil: fix unit tests on systems without dbus or + without ntp-sync + - cmd/snap-bootstrap/README: explain all the things (well most of + them anyways) + - docs: add run-checks dependency install instruction + - o/snapstate: do not prune refresh-candidates if gate-auto-refresh- + hook feature is not enabled + - o/snapstate: test relink remodel helpers do a proper subset of + doInstall and rework the verify*Tasks helpers + - tests/main/mount-ns: make the test run early + - tests: add `--debug` to netplan apply + - many: wait for up to 10min for NTP synchronization before + autorefresh + - tests: initialize CHANGE_ID in _wait_autorefresh + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - tests: add more debug around qemu-nbd + - o/hookstate: print cohort with snapctl refresh --pending (#10985) + - tests: misc robustness changes + - o/snapstate: improve install/update tests (#10850) + - tests: clean up test tools + - spread.yaml: show `journalctl -e` for all suites on debug + - tests: give interfaces-udisks2 more time for the loop device to + appear + - tests: set memory limit for snapd + - tests: increase timeout/add debug around nbd0 mounting (up, see + LP:#1949513) + - snapstate: add debug message where a snap is mounted + - tests: give nbd0 more time to show up in preseed-lxd + - interfaces/dsp: add more ambarella things + - cmd/snap: improve snap disconnect arg parsing and err msg + - tests: disable nested lxd snapd testing + - tests: disable flaky "interfaces-udisks2" on ubuntu-18.04-32 + - o/snapstate: avoid validationSetsSuite repeating snapmgrTestSuite + - sandbox/cgroup: wait for start transient unit job to finish + - o/snapstate: fix task order, tweak errors, add unit tests for + remodel helpers + - osutil/disks: re-org methods for end of usable region, size + information + - build-aux: ensure that debian packaging matches build-base + - docs: update HACKING.md instructions for snapd 2.52 and later + - spread: run lxd tests with version from latest/edge + - interfaces: suppress denial of sys_module capability + - osutil/disks: add methods to replace gadget/ondisk functions + - tests: split test tools - part 1 + - tests: fix nested tests on uc20 + - data/selinux: allow snap-confine to read udev's database + - i/b/common_test: refactor AppArmor features test + - tests: run spread tests on debian 11 + - o/devicestate: copy timesyncd clock timestamp during install + - interfaces/builtin: do not probe parser features when apparmor + isn't available + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - tests: fix error message in run-checks + - tests: spread test for validation sets enforcing + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - o/snapstate: deduplicate snap names in remove/install/update + - tests/main/selinux-data-context: use session when performing + actions as test user + - packaging/opensuse: sync with openSUSE packaging, enable AppArmor + on 15.3+ + - interfaces: skip connection of netlink interface on older + systems + - asserts, o/snapstate: honor IgnoreValidation flag when checking + installed snaps + - tests/main/apparmor-batch-reload: fix fake apparmor_parser to + handle --preprocess + - sandbox/apparmor, interfaces/apparmor: detect bpf capability, + generate snippet for s-c + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - tests: test for enforcing with prerequisites + - tests/main/snapd-sigterm: fix race conditions + - spread: run lxd tests with version from latest/stable + - run-checks: remove --spread from help message + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests: ensure systemd-timesyncd is installed on debian + - interfaces/u2f-devices: add Nitrokey 3 + - tests: update the ubuntu-image channel to candidate + - osutil/disks/labels: simplify decoding algorithm + - tests: not testing lxd snap anymore on i386 architecture + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - cmd/snap: support --ignore-validation with snap install client + command + - tests/snapd-sigterm: be more robust against service restart + - tests: simplify mock script for apparmor_parser + - o/devicestate, o/servicestate: update gadget assets and cmdline + when remodeling + - tests/nested/manual/refresh-revert-fundamentals: re-enable + encryption + - osutil/disks: fix bug in BlkIDEncodeLabel, add BlkIDDecodeLabel + - gadget, osutil/disks: fix some bugs from prior PR'sin the dir. + - secboot: revert move to new version (revert #10715) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup + - many: mv MockDeviceNameDisksToPartitionMapping -> + MockDeviceNameToDiskMapping + - interfaces/builtin: Add '/com/canonical/dbusmenu' path access to + 'unity7' interface + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - osutil/disks, many: switch to defining Partitions directly for + MockDiskMapping + - tests: remove extra-snaps-assertions test + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - tests/nested/core/core20-create-recovery: fix passing of data to + curl + - daemon: allow enabling enforce mode + - daemon: use the syscall connection to get the socket credentials + - i/builtin/kubernetes_support: add access to Calico lock file + - osutil: ensure parent dir is opened and sync'd + - tests: using test-snapd-curl snap instead of http snap + - overlord: add managers unit test demonstrating cyclic dependency + between gadget and kernel updates + - gadget/ondisk.go: include the filesystem UUID in the returned + OnDiskVolume + - packaging: fixes for building on openSUSE + - o/configcore: allow hostnames up to 253 characters, with dot- + delimited elements + - gadget/ondisk.go: add listBlockDevices() to get all block devices + on a system + - gadget: add mapping trait types + functions to save/load + - interfaces: add polkit security backend + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - tests: merge coverage results + - tests: remove "features" from fde-setup.go example + - fde: add new device-setup support to fde-setup + - gadget: add `encryptedDevice` and add encryptedDeviceLUKS + - spread: use `bios: uefi` for uc20 + - client: fail fast on non-retryable errors + - tests: support running all spread tests with experimental features + - tests: check that a snap that doesn't have gate-auto-refresh hook + can call --proceed + - o/snapstate: support ignore-validation flag when updating to a + specific snap revision + - o/snapstate: test prereq update if started by old version + - tests/main: disable cgroup-devices-v1 and freezer tests on 21.10 + - tests/main/interfaces-many: run both variants on all possible + Ubuntu systems + - gadget: mv ensureLayoutCompatibility to gadget proper, add + gadgettest pkg + - many: replace state.State restart support with overlord/restart + - overlord: fix generated snap-revision assertions in remodel unit + tests + + -- Michael Vogt Fri, 17 Dec 2021 15:49:18 +0100 + +snapd (2.53.4) xenial; urgency=medium + + * New upstream release, LP: #1929842 + - devicestate: mock devicestate.MockTimeutilIsNTPSynchronized to + avoid host env leaking into tests + - timeutil: return NoTimedate1Error if it can't connect to the + system bus + + -- Ian Johnson Thu, 02 Dec 2021 17:16:48 -0600 + +snapd (2.53.3) xenial; urgency=medium + + * New upstream release, LP: #1929842 + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - configcore: relax validation rules for hostname + - o/devicestate: introduce DeviceManager.Unregister + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider + - many: wait for up to 10min for NTP synchronization before + autorefresh + - interfaces/interfaces/scsi_generic: add interface for scsi generic + devices + - interfaces/microstack-support: set controlsDeviceCgroup to true + - interface/builtin/log_observe: allow to access /dev/kmsg + - daemon: write formdata file parts to snaps dir + - spread: run lxd tests with version from latest/edge + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - interfaces/builtin/dsp: add proc files for monitoring Ambarella + DSP firmware + - interfaces/builtin/dsp: update proc file accordingly + + -- Ian Johnson Thu, 02 Dec 2021 11:42:15 -0600 + +snapd (2.53.2) xenial; urgency=medium + + * New upstream release, LP: #1946127 + - interfaces/builtin/block_devices: allow blkid to print block + device attributes/run/udev/data/b{major}:{minor} + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - interfaces/network-manager-observe: Update for libnm client + library + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - o/hookstate: print cohort with snapctl refresh --pending + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - tests: ensure systemd-timesyncd is installed on debian + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests/main/snapd-sigterm: flush, use retry + - tests/main/snapd-sigterm: fix race conditions + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - data/selinux: allow snap-confine to read udev's database + - interfaces/dsp: add more ambarella things* interfaces/dsp: add + more ambarella things + + -- Ian Johnson Mon, 15 Nov 2021 16:09:09 -0600 + +snapd (2.53.1) xenial; urgency=medium + + * New upstream release, LP: #1946127 + - spread: run lxd tests with version from latest/stable + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional (#10946) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup (2.53) + - interfaces/u2f-devices: add Nitrokey 3 + - Update the ubuntu-image channel to candidate + - Allow hostnames up to 253 characters, with dot-delimited elements + (as suggested by man 7 hostname). + - Disable i386 until it is possible to build snapd using lxd + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - tests/snapd-sigterm: be more robust against service restart + - tests: add a regression test for snapd hanging on SIGTERM + - daemon: use the syscall connection to get the socket + credentials + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - secboot: revert move to new version + + -- Ian Johnson Thu, 21 Oct 2021 11:55:31 -0500 + +snapd (2.53) xenial; urgency=medium * New upstream release, LP: #1946127 - overlord: fix generated snap-revision assertions in remodel unit diff -Nru snapd-2.53+21.10ubuntu1/debian/control snapd-2.54.2+21.10/debian/control --- snapd-2.53+21.10ubuntu1/debian/control 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/debian/control 2022-01-06 21:25:16.000000000 +0000 @@ -70,7 +70,8 @@ systemd, udev, ${misc:Depends}, - ${shlibs:Depends} + ${shlibs:Depends}, + ${dbussession:Depends} Replaces: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9), snap-confine (<< 2.23), ubuntu-core-launcher (<< 2.22), snapd-xdg-open (<= 0.0.0) Breaks: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9), snap-confine (<< 2.23), ubuntu-core-launcher (<< 2.22), snapd-xdg-open (<= 0.0.0), ${snapd:Breaks} Recommends: gnupg diff -Nru snapd-2.53+21.10ubuntu1/debian/rules snapd-2.54.2+21.10/debian/rules --- snapd-2.53+21.10ubuntu1/debian/rules 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/debian/rules 2022-01-06 21:25:16.000000000 +0000 @@ -24,15 +24,25 @@ include /etc/os-release +SUBSTVARS = # On 18.04 the released version of apt (1.6.1) has a bug that causes # problem on "apt purge snapd". To ensure this won't happen add the # right dependency on 18.04. ifeq (${VERSION_ID},"18.04") - SUBSTVARS = -Vsnapd:Breaks="systemd (<< 237-3ubuntu10.24), apt (<< 1.6.3)" + SUBSTVARS += -Vsnapd:Breaks="systemd (<< 237-3ubuntu10.24), apt (<< 1.6.3)" endif # Same as above for 18.10 just a different version. ifeq (${VERSION_ID},"18.10") - SUBSTVARS = -Vsnapd:Breaks="apt (<< 1.7.0~alpha2)" + SUBSTVARS += -Vsnapd:Breaks="apt (<< 1.7.0~alpha2)" +endif +# Since 21.10 is using cgroups v2, having a session bus is a hard requirement, +# for earlier versions it's nice to have and allows snaps to be tracked +# correctly. However, the (default-)dbus-session-bus virtual packages were only +# introduced in 2018, so earlier supported releases (16.04) have to explicitly +# specify the requirement. +ifneq (${VERSION_ID},"16.04") + # version with appropriate virtual packages + SUBSTVARS += -Vdbussession:Depends="default-dbus-session-bus | dbus-session-bus" endif # this is overridden in the ubuntu/14.04 release branch diff -Nru snapd-2.53+21.10ubuntu1/debian/snapd.postrm snapd-2.54.2+21.10/debian/snapd.postrm --- snapd-2.53+21.10ubuntu1/debian/snapd.postrm 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/debian/snapd.postrm 2022-01-06 21:25:16.000000000 +0000 @@ -85,6 +85,7 @@ fi # modules rm -f "/etc/modules-load.d/snap.${snap}.conf" + rm -f "/etc/modprobe.d/snap.${snap}.conf" # timer and socket units find /etc/systemd/system -name "snap.${snap}.*.timer" -o -name "snap.${snap}.*.socket" | while read -r f; do systemctl_stop "$(basename "$f")" @@ -137,7 +138,7 @@ echo "Final directory cleanup" for d in "/snap/bin" "/snap" "/var/snap"; do # Force remove due to directories for old revisions could still exist - rm -rf "$d" + rm -rf "$d" || true if [ -d "$d" ]; then echo "Cannot remove directory $d" fi diff -Nru snapd-2.53+21.10ubuntu1/desktop/notification/fdo.go snapd-2.54.2+21.10/desktop/notification/fdo.go --- snapd-2.53+21.10ubuntu1/desktop/notification/fdo.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/desktop/notification/fdo.go 2022-01-06 21:25:16.000000000 +0000 @@ -22,6 +22,8 @@ import ( "context" "fmt" + "sync" + "time" "github.com/godbus/dbus" @@ -38,8 +40,10 @@ type fdoBackend struct { conn *dbus.Conn obj dbus.BusObject + mu sync.Mutex serverToLocalID map[uint32]ID localToServerID map[ID]uint32 + lastRemove time.Time desktopID string } @@ -92,7 +96,9 @@ // serverSideId may be 0, but if it exists it is going to replace previous // notification with same local id. + srv.mu.Lock() serverSideId := srv.localToServerID[id] + srv.mu.Unlock() call := srv.obj.Call(dBusInterfaceName+".Notify", 0, msg.AppName, serverSideId, msg.Icon, msg.Title, msg.Body, flattenActions(msg.Actions), hints, @@ -100,6 +106,9 @@ if err := call.Store(&serverSideId); err != nil { return err } + + srv.mu.Lock() + defer srv.mu.Unlock() srv.serverToLocalID[serverSideId] = id srv.localToServerID[id] = serverSideId return nil @@ -137,17 +146,28 @@ // CloseNotification closes a notification message with the given ID. func (srv *fdoBackend) CloseNotification(id ID) error { + srv.mu.Lock() serverSideId, ok := srv.localToServerID[id] + srv.mu.Unlock() + if !ok { return fmt.Errorf("unknown notification with id %q", id) } call := srv.obj.Call(dBusInterfaceName+".CloseNotification", 0, serverSideId) - if err := call.Store(); err != nil { - return err + return call.Store() +} + +func (srv *fdoBackend) IdleDuration() time.Duration { + srv.mu.Lock() + defer srv.mu.Unlock() + if len(srv.serverToLocalID) > 0 { + return 0 } - delete(srv.localToServerID, id) - delete(srv.serverToLocalID, serverSideId) - return nil + return time.Since(srv.lastRemove) +} + +func (srv *fdoBackend) HandleNotifications(ctx context.Context) error { + return srv.ObserveNotifications(ctx, nil) } // ObserveNotifications blocks and processes message notification signals. @@ -158,7 +178,10 @@ func (srv *fdoBackend) ObserveNotifications(ctx context.Context, observer Observer) (err error) { // TODO: upgrade godbus and use un-buffered channel. ch := make(chan *dbus.Signal, 10) - defer close(ch) + + // XXX: do not close as this may lead to panic on already closed channel due + // to https://github.com/godbus/dbus/issues/271 + // defer close(ch) srv.conn.Signal(ch) defer srv.conn.RemoveSignal(ch) @@ -185,7 +208,10 @@ select { case <-ctx.Done(): return ctx.Err() - case sig := <-ch: + case sig, ok := <-ch: + if !ok { + return nil + } if err := srv.processSignal(sig, observer); err != nil { return err } @@ -220,11 +246,26 @@ return fmt.Errorf("expected second body element to be uint32, got %T", sig.Body[1]) } + srv.mu.Lock() + // we may receive signals for notifications we don't know about, silently // ignore them. - if localID, ok := srv.serverToLocalID[id]; ok { - delete(srv.localToServerID, localID) - delete(srv.serverToLocalID, id) + localID, ok := srv.serverToLocalID[id] + if !ok { + srv.mu.Unlock() + return nil + } + + delete(srv.localToServerID, localID) + delete(srv.serverToLocalID, id) + if len(srv.serverToLocalID) == 0 { + srv.lastRemove = time.Now() + } + + // unlock the mutex before calling observer + srv.mu.Unlock() + + if observer != nil { return observer.NotificationClosed(localID, CloseReason(reason)) } return nil @@ -242,5 +283,9 @@ if !ok { return fmt.Errorf("expected second body element to be string, got %T", sig.Body[1]) } - return observer.ActionInvoked(id, actionKey) + + if observer != nil { + return observer.ActionInvoked(id, actionKey) + } + return nil } diff -Nru snapd-2.53+21.10ubuntu1/desktop/notification/gtk.go snapd-2.54.2+21.10/desktop/notification/gtk.go --- snapd-2.53+21.10ubuntu1/desktop/notification/gtk.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/desktop/notification/gtk.go 2022-01-06 21:25:16.000000000 +0000 @@ -20,6 +20,9 @@ package notification import ( + "context" + "time" + "github.com/godbus/dbus" ) @@ -33,6 +36,7 @@ conn *dbus.Conn manager dbus.BusObject desktopID string + firstUse time.Time } // TODO: support actions via session agent. @@ -50,6 +54,7 @@ conn: conn, manager: conn.Object(gtkBusName, gtkObjectPath), desktopID: desktopID, + firstUse: time.Now(), } return b, nil } @@ -95,3 +100,12 @@ call := srv.manager.Call(gtkInterface+".RemoveNotification", 0, srv.desktopID, id) return call.Store() } + +func (srv *gtkBackend) HandleNotifications(context.Context) error { + // do nothing + return nil +} + +func (srv *gtkBackend) IdleDuration() time.Duration { + return time.Since(srv.firstUse) +} diff -Nru snapd-2.53+21.10ubuntu1/desktop/notification/manager.go snapd-2.54.2+21.10/desktop/notification/manager.go --- snapd-2.53+21.10ubuntu1/desktop/notification/manager.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/desktop/notification/manager.go 2022-01-06 21:25:16.000000000 +0000 @@ -20,12 +20,18 @@ package notification import ( + "context" + "time" + "github.com/godbus/dbus" ) type NotificationManager interface { SendNotification(id ID, msg *Message) error CloseNotification(id ID) error + IdleDuration() time.Duration + + HandleNotifications(ctx context.Context) error } func NewNotificationManager(conn *dbus.Conn, desktopID string) NotificationManager { diff -Nru snapd-2.53+21.10ubuntu1/dirs/dirs.go snapd-2.54.2+21.10/dirs/dirs.go --- snapd-2.53+21.10ubuntu1/dirs/dirs.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/dirs/dirs.go 2022-01-06 21:25:16.000000000 +0000 @@ -37,6 +37,8 @@ DistroLibExecDir string + HiddenSnapDataHomeGlob string + SnapBlobDir string SnapDataDir string SnapDataHomeGlob string @@ -49,6 +51,7 @@ SnapMountPolicyDir string SnapUdevRulesDir string SnapKModModulesDir string + SnapKModModprobeDir string LocaleDir string SnapMetaDir string SnapdSocket string @@ -95,6 +98,7 @@ SnapSystemdConfDir string SnapDesktopFilesDir string SnapDesktopIconsDir string + SnapPolkitPolicyDir string SnapDBusSessionPolicyDir string SnapDBusSystemPolicyDir string @@ -142,6 +146,9 @@ // Directory with snap data inside user's home UserHomeSnapDir = "snap" + // HiddenSnapDataHomeDir is an experimental hidden directory for snap data + HiddenSnapDataHomeDir = ".snap/data" + // LocalInstallBlobTempPrefix is used by local install code: // * in daemon to spool the snap file to /* // * in snapstate to auto-cleans them up using the same prefix @@ -155,6 +162,11 @@ callbacks = []func(string){} ) +type SnapDirOptions struct { + // HiddenSnapDataDir determines if the snaps' data is in ~/.snap/data instead of ~/snap + HiddenSnapDataDir bool +} + func init() { // init the global directories at startup root := os.Getenv("SNAPPY_GLOBAL_ROOT") @@ -255,6 +267,12 @@ return filepath.Join(rootdir, "/etc/systemd/system.conf.d") } +// SnapSystemdConfDirUnder returns the path to the systemd conf dir under +// rootdir. +func SnapServicesDirUnder(rootdir string) string { + return filepath.Join(rootdir, "/etc/systemd/system") +} + // SnapBootAssetsDirUnder returns the path to boot assets directory under a // rootdir. func SnapBootAssetsDirUnder(rootdir string) string { @@ -317,6 +335,7 @@ SnapDataDir = filepath.Join(rootdir, "/var/snap") SnapDataHomeGlob = filepath.Join(rootdir, "/home/*/", UserHomeSnapDir) + HiddenSnapDataHomeGlob = filepath.Join(rootdir, "/home/*/", HiddenSnapDataHomeDir) SnapAppArmorDir = filepath.Join(rootdir, snappyDir, "apparmor", "profiles") SnapConfineAppArmorDir = filepath.Join(rootdir, snappyDir, "apparmor", "snap-confine") SnapAppArmorAdditionalDir = filepath.Join(rootdir, snappyDir, "apparmor", "additional") @@ -388,11 +407,14 @@ SnapDBusSessionServicesDir = filepath.Join(rootdir, snappyDir, "dbus-1", "services") SnapDBusSystemServicesDir = filepath.Join(rootdir, snappyDir, "dbus-1", "system-services") + SnapPolkitPolicyDir = filepath.Join(rootdir, "/usr/share/polkit-1/actions") + CloudInstanceDataFile = filepath.Join(rootdir, "/run/cloud-init/instance-data.json") SnapUdevRulesDir = filepath.Join(rootdir, "/etc/udev/rules.d") SnapKModModulesDir = filepath.Join(rootdir, "/etc/modules-load.d/") + SnapKModModprobeDir = filepath.Join(rootdir, "/etc/modprobe.d/") LocaleDir = filepath.Join(rootdir, "/usr/share/locale") ClassicDir = filepath.Join(rootdir, "/writable/classic") diff -Nru snapd-2.53+21.10ubuntu1/features/features.go snapd-2.54.2+21.10/features/features.go --- snapd-2.53+21.10ubuntu1/features/features.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/features/features.go 2022-01-06 21:25:16.000000000 +0000 @@ -51,8 +51,8 @@ UserDaemons // DbusActivation controls whether snaps daemons can be activated via D-Bus DbusActivation - // HiddenSnapFolder moves ~/snap to ~/.snapdata. - HiddenSnapFolder + // HiddenSnapDataHomeDir controls if the snaps' data dir is ~/.snap/data instead of ~/snap + HiddenSnapDataHomeDir // CheckDiskSpaceRemove controls free disk space check on remove whenever automatic snapshot needs to be created. CheckDiskSpaceRemove // CheckDiskSpaceInstall controls free disk space check on snap install. @@ -94,7 +94,7 @@ UserDaemons: "user-daemons", DbusActivation: "dbus-activation", - HiddenSnapFolder: "hidden-snap-folder", + HiddenSnapDataHomeDir: "hidden-snap-folder", CheckDiskSpaceInstall: "check-disk-space-install", CheckDiskSpaceRefresh: "check-disk-space-refresh", @@ -121,7 +121,7 @@ ClassicPreservesXdgRuntimeDir: true, RobustMountNamespaceUpdates: true, - HiddenSnapFolder: true, + HiddenSnapDataHomeDir: true, } // String returns the name of a snapd feature. @@ -176,6 +176,9 @@ if !f.IsExported() { panic(fmt.Sprintf("cannot check if feature %q is enabled because that feature is not exported", f)) } + + // TODO: this returns false on errors != ErrNotExist. + // Consider using os.Stat and handling other errors return osutil.FileExists(f.ControlFile()) } diff -Nru snapd-2.53+21.10ubuntu1/features/features_test.go snapd-2.54.2+21.10/features/features_test.go --- snapd-2.53+21.10ubuntu1/features/features_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/features/features_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -49,7 +49,7 @@ c.Check(features.RobustMountNamespaceUpdates.String(), Equals, "robust-mount-namespace-updates") c.Check(features.UserDaemons.String(), Equals, "user-daemons") c.Check(features.DbusActivation.String(), Equals, "dbus-activation") - c.Check(features.HiddenSnapFolder.String(), Equals, "hidden-snap-folder") + c.Check(features.HiddenSnapDataHomeDir.String(), Equals, "hidden-snap-folder") c.Check(features.CheckDiskSpaceInstall.String(), Equals, "check-disk-space-install") c.Check(features.CheckDiskSpaceRefresh.String(), Equals, "check-disk-space-refresh") c.Check(features.CheckDiskSpaceRemove.String(), Equals, "check-disk-space-remove") @@ -78,7 +78,7 @@ c.Check(features.ClassicPreservesXdgRuntimeDir.IsExported(), Equals, true) c.Check(features.UserDaemons.IsExported(), Equals, false) c.Check(features.DbusActivation.IsExported(), Equals, false) - c.Check(features.HiddenSnapFolder.IsExported(), Equals, true) + c.Check(features.HiddenSnapDataHomeDir.IsExported(), Equals, true) c.Check(features.CheckDiskSpaceInstall.IsExported(), Equals, false) c.Check(features.CheckDiskSpaceRefresh.IsExported(), Equals, false) c.Check(features.CheckDiskSpaceRemove.IsExported(), Equals, false) @@ -115,7 +115,7 @@ c.Check(features.RobustMountNamespaceUpdates.IsEnabledWhenUnset(), Equals, true) c.Check(features.UserDaemons.IsEnabledWhenUnset(), Equals, false) c.Check(features.DbusActivation.IsEnabledWhenUnset(), Equals, true) - c.Check(features.HiddenSnapFolder.IsEnabledWhenUnset(), Equals, false) + c.Check(features.HiddenSnapDataHomeDir.IsEnabledWhenUnset(), Equals, false) c.Check(features.CheckDiskSpaceInstall.IsEnabledWhenUnset(), Equals, false) c.Check(features.CheckDiskSpaceRefresh.IsEnabledWhenUnset(), Equals, false) c.Check(features.CheckDiskSpaceRemove.IsEnabledWhenUnset(), Equals, false) @@ -127,7 +127,7 @@ c.Check(features.RefreshAppAwareness.ControlFile(), Equals, "/var/lib/snapd/features/refresh-app-awareness") c.Check(features.ParallelInstances.ControlFile(), Equals, "/var/lib/snapd/features/parallel-instances") c.Check(features.RobustMountNamespaceUpdates.ControlFile(), Equals, "/var/lib/snapd/features/robust-mount-namespace-updates") - c.Check(features.HiddenSnapFolder.ControlFile(), Equals, "/var/lib/snapd/features/hidden-snap-folder") + c.Check(features.HiddenSnapDataHomeDir.ControlFile(), Equals, "/var/lib/snapd/features/hidden-snap-folder") // Features that are not exported don't have a control file. c.Check(features.Layouts.ControlFile, PanicMatches, `cannot compute the control file of feature "layouts" because that feature is not exported`) } diff -Nru snapd-2.53+21.10ubuntu1/gadget/export_test.go snapd-2.54.2+21.10/gadget/export_test.go --- snapd-2.53+21.10ubuntu1/gadget/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -44,8 +44,6 @@ Flatten = flatten - FilesystemInfo = filesystemInfo - NewRawStructureUpdater = newRawStructureUpdater NewMountedFilesystemUpdater = newMountedFilesystemUpdater diff -Nru snapd-2.53+21.10ubuntu1/gadget/gadget.go snapd-2.54.2+21.10/gadget/gadget.go --- snapd-2.53+21.10ubuntu1/gadget/gadget.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/gadget.go 2022-01-06 21:25:16.000000000 +0000 @@ -22,6 +22,7 @@ import ( "bufio" "bytes" + "encoding/json" "errors" "fmt" "io/ioutil" @@ -147,7 +148,8 @@ // 'system-boot-select' or 'system-recovery-select'. Structures of type 'mbr', must have a // size of 446 bytes and must start at 0 offset. Role string `yaml:"role"` - // ID is the GPT partition ID + // ID is the GPT partition ID, this should always be made upper case for + // comparison purposes. ID string `yaml:"id"` // Filesystem used for the partition, 'vfat', 'ext4' or 'none' for // structures of type 'bare' @@ -206,6 +208,124 @@ Preserve []string `yaml:"preserve"` } +// DiskVolumeDeviceTraits is a set of traits about a disk that were measured at +// a previous point in time on the same device, and is used primarily to try and +// map a volume in the gadget.yaml to a physical device on the system after the +// initial installation is done. We don't have a steadfast and predictable way +// to always find the device again, so we need to do a search, trying to find a +// device which matches each trait in turn, and verify it matches the physical +// structure layout and if not move on to using the next trait. +type DiskVolumeDeviceTraits struct { + // each member here is presented in descending order of certainty about the + // likelihood of being compatible if a candidate physical device matches the + // member. I.e. OriginalDevicePath is more trusted than OriginalKernelPath is + // more trusted than DiskID is more trusted than using the MappedStructures + + // OriginalDevicePath is the device path in sysfs and in /dev/disk/by-path + // the volume was measured and observed at during UC20+ install mode. + OriginalDevicePath string `json:"device-path"` + + // OriginalKernelPath is the device path like /dev/vda the volume was + // measured and observed at during UC20+ install mode. + OriginalKernelPath string `json:"kernel-path"` + + // DiskID is the disk's identifier, it is a UUID for GPT disks or an + // unsigned integer for DOS disks encoded as a string in hexadecimal as in + // "0x1212e868". + DiskID string `json:"disk-id"` + + // Size is the physical size of the disk, regardless of usable space + // considerations. + Size quantity.Size `json:"size"` + + // SectorSize is the physical sector size of the disk, typically 512 or + // 4096. + SectorSize quantity.Size `json:"sector-size"` + + // Schema is the disk schema, either dos or gpt in lowercase. + Schema string `json:"schema"` + + // Structure contains trait information about each individual structure in + // the volume that may be useful in identifying whether a disk matches a + // volume or not. + Structure []DiskStructureDeviceTraits `json:"structure"` +} + +// DiskStructureDeviceTraits is a similar to DiskVolumeDeviceTraits, but is a +// set of traits for a specific structure on a disk rather than the full disk +// itself. Structures can be full partitions or just raw slices on a disk like +// the "BIOS Boot" structure on default amd64 grub Ubuntu Core systems. +type DiskStructureDeviceTraits struct { + // OriginalDevicePath is the device path in sysfs and in /dev/disk/by-path the + // partition was measured and observed at during UC20+ install mode. + OriginalDevicePath string `json:"device-path"` + // OriginalKernelPath is the device path like /dev/vda1 the partition was + // measured and observed at during UC20+ install mode. + OriginalKernelPath string `json:"kernel-path"` + // PartitionUUID is the partuuid as defined by i.e. /dev/disk/by-partuuid + PartitionUUID string `json:"partition-uuid"` + // PartitionLabel is the label of the partition for GPT disks, i.e. + // /dev/disk/by-partlabel + PartitionLabel string `json:"partition-label"` + // PartitionType is the type of the partition i.e. 0x83 for a + // Linux native partition on DOS, or + // 0FC63DAF-8483-4772-8E79-3D69D8477DE4 for a Linux filesystem + // data partition on GPT. + PartitionType string `json:"partition-type"` + // FilesystemUUID is the UUID of the filesystem on the partition, i.e. + // /dev/disk/by-uuid + FilesystemUUID string `json:"filesystem-uuid"` + // FilesystemLabel is the label of the filesystem for structures that have + // filesystems, i.e. /dev/disk/by-label + FilesystemLabel string `json:"filesystem-label"` + // FilesystemType is the type of the filesystem, i.e. vfat or ext4, etc. + FilesystemType string `json:"filesystem-type"` + // Offset is the offset of the structure + Offset quantity.Offset `json:"offset"` + // Size is the size of the structure + Size quantity.Size `json:"size"` +} + +// SaveDiskVolumesDeviceTraits saves the mapping of volume names to volume / +// device traits to a file inside the provided directory on disk for +// later loading and verification. +func SaveDiskVolumesDeviceTraits(dir string, mapping map[string]DiskVolumeDeviceTraits) error { + b, err := json.Marshal(mapping) + if err != nil { + return err + } + + filename := filepath.Join(dir, "disk-mapping.json") + + if err := os.MkdirAll(filepath.Dir(filename), 0755); err != nil { + return err + } + return osutil.AtomicWriteFile(filename, b, 0644, 0) +} + +// LoadDiskVolumesDeviceTraits loads the mapping of volumes to disk traits if +// there is any. If there is no file with the mapping available, nil is +// returned. +func LoadDiskVolumesDeviceTraits(dir string) (map[string]DiskVolumeDeviceTraits, error) { + var mapping map[string]DiskVolumeDeviceTraits + + filename := filepath.Join(dir, "disk-mapping.json") + if !osutil.FileExists(filename) { + return nil, nil + } + + b, err := ioutil.ReadFile(filename) + if err != nil { + return nil, err + } + + if err := json.Unmarshal(b, &mapping); err != nil { + return nil, err + } + + return mapping, nil +} + // GadgetConnect describes an interface connection requested by the gadget // between seeded snaps. The syntax is of a mapping like: // diff -Nru snapd-2.53+21.10ubuntu1/gadget/gadgettest/gadgettest.go snapd-2.54.2+21.10/gadget/gadgettest/gadgettest.go --- snapd-2.53+21.10ubuntu1/gadget/gadgettest/gadgettest.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/gadgettest/gadgettest.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,74 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package gadgettest + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + + "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/quantity" +) + +// LayoutFromYaml returns a LaidOutVolume for the given gadget.yaml string. It +// currently only supports gadget.yaml's with a single volume in them. An empty +// directory to use to create a gadget.yaml file should be provided, such as +// c.MkDir() in tests. +func LayoutFromYaml(newDir, gadgetYaml string, model gadget.Model) (*gadget.LaidOutVolume, error) { + gadgetRoot := filepath.Join(newDir, "gadget") + if err := os.MkdirAll(filepath.Join(gadgetRoot, "meta"), 0755); err != nil { + return nil, err + } + + if err := ioutil.WriteFile(filepath.Join(gadgetRoot, "meta", "gadget.yaml"), []byte(gadgetYaml), 0644); err != nil { + return nil, err + } + return MustLayOutSingleVolumeFromGadget(gadgetRoot, "", model) +} + +// MustLayOutSingleVolumeFromGadget takes a gadget rootdir and lays out the +// partitions as specified. This function does not handle multiple volumes and +// is meant for test helpers only. For runtime users, with multiple volumes +// handled by choosing the ubuntu-* role volume, see LaidOutSystemVolumeFromGadget +func MustLayOutSingleVolumeFromGadget(gadgetRoot, kernelRoot string, model gadget.Model) (*gadget.LaidOutVolume, error) { + info, err := gadget.ReadInfo(gadgetRoot, model) + if err != nil { + return nil, err + } + + if len(info.Volumes) != 1 { + return nil, fmt.Errorf("only single volumes supported in test helper") + } + + constraints := gadget.LayoutConstraints{ + NonMBRStartOffset: 1 * quantity.OffsetMiB, + } + + for _, vol := range info.Volumes { + // we know info.Volumes map has size 1 so we can return here + return gadget.LayoutVolume(gadgetRoot, kernelRoot, vol, constraints) + } + + // this is impossible to reach, we already checked that info.Volumes has a + // length of 1 + panic("impossible logic error") +} diff -Nru snapd-2.53+21.10ubuntu1/gadget/gadget_test.go snapd-2.54.2+21.10/gadget/gadget_test.go --- snapd-2.53+21.10ubuntu1/gadget/gadget_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/gadget_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -34,6 +34,7 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/gadgettest" "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snapfile" @@ -2584,3 +2585,732 @@ func (s *gadgetYamlTestSuite) TestKernelCommandLineArgsFull(c *C) { s.testKernelCommandLineArgs(c, "cmdline.full") } + +var mockDeviceLayout = gadget.OnDiskVolume{ + Structure: []gadget.OnDiskStructure{ + // Note that the first ondisk structure we have is BIOS Boot, even + // though "in reality" the first ondisk structure is MBR, but the MBR + // doesn't actually show up in /dev at all, so we don't ever measure it + // as existing on the disk - the code and test accounts for the MBR + // structure not being present in the OnDiskVolume + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Name: "BIOS Boot", + Size: 1 * quantity.SizeMiB, + }, + StartOffset: 1 * quantity.OffsetMiB, + }, + Node: "/dev/node1", + }, + }, + ID: "anything", + Device: "/dev/node", + Schema: "gpt", + Size: 2 * quantity.SizeGiB, + SectorSize: 512, + + // ( 2 GB / 512 B sector size ) - 33 typical GPT header backup sectors + + // 1 sector to get the exclusive end + UsableSectorsEnd: uint64((2*quantity.SizeGiB/512)-33) + 1, +} + +const mockSimpleGadgetYaml = `volumes: + pc: + bootloader: grub + structure: + - name: mbr + type: mbr + size: 440 + - name: BIOS Boot + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + offset: 1M + offset-write: mbr+92 +` + +const mockExtraStructure = ` + - name: Writable + role: system-data + filesystem-label: writable + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 1200M +` + +const mockExtraNonInstallableStructure = ` + - name: foobar + filesystem-label: the-great-foo + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 1200M +` + +func (s *gadgetYamlTestSuite) TestLayoutCompatibilityExtraLaidOutStructureNotOnDisk(c *C) { + // with an extra non-installable structure in the YAML that is not present + // on disk, we are not compatible + gadgetLayout, err := gadgettest.LayoutFromYaml(c.MkDir(), mockSimpleGadgetYaml+mockExtraNonInstallableStructure, nil) + c.Assert(err, IsNil) + err = gadget.EnsureLayoutCompatibility(gadgetLayout, &mockDeviceLayout, nil) + c.Assert(err, ErrorMatches, `cannot find gadget structure #2 \("foobar"\) on disk`) + + // note we don't test adding a non-matching structure, since that is already + // handled in other tests, if we added a non-matching structure the failure + // will be handled in the first loop checking that all ondisk structures + // belong to something in the YAML and that will fail, it will not get to + // the second loop which is what this test is about. +} + +func (s *gadgetYamlTestSuite) TestLayoutCompatibilityMBRStructureAllowedMissingWithStruct(c *C) { + // we are compatible with the MBR structure in the YAML not present in the + // ondisk structure + + gadgetLayout, err := gadgettest.LayoutFromYaml(c.MkDir(), mockSimpleGadgetYaml, nil) + c.Assert(err, IsNil) + + // ensure the first structure is the MBR in the YAML, but the first + // structure in the device layout is BIOS Boot + c.Assert(gadgetLayout.LaidOutStructure[0].Role, Equals, "mbr") + c.Assert(mockDeviceLayout.Structure[0].Name, Equals, "BIOS Boot") + + err = gadget.EnsureLayoutCompatibility(gadgetLayout, &mockDeviceLayout, nil) + c.Assert(err, IsNil) + + // still okay even with strict options - the absence of the MBR in the + // ondisk volume is allowed + opts := &gadget.EnsureLayoutCompatibilityOptions{AssumeCreatablePartitionsCreated: true} + err = gadget.EnsureLayoutCompatibility(gadgetLayout, &mockDeviceLayout, opts) + c.Assert(err, IsNil) +} + +func (s *gadgetYamlTestSuite) TestLayoutCompatibilityTypeBareStructureAllowedMissingWithStruct(c *C) { + // we are compatible with the type: bare structure in the YAML not present + // in the ondisk structure + + const typeBareYAML = `volumes: + foo: + bootloader: u-boot + structure: + - name: barething + type: bare + size: 4096 + - name: some-filesystem + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 1G +` + + simpleDeviceLayout := gadget.OnDiskVolume{ + Structure: []gadget.OnDiskStructure{ + // Note that the first ondisk structure we have is not barething, + // even though "in reality" the first ondisk structure is MBR, but the MBR + // doesn't actually show up in /dev at all, so we don't ever measure it + // as existing on the disk - the code and test accounts for the MBR + // structure not being present in the OnDiskVolume + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Name: "some-filesystem", + Size: 1 * quantity.SizeGiB, + Filesystem: "ext4", + }, + StartOffset: 1*quantity.OffsetMiB + 4096, + }, + Node: "/dev/node1", + }, + }, + ID: "anything", + Device: "/dev/node", + Schema: "gpt", + Size: 2 * quantity.SizeGiB, + SectorSize: 512, + + // ( 2 GB / 512 B sector size ) - 33 typical GPT header backup sectors + + // 1 sector to get the exclusive end + UsableSectorsEnd: uint64((2*quantity.SizeGiB/512)-33) + 1, + } + + gadgetLayout, err := gadgettest.LayoutFromYaml(c.MkDir(), typeBareYAML, nil) + c.Assert(err, IsNil) + + // ensure the first structure is barething in the YAML, but the first + // structure in the device layout is some-filesystem + c.Assert(gadgetLayout.LaidOutStructure[0].Type, Equals, "bare") + c.Assert(simpleDeviceLayout.Structure[0].Name, Equals, "some-filesystem") + + err = gadget.EnsureLayoutCompatibility(gadgetLayout, &simpleDeviceLayout, nil) + c.Assert(err, IsNil) + + // still okay even with strict options - the absence of the bare structure + // in the ondisk volume is allowed + opts := &gadget.EnsureLayoutCompatibilityOptions{AssumeCreatablePartitionsCreated: true} + err = gadget.EnsureLayoutCompatibility(gadgetLayout, &simpleDeviceLayout, opts) + c.Assert(err, IsNil) +} + +func (s *gadgetYamlTestSuite) TestLayoutCompatibility(c *C) { + // same contents (the locally created structure should be ignored) + gadgetLayout, err := gadgettest.LayoutFromYaml(c.MkDir(), mockSimpleGadgetYaml, nil) + c.Assert(err, IsNil) + err = gadget.EnsureLayoutCompatibility(gadgetLayout, &mockDeviceLayout, nil) + c.Assert(err, IsNil) + + // layout still compatible with a larger disk sector size + mockDeviceLayout.SectorSize = 4096 + err = gadget.EnsureLayoutCompatibility(gadgetLayout, &mockDeviceLayout, nil) + c.Assert(err, IsNil) + + // layout not compatible with a sector size that's not a factor of the + // structure sizes + mockDeviceLayout.SectorSize = 513 + err = gadget.EnsureLayoutCompatibility(gadgetLayout, &mockDeviceLayout, nil) + c.Assert(err, ErrorMatches, `gadget volume structure #1 \(\"BIOS Boot\"\) size is not a multiple of disk sector size 513`) + + // reset for the rest of the test + mockDeviceLayout.SectorSize = 512 + + // missing structure (that's ok with default opts) + gadgetLayoutWithExtras, err := gadgettest.LayoutFromYaml(c.MkDir(), mockSimpleGadgetYaml+mockExtraStructure, nil) + c.Assert(err, IsNil) + err = gadget.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &mockDeviceLayout, nil) + c.Assert(err, IsNil) + + // with strict opts, not okay + opts := &gadget.EnsureLayoutCompatibilityOptions{AssumeCreatablePartitionsCreated: true} + err = gadget.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &mockDeviceLayout, opts) + c.Assert(err, ErrorMatches, `cannot find gadget structure #2 \("Writable"\) on disk`) + + deviceLayoutWithExtras := mockDeviceLayout + deviceLayoutWithExtras.Structure = append(deviceLayoutWithExtras.Structure, + gadget.OnDiskStructure{ + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Name: "Extra partition", + Size: 10 * quantity.SizeMiB, + Label: "extra", + }, + StartOffset: 2 * quantity.OffsetMiB, + }, + Node: "/dev/node2", + }, + ) + // extra structure (should fail) + err = gadget.EnsureLayoutCompatibility(gadgetLayout, &deviceLayoutWithExtras, nil) + c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node2.* in gadget`) + + // layout is not compatible if the device is too small + smallDeviceLayout := mockDeviceLayout + smallDeviceLayout.UsableSectorsEnd = uint64(100 * quantity.SizeMiB / 512) + + // sanity check + c.Check(gadgetLayoutWithExtras.Size > quantity.Size(smallDeviceLayout.UsableSectorsEnd*uint64(smallDeviceLayout.SectorSize)), Equals, true) + err = gadget.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &smallDeviceLayout, nil) + c.Assert(err, ErrorMatches, `device /dev/node \(last usable byte at 100 MiB\) is too small to fit the requested layout \(1\.17 GiB\)`) +} + +func (s *gadgetYamlTestSuite) TestMBRLayoutCompatibility(c *C) { + const mockMBRGadgetYaml = `volumes: + pc: + schema: mbr + bootloader: grub + structure: + - name: mbr + type: mbr + size: 440 + - name: BIOS Boot + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + offset: 1M + offset-write: mbr+92 +` + var mockMBRDeviceLayout = gadget.OnDiskVolume{ + Structure: []gadget.OnDiskStructure{ + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + // partition names have no + // meaning in MBR schema + Name: "other", + Size: 440, + }, + StartOffset: 0, + }, + Node: "/dev/node1", + }, + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + // partition names have no + // meaning in MBR schema + Name: "different BIOS Boot", + Size: 1 * quantity.SizeMiB, + }, + StartOffset: 1 * quantity.OffsetMiB, + }, + Node: "/dev/node2", + }, + }, + ID: "anything", + Device: "/dev/node", + Schema: "dos", + Size: 2 * quantity.SizeGiB, + UsableSectorsEnd: uint64(2*quantity.SizeGiB/512 - 34 + 1), + SectorSize: 512, + } + gadgetLayout, err := gadgettest.LayoutFromYaml(c.MkDir(), mockMBRGadgetYaml, nil) + c.Assert(err, IsNil) + err = gadget.EnsureLayoutCompatibility(gadgetLayout, &mockMBRDeviceLayout, nil) + c.Assert(err, IsNil) + // structure is missing from disk + gadgetLayoutWithExtras, err := gadgettest.LayoutFromYaml(c.MkDir(), mockMBRGadgetYaml+mockExtraStructure, nil) + c.Assert(err, IsNil) + err = gadget.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &mockMBRDeviceLayout, nil) + c.Assert(err, IsNil) + // add it now + deviceLayoutWithExtras := mockMBRDeviceLayout + deviceLayoutWithExtras.Structure = append(deviceLayoutWithExtras.Structure, + gadget.OnDiskStructure{ + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + // name is ignored with MBR schema + Name: "Extra partition", + Size: 1200 * quantity.SizeMiB, + Label: "extra", + Filesystem: "ext4", + Type: "83", + }, + StartOffset: 2 * quantity.OffsetMiB, + }, + Node: "/dev/node2", + }, + ) + err = gadget.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayoutWithExtras, nil) + c.Assert(err, IsNil) + + // test with a larger sector size that is still an even multiple of the + // structure sizes in the gadget + mockMBRDeviceLayout.SectorSize = 4096 + err = gadget.EnsureLayoutCompatibility(gadgetLayout, &mockMBRDeviceLayout, nil) + c.Assert(err, IsNil) + + // but with a sector size that is not an even multiple of the structure size + // then we have an error + mockMBRDeviceLayout.SectorSize = 513 + err = gadget.EnsureLayoutCompatibility(gadgetLayout, &mockMBRDeviceLayout, nil) + c.Assert(err, ErrorMatches, `gadget volume structure #1 \(\"BIOS Boot\"\) size is not a multiple of disk sector size 513`) + + // add another structure that's not part of the gadget + deviceLayoutWithExtras.Structure = append(deviceLayoutWithExtras.Structure, + gadget.OnDiskStructure{ + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + // name is ignored with MBR schema + Name: "Extra extra partition", + Size: 1 * quantity.SizeMiB, + }, + StartOffset: 1202 * quantity.OffsetMiB, + }, + Node: "/dev/node4", + }, + ) + err = gadget.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayoutWithExtras, nil) + c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node4 \(starting at 1260388352\) in gadget: start offsets do not match \(disk: 1260388352 \(1.17 GiB\) and gadget: 2097152 \(2 MiB\)\)`) +} + +func (s *gadgetYamlTestSuite) TestLayoutCompatibilityWithCreatedPartitions(c *C) { + gadgetLayoutWithExtras, err := gadgettest.LayoutFromYaml(c.MkDir(), mockSimpleGadgetYaml+mockExtraStructure, nil) + c.Assert(err, IsNil) + deviceLayout := mockDeviceLayout + + // device matches gadget except for the filesystem type + deviceLayout.Structure = append(deviceLayout.Structure, + gadget.OnDiskStructure{ + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Name: "Writable", + // Role: "system-data", + Size: 1200 * quantity.SizeMiB, + Label: "writable", + Filesystem: "something_else", + }, + StartOffset: 2 * quantity.OffsetMiB, + }, + Node: "/dev/node2", + }, + ) + + // with no/default opts, then they are compatible + err = gadget.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout, nil) + c.Assert(err, IsNil) + + // but strict compatibility check, assuming that the creatable partitions + // have already been created will fail + opts := &gadget.EnsureLayoutCompatibilityOptions{AssumeCreatablePartitionsCreated: true} + err = gadget.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout, opts) + c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node2 \(starting at 2097152\) in gadget: filesystems do not match`) + + // we are going to manipulate last structure, which has system-data role + c.Assert(gadgetLayoutWithExtras.Structure[len(gadgetLayoutWithExtras.Structure)-1].Role, Equals, gadget.SystemData) + + // change the role for the laid out volume to not be a partition role that + // is created at install time (note that the duplicated seed role here is + // technically incorrect, you can't have duplicated roles, but this + // demonstrates that a structure that otherwise fits the bill but isn't a + // role that is created during install will fail the filesystem match check) + gadgetLayoutWithExtras.Structure[len(gadgetLayoutWithExtras.Structure)-1].Role = gadget.SystemSeed + + // now we fail to find the /dev/node2 structure from the gadget on disk + err = gadget.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout, nil) + c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node2 \(starting at 2097152\) in gadget: filesystems do not match and the partition is not creatable at install`) + + // undo the role change + gadgetLayoutWithExtras.Structure[len(gadgetLayoutWithExtras.Structure)-1].Role = gadget.SystemData + + // change the gadget size to be bigger than the on disk size + gadgetLayoutWithExtras.Structure[len(gadgetLayoutWithExtras.Structure)-1].Size = 10000000 * quantity.SizeMiB + + // now we fail to find the /dev/node2 structure from the gadget on disk because the gadget says it must be bigger + err = gadget.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout, nil) + c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node2 \(starting at 2097152\) in gadget: on disk size 1258291200 \(1.17 GiB\) is smaller than gadget size 10485760000000 \(9.54 TiB\)`) + + // change the gadget size to be smaller than the on disk size and the role to be one that is not expanded + gadgetLayoutWithExtras.Structure[len(gadgetLayoutWithExtras.Structure)-1].Size = 1 * quantity.SizeMiB + gadgetLayoutWithExtras.Structure[len(gadgetLayoutWithExtras.Structure)-1].Role = gadget.SystemBoot + + // now we fail because the gadget says it should be smaller and it can't be expanded + err = gadget.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout, nil) + c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node2 \(starting at 2097152\) in gadget: on disk size 1258291200 \(1.17 GiB\) is larger than gadget size 1048576 \(1 MiB\) \(and the role should not be expanded\)`) + + // but a smaller partition on disk for SystemData role is okay + gadgetLayoutWithExtras.Structure[len(gadgetLayoutWithExtras.Structure)-1].Role = gadget.SystemData + err = gadget.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout, nil) + c.Assert(err, IsNil) +} + +func (s *gadgetYamlTestSuite) TestSchemaCompatibility(c *C) { + gadgetLayout, err := gadgettest.LayoutFromYaml(c.MkDir(), mockSimpleGadgetYaml, nil) + c.Assert(err, IsNil) + deviceLayout := mockDeviceLayout + + error_msg := "disk partitioning.* doesn't match gadget.*" + + for i, tc := range []struct { + gs string + ds string + e string + }{ + {"", "dos", error_msg}, + {"", "gpt", ""}, + {"", "xxx", error_msg}, + {"mbr", "dos", ""}, + {"mbr", "gpt", error_msg}, + {"mbr", "xxx", error_msg}, + {"gpt", "dos", error_msg}, + {"gpt", "gpt", ""}, + {"gpt", "xxx", error_msg}, + // XXX: "mbr,gpt" is currently unsupported + {"mbr,gpt", "dos", error_msg}, + {"mbr,gpt", "gpt", error_msg}, + {"mbr,gpt", "xxx", error_msg}, + } { + c.Logf("%d: %q %q\n", i, tc.gs, tc.ds) + gadgetLayout.Volume.Schema = tc.gs + deviceLayout.Schema = tc.ds + err := gadget.EnsureLayoutCompatibility(gadgetLayout, &deviceLayout, nil) + if tc.e == "" { + c.Assert(err, IsNil) + } else { + c.Assert(err, ErrorMatches, tc.e) + } + } + c.Logf("-----") +} + +func (s *gadgetYamlTestSuite) TestIDCompatibility(c *C) { + gadgetLayout, err := gadgettest.LayoutFromYaml(c.MkDir(), mockSimpleGadgetYaml, nil) + c.Assert(err, IsNil) + deviceLayout := mockDeviceLayout + + error_msg := "disk ID.* doesn't match gadget volume ID.*" + + for i, tc := range []struct { + gid string + did string + e string + }{ + {"", "", ""}, + {"", "123", ""}, + {"123", "345", error_msg}, + {"123", "123", ""}, + } { + c.Logf("%d: %q %q\n", i, tc.gid, tc.did) + gadgetLayout.Volume.ID = tc.gid + deviceLayout.ID = tc.did + err := gadget.EnsureLayoutCompatibility(gadgetLayout, &deviceLayout, nil) + if tc.e == "" { + c.Assert(err, IsNil) + } else { + c.Assert(err, ErrorMatches, tc.e) + } + } + c.Logf("-----") +} + +func (s *gadgetYamlTestSuite) TestSaveLoadDiskVolumeDeviceTraits(c *C) { + // example output from a real installed VM + m := map[string]gadget.DiskVolumeDeviceTraits{ + "foo": { + OriginalDevicePath: "/sys/devices/pci0000:00/0000:00:04.0/virtio2/block/vdb", + OriginalKernelPath: "/dev/vdb", + DiskID: "484B4BA1-3EDF-4270-A1A8-378FCBB0E1DE", + Size: 10 * quantity.SizeGiB, + SectorSize: quantity.Size(512), + Schema: "gpt", + Structure: []gadget.DiskStructureDeviceTraits{ + // first structure is a bare structure with no filesystem + { + OriginalDevicePath: "/dev/vdb1", + OriginalKernelPath: "/sys/devices/pci0000:00/0000:00:04.0/virtio2/block/vdb/vdb1", + PartitionUUID: "C06F16ED-A587-4D0E-8EE4-2C3AE8BECE68", + PartitionLabel: "barething", + PartitionType: "EBBEADAF-22C9-E33B-8F5D-0E81686A68CB", + Offset: 0x100000, + Size: 0x1000, + }, + // this one has a filesystem though + { + OriginalDevicePath: "/dev/vdb2", + OriginalKernelPath: "/sys/devices/pci0000:00/0000:00:04.0/virtio2/block/vdb/vdb2", + PartitionUUID: "48ECAEB8-8DD0-41BB-A7A8-5E12FC5985FD", + PartitionLabel: "some-filesystem", + PartitionType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + FilesystemUUID: "f384e18c-56a3-458a-ac80-4cc29b3d69d9", + FilesystemLabel: "some-filesystem", + FilesystemType: "ext4", + Offset: 0x101000, + Size: 0x40000000, + }, + }, + }, + "pc": { + OriginalDevicePath: "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda", + OriginalKernelPath: "/dev/vda", + DiskID: "46E2573B-7891-4316-B83C-DE0817A7CFB5", + Schema: "gpt", + Structure: []gadget.DiskStructureDeviceTraits{ + { + OriginalDevicePath: "/dev/vda1", + OriginalKernelPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda1", + PartitionUUID: "21EF798E-4AEE-4941-9AF4-7277437F752F", + PartitionLabel: "BIOS\\x20Boot", + PartitionType: "21686148-6449-6E6F-744E-656564454649", + Offset: 0x100000, + Size: 0x1b8, + }, + { + OriginalDevicePath: "/dev/vda2", + OriginalKernelPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda2", + PartitionUUID: "F3C5B560-EF24-48A5-862B-361BCD180464", + PartitionLabel: "ubuntu-seed", + PartitionType: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + FilesystemUUID: "EC87-231A", + FilesystemLabel: "ubuntu-seed", + FilesystemType: "vfat", + Offset: 0x200000, + Size: 0x100000, + }, + { + OriginalDevicePath: "/dev/vda3", + OriginalKernelPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda3", + PartitionUUID: "CEDA6CFC-B019-0F4F-9FCE-9A41FF1D444A", + PartitionLabel: "ubuntu-boot", + PartitionType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + FilesystemUUID: "922f6d2b-520b-4213-8691-81ace98009ff", + FilesystemLabel: "ubuntu-boot", + FilesystemType: "ext4", + Offset: 0x4b200000, + Size: 0x4b000000, + }, + { + OriginalDevicePath: "/dev/vda4", + OriginalKernelPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda4", + PartitionUUID: "902A51E4-7B50-EF4C-B3DF-4D2B1E73307B", + PartitionLabel: "ubuntu-save", + PartitionType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + FilesystemUUID: "7a72b2be-753a-4ce5-ab71-189f4b832ff5", + FilesystemLabel: "ubuntu-save", + FilesystemType: "ext4", + Offset: 0x7a000000, + Size: 0x2ee00000, + }, + { + OriginalDevicePath: "/dev/vda5", + OriginalKernelPath: "/sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/vda5", + PartitionUUID: "C1C2C91D-9C00-A045-9C8F-A8779BDA5E74", + PartitionLabel: "ubuntu-data", + PartitionType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + FilesystemUUID: "b0a2e964-7bfc-4fbf-b48a-1fdca17b0539", + FilesystemLabel: "ubuntu-data", + FilesystemType: "ext4", + Offset: 0x7b000000, + Size: 0x1000000, + }, + }, + }, + } + + // when there is no mapping file, it is not an error, the map returned is + // just nil/has no items in it + mAbsent, err := gadget.LoadDiskVolumesDeviceTraits(dirs.SnapDeviceDir) + c.Assert(err, IsNil) + c.Assert(mAbsent, HasLen, 0) + + // load looks in SnapDeviceDir since it is meant to be used during run mode + // when /var/lib/snapd/device/disk-mapping.json is the real version from + // ubuntu-data, but during install mode, we will need to save to the host + // ubuntu-data which is not located at /run/mnt/data or + // /var/lib/snapd/device, but rather + // /run/mnt/ubuntu-data/system-data/var/lib/snapd/device so this takes a + // directory argument when we save it + err = gadget.SaveDiskVolumesDeviceTraits(dirs.SnapDeviceDir, m) + c.Assert(err, IsNil) + + // now that it was saved to dirs.SnapDeviceDir, we can load it correctly + m2, err := gadget.LoadDiskVolumesDeviceTraits(dirs.SnapDeviceDir) + c.Assert(err, IsNil) + + c.Assert(m, DeepEquals, m2) + + // example output from a Raspi - write the JSON out manually so we can catch + // regressions between JSON -> go object importing + const piJson = ` +{ + "pi": { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0", + "kernel-path": "/dev/mmcblk0", + "disk-id": "7c301cbd", + "size": 32010928128, + "sector-size": 512, + "schema": "dos", + "structure": [ + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1", + "kernel-path": "/dev/mmcblk0p1", + "partition-uuid": "7c301cbd-01", + "partition-label": "", + "partition-type": "0C", + "filesystem-label": "ubuntu-seed", + "filesystem-uuid": "0E09-0822", + "filesystem-type": "vfat", + "offset": 1048576, + "size": 1258291200 + }, + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p2", + "kernel-path": "/dev/mmcblk0p2", + "partition-uuid": "7c301cbd-02", + "partition-label": "", + "partition-type": "0C", + "filesystem-label": "ubuntu-boot", + "filesystem-uuid": "23F9-881F", + "filesystem-type": "vfat", + "offset": 1259339776, + "size": 786432000 + }, + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p3", + "kernel-path": "/dev/mmcblk0p3", + "partition-uuid": "7c301cbd-03", + "partition-label": "", + "partition-type": "83", + "filesystem-label": "ubuntu-save", + "filesystem-uuid": "1cdd5826-e9de-4d27-83f7-20249e710590", + "filesystem-type": "ext4", + "offset": 2045771776, + "size": 16777216 + }, + { + "device-path": "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p4", + "kernel-path": "/dev/mmcblk0p4", + "partition-uuid": "7c301cbd-04", + "partition-label": "", + "partition-type": "83", + "filesystem-label": "ubuntu-data", + "filesystem-uuid": "d7f39661-1da0-48de-8967-ce41343d4345", + "filesystem-type": "ext4", + "offset": 2062548992, + "size": 29948379136 + } + ] + } +} +` + + const oneMeg = 1024 * 1024 + + expPiMap := map[string]gadget.DiskVolumeDeviceTraits{ + "pi": { + OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0", + OriginalKernelPath: "/dev/mmcblk0", + DiskID: "7c301cbd", + Size: 30528 * oneMeg, // ~ 32 GB SD card + SectorSize: 512, + Schema: "dos", + Structure: []gadget.DiskStructureDeviceTraits{ + { + OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p1", + OriginalKernelPath: "/dev/mmcblk0p1", + PartitionUUID: "7c301cbd-01", + PartitionType: "0C", + FilesystemUUID: "0E09-0822", + FilesystemLabel: "ubuntu-seed", + FilesystemType: "vfat", + Offset: oneMeg, + Size: (1200) * oneMeg, + }, + { + OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p2", + OriginalKernelPath: "/dev/mmcblk0p2", + PartitionUUID: "7c301cbd-02", + PartitionType: "0C", + FilesystemUUID: "23F9-881F", + FilesystemLabel: "ubuntu-boot", + FilesystemType: "vfat", + Offset: (1 + 1200) * oneMeg, + Size: (750) * oneMeg, + }, + { + OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p3", + OriginalKernelPath: "/dev/mmcblk0p3", + PartitionUUID: "7c301cbd-03", + PartitionType: "83", + FilesystemUUID: "1cdd5826-e9de-4d27-83f7-20249e710590", + FilesystemType: "ext4", + FilesystemLabel: "ubuntu-save", + Offset: (1 + 1200 + 750) * oneMeg, + Size: 16 * oneMeg, + }, + { + OriginalDevicePath: "/sys/devices/platform/emmc2bus/fe340000.emmc2/mmc_host/mmc0/mmc0:0001/block/mmcblk0/mmcblk0p4", + OriginalKernelPath: "/dev/mmcblk0p4", + PartitionUUID: "7c301cbd-04", + PartitionType: "83", + FilesystemUUID: "d7f39661-1da0-48de-8967-ce41343d4345", + FilesystemLabel: "ubuntu-data", + FilesystemType: "ext4", + Offset: (1 + 1200 + 750 + 16) * oneMeg, + // total size - offset of last structure + Size: (30528 - (1 + 1200 + 750 + 16)) * oneMeg, + }, + }, + }, + } + + err = ioutil.WriteFile(filepath.Join(dirs.SnapDeviceDir, "disk-mapping.json"), []byte(piJson), 0644) + c.Assert(err, IsNil) + + m3, err := gadget.LoadDiskVolumesDeviceTraits(dirs.SnapDeviceDir) + c.Assert(err, IsNil) + + c.Assert(m3, DeepEquals, expPiMap) +} diff -Nru snapd-2.53+21.10ubuntu1/gadget/install/encrypt.go snapd-2.54.2+21.10/gadget/install/encrypt.go --- snapd-2.53+21.10ubuntu1/gadget/install/encrypt.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/install/encrypt.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* @@ -35,23 +36,33 @@ secbootAddRecoveryKey = secboot.AddRecoveryKey ) -// encryptedDevice represents a LUKS-backed encrypted block device. -type encryptedDevice struct { +// encryptedDeviceCryptsetup represents a encrypted block device. +type encryptedDevice interface { + Node() string + AddRecoveryKey(key secboot.EncryptionKey, rkey secboot.RecoveryKey) error + Close() error +} + +// encryptedDeviceLUKS represents a LUKS-backed encrypted block device. +type encryptedDeviceLUKS struct { parent *gadget.OnDiskStructure name string - Node string + node string } -// newEncryptedDevice creates an encrypted device in the existing partition using the -// specified key. -func newEncryptedDevice(part *gadget.OnDiskStructure, key secboot.EncryptionKey, name string) (*encryptedDevice, error) { - dev := &encryptedDevice{ +// sanity +var _ = encryptedDevice(&encryptedDeviceLUKS{}) + +// newEncryptedDeviceLUKS creates an encrypted device in the existing +// partition using the specified key with the LUKS backend. +func newEncryptedDeviceLUKS(part *gadget.OnDiskStructure, key secboot.EncryptionKey, name string) (encryptedDevice, error) { + dev := &encryptedDeviceLUKS{ parent: part, name: name, // A new block device is used to access the encrypted data. Note that // you can't open an encrypted device under different names and a name // can't be used in more than one device at the same time. - Node: fmt.Sprintf("/dev/mapper/%s", name), + node: fmt.Sprintf("/dev/mapper/%s", name), } if err := secbootFormatEncryptedDevice(key, name+"-enc", part.Node); err != nil { @@ -65,11 +76,15 @@ return dev, nil } -func (dev *encryptedDevice) AddRecoveryKey(key secboot.EncryptionKey, rkey secboot.RecoveryKey) error { +func (dev *encryptedDeviceLUKS) AddRecoveryKey(key secboot.EncryptionKey, rkey secboot.RecoveryKey) error { return secbootAddRecoveryKey(key, rkey, dev.parent.Node) } -func (dev *encryptedDevice) Close() error { +func (dev *encryptedDeviceLUKS) Node() string { + return dev.node +} + +func (dev *encryptedDeviceLUKS) Close() error { return cryptsetupClose(dev.name) } diff -Nru snapd-2.53+21.10ubuntu1/gadget/install/encrypt_test.go snapd-2.54.2+21.10/gadget/install/encrypt_test.go --- snapd-2.53+21.10ubuntu1/gadget/install/encrypt_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/install/encrypt_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* @@ -69,7 +70,7 @@ s.mockedRecoveryKey = secboot.RecoveryKey{15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, 0} } -func (s *encryptSuite) TestNewEncryptedDevice(c *C) { +func (s *encryptSuite) TestNewEncryptedDeviceLUKS(c *C) { for _, tc := range []struct { mockedFormatErr error mockedOpenErr string @@ -109,7 +110,7 @@ }) defer restore() - dev, err := install.NewEncryptedDevice(&mockDeviceStructure, s.mockedEncryptionKey, "some-label") + dev, err := install.NewEncryptedDeviceLUKS(&mockDeviceStructure, s.mockedEncryptionKey, "some-label") c.Assert(calls, Equals, 1) if tc.expectedErr == "" { c.Assert(err, IsNil) @@ -117,7 +118,7 @@ c.Assert(err, ErrorMatches, tc.expectedErr) continue } - c.Assert(dev.Node, Equals, "/dev/mapper/some-label") + c.Assert(dev.Node(), Equals, "/dev/mapper/some-label") err = dev.Close() c.Assert(err, IsNil) @@ -155,7 +156,7 @@ }) defer restore() - dev, err := install.NewEncryptedDevice(&mockDeviceStructure, s.mockedEncryptionKey, "some-label") + dev, err := install.NewEncryptedDeviceLUKS(&mockDeviceStructure, s.mockedEncryptionKey, "some-label") c.Assert(err, IsNil) err = dev.AddRecoveryKey(s.mockedEncryptionKey, s.mockedRecoveryKey) diff -Nru snapd-2.53+21.10ubuntu1/gadget/install/export_secboot_test.go snapd-2.54.2+21.10/gadget/install/export_secboot_test.go --- snapd-2.53+21.10ubuntu1/gadget/install/export_secboot_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/install/export_secboot_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* @@ -25,9 +26,8 @@ ) var ( - EnsureLayoutCompatibility = ensureLayoutCompatibility - DeviceFromRole = deviceFromRole - NewEncryptedDevice = newEncryptedDevice + DiskWithSystemSeed = diskWithSystemSeed + NewEncryptedDeviceLUKS = newEncryptedDeviceLUKS ) func MockSecbootFormatEncryptedDevice(f func(key secboot.EncryptionKey, label, node string) error) (restore func()) { diff -Nru snapd-2.53+21.10ubuntu1/gadget/install/export_test.go snapd-2.54.2+21.10/gadget/install/export_test.go --- snapd-2.53+21.10ubuntu1/gadget/install/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/install/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -36,7 +36,6 @@ EnsureNodesExist = ensureNodesExist CreatedDuringInstall = createdDuringInstall - CreationSupported = creationSupported ) func MockContentMountpoint(new string) (restore func()) { diff -Nru snapd-2.53+21.10ubuntu1/gadget/install/install_dummy.go snapd-2.54.2+21.10/gadget/install/install_dummy.go --- snapd-2.53+21.10ubuntu1/gadget/install/install_dummy.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/install/install_dummy.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build nosecboot // +build nosecboot /* diff -Nru snapd-2.53+21.10ubuntu1/gadget/install/install.go snapd-2.54.2+21.10/gadget/install/install.go --- snapd-2.53+21.10ubuntu1/gadget/install/install.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/install/install.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* @@ -28,23 +29,32 @@ "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil/disks" "github.com/snapcore/snapd/secboot" "github.com/snapcore/snapd/timings" ) -func deviceFromRole(lv *gadget.LaidOutVolume, role string) (device string, err error) { +// diskWithSystemSeed will locate a disk that has the partition corresponding +// to a structure with SystemSeed role of the specified gadget volume and return +// the device node. +func diskWithSystemSeed(lv *gadget.LaidOutVolume) (device string, err error) { for _, vs := range lv.LaidOutStructure { // XXX: this part of the finding maybe should be a // method on gadget.*Volume - if vs.Role == role { + if vs.Role == gadget.SystemSeed { device, err = gadget.FindDeviceForStructure(&vs) if err != nil { - return "", fmt.Errorf("cannot find device for role %q: %v", role, err) + return "", fmt.Errorf("cannot find device for role system-seed: %v", err) } - return gadget.ParentDiskFromMountSource(device) + + disk, err := disks.DiskFromPartitionDeviceNode(device) + if err != nil { + return "", err + } + return disk.KernelDeviceNode(), nil } } - return "", fmt.Errorf("cannot find role %s in gadget", role) + return "", fmt.Errorf("cannot find role system-seed in gadget") } func roleOrLabelOrName(part gadget.OnDiskStructure) string { @@ -81,7 +91,7 @@ // // auto-detect device if no device is forced if device == "" { - device, err = deviceFromRole(lv, gadget.SystemSeed) + device, err = diskWithSystemSeed(lv) if err != nil { return nil, fmt.Errorf("cannot find device to create partitions on: %v", err) } @@ -94,7 +104,7 @@ // check if the current partition table is compatible with the gadget, // ignoring partitions added by the installer (will be removed later) - if err := ensureLayoutCompatibility(lv, diskLayout); err != nil { + if err := gadget.EnsureLayoutCompatibility(lv, diskLayout, nil); err != nil { return nil, fmt.Errorf("gadget and %v partition table not compatible: %v", device, err) } @@ -157,9 +167,9 @@ return nil, err } logger.Noticef("encrypting partition device %v", part.Node) - var dataPart *encryptedDevice + var dataPart encryptedDevice timings.Run(perfTimings, fmt.Sprintf("new-encrypted-device[%s]", roleOrLabelOrName(part)), fmt.Sprintf("Create encryption device for %s", roleOrLabelOrName(part)), func(timings.Measurer) { - dataPart, err = newEncryptedDevice(&part, keys.Key, part.Label) + dataPart, err = newEncryptedDeviceLUKS(&part, keys.Key, part.Label) }) if err != nil { return nil, err @@ -173,7 +183,7 @@ } // update the encrypted device node - part.Node = dataPart.Node + part.Node = dataPart.Node() if keysForRoles == nil { keysForRoles = map[string]*EncryptionKeySet{} } @@ -211,126 +221,3 @@ KeysForRoles: keysForRoles, }, nil } - -// isCreatableAtInstall returns whether the gadget structure would be created at -// install - currently that is only ubuntu-save, ubuntu-data, and ubuntu-boot -func isCreatableAtInstall(gv *gadget.VolumeStructure) bool { - // a structure is creatable at install if it is one of the roles for - // system-save, system-data, or system-boot - switch gv.Role { - case gadget.SystemSave, gadget.SystemData, gadget.SystemBoot: - return true - default: - return false - } -} - -func ensureLayoutCompatibility(gadgetLayout *gadget.LaidOutVolume, diskLayout *gadget.OnDiskVolume) error { - eq := func(ds gadget.OnDiskStructure, gs gadget.LaidOutStructure) (bool, string) { - dv := ds.VolumeStructure - gv := gs.VolumeStructure - nameMatch := gv.Name == dv.Name - if gadgetLayout.Schema == "mbr" { - // partitions have no names in MBR so bypass the name check - nameMatch = true - } - // Previous installation may have failed before filesystem creation or - // partition may be encrypted, so if the on disk offset matches the - // gadget offset, and the gadget structure is creatable during install, - // then they are equal - // otherwise, if they are not created during installation, the - // filesystem must be the same - check := nameMatch && ds.StartOffset == gs.StartOffset && (isCreatableAtInstall(gv) || dv.Filesystem == gv.Filesystem) - sizeMatches := dv.Size == gv.Size - if gv.Role == gadget.SystemData { - // system-data may have been expanded - sizeMatches = dv.Size >= gv.Size - } - if check && sizeMatches { - return true, "" - } - switch { - case !nameMatch: - // don't return a reason if the names don't match - return false, "" - case ds.StartOffset != gs.StartOffset: - return false, fmt.Sprintf("start offsets do not match (disk: %d (%s) and gadget: %d (%s))", ds.StartOffset, ds.StartOffset.IECString(), gs.StartOffset, gs.StartOffset.IECString()) - case !isCreatableAtInstall(gv) && dv.Filesystem != gv.Filesystem: - return false, "filesystems do not match and the partition is not creatable at install" - case dv.Size < gv.Size: - return false, "on disk size is smaller than gadget size" - case gv.Role != gadget.SystemData && dv.Size > gv.Size: - return false, "on disk size is larger than gadget size (and the role should not be expanded)" - default: - return false, "some other logic condition (should be impossible?)" - } - } - - contains := func(haystack []gadget.LaidOutStructure, needle gadget.OnDiskStructure) (bool, string) { - reasonAbsent := "" - for _, h := range haystack { - matches, reasonNotMatches := eq(needle, h) - if matches { - return true, "" - } - // this has the effect of only returning the last non-empty reason - // string - if reasonNotMatches != "" { - reasonAbsent = reasonNotMatches - } - } - return false, reasonAbsent - } - - // check size of volumes - if gadgetLayout.Size > diskLayout.Size { - return fmt.Errorf("device %v (%s) is too small to fit the requested layout (%s)", diskLayout.Device, - diskLayout.Size.IECString(), gadgetLayout.Size.IECString()) - } - - // check that the sizes of all structures in the gadget are multiples of - // the disk sector size (unless the structure is the MBR) - for _, ls := range gadgetLayout.LaidOutStructure { - if !gadget.IsRoleMBR(ls) { - if ls.Size%diskLayout.SectorSize != 0 { - return fmt.Errorf("gadget volume structure %v size is not a multiple of disk sector size %v", - ls, diskLayout.SectorSize) - } - } - } - - // Check if top level properties match - if !isCompatibleSchema(gadgetLayout.Volume.Schema, diskLayout.Schema) { - return fmt.Errorf("disk partitioning schema %q doesn't match gadget schema %q", diskLayout.Schema, gadgetLayout.Volume.Schema) - } - if gadgetLayout.Volume.ID != "" && gadgetLayout.Volume.ID != diskLayout.ID { - return fmt.Errorf("disk ID %q doesn't match gadget volume ID %q", diskLayout.ID, gadgetLayout.Volume.ID) - } - - // Check if all existing device partitions are also in gadget - for _, ds := range diskLayout.Structure { - present, reasonAbsent := contains(gadgetLayout.LaidOutStructure, ds) - if !present { - if reasonAbsent != "" { - // use the right format so that it can be - // appended to the error message - reasonAbsent = fmt.Sprintf(": %s", reasonAbsent) - } - return fmt.Errorf("cannot find disk partition %s (starting at %d) in gadget%s", ds.Node, ds.StartOffset, reasonAbsent) - } - } - - return nil -} - -func isCompatibleSchema(gadgetSchema, diskSchema string) bool { - switch gadgetSchema { - // XXX: "mbr,gpt" is currently unsupported - case "", "gpt": - return diskSchema == "gpt" - case "mbr": - return diskSchema == "dos" - default: - return false - } -} diff -Nru snapd-2.53+21.10ubuntu1/gadget/install/install_test.go snapd-2.54.2+21.10/gadget/install/install_test.go --- snapd-2.53+21.10ubuntu1/gadget/install/install_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/install/install_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nosecboot // +build !nosecboot /* @@ -28,9 +29,9 @@ . "gopkg.in/check.v1" "github.com/snapcore/snapd/dirs" - "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/gadgettest" "github.com/snapcore/snapd/gadget/install" - "github.com/snapcore/snapd/gadget/quantity" + "github.com/snapcore/snapd/osutil/disks" "github.com/snapcore/snapd/testutil" "github.com/snapcore/snapd/timings" ) @@ -75,342 +76,6 @@ offset-write: mbr+92 ` -const mockExtraStructure = ` - - name: Writable - role: system-data - filesystem-label: writable - filesystem: ext4 - type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 - size: 1200M -` - -var mockDeviceLayout = gadget.OnDiskVolume{ - Structure: []gadget.OnDiskStructure{ - { - LaidOutStructure: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "mbr", - Size: 440, - }, - StartOffset: 0, - }, - Node: "/dev/node1", - }, - { - LaidOutStructure: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "BIOS Boot", - Size: 1 * quantity.SizeMiB, - }, - StartOffset: 1 * quantity.OffsetMiB, - }, - Node: "/dev/node2", - }, - }, - ID: "anything", - Device: "/dev/node", - Schema: "gpt", - Size: 2 * quantity.SizeGiB, - SectorSize: 512, -} - -func (s *installSuite) TestLayoutCompatibility(c *C) { - // same contents (the locally created structure should be ignored) - gadgetLayout := layoutFromYaml(c, mockGadgetYaml, nil) - err := install.EnsureLayoutCompatibility(gadgetLayout, &mockDeviceLayout) - c.Assert(err, IsNil) - - // layout still compatible with a larger disk sector size - mockDeviceLayout.SectorSize = 4096 - err = install.EnsureLayoutCompatibility(gadgetLayout, &mockDeviceLayout) - c.Assert(err, IsNil) - - // layout not compatible with a sector size that's not a factor of the - // structure sizes - mockDeviceLayout.SectorSize = 513 - err = install.EnsureLayoutCompatibility(gadgetLayout, &mockDeviceLayout) - c.Assert(err, ErrorMatches, `gadget volume structure #1 \(\"BIOS Boot\"\) size is not a multiple of disk sector size 513`) - - // rest for the rest of the test - mockDeviceLayout.SectorSize = 512 - - // missing structure (that's ok) - gadgetLayoutWithExtras := layoutFromYaml(c, mockGadgetYaml+mockExtraStructure, nil) - err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &mockDeviceLayout) - c.Assert(err, IsNil) - - deviceLayoutWithExtras := mockDeviceLayout - deviceLayoutWithExtras.Structure = append(deviceLayoutWithExtras.Structure, - gadget.OnDiskStructure{ - LaidOutStructure: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "Extra partition", - Size: 10 * quantity.SizeMiB, - Label: "extra", - }, - StartOffset: 2 * quantity.OffsetMiB, - }, - Node: "/dev/node3", - }, - ) - // extra structure (should fail) - err = install.EnsureLayoutCompatibility(gadgetLayout, &deviceLayoutWithExtras) - c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3.* in gadget`) - - // layout is not compatible if the device is too small - smallDeviceLayout := mockDeviceLayout - smallDeviceLayout.Size = 100 * quantity.SizeMiB - // sanity check - c.Check(gadgetLayoutWithExtras.Size > smallDeviceLayout.Size, Equals, true) - err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &smallDeviceLayout) - c.Assert(err, ErrorMatches, `device /dev/node \(100 MiB\) is too small to fit the requested layout \(1\.17 GiB\)`) - -} - -func (s *installSuite) TestMBRLayoutCompatibility(c *C) { - const mockMBRGadgetYaml = `volumes: - pc: - schema: mbr - bootloader: grub - structure: - - name: mbr - type: mbr - size: 440 - - name: BIOS Boot - type: DA,21686148-6449-6E6F-744E-656564454649 - size: 1M - offset: 1M - offset-write: mbr+92 -` - var mockMBRDeviceLayout = gadget.OnDiskVolume{ - Structure: []gadget.OnDiskStructure{ - { - LaidOutStructure: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - // partition names have no - // meaning in MBR schema - Name: "other", - Size: 440, - }, - StartOffset: 0, - }, - Node: "/dev/node1", - }, - { - LaidOutStructure: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - // partition names have no - // meaning in MBR schema - Name: "different BIOS Boot", - Size: 1 * quantity.SizeMiB, - }, - StartOffset: 1 * quantity.OffsetMiB, - }, - Node: "/dev/node2", - }, - }, - ID: "anything", - Device: "/dev/node", - Schema: "dos", - Size: 2 * quantity.SizeGiB, - SectorSize: 512, - } - gadgetLayout := layoutFromYaml(c, mockMBRGadgetYaml, nil) - err := install.EnsureLayoutCompatibility(gadgetLayout, &mockMBRDeviceLayout) - c.Assert(err, IsNil) - // structure is missing from disk - gadgetLayoutWithExtras := layoutFromYaml(c, mockMBRGadgetYaml+mockExtraStructure, nil) - err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &mockMBRDeviceLayout) - c.Assert(err, IsNil) - // add it now - deviceLayoutWithExtras := mockMBRDeviceLayout - deviceLayoutWithExtras.Structure = append(deviceLayoutWithExtras.Structure, - gadget.OnDiskStructure{ - LaidOutStructure: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - // name is ignored with MBR schema - Name: "Extra partition", - Size: 1200 * quantity.SizeMiB, - Label: "extra", - Filesystem: "ext4", - Type: "83", - }, - StartOffset: 2 * quantity.OffsetMiB, - }, - Node: "/dev/node3", - }, - ) - err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayoutWithExtras) - c.Assert(err, IsNil) - - // test with a larger sector size that is still an even multiple of the - // structure sizes in the gadget - mockMBRDeviceLayout.SectorSize = 4096 - err = install.EnsureLayoutCompatibility(gadgetLayout, &mockMBRDeviceLayout) - c.Assert(err, IsNil) - - // but with a sector size that is not an even multiple of the structure size - // then we have an error - mockMBRDeviceLayout.SectorSize = 513 - err = install.EnsureLayoutCompatibility(gadgetLayout, &mockMBRDeviceLayout) - c.Assert(err, ErrorMatches, `gadget volume structure #1 \(\"BIOS Boot\"\) size is not a multiple of disk sector size 513`) - - // add another structure that's not part of the gadget - deviceLayoutWithExtras.Structure = append(deviceLayoutWithExtras.Structure, - gadget.OnDiskStructure{ - LaidOutStructure: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - // name is ignored with MBR schema - Name: "Extra extra partition", - Size: 1 * quantity.SizeMiB, - }, - StartOffset: 1202 * quantity.OffsetMiB, - }, - Node: "/dev/node4", - }, - ) - err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayoutWithExtras) - c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node4 \(starting at 1260388352\) in gadget: start offsets do not match \(disk: 1260388352 \(1.17 GiB\) and gadget: 2097152 \(2 MiB\)\)`) -} - -func (s *installSuite) TestLayoutCompatibilityWithCreatedPartitions(c *C) { - gadgetLayoutWithExtras := layoutFromYaml(c, mockGadgetYaml+mockExtraStructure, nil) - deviceLayout := mockDeviceLayout - - // device matches gadget except for the filesystem type - deviceLayout.Structure = append(deviceLayout.Structure, - gadget.OnDiskStructure{ - LaidOutStructure: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Name: "Writable", - Size: 1200 * quantity.SizeMiB, - Label: "writable", - Filesystem: "something_else", - }, - StartOffset: 2 * quantity.OffsetMiB, - }, - Node: "/dev/node3", - }, - ) - err := install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout) - c.Assert(err, IsNil) - - // we are going to manipulate last structure, which has system-data role - c.Assert(gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role, Equals, gadget.SystemData) - - // change the role for the laid out volume to not be a partition role that - // is created at install time (note that the duplicated seed role here is - // technically incorrect, you can't have duplicated roles, but this - // demonstrates that a structure that otherwise fits the bill but isn't a - // role that is created during install will fail the filesystem match check) - gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemSeed - - // now we fail to find the /dev/node3 structure from the gadget on disk - err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout) - c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3 \(starting at 2097152\) in gadget: filesystems do not match and the partition is not creatable at install`) - - // undo the role change - gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemData - - // change the gadget size to be bigger than the on disk size - gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Size = 10000000 * quantity.SizeMiB - - // now we fail to find the /dev/node3 structure from the gadget on disk because the gadget says it must be bigger - err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout) - c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3 \(starting at 2097152\) in gadget: on disk size is smaller than gadget size`) - - // change the gadget size to be smaller than the on disk size and the role to be one that is not expanded - gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Size = 1 * quantity.SizeMiB - gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemBoot - - // now we fail because the gadget says it should be smaller and it can't be expanded - err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout) - c.Assert(err, ErrorMatches, `cannot find disk partition /dev/node3 \(starting at 2097152\) in gadget: on disk size is larger than gadget size \(and the role should not be expanded\)`) - - // but a smaller partition on disk for SystemData role is okay - gadgetLayoutWithExtras.Structure[len(deviceLayout.Structure)-1].Role = gadget.SystemData - err = install.EnsureLayoutCompatibility(gadgetLayoutWithExtras, &deviceLayout) - c.Assert(err, IsNil) -} - -func (s *installSuite) TestSchemaCompatibility(c *C) { - gadgetLayout := layoutFromYaml(c, mockGadgetYaml, nil) - deviceLayout := mockDeviceLayout - - error_msg := "disk partitioning.* doesn't match gadget.*" - - for i, tc := range []struct { - gs string - ds string - e string - }{ - {"", "dos", error_msg}, - {"", "gpt", ""}, - {"", "xxx", error_msg}, - {"mbr", "dos", ""}, - {"mbr", "gpt", error_msg}, - {"mbr", "xxx", error_msg}, - {"gpt", "dos", error_msg}, - {"gpt", "gpt", ""}, - {"gpt", "xxx", error_msg}, - // XXX: "mbr,gpt" is currently unsupported - {"mbr,gpt", "dos", error_msg}, - {"mbr,gpt", "gpt", error_msg}, - {"mbr,gpt", "xxx", error_msg}, - } { - c.Logf("%d: %q %q\n", i, tc.gs, tc.ds) - gadgetLayout.Volume.Schema = tc.gs - deviceLayout.Schema = tc.ds - err := install.EnsureLayoutCompatibility(gadgetLayout, &deviceLayout) - if tc.e == "" { - c.Assert(err, IsNil) - } else { - c.Assert(err, ErrorMatches, tc.e) - } - } - c.Logf("-----") -} - -func (s *installSuite) TestIDCompatibility(c *C) { - gadgetLayout := layoutFromYaml(c, mockGadgetYaml, nil) - deviceLayout := mockDeviceLayout - - error_msg := "disk ID.* doesn't match gadget volume ID.*" - - for i, tc := range []struct { - gid string - did string - e string - }{ - {"", "", ""}, - {"", "123", ""}, - {"123", "345", error_msg}, - {"123", "123", ""}, - } { - c.Logf("%d: %q %q\n", i, tc.gid, tc.did) - gadgetLayout.Volume.ID = tc.gid - deviceLayout.ID = tc.did - err := install.EnsureLayoutCompatibility(gadgetLayout, &deviceLayout) - if tc.e == "" { - c.Assert(err, IsNil) - } else { - c.Assert(err, ErrorMatches, tc.e) - } - } - c.Logf("-----") -} - -func layoutFromYaml(c *C, gadgetYaml string, model gadget.Model) *gadget.LaidOutVolume { - gadgetRoot := filepath.Join(c.MkDir(), "gadget") - err := os.MkdirAll(filepath.Join(gadgetRoot, "meta"), 0755) - c.Assert(err, IsNil) - err = ioutil.WriteFile(filepath.Join(gadgetRoot, "meta", "gadget.yaml"), []byte(gadgetYaml), 0644) - c.Assert(err, IsNil) - pv, err := mustLayOutVolumeFromGadget(c, gadgetRoot, "", model) - c.Assert(err, IsNil) - return pv -} - const mockUC20GadgetYaml = `volumes: pc: bootloader: grub @@ -441,7 +106,7 @@ size: 750M ` -func (s *installSuite) setupMockSysfs(c *C) { +func (s *installSuite) setupMockUdevSymlinks(c *C) { err := os.MkdirAll(filepath.Join(s.dir, "/dev/disk/by-partlabel"), 0755) c.Assert(err, IsNil) @@ -449,36 +114,45 @@ c.Assert(err, IsNil) err = os.Symlink("../../fakedevice0p1", filepath.Join(s.dir, "/dev/disk/by-partlabel/ubuntu-seed")) c.Assert(err, IsNil) - - // make parent device - err = ioutil.WriteFile(filepath.Join(s.dir, "/dev/fakedevice0"), nil, 0644) - c.Assert(err, IsNil) - // and fake /sys/block structure - err = os.MkdirAll(filepath.Join(s.dir, "/sys/block/fakedevice0/fakedevice0p1"), 0755) - c.Assert(err, IsNil) } func (s *installSuite) TestDeviceFromRoleHappy(c *C) { - s.setupMockSysfs(c) - lv := layoutFromYaml(c, mockUC20GadgetYaml, uc20Mod) - device, err := install.DeviceFromRole(lv, gadget.SystemSeed) + s.setupMockUdevSymlinks(c) + + m := map[string]*disks.MockDiskMapping{ + filepath.Join(s.dir, "/dev/fakedevice0p1"): { + DevNum: "42:0", + DevNode: "/dev/fakedevice0", + DevPath: "/sys/block/fakedevice0", + }, + } + + restore := disks.MockPartitionDeviceNodeToDiskMapping(m) + defer restore() + + lv, err := gadgettest.LayoutFromYaml(c.MkDir(), mockUC20GadgetYaml, uc20Mod) c.Assert(err, IsNil) - c.Check(device, Matches, ".*/dev/fakedevice0") + + device, err := install.DiskWithSystemSeed(lv) + c.Assert(err, IsNil) + c.Check(device, Equals, "/dev/fakedevice0") } func (s *installSuite) TestDeviceFromRoleErrorNoMatchingSysfs(c *C) { // note no sysfs mocking - lv := layoutFromYaml(c, mockUC20GadgetYaml, uc20Mod) + lv, err := gadgettest.LayoutFromYaml(c.MkDir(), mockUC20GadgetYaml, uc20Mod) + c.Assert(err, IsNil) - _, err := install.DeviceFromRole(lv, gadget.SystemSeed) - c.Assert(err, ErrorMatches, `cannot find device for role "system-seed": device not found`) + _, err = install.DiskWithSystemSeed(lv) + c.Assert(err, ErrorMatches, `cannot find device for role system-seed: device not found`) } func (s *installSuite) TestDeviceFromRoleErrorNoRole(c *C) { - s.setupMockSysfs(c) - lv := layoutFromYaml(c, mockGadgetYaml, nil) + s.setupMockUdevSymlinks(c) + lv, err := gadgettest.LayoutFromYaml(c.MkDir(), mockGadgetYaml, nil) + c.Assert(err, IsNil) - _, err := install.DeviceFromRole(lv, gadget.SystemSeed) + _, err = install.DiskWithSystemSeed(lv) c.Assert(err, ErrorMatches, "cannot find role system-seed in gadget") } diff -Nru snapd-2.53+21.10ubuntu1/gadget/install/mount_other.go snapd-2.54.2+21.10/gadget/install/mount_other.go --- snapd-2.53+21.10ubuntu1/gadget/install/mount_other.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/install/mount_other.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !linux // +build !linux /* diff -Nru snapd-2.53+21.10ubuntu1/gadget/install/partition.go snapd-2.54.2+21.10/gadget/install/partition.go --- snapd-2.53+21.10ubuntu1/gadget/install/partition.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/install/partition.go 2022-01-06 21:25:16.000000000 +0000 @@ -31,31 +31,20 @@ "github.com/snapcore/snapd/gadget/quantity" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/strutil" ) var ( ensureNodesExist = ensureNodesExistImpl ) -var createdPartitionGUID = []string{ - "0FC63DAF-8483-4772-8E79-3D69D8477DE4", // Linux filesystem data - "0657FD6D-A4AB-43C4-84E5-0933C84B4F4F", // Linux swap partition - "EBD0A0A2-B9E5-4433-87C0-68B6B72699C7", // Windows Basic Data Partition -} - -// creationSupported returns whether we support and expect to create partitions -// of the given type, it also means we are ready to remove them for re-installation -// or retried installation if they are appropriately marked with createdPartitionAttr. -func creationSupported(ptype string) bool { - return strutil.ListContains(createdPartitionGUID, strings.ToUpper(ptype)) -} - // createMissingPartitions creates the partitions listed in the laid out volume // pv that are missing from the existing device layout, returning a list of // structures that have been created. func createMissingPartitions(dl *gadget.OnDiskVolume, pv *gadget.LaidOutVolume) ([]gadget.OnDiskStructure, error) { - buf, created := buildPartitionList(dl, pv) + buf, created, err := buildPartitionList(dl, pv) + if err != nil { + return nil, err + } if len(created) == 0 { return created, nil } @@ -89,13 +78,16 @@ // device contents and gadget structure list, in sfdisk dump format, and // returns a partitioning description suitable for sfdisk input and a // list of the partitions to be created. -func buildPartitionList(dl *gadget.OnDiskVolume, pv *gadget.LaidOutVolume) (sfdiskInput *bytes.Buffer, toBeCreated []gadget.OnDiskStructure) { - sectorSize := dl.SectorSize +func buildPartitionList(dl *gadget.OnDiskVolume, pv *gadget.LaidOutVolume) (sfdiskInput *bytes.Buffer, toBeCreated []gadget.OnDiskStructure, err error) { + sectorSize := uint64(dl.SectorSize) + + // Keep track what partitions we already have on disk - the keys to this map + // is the starting sector of the structure we have seen. + // TODO: use quantity.SectorOffset or similar when that is available - // Keep track what partitions we already have on disk - seen := map[quantity.Offset]bool{} + seen := map[uint64]bool{} for _, s := range dl.Structure { - start := s.StartOffset / quantity.Offset(sectorSize) + start := uint64(s.StartOffset) / sectorSize seen[start] = true } @@ -122,40 +114,41 @@ s := p.VolumeStructure // Skip partitions that are already in the volume - start := p.StartOffset / quantity.Offset(sectorSize) - if seen[start] { + startInSectors := uint64(p.StartOffset) / sectorSize + if seen[startInSectors] { continue } - // Only allow the creation of partitions with known GUIDs - // TODO:UC20: also provide a mechanism for MBR (RPi) - ptype := partitionType(dl.Schema, p.Type) - if dl.Schema == "gpt" && !creationSupported(ptype) { - logger.Noticef("cannot create partition with unsupported type %s", ptype) - continue + // Only allow creating certain partitions, namely the ubuntu-* roles + if !gadget.IsCreatableAtInstall(p.VolumeStructure) { + return nil, nil, fmt.Errorf("cannot create partition %s", p) } // Check if the data partition should be expanded - size := s.Size - if s.Role == gadget.SystemData && canExpandData && quantity.Size(p.StartOffset)+s.Size < dl.Size { - size = dl.Size - quantity.Size(p.StartOffset) + newSizeInSectors := uint64(s.Size) / sectorSize + if s.Role == gadget.SystemData && canExpandData && startInSectors+newSizeInSectors < dl.UsableSectorsEnd { + // note that if startInSectors + newSizeInSectors == dl.UsableSectorEnd + // then we won't hit this branch, but it would be redundant anyways + newSizeInSectors = dl.UsableSectorsEnd - startInSectors } + ptype := partitionType(dl.Schema, p.Type) + // Can we use the index here? Get the largest existing partition number and // build from there could be safer if the disk partitions are not consecutive // (can this actually happen in our images?) node := deviceName(dl.Device, pIndex) fmt.Fprintf(buf, "%s : start=%12d, size=%12d, type=%s, name=%q\n", node, - p.StartOffset/quantity.Offset(sectorSize), size/sectorSize, ptype, s.Name) + startInSectors, newSizeInSectors, ptype, s.Name) toBeCreated = append(toBeCreated, gadget.OnDiskStructure{ LaidOutStructure: p, Node: node, - Size: size, + Size: quantity.Size(newSizeInSectors * sectorSize), }) } - return buf, toBeCreated + return buf, toBeCreated, nil } func partitionType(label, ptype string) string { @@ -184,34 +177,58 @@ // removeCreatedPartitions removes partitions added during a previous install. func removeCreatedPartitions(lv *gadget.LaidOutVolume, dl *gadget.OnDiskVolume) error { - indexes := make([]string, 0, len(dl.Structure)) + sfdiskIndexes := make([]string, 0, len(dl.Structure)) + // up to 3 possible partitions are creatable and thus removable: + // ubuntu-data, ubuntu-boot, and ubuntu-save + deletedIndexes := make(map[int]bool, 3) for i, s := range dl.Structure { if wasCreatedDuringInstall(lv, s) { logger.Noticef("partition %s was created during previous install", s.Node) - indexes = append(indexes, strconv.Itoa(i+1)) + sfdiskIndexes = append(sfdiskIndexes, strconv.Itoa(i+1)) + deletedIndexes[i] = true } } - if len(indexes) == 0 { + if len(sfdiskIndexes) == 0 { return nil } // Delete disk partitions - logger.Debugf("delete disk partitions %v", indexes) - cmd := exec.Command("sfdisk", append([]string{"--no-reread", "--delete", dl.Device}, indexes...)...) + logger.Debugf("delete disk partitions %v", sfdiskIndexes) + cmd := exec.Command("sfdisk", append([]string{"--no-reread", "--delete", dl.Device}, sfdiskIndexes...)...) if output, err := cmd.CombinedOutput(); err != nil { return osutil.OutputErr(output, err) } - // Reload the partition table + // Reload the partition table - note that this specifically does not trigger + // udev events to remove the deleted devices, see the doc-comment in + // reloadPartitionTable for more details if err := reloadPartitionTable(dl.Device); err != nil { return err } - // Re-read the partition table from the device to update our partition list - if err := gadget.UpdatePartitionList(dl); err != nil { - return err + // Remove the partitions we deleted from the OnDiskVolume - note that we + // specifically don't try to just re-build the OnDiskVolume since doing + // so correctly requires using only information from the partition table + // we just updated with sfdisk (since we used --no-reread above, and we can't + // really tell the kernel to re-read the partition table without hitting + // EBUSY as the disk is still mounted even though the deleted partitions + // were deleted), but to do so would essentially just be testing that sfdisk + // updated the partition table in a way we expect. The partition parsing + // code we use to build the OnDiskVolume also must not be reliant on using + // sfdisk (since it has to work in the initrd where we don't have sfdisk), + // so either that code would just be a duplication of what sfdisk is doing + // or that code would fail to update the deleted partitions anyways since + // at this point the only thing that knows about the deleted partitions is + // the physical partition table on the disk. + newStructure := make([]gadget.OnDiskStructure, 0, len(dl.Structure)-len(deletedIndexes)) + for i, structure := range dl.Structure { + if !deletedIndexes[i] { + newStructure = append(newStructure, structure) + } } + dl.Structure = newStructure + // Ensure all created partitions were removed if remaining := createdDuringInstall(lv, dl); len(remaining) > 0 { return fmt.Errorf("cannot remove partitions: %s", strings.Join(remaining, ", ")) diff -Nru snapd-2.53+21.10ubuntu1/gadget/install/partition_test.go snapd-2.54.2+21.10/gadget/install/partition_test.go --- snapd-2.53+21.10ubuntu1/gadget/install/partition_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/install/partition_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -20,7 +20,6 @@ package install_test import ( - "bytes" "fmt" "io/ioutil" "path/filepath" @@ -31,8 +30,10 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/gadget" + "github.com/snapcore/snapd/gadget/gadgettest" "github.com/snapcore/snapd/gadget/install" "github.com/snapcore/snapd/gadget/quantity" + "github.com/snapcore/snapd/osutil/disks" "github.com/snapcore/snapd/testutil" ) @@ -62,9 +63,8 @@ cmdLsblk := testutil.MockCommand(c, "lsblk", `echo "lsblk was not mocked"; exit 1`) s.AddCleanup(cmdLsblk.Restore) - // we test different sector sizes elsewhere, here we always use it to get the sector size - cmdBlockdev := testutil.MockCommand(c, "blockdev", blockdevSectorSize512Script) - s.AddCleanup(cmdBlockdev.Restore) + cmdUdevadm := testutil.MockCommand(c, "udevadm", `echo "udevadm was not mocked"; exit 1`) + s.AddCleanup(cmdUdevadm.Restore) } const ( @@ -73,109 +73,69 @@ scriptPartitionsBiosSeedData ) -func makeSfdiskScript(num int) string { - var b bytes.Buffer - - b.WriteString(` ->&2 echo "Some warning from sfdisk" -echo '{ - "partitiontable": { - "label": "gpt", - "id": "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", - "device": "/dev/node", - "unit": "sectors", - "firstlba": 34, - "lastlba": 8388574, - "partitions": [`) - - // BIOS boot partition - if num >= scriptPartitionsBios { - b.WriteString(` - { - "node": "/dev/node1", - "start": 2048, - "size": 2048, - "type": "21686148-6449-6E6F-744E-656564454649", - "uuid": "2E59D969-52AB-430B-88AC-F83873519F6F", - "name": "BIOS Boot" - }`) +func makeMockDiskMappingIncludingPartitions(num int) *disks.MockDiskMapping { + disk := &disks.MockDiskMapping{ + DevNum: "42:0", + DiskSizeInBytes: (8388574 + 34) * 512, + DiskUsableSectorEnd: 8388574 + 1, + DiskSchema: "gpt", + ID: "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", + SectorSizeBytes: 512, + Structure: []disks.Partition{}, + DevNode: "/dev/node", } - // Seed partition - if num >= scriptPartitionsBiosSeed { - b.WriteString(`, - { - "node": "/dev/node2", - "start": 4096, - "size": 2457600, - "type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", - "uuid": "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", - "name": "Recovery" - }`) - } - - // Data partition - if num >= scriptPartitionsBiosSeedData { - b.WriteString(`, - { - "node": "/dev/node3", - "start": 2461696, - "size": 2457600, - "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", - "uuid": "f940029d-bfbb-4887-9d44-321e85c63866", - "name": "Writable" - }`) - } - - b.WriteString(` - ] - } -}'`) - return b.String() -} - -func makeLsblkScript(num int) string { - var b bytes.Buffer - - // BIOS boot partition if num >= scriptPartitionsBios { - b.WriteString(` -[ "$3" == "/dev/node1" ] && echo '{ - "blockdevices": [ {"name": "node1", "fstype": null, "label": null, "uuid": null, "mountpoint": null} ] -}'`) + disk.Structure = append(disk.Structure, disks.Partition{ + KernelDeviceNode: "/dev/node1", + StartInBytes: 2048 * 512, + SizeInBytes: 2048 * 512, + PartitionType: "21686148-6449-6E6F-744E-656564454649", + PartitionUUID: "2E59D969-52AB-430B-88AC-F83873519F6F", + PartitionLabel: "BIOS Boot", + Major: 42, + Minor: 1, + StructureIndex: 1, + }) } - // Seed partition if num >= scriptPartitionsBiosSeed { - b.WriteString(` -[ "$3" == "/dev/node2" ] && echo '{ - "blockdevices": [ {"name": "node2", "fstype": "vfat", "label": "ubuntu-seed", "uuid": "A644-B807", "mountpoint": null} ] -}'`) + disk.Structure = append(disk.Structure, disks.Partition{ + KernelDeviceNode: "/dev/node2", + StartInBytes: 4096 * 512, + SizeInBytes: 2457600 * 512, + PartitionType: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + PartitionUUID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", + PartitionLabel: "Recovery", + Major: 42, + Minor: 2, + StructureIndex: 2, + FilesystemType: "vfat", + FilesystemUUID: "A644-B807", + FilesystemLabel: "ubuntu-seed", + }) } - // Data partition if num >= scriptPartitionsBiosSeedData { - b.WriteString(` -[ "$3" == "/dev/node3" ] && echo '{ - "blockdevices": [ {"name": "node3", "fstype": "ext4", "label": "ubuntu-data", "uuid": "8781-433a", "mountpoint": null} ] -}'`) + disk.Structure = append(disk.Structure, disks.Partition{ + KernelDeviceNode: "/dev/node3", + StartInBytes: 2461696 * 512, + SizeInBytes: 2457600 * 512, + PartitionType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + PartitionUUID: "F940029D-BFBB-4887-9D44-321E85C63866", + PartitionLabel: "Writable", + Major: 42, + Minor: 3, + StructureIndex: 3, + FilesystemType: "ext4", + FilesystemUUID: "8781-433a", + FilesystemLabel: "ubuntu-data", + }) } - b.WriteString(` -exit 0`) - - return b.String() + return disk } -const blockdevSectorSize512Script = ` -if [ "$1" == "--getss" ]; then - echo 512 - exit 0 -fi -echo "unexpected cmdline opts $*" -exit 1 -` - var mockOnDiskStructureWritable = gadget.OnDiskStructure{ Node: "/dev/node3", LaidOutStructure: gadget.LaidOutStructure{ @@ -232,31 +192,6 @@ Size: 2*quantity.SizeGiB + 717*quantity.SizeMiB + 1031680, } -// mustLayOutVolumeFromGadget takes a gadget rootdir and lays out the -// partitions as specified. This function does not handle multiple volumes and -// is meant for test helpers only. For runtime users, with multiple volumes -// handled by choosing the ubuntu-* role volume, see LaidOutSystemVolumeFromGadget -func mustLayOutVolumeFromGadget(c *C, gadgetRoot, kernelRoot string, model gadget.Model) (*gadget.LaidOutVolume, error) { - info, err := gadget.ReadInfo(gadgetRoot, model) - c.Assert(err, IsNil) - - c.Assert(info.Volumes, HasLen, 1, Commentf("only single volumes supported in test helper")) - - constraints := gadget.LayoutConstraints{ - NonMBRStartOffset: 1 * quantity.OffsetMiB, - } - - for _, vol := range info.Volumes { - pvol, err := gadget.LayoutVolume(gadgetRoot, kernelRoot, vol, constraints) - c.Assert(err, IsNil) - // we know info.Volumes map has size 1 so we can return here - return pvol, nil - } - // this is impossible to reach, we already asserted that info.Volumes has a - // length of 1 - panic("impossible test error") -} - type uc20Model struct{} func (c uc20Model) Classic() bool { return false } @@ -265,15 +200,16 @@ var uc20Mod = uc20Model{} func (s *partitionTestSuite) TestBuildPartitionList(c *C) { - cmdSfdisk := testutil.MockCommand(c, "sfdisk", makeSfdiskScript(scriptPartitionsBiosSeed)) - defer cmdSfdisk.Restore() + m := map[string]*disks.MockDiskMapping{ + "/dev/node": makeMockDiskMappingIncludingPartitions(scriptPartitionsBiosSeed), + } - cmdLsblk := testutil.MockCommand(c, "lsblk", makeLsblkScript(scriptPartitionsBiosSeed)) - defer cmdLsblk.Restore() + restore := disks.MockDeviceNameToDiskMapping(m) + defer restore() err := makeMockGadget(s.gadgetRoot, gptGadgetContentWithSave) c.Assert(err, IsNil) - pv, err := mustLayOutVolumeFromGadget(c, s.gadgetRoot, "", uc20Mod) + pv, err := gadgettest.MustLayOutSingleVolumeFromGadget(s.gadgetRoot, "", uc20Mod) c.Assert(err, IsNil) dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") @@ -281,7 +217,8 @@ // the expected expanded writable partition size is: // start offset = (2M + 1200M), expanded size in sectors = (8388575*512 - start offset)/512 - sfdiskInput, create := install.BuildPartitionList(dl, pv) + sfdiskInput, create, err := install.BuildPartitionList(dl, pv) + c.Assert(err, IsNil) c.Assert(sfdiskInput.String(), Equals, `/dev/node3 : start= 2461696, size= 262144, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name="Save" /dev/node4 : start= 2723840, size= 5664735, type=0FC63DAF-8483-4772-8E79-3D69D8477DE4, name="Writable" @@ -290,15 +227,45 @@ c.Assert(create, DeepEquals, []gadget.OnDiskStructure{mockOnDiskStructureSave, mockOnDiskStructureWritableAfterSave}) } +func (s *partitionTestSuite) TestBuildPartitionListOnlyCreatablePartitions(c *C) { + // drop the "BIOS Boot" partition from the mock disk so that we only have + // ubuntu-seed (at normal location for the second partition, as if the first + // partition just vanished from the disk) + mockDisk := makeMockDiskMappingIncludingPartitions(scriptPartitionsBiosSeed) + mockDisk.Structure = mockDisk.Structure[1:] + mockDisk.Structure[0].StructureIndex = 1 + m := map[string]*disks.MockDiskMapping{ + "/dev/node": mockDisk, + } + + restore := disks.MockDeviceNameToDiskMapping(m) + defer restore() + + err := makeMockGadget(s.gadgetRoot, gptGadgetContentWithSave) + c.Assert(err, IsNil) + pv, err := gadgettest.MustLayOutSingleVolumeFromGadget(s.gadgetRoot, "", uc20Mod) + c.Assert(err, IsNil) + + dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") + c.Assert(err, IsNil) + + _, _, err = install.BuildPartitionList(dl, pv) + c.Assert(err, ErrorMatches, `cannot create partition #1 \(\"BIOS Boot\"\)`) +} + func (s *partitionTestSuite) TestCreatePartitions(c *C) { - cmdSfdisk := testutil.MockCommand(c, "sfdisk", makeSfdiskScript(scriptPartitionsBiosSeed)) + cmdSfdisk := testutil.MockCommand(c, "sfdisk", "") defer cmdSfdisk.Restore() - cmdLsblk := testutil.MockCommand(c, "lsblk", makeLsblkScript(scriptPartitionsBiosSeed)) - defer cmdLsblk.Restore() + m := map[string]*disks.MockDiskMapping{ + "/dev/node": makeMockDiskMappingIncludingPartitions(scriptPartitionsBiosSeed), + } + + restore := disks.MockDeviceNameToDiskMapping(m) + defer restore() calls := 0 - restore := install.MockEnsureNodesExist(func(ds []gadget.OnDiskStructure, timeout time.Duration) error { + restore = install.MockEnsureNodesExist(func(ds []gadget.OnDiskStructure, timeout time.Duration) error { calls++ c.Assert(ds, HasLen, 1) c.Assert(ds[0].Node, Equals, "/dev/node3") @@ -308,7 +275,7 @@ err := makeMockGadget(s.gadgetRoot, gadgetContent) c.Assert(err, IsNil) - pv, err := mustLayOutVolumeFromGadget(c, s.gadgetRoot, "", uc20Mod) + pv, err := gadgettest.MustLayOutSingleVolumeFromGadget(s.gadgetRoot, "", uc20Mod) c.Assert(err, IsNil) dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") @@ -318,9 +285,8 @@ c.Assert(created, DeepEquals, []gadget.OnDiskStructure{mockOnDiskStructureWritable}) c.Assert(calls, Equals, 1) - // Check partition table read and write + // Check partition table write c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "/dev/node"}, {"sfdisk", "--append", "--no-reread", "/dev/node"}, }) @@ -332,15 +298,16 @@ func (s *partitionTestSuite) TestRemovePartitionsTrivial(c *C) { // no locally created partitions - cmdSfdisk := testutil.MockCommand(c, "sfdisk", makeSfdiskScript(scriptPartitionsBios)) - defer cmdSfdisk.Restore() + m := map[string]*disks.MockDiskMapping{ + "/dev/node": makeMockDiskMappingIncludingPartitions(scriptPartitionsBios), + } - cmdLsblk := testutil.MockCommand(c, "lsblk", makeLsblkScript(scriptPartitionsBios)) - defer cmdLsblk.Restore() + restore := disks.MockDeviceNameToDiskMapping(m) + defer restore() err := makeMockGadget(s.gadgetRoot, gadgetContent) c.Assert(err, IsNil) - pv, err := mustLayOutVolumeFromGadget(c, s.gadgetRoot, "", uc20Mod) + pv, err := gadgettest.MustLayOutSingleVolumeFromGadget(s.gadgetRoot, "", uc20Mod) c.Assert(err, IsNil) dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") @@ -348,92 +315,272 @@ err = install.RemoveCreatedPartitions(pv, dl) c.Assert(err, IsNil) - - c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "/dev/node"}, - }) - - c.Assert(cmdLsblk.Calls(), DeepEquals, [][]string{ - {"lsblk", "--fs", "--json", "/dev/node1"}, - }) } func (s *partitionTestSuite) TestRemovePartitions(c *C) { - const mockSfdiskScriptRemovablePartition = ` -if [ -f %[1]s/2 ]; then - rm %[1]s/[0-9] -elif [ -f %[1]s/1 ]; then - touch %[1]s/2 - exit 0 -else - PART=', - {"node": "/dev/node2", "start": 4096, "size": 2457600, "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", "uuid": "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", "name": "Recovery"}, - {"node": "/dev/node3", "start": 2461696, "size": 2457600, "type": "0FC63DAF-8483-4772-8E79-3D69D8477DE4", "uuid": "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", "name": "Recovery"} - ' - touch %[1]s/1 -fi -echo '{ - "partitiontable": { - "label": "gpt", - "id": "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", - "device": "/dev/node", - "unit": "sectors", - "firstlba": 34, - "lastlba": 8388574, - "partitions": [ - {"node": "/dev/node1", "start": 2048, "size": 2048, "type": "21686148-6449-6E6F-744E-656564454649", "uuid": "2E59D969-52AB-430B-88AC-F83873519F6F", "name": "BIOS Boot"} - '"$PART - ] - } -}"` + m := map[string]*disks.MockDiskMapping{ + "/dev/node": { + DevNum: "42:0", + DevNode: "/dev/node", + // assume GPT backup header section is 34 sectors long + DiskSizeInBytes: (8388574 + 34) * 512, + DiskUsableSectorEnd: 8388574 + 1, + DiskSchema: "gpt", + ID: "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", + SectorSizeBytes: 512, + Structure: []disks.Partition{ + // all 3 partitions present + { + KernelDeviceNode: "/dev/node1", + StartInBytes: 2048 * 512, + SizeInBytes: 2048 * 512, + PartitionType: "21686148-6449-6E6F-744E-656564454649", + PartitionUUID: "2E59D969-52AB-430B-88AC-F83873519F6F", + PartitionLabel: "BIOS Boot", + Major: 42, + Minor: 1, + StructureIndex: 1, + }, + { + KernelDeviceNode: "/dev/node2", + StartInBytes: 4096 * 512, + SizeInBytes: 2457600 * 512, + PartitionType: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + PartitionUUID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", + PartitionLabel: "Recovery", + Major: 42, + Minor: 2, + StructureIndex: 2, + FilesystemType: "vfat", + FilesystemUUID: "A644-B807", + FilesystemLabel: "ubuntu-seed", + }, + { + KernelDeviceNode: "/dev/node3", + StartInBytes: 2461696 * 512, + SizeInBytes: 2457600 * 512, + PartitionType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + PartitionUUID: "F940029D-BFBB-4887-9D44-321E85C63866", + PartitionLabel: "Writable", + Major: 42, + Minor: 3, + StructureIndex: 3, + FilesystemType: "ext4", + FilesystemUUID: "8781-433a", + FilesystemLabel: "ubuntu-data", + }, + }, + }, + } - cmdSfdisk := testutil.MockCommand(c, "sfdisk", fmt.Sprintf(mockSfdiskScriptRemovablePartition, s.dir)) + restore := disks.MockDeviceNameToDiskMapping(m) + defer restore() + + cmdSfdisk := testutil.MockCommand(c, "sfdisk", "") defer cmdSfdisk.Restore() - cmdLsblk := testutil.MockCommand(c, "lsblk", makeLsblkScript(scriptPartitionsBiosSeedData)) - defer cmdLsblk.Restore() + cmdUdevadm := testutil.MockCommand(c, "udevadm", "") + defer cmdUdevadm.Restore() dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") c.Assert(err, IsNil) - c.Assert(cmdLsblk.Calls(), DeepEquals, [][]string{ - {"lsblk", "--fs", "--json", "/dev/node1"}, - {"lsblk", "--fs", "--json", "/dev/node2"}, - {"lsblk", "--fs", "--json", "/dev/node3"}, - }) - err = makeMockGadget(s.gadgetRoot, gadgetContent) c.Assert(err, IsNil) - pv, err := mustLayOutVolumeFromGadget(c, s.gadgetRoot, "", uc20Mod) + pv, err := gadgettest.MustLayOutSingleVolumeFromGadget(s.gadgetRoot, "", uc20Mod) c.Assert(err, IsNil) err = install.RemoveCreatedPartitions(pv, dl) c.Assert(err, IsNil) c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "/dev/node"}, {"sfdisk", "--no-reread", "--delete", "/dev/node", "3"}, - {"sfdisk", "--json", "/dev/node"}, + }) + + // check that the OnDiskVolume was updated as expected + c.Assert(dl.Structure, DeepEquals, []gadget.OnDiskStructure{ + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Name: "BIOS Boot", + Size: 1024 * 1024, + Type: "21686148-6449-6E6F-744E-656564454649", + ID: "2E59D969-52AB-430B-88AC-F83873519F6F", + }, + StartOffset: 1024 * 1024, + Index: 1, + }, + Node: "/dev/node1", + Size: 1024 * 1024, + }, + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Label: "ubuntu-seed", + Name: "Recovery", + Size: 2457600 * 512, + Type: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + ID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", + Filesystem: "vfat", + }, + + StartOffset: 1024*1024 + 1024*1024, + Index: 2, + }, + + Node: "/dev/node2", + Size: 2457600 * 512, + }, }) } -func (s *partitionTestSuite) TestRemovePartitionsError(c *C) { - cmdSfdisk := testutil.MockCommand(c, "sfdisk", makeSfdiskScript(scriptPartitionsBiosSeedData)) +const gadgetContentDifferentOrder = `volumes: + pc: + bootloader: grub + structure: + - name: mbr + type: mbr + size: 440 + content: + - image: pc-boot.img + - name: BIOS Boot + type: DA,21686148-6449-6E6F-744E-656564454649 + size: 1M + offset: 1M + offset-write: mbr+92 + content: + - image: pc-core.img + - name: Writable + role: system-data + filesystem: ext4 + type: 83,0FC63DAF-8483-4772-8E79-3D69D8477DE4 + size: 1200M + - name: Recovery + role: system-seed + filesystem: vfat + # UEFI will boot the ESP partition by default first + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + size: 1200M + content: + - source: grubx64.efi + target: EFI/boot/grubx64.efi +` + +func (s *partitionTestSuite) TestRemovePartitionsNonAdjacent(c *C) { + m := map[string]*disks.MockDiskMapping{ + "/dev/node": { + DevNum: "42:0", + DevNode: "/dev/node", + // assume GPT backup header section is 34 sectors long + DiskSizeInBytes: (8388574 + 34) * 512, + DiskUsableSectorEnd: 8388574 + 1, + DiskSchema: "gpt", + ID: "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", + SectorSizeBytes: 512, + Structure: []disks.Partition{ + // all 3 partitions present + { + KernelDeviceNode: "/dev/node1", + StartInBytes: 1024 * 1024, + SizeInBytes: 2048 * 512, + PartitionType: "21686148-6449-6E6F-744E-656564454649", + PartitionUUID: "2E59D969-52AB-430B-88AC-F83873519F6F", + PartitionLabel: "BIOS Boot", + Major: 42, + Minor: 1, + StructureIndex: 1, + }, + { + KernelDeviceNode: "/dev/node2", + StartInBytes: 1024*1024 + 1024*1024, + SizeInBytes: 2457600 * 512, + PartitionType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + PartitionUUID: "F940029D-BFBB-4887-9D44-321E85C63866", + PartitionLabel: "Writable", + Major: 42, + Minor: 2, + StructureIndex: 2, + FilesystemType: "ext4", + FilesystemUUID: "8781-433a", + FilesystemLabel: "ubuntu-data", + }, + { + KernelDeviceNode: "/dev/node3", + StartInBytes: 1024*1024 + 1024*1024 + 2457600*512, + SizeInBytes: 2457600 * 512, + PartitionType: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + PartitionUUID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", + PartitionLabel: "Recovery", + Major: 42, + Minor: 3, + StructureIndex: 3, + FilesystemType: "vfat", + FilesystemUUID: "A644-B807", + FilesystemLabel: "ubuntu-seed", + }, + }, + }, + } + + restore := disks.MockDeviceNameToDiskMapping(m) + defer restore() + + cmdSfdisk := testutil.MockCommand(c, "sfdisk", "") defer cmdSfdisk.Restore() - cmdLsblk := testutil.MockCommand(c, "lsblk", makeLsblkScript(scriptPartitionsBiosSeedData)) - defer cmdLsblk.Restore() + cmdUdevadm := testutil.MockCommand(c, "udevadm", "") + defer cmdUdevadm.Restore() - dl, err := gadget.OnDiskVolumeFromDevice("node") + dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") c.Assert(err, IsNil) - err = makeMockGadget(s.gadgetRoot, gadgetContent) + err = makeMockGadget(s.gadgetRoot, gadgetContentDifferentOrder) c.Assert(err, IsNil) - pv, err := mustLayOutVolumeFromGadget(c, s.gadgetRoot, "", uc20Mod) + pv, err := gadgettest.MustLayOutSingleVolumeFromGadget(s.gadgetRoot, "", uc20Mod) c.Assert(err, IsNil) err = install.RemoveCreatedPartitions(pv, dl) - c.Assert(err, ErrorMatches, "cannot remove partitions: /dev/node3") + c.Assert(err, IsNil) + + c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ + {"sfdisk", "--no-reread", "--delete", "/dev/node", "2"}, + }) + + // check that the OnDiskVolume was updated as expected + c.Assert(dl.Structure, DeepEquals, []gadget.OnDiskStructure{ + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Name: "BIOS Boot", + Size: 1024 * 1024, + Type: "21686148-6449-6E6F-744E-656564454649", + ID: "2E59D969-52AB-430B-88AC-F83873519F6F", + }, + StartOffset: 1024 * 1024, + Index: 1, + }, + Node: "/dev/node1", + Size: 1024 * 1024, + }, + { + LaidOutStructure: gadget.LaidOutStructure{ + VolumeStructure: &gadget.VolumeStructure{ + Label: "ubuntu-seed", + Name: "Recovery", + Size: 2457600 * 512, + Type: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + ID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", + Filesystem: "vfat", + }, + + StartOffset: 1024*1024 + 1024*1024 + 2457600*512, + Index: 3, + }, + + Node: "/dev/node3", + Size: 2457600 * 512, + }, + }) } func (s *partitionTestSuite) TestEnsureNodesExist(c *C) { @@ -520,78 +667,79 @@ ` func (s *partitionTestSuite) TestCreatedDuringInstallGPT(c *C) { - cmdLsblk := testutil.MockCommand(c, "lsblk", ` -case $3 in - /dev/node1) - echo '{ "blockdevices": [ {"fstype":"ext4", "label":null} ] }' - ;; - /dev/node2) - echo '{ "blockdevices": [ {"fstype":"ext4", "label":"ubuntu-seed"} ] }' - ;; - /dev/node3) - echo '{ "blockdevices": [ {"fstype":"ext4", "label":"ubuntu-save"} ] }' - ;; - /dev/node4) - echo '{ "blockdevices": [ {"fstype":"ext4", "label":"ubuntu-data"} ] }' - ;; - *) - echo "unexpected args: $*" - exit 1 - ;; -esac -`) - defer cmdLsblk.Restore() - cmdSfdisk := testutil.MockCommand(c, "sfdisk", ` -echo '{ - "partitiontable": { - "label": "gpt", - "id": "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", - "device": "/dev/node", - "unit": "sectors", - "firstlba": 34, - "lastlba": 8388574, - "partitions": [ - { - "node": "/dev/node1", - "start": 2048, - "size": 2048, - "type": "21686148-6449-6E6F-744E-656564454649", - "uuid": "30a26851-4b08-4b8d-8aea-f686e723ed8c", - "name": "BIOS boot partition" - }, - { - "node": "/dev/node2", - "start": 4096, - "size": 2457600, - "type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", - "uuid": "7ea3a75a-3f6d-4647-8134-89ae61fe88d5", - "name": "Linux filesystem" - }, - { - "node": "/dev/node3", - "start": 2461696, - "size": 262144, - "type": "0fc63daf-8483-4772-8e79-3d69d8477de4", - "uuid": "641764aa-a680-4d36-a7ad-f7bd01fd8d12", - "name": "Linux filesystem" - }, - { - "node": "/dev/node4", - "start": 2723840, - "size": 2457600, - "type": "0fc63daf-8483-4772-8e79-3d69d8477de4", - "uuid": "8ab3e8fd-d53d-4d72-9c5e-56146915fd07", - "name": "Another Linux filesystem" - } - ] - } -}' -`) - defer cmdSfdisk.Restore() + m := map[string]*disks.MockDiskMapping{ + "node": { + DevNum: "42:0", + DiskSizeInBytes: (8388574 + 34) * 512, + DiskUsableSectorEnd: 8388574 + 1, + DiskSchema: "gpt", + ID: "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", + SectorSizeBytes: 512, + DevNode: "/dev/node", + Structure: []disks.Partition{ + { + KernelDeviceNode: "/dev/node1", + StartInBytes: 2048 * 512, + SizeInBytes: 2048 * 512, + PartitionType: "21686148-6449-6E6F-744E-656564454649", + PartitionUUID: "2E59D969-52AB-430B-88AC-F83873519F6F", + PartitionLabel: "BIOS Boot", + Major: 42, + Minor: 1, + StructureIndex: 1, + }, + { + KernelDeviceNode: "/dev/node2", + StartInBytes: 4096 * 512, + SizeInBytes: 2457600 * 512, + PartitionType: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + PartitionUUID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", + PartitionLabel: "ubuntu-seed", + Major: 42, + Minor: 2, + StructureIndex: 2, + FilesystemType: "vfat", + FilesystemUUID: "A644-B807", + FilesystemLabel: "ubuntu-seed", + }, + { + KernelDeviceNode: "/dev/node3", + StartInBytes: 2461696 * 512, + SizeInBytes: 262144 * 512, + PartitionType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + PartitionUUID: "F940029D-BFBB-4887-9D44-321E85C63866", + PartitionLabel: "ubuntu-boot", + Major: 42, + Minor: 3, + StructureIndex: 3, + FilesystemType: "ext4", + FilesystemUUID: "8781-433a", + FilesystemLabel: "ubuntu-boot", + }, + { + KernelDeviceNode: "/dev/node4", + StartInBytes: 2723840 * 512, + SizeInBytes: 2457600 * 512, + PartitionType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + PartitionUUID: "G940029D-BFBB-4887-9D44-321E85C63866", + PartitionLabel: "ubuntu-data", + Major: 42, + Minor: 4, + StructureIndex: 4, + FilesystemType: "ext4", + FilesystemUUID: "8123-433a", + FilesystemLabel: "ubuntu-data", + }, + }, + }, + } + + restore := disks.MockDeviceNameToDiskMapping(m) + defer restore() err := makeMockGadget(s.gadgetRoot, gptGadgetContentWithSave) c.Assert(err, IsNil) - pv, err := mustLayOutVolumeFromGadget(c, s.gadgetRoot, "", uc20Mod) + pv, err := gadgettest.MustLayOutSingleVolumeFromGadget(s.gadgetRoot, "", uc20Mod) c.Assert(err, IsNil) dl, err := gadget.OnDiskVolumeFromDevice("node") @@ -636,102 +784,89 @@ ` func (s *partitionTestSuite) TestCreatedDuringInstallMBR(c *C) { - cmdLsblk := testutil.MockCommand(c, "lsblk", ` -what= -shift 2 -case "$1" in - /dev/node1) - what='{"name": "node1", "fstype":"ext4", "label":"ubuntu-seed"}' - ;; - /dev/node2) - what='{"name": "node2", "fstype":"vfat", "label":"ubuntu-boot"}' - ;; - /dev/node3) - what='{"name": "node3", "fstype":"ext4", "label":"ubuntu-save"}' - ;; - /dev/node4) - what='{"name": "node4", "fstype":"ext4", "label":"ubuntu-data"}' - ;; - *) - echo "unexpected call" - exit 1 -esac - -cat <[ ] format, numeric attributes - // are listed as GUID:[,]. Note that the even though the sfdisk(8) manpage - // says --part-attrs takes a space or comma separated list, the output from - // --json/--dump uses a different format. - Attrs string `json:"attrs"` - Type string `json:"type"` - UUID string `json:"uuid"` - Name string `json:"name"` -} - // TODO: consider looking into merging LaidOutVolume/Structure OnDiskVolume/Structure // OnDiskStructure represents a gadget structure laid on a block device. @@ -78,11 +45,22 @@ // schema, the partition table, and the structure layout it contains. type OnDiskVolume struct { Structure []OnDiskStructure - ID string - Device string - Schema string + // ID is the disk's identifier, it is a UUID for GPT disks or an unsigned + // integer for DOS disks encoded as a string in hexadecimal as in + // "0x1212e868". + ID string + // Device is the full device node path for the disk, such as /dev/vda. + Device string + // Schema is the disk schema, GPT or DOS. + Schema string // size in bytes Size quantity.Size + // UsableSectorsEnd is the end (exclusive) of usable sectors on the disk, + // this sector specifically is not usable for partitions, though it may be + // used for i.e. GPT header backups on some disks. This should be used when + // calculating the size of an auto-expanded partition instead of the Size + // parameter which does not take this into account. + UsableSectorsEnd uint64 // sector size in bytes SectorSize quantity.Size } @@ -90,207 +68,92 @@ // OnDiskVolumeFromDevice obtains the partitioning and filesystem information from // the block device. func OnDiskVolumeFromDevice(device string) (*OnDiskVolume, error) { - output, err := exec.Command("sfdisk", "--json", device).Output() - if err != nil { - return nil, osutil.OutputErr(output, err) - } - - var dump sfdiskDeviceDump - if err := json.Unmarshal(output, &dump); err != nil { - return nil, fmt.Errorf("cannot parse sfdisk output: %v", err) - } - - dl, err := onDiskVolumeFromPartitionTable(dump.PartitionTable) + disk, err := disks.DiskFromDeviceName(device) if err != nil { return nil, err } - dl.Device = device - return dl, nil + return OnDiskVolumeFromDisk(disk) } -func fromSfdiskPartitionType(st string, sfdiskLabel string) (string, error) { - switch sfdiskLabel { - case "dos": - // sometimes sfdisk reports what is "0C" in gadget.yaml as "c", - // normalize the values - v, err := strconv.ParseUint(st, 16, 8) - if err != nil { - return "", fmt.Errorf("cannot convert MBR partition type %q", st) - } - return fmt.Sprintf("%02X", v), nil - case "gpt": - return st, nil - default: - return "", fmt.Errorf("unsupported partitioning schema %q", sfdiskLabel) - } -} - -func blockdevSizeCmd(cmd, devpath string) (quantity.Size, error) { - out, err := exec.Command("blockdev", cmd, devpath).CombinedOutput() - if err != nil { - return 0, osutil.OutputErr(out, err) - } - nospace := strings.TrimSpace(string(out)) - sz, err := strconv.Atoi(nospace) - if err != nil { - return 0, fmt.Errorf("cannot parse blockdev %s result size %q: %v", cmd, nospace, err) - } - return quantity.Size(sz), nil -} - -func blockDeviceSizeInSectors(devpath string) (quantity.Size, error) { - // the size is always reported in 512-byte sectors, even if the device does - // not have a physical sector size of 512 - // XXX: consider using /sys/block//size directly - return blockdevSizeCmd("--getsz", devpath) -} - -func blockDeviceSectorSize(devpath string) (quantity.Size, error) { - // the size is reported in raw bytes - sz, err := blockdevSizeCmd("--getss", devpath) +func OnDiskVolumeFromDisk(disk disks.Disk) (*OnDiskVolume, error) { + parts, err := disk.Partitions() if err != nil { - return 0, err - } - - // ensure that the sector size is a multiple of 512, since we rely on that - // when we calculate the size in sectors, as blockdev --getsz always returns - // the size in 512-byte sectors - if sz%512 != 0 { - return 0, fmt.Errorf("cannot calculate structure size: sector size (%s) is not a multiple of 512", sz.String()) - } - if sz == 0 { - // extra paranoia - return 0, fmt.Errorf("internal error: sector size returned as 0") + return nil, err } - return sz, nil -} -// onDiskVolumeFromPartitionTable takes an sfdisk dump partition table and returns -// the partitioning information as an on-disk volume. -func onDiskVolumeFromPartitionTable(ptable sfdiskPartitionTable) (*OnDiskVolume, error) { - if ptable.Unit != "sectors" { - return nil, fmt.Errorf("cannot position partitions: unknown unit %q", ptable.Unit) - } + structure := make([]VolumeStructure, len(parts)) + ds := make([]OnDiskStructure, len(parts)) - structure := make([]VolumeStructure, len(ptable.Partitions)) - ds := make([]OnDiskStructure, len(ptable.Partitions)) + for _, p := range parts { + // Use the index of the structure on the disk rather than the order in + // which we iterate over the list of partitions, since the order of the + // partitions is returned "last seen first" which matches the behavior + // of udev when picking partitions with the same filesystem label and + // populating /dev/disk/by-label/ and friends. + // All that is to say the order that the list of partitions from + // Partitions() is in is _not_ the same as the order that the structures + // actually appear in on disk, but this is why the StructureIndex + // property exists. Also note that StructureIndex starts at 1, as + // opposed to gadget.LaidOutVolume.Structure's Index which starts at 0. + i := p.StructureIndex - 1 - sectorSize, err := blockDeviceSectorSize(ptable.Device) - if err != nil { - return nil, err - } + // the PartitionLabel and FilesystemLabel are encoded, so they must be + // decoded before they can be used in other gadget functions - for i, p := range ptable.Partitions { - info, err := filesystemInfo(p.Node) + decodedPartLabel, err := disks.BlkIDDecodeLabel(p.PartitionLabel) if err != nil { - return nil, fmt.Errorf("cannot obtain filesystem information: %v", err) - } - switch { - case len(info.BlockDevices) == 0: - continue - case len(info.BlockDevices) > 1: - return nil, fmt.Errorf("unexpected number of blockdevices for node %q: %v", p.Node, info.BlockDevices) + return nil, fmt.Errorf("cannot decode partition label for partition on disk %s: %v", disk.KernelDeviceNode(), err) } - bd := info.BlockDevices[0] - - vsType, err := fromSfdiskPartitionType(p.Type, ptable.Label) + decodedFsLabel, err := disks.BlkIDDecodeLabel(p.FilesystemLabel) if err != nil { - return nil, fmt.Errorf("cannot convert sfdisk partition type %q: %v", p.Type, err) + return nil, fmt.Errorf("cannot decode partition label for partition on disk %s: %v", disk.KernelDeviceNode(), err) } structure[i] = VolumeStructure{ - Name: p.Name, - Size: quantity.Size(p.Size) * sectorSize, - Label: bd.Label, - Type: vsType, - Filesystem: bd.FSType, + Name: decodedPartLabel, + Size: quantity.Size(p.SizeInBytes), + Label: decodedFsLabel, + Type: p.PartitionType, + Filesystem: p.FilesystemType, + ID: p.PartitionUUID, } ds[i] = OnDiskStructure{ LaidOutStructure: LaidOutStructure{ VolumeStructure: &structure[i], - StartOffset: quantity.Offset(p.Start) * quantity.Offset(sectorSize), - Index: i + 1, + StartOffset: quantity.Offset(p.StartInBytes), + Index: int(p.StructureIndex), }, - Node: p.Node, + Size: quantity.Size(p.SizeInBytes), + Node: p.KernelDeviceNode, } } - var numSectors quantity.Size - if ptable.LastLBA != 0 { - // sfdisk reports the last usable LBA for GPT disks only - numSectors = quantity.Size(ptable.LastLBA + 1) - } else { - // sfdisk does not report any information about the size of a - // MBR partitioned disk, find out the size of the device by - // other means - sz, err := blockDeviceSizeInSectors(ptable.Device) - if err != nil { - return nil, fmt.Errorf("cannot obtain the size of device %q: %v", ptable.Device, err) - } - - // since blockdev always reports the size in 512-byte sectors, if for - // some reason we are on a disk that does not 512-byte sectors, we will - // get confused, so in this case, multiply the number of 512-byte - // sectors by 512, then divide by the actual sector size to get the - // number of sectors - - // this will never have a divisor, since we verified that sector size is - // a multiple of 512 above - numSectors = sz * 512 / sectorSize - } - - dl := &OnDiskVolume{ - Structure: ds, - ID: ptable.ID, - Device: ptable.Device, - Schema: ptable.Label, - Size: numSectors * sectorSize, - SectorSize: sectorSize, + diskSz, err := disk.SizeInBytes() + if err != nil { + return nil, err } - return dl, nil -} - -// UpdatePartitionList re-reads the partitioning data from the device and -// updates the volume structures in the specified volume. -func UpdatePartitionList(dl *OnDiskVolume) error { - layout, err := OnDiskVolumeFromDevice(dl.Device) + sectorSz, err := disk.SectorSize() if err != nil { - return fmt.Errorf("cannot read disk layout: %v", err) - } - if dl.ID != layout.ID { - return fmt.Errorf("partition table IDs don't match") + return nil, err } - dl.Structure = layout.Structure - return nil -} - -// lsblkFilesystemInfo represents the lsblk --fs JSON output format. -type lsblkFilesystemInfo struct { - BlockDevices []lsblkBlockDevice `json:"blockdevices"` -} - -type lsblkBlockDevice struct { - Name string `json:"name"` - FSType string `json:"fstype"` - Label string `json:"label"` - UUID string `json:"uuid"` - Mountpoint string `json:"mountpoint"` -} - -func filesystemInfo(node string) (*lsblkFilesystemInfo, error) { - output, err := exec.Command("lsblk", "--fs", "--json", node).CombinedOutput() + sectorsEnd, err := disk.UsableSectorsEnd() if err != nil { - return nil, osutil.OutputErr(output, err) + return nil, err } - var info lsblkFilesystemInfo - if err := json.Unmarshal(output, &info); err != nil { - return nil, fmt.Errorf("cannot parse lsblk output: %v", err) + dl := &OnDiskVolume{ + Structure: ds, + ID: disk.DiskID(), + Device: disk.KernelDeviceNode(), + Schema: disk.Schema(), + Size: quantity.Size(diskSz), + UsableSectorsEnd: sectorsEnd, + SectorSize: quantity.Size(sectorSz), } - return &info, nil + return dl, nil } diff -Nru snapd-2.53+21.10ubuntu1/gadget/ondisk_test.go snapd-2.54.2+21.10/gadget/ondisk_test.go --- snapd-2.53+21.10ubuntu1/gadget/ondisk_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/ondisk_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -28,6 +28,7 @@ "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/gadget/quantity" + "github.com/snapcore/snapd/osutil/disks" "github.com/snapcore/snapd/testutil" ) @@ -51,46 +52,6 @@ c.Assert(err, IsNil) } -const mockSfdiskScriptBiosSeed = ` ->&2 echo "Some warning from sfdisk" -echo '{ - "partitiontable": { - "label": "gpt", - "id": "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", - "device": "/dev/node", - "unit": "sectors", - "firstlba": 34, - "lastlba": 8388574, - "partitions": [ - { - "node": "/dev/node1", - "start": 2048, - "size": 2048, - "type": "21686148-6449-6E6F-744E-656564454649", - "uuid": "2E59D969-52AB-430B-88AC-F83873519F6F", - "name": "BIOS Boot" - }, - { - "node": "/dev/node2", - "start": 4096, - "size": 2457600, - "type": "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", - "uuid": "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", - "name": "Recovery" - } - ] - } -}'` - -const mockLsblkScriptBiosSeed = ` -[ "$3" == "/dev/node1" ] && echo '{ - "blockdevices": [ {"name": "node1", "fstype": null, "label": null, "uuid": null, "mountpoint": null} ] -}' -[ "$3" == "/dev/node2" ] && echo '{ - "blockdevices": [ {"name": "node2", "fstype": "vfat", "label": "ubuntu-seed", "uuid": "A644-B807", "mountpoint": null} ] -}' -exit 0` - func makeMockGadget(gadgetRoot, gadgetContent string) error { if err := os.MkdirAll(filepath.Join(gadgetRoot, "meta"), 0755); err != nil { return err @@ -149,36 +110,55 @@ ` func (s *ondiskTestSuite) TestDeviceInfoGPT(c *C) { - cmdSfdisk := testutil.MockCommand(c, "sfdisk", mockSfdiskScriptBiosSeed) - defer cmdSfdisk.Restore() - - cmdLsblk := testutil.MockCommand(c, "lsblk", mockLsblkScriptBiosSeed) - defer cmdLsblk.Restore() + m := map[string]*disks.MockDiskMapping{ + "/dev/node": { + DevNum: "42:0", + DevNode: "/dev/node", + DiskSizeInBytes: (8388574 + 1) * 512, + DiskSchema: "gpt", + ID: "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", + SectorSizeBytes: 512, + // the actual order of the structure partitions does not matter, + // they will be put into the right order in the returned + // OnDiskVolume + Structure: []disks.Partition{ + { + KernelDeviceNode: "/dev/node2", + StartInBytes: 4096 * 512, + SizeInBytes: 2457600 * 512, + PartitionType: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + PartitionUUID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", + PartitionLabel: "Recovery", + Major: 42, + Minor: 2, + StructureIndex: 2, + FilesystemType: "vfat", + FilesystemUUID: "A644-B807", + // The filesystem label will be properly decoded + FilesystemLabel: "ubuntu\x20seed", + }, + { + KernelDeviceNode: "/dev/node1", + StartInBytes: 2048 * 512, + SizeInBytes: 2048 * 512, + PartitionType: "21686148-6449-6E6F-744E-656564454649", + PartitionUUID: "2E59D969-52AB-430B-88AC-F83873519F6F", + // the PartitionLabel will be properly decoded + PartitionLabel: "BIOS\x20Boot", + Major: 42, + Minor: 1, + StructureIndex: 1, + }, + }, + }, + } - cmdBlockdev := testutil.MockCommand(c, "blockdev", ` -if [ "$1" == --getss ]; then - # sector size - echo 512 - exit 0 -fi -echo "unexpected cmdline opts: $*" -exit 1 - `) - defer cmdBlockdev.Restore() + restore := disks.MockDeviceNameToDiskMapping(m) + defer restore() dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") c.Assert(err, IsNil) - c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "/dev/node"}, - }) - c.Assert(cmdLsblk.Calls(), DeepEquals, [][]string{ - {"lsblk", "--fs", "--json", "/dev/node1"}, - {"lsblk", "--fs", "--json", "/dev/node2"}, - }) - c.Assert(cmdBlockdev.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getss", "/dev/node"}, - }) - c.Assert(err, IsNil) + c.Assert(dl, DeepEquals, &gadget.OnDiskVolume{ Device: "/dev/node", ID: "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", @@ -193,11 +173,13 @@ Size: 0x100000, Label: "", Type: "21686148-6449-6E6F-744E-656564454649", + ID: "2E59D969-52AB-430B-88AC-F83873519F6F", Filesystem: "", }, StartOffset: 0x100000, Index: 1, }, + Size: 0x100000, Node: "/dev/node1", }, { @@ -205,13 +187,15 @@ VolumeStructure: &gadget.VolumeStructure{ Name: "Recovery", Size: 0x4b000000, - Label: "ubuntu-seed", + Label: "ubuntu seed", Type: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + ID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", Filesystem: "vfat", }, StartOffset: 0x200000, Index: 2, }, + Size: 0x4b000000, Node: "/dev/node2", }, }, @@ -219,36 +203,49 @@ } func (s *ondiskTestSuite) TestDeviceInfoGPT4096SectorSize(c *C) { - cmdSfdisk := testutil.MockCommand(c, "sfdisk", mockSfdiskScriptBiosSeed) - defer cmdSfdisk.Restore() - - cmdLsblk := testutil.MockCommand(c, "lsblk", mockLsblkScriptBiosSeed) - defer cmdLsblk.Restore() + m := map[string]*disks.MockDiskMapping{ + "/dev/node": { + DevNum: "42:0", + DevNode: "/dev/node", + DiskSizeInBytes: (8388574 + 1) * 4096, + DiskSchema: "gpt", + ID: "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", + SectorSizeBytes: 4096, + Structure: []disks.Partition{ + { + KernelDeviceNode: "/dev/node1", + StartInBytes: 2048 * 4096, + SizeInBytes: 2048 * 4096, + PartitionType: "21686148-6449-6E6F-744E-656564454649", + PartitionUUID: "2E59D969-52AB-430B-88AC-F83873519F6F", + PartitionLabel: "BIOS Boot", + Major: 42, + Minor: 1, + StructureIndex: 1, + }, + { + KernelDeviceNode: "/dev/node2", + StartInBytes: 4096 * 4096, + SizeInBytes: 2457600 * 4096, + PartitionType: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + PartitionUUID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", + PartitionLabel: "Recovery", + Major: 42, + Minor: 2, + StructureIndex: 2, + FilesystemType: "vfat", + FilesystemUUID: "A644-B807", + FilesystemLabel: "ubuntu-seed", + }, + }, + }, + } - cmdBlockdev := testutil.MockCommand(c, "blockdev", ` -if [ "$1" == --getss ]; then - # sector size - echo 4096 - exit 0 -fi -echo "unexpected cmdline opts: $*" -exit 1 - `) - defer cmdBlockdev.Restore() + restore := disks.MockDeviceNameToDiskMapping(m) + defer restore() dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") c.Assert(err, IsNil) - c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "/dev/node"}, - }) - c.Assert(cmdLsblk.Calls(), DeepEquals, [][]string{ - {"lsblk", "--fs", "--json", "/dev/node1"}, - {"lsblk", "--fs", "--json", "/dev/node2"}, - }) - c.Assert(cmdBlockdev.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getss", "/dev/node"}, - }) - c.Assert(err, IsNil) c.Assert(dl, DeepEquals, &gadget.OnDiskVolume{ Device: "/dev/node", ID: "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", @@ -263,11 +260,13 @@ Size: 0x800000, Label: "", Type: "21686148-6449-6E6F-744E-656564454649", + ID: "2E59D969-52AB-430B-88AC-F83873519F6F", Filesystem: "", }, StartOffset: 0x800000, Index: 1, }, + Size: 0x800000, Node: "/dev/node1", }, { @@ -277,11 +276,13 @@ Size: 0x258000000, Label: "ubuntu-seed", Type: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + ID: "44C3D5C3-CAE1-4306-83E8-DF437ACDB32F", Filesystem: "vfat", }, StartOffset: 0x1000000, Index: 2, }, + Size: 0x258000000, Node: "/dev/node2", }, }, @@ -289,83 +290,89 @@ } func (s *ondiskTestSuite) TestDeviceInfoMBR(c *C) { - const mockSfdiskWithMBR = ` ->&2 echo "Some warning from sfdisk" -echo '{ - "partitiontable": { - "label": "dos", - "device": "/dev/node", - "unit": "sectors", - "partitions": [ - {"node": "/dev/node1", "start": 4096, "size": 2457600, "type": "c"}, - {"node": "/dev/node2", "start": 2461696, "size": 1048576, "type": "d"}, - {"node": "/dev/node3", "start": 3510272, "size": 1048576, "type": "d"}, - {"node": "/dev/node4", "start": 4558848, "size": 1048576, "type": "d"} - ] - } -}'` - const mockLsblkForMBR = ` -[ "$3" == "/dev/node1" ] && echo '{ - "blockdevices": [ {"name": "node1", "fstype": "vfat", "label": "ubuntu-seed", "uuid": "A644-B807", "mountpoint": null} ] -}' -[ "$3" == "/dev/node2" ] && echo '{ - "blockdevices": [ {"name": "node2", "fstype": "vfat", "label": "ubuntu-boot", "uuid": "A644-B808", "mountpoint": null} ] -}' -[ "$3" == "/dev/node3" ] && echo '{ - "blockdevices": [ {"name": "node3", "fstype": "ext4", "label": "ubuntu-save", "mountpoint": null} ] -}' -[ "$3" == "/dev/node4" ] && echo '{ - "blockdevices": [ {"name": "node3", "fstype": "ext4", "label": "ubuntu-data", "mountpoint": null} ] -}' -exit 0` - - cmdSfdisk := testutil.MockCommand(c, "sfdisk", mockSfdiskWithMBR) - defer cmdSfdisk.Restore() - - cmdLsblk := testutil.MockCommand(c, "lsblk", mockLsblkForMBR) - defer cmdLsblk.Restore() - - cmdBlockdev := testutil.MockCommand(c, "blockdev", ` -if [ "$1" == --getss ]; then - # sector size - echo 512 - exit 0 -elif [ "$1" == --getsz ]; then -# disk size in 512-byte sectors - echo 12345670 - exit 0 -fi -echo "unexpected cmdline opts: $*" -exit 1 - `) - defer cmdBlockdev.Restore() + + m := map[string]*disks.MockDiskMapping{ + "/dev/node": { + DevNum: "42:0", + DevNode: "/dev/node", + DiskSizeInBytes: 12345670 * 512, + DiskSchema: "dos", + ID: "0x1234567", + SectorSizeBytes: 512, + Structure: []disks.Partition{ + { + KernelDeviceNode: "/dev/node1", + StartInBytes: 4096 * 512, + SizeInBytes: 2457600 * 512, + PartitionType: "0C", + PartitionLabel: "ubuntu-seed", + Major: 42, + Minor: 1, + StructureIndex: 1, + FilesystemType: "vfat", + FilesystemUUID: "FF44-B807", + FilesystemLabel: "ubuntu-seed", + }, + { + KernelDeviceNode: "/dev/node2", + StartInBytes: (4096 + 2457600) * 512, + SizeInBytes: 1048576 * 512, + PartitionType: "0D", + PartitionLabel: "ubuntu-boot", + Major: 42, + Minor: 2, + StructureIndex: 2, + FilesystemType: "vfat", + FilesystemUUID: "A644-B807", + FilesystemLabel: "ubuntu-boot", + }, + { + KernelDeviceNode: "/dev/node3", + StartInBytes: (4096 + 2457600 + 1048576) * 512, + SizeInBytes: 1048576 * 512, + PartitionType: "0D", + PartitionLabel: "ubuntu-save", + Major: 42, + Minor: 3, + StructureIndex: 3, + FilesystemType: "ext4", + FilesystemUUID: "8781-433a", + FilesystemLabel: "ubuntu-save", + }, + { + KernelDeviceNode: "/dev/node4", + StartInBytes: (4096 + 2457600 + 1048576 + 1048576) * 512, + SizeInBytes: 1048576 * 512, + PartitionType: "0D", + PartitionLabel: "ubuntu-data", + Major: 42, + Minor: 4, + StructureIndex: 4, + FilesystemType: "ext4", + FilesystemUUID: "8123-433a", + FilesystemLabel: "ubuntu-data", + }, + }, + }, + } + + restore := disks.MockDeviceNameToDiskMapping(m) + defer restore() dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") c.Assert(err, IsNil) - c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "/dev/node"}, - }) - c.Assert(cmdLsblk.Calls(), DeepEquals, [][]string{ - {"lsblk", "--fs", "--json", "/dev/node1"}, - {"lsblk", "--fs", "--json", "/dev/node2"}, - {"lsblk", "--fs", "--json", "/dev/node3"}, - {"lsblk", "--fs", "--json", "/dev/node4"}, - }) - c.Assert(cmdBlockdev.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getss", "/dev/node"}, - {"blockdev", "--getsz", "/dev/node"}, - }) - c.Assert(err, IsNil) c.Assert(dl, DeepEquals, &gadget.OnDiskVolume{ Device: "/dev/node", Schema: "dos", + ID: "0x1234567", SectorSize: quantity.Size(512), Size: quantity.Size(12345670 * 512), Structure: []gadget.OnDiskStructure{ { LaidOutStructure: gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ + Name: "ubuntu-seed", Size: 2457600 * 512, Label: "ubuntu-seed", Type: "0C", @@ -374,11 +381,13 @@ StartOffset: 4096 * 512, Index: 1, }, + Size: 2457600 * 512, Node: "/dev/node1", }, { LaidOutStructure: gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ + Name: "ubuntu-boot", Size: 1048576 * 512, Label: "ubuntu-boot", Type: "0D", @@ -387,11 +396,13 @@ StartOffset: (4096 + 2457600) * 512, Index: 2, }, + Size: 1048576 * 512, Node: "/dev/node2", }, { LaidOutStructure: gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ + Name: "ubuntu-save", Size: 1048576 * 512, Label: "ubuntu-save", Type: "0D", @@ -400,11 +411,13 @@ StartOffset: (4096 + 2457600 + 1048576) * 512, Index: 3, }, + Size: 1048576 * 512, Node: "/dev/node3", }, { LaidOutStructure: gadget.LaidOutStructure{ VolumeStructure: &gadget.VolumeStructure{ + Name: "ubuntu-data", Size: 1048576 * 512, Label: "ubuntu-data", Type: "0D", @@ -413,396 +426,9 @@ StartOffset: (4096 + 2457600 + 1048576 + 1048576) * 512, Index: 4, }, + Size: 1048576 * 512, Node: "/dev/node4", }, }, }) } - -func (s *ondiskTestSuite) TestDeviceInfoMBR4096SectorSize(c *C) { - const mockSfdiskWithMBR = ` ->&2 echo "Some warning from sfdisk" -echo '{ - "partitiontable": { - "label": "dos", - "device": "/dev/node", - "unit": "sectors", - "partitions": [ - {"node": "/dev/node1", "start":256, "size":2560, "type": "c"}, - {"node": "/dev/node2", "start":2816, "size":2560, "type": "d"}, - {"node": "/dev/node3", "start":5376, "size":128000, "type": "d"}, - {"node": "/dev/node4", "start":133376, "size":6202079, "type": "d"} - ] - } -}'` - const mockLsblkForMBR = ` -[ "$3" == "/dev/node1" ] && echo '{ - "blockdevices": [ {"name": "node1", "fstype": "vfat", "label": "ubuntu-seed", "uuid": "A644-B807", "mountpoint": null} ] -}' -[ "$3" == "/dev/node2" ] && echo '{ - "blockdevices": [ {"name": "node2", "fstype": "vfat", "label": "ubuntu-boot", "uuid": "A644-B808", "mountpoint": null} ] -}' -[ "$3" == "/dev/node3" ] && echo '{ - "blockdevices": [ {"name": "node3", "fstype": "ext4", "label": "ubuntu-save", "mountpoint": null} ] -}' -[ "$3" == "/dev/node4" ] && echo '{ - "blockdevices": [ {"name": "node3", "fstype": "ext4", "label": "ubuntu-data", "mountpoint": null} ] -}' -exit 0` - - cmdSfdisk := testutil.MockCommand(c, "sfdisk", mockSfdiskWithMBR) - defer cmdSfdisk.Restore() - - cmdLsblk := testutil.MockCommand(c, "lsblk", mockLsblkForMBR) - defer cmdLsblk.Restore() - - cmdBlockdev := testutil.MockCommand(c, "blockdev", ` -if [ "$1" == --getss ]; then - # sector size - echo 4096 - exit 0 -elif [ "$1" == --getsz ]; then - # disk size in 512-byte sectors - echo 50683904 - exit 0 -fi -echo "unexpected cmdline opts: $*" -exit 1 - `) - defer cmdBlockdev.Restore() - - dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") - c.Assert(err, IsNil) - c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "/dev/node"}, - }) - c.Assert(cmdLsblk.Calls(), DeepEquals, [][]string{ - {"lsblk", "--fs", "--json", "/dev/node1"}, - {"lsblk", "--fs", "--json", "/dev/node2"}, - {"lsblk", "--fs", "--json", "/dev/node3"}, - {"lsblk", "--fs", "--json", "/dev/node4"}, - }) - c.Assert(cmdBlockdev.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getss", "/dev/node"}, - {"blockdev", "--getsz", "/dev/node"}, - }) - c.Assert(err, IsNil) - - c.Assert(dl, DeepEquals, &gadget.OnDiskVolume{ - Device: "/dev/node", - Schema: "dos", - SectorSize: quantity.Size(4096), - Size: quantity.Size(6335488 * 4096), - Structure: []gadget.OnDiskStructure{ - { - LaidOutStructure: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Size: 2560 * 4096, - Label: "ubuntu-seed", - Type: "0C", - Filesystem: "vfat", - }, - StartOffset: 256 * 4096, - Index: 1, - }, - Node: "/dev/node1", - }, - { - LaidOutStructure: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Size: 2560 * 4096, - Label: "ubuntu-boot", - Type: "0D", - Filesystem: "vfat", - }, - StartOffset: (256 + 2560) * 4096, - Index: 2, - }, - Node: "/dev/node2", - }, - { - LaidOutStructure: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Size: 128000 * 4096, - Label: "ubuntu-save", - Type: "0D", - Filesystem: "ext4", - }, - StartOffset: (256 + 2560 + 2560) * 4096, - Index: 3, - }, - Node: "/dev/node3", - }, - { - LaidOutStructure: gadget.LaidOutStructure{ - VolumeStructure: &gadget.VolumeStructure{ - Size: 6202079 * 4096, - Label: "ubuntu-data", - Type: "0D", - Filesystem: "ext4", - }, - StartOffset: (256 + 2560 + 2560 + 128000) * 4096, - Index: 4, - }, - Node: "/dev/node4", - }, - }, - }) -} - -func (s *ondiskTestSuite) TestDeviceInfoNotSectors(c *C) { - cmdSfdisk := testutil.MockCommand(c, "sfdisk", `echo '{ - "partitiontable": { - "label": "gpt", - "id": "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", - "device": "/dev/node", - "unit": "not_sectors", - "firstlba": 34, - "lastlba": 8388574, - "partitions": [ - {"node": "/dev/node1", "start": 2048, "size": 2048, "type": "21686148-6449-6E6F-744E-656564454649", "uuid": "2E59D969-52AB-430B-88AC-F83873519F6F", "name": "BIOS Boot"} - ] - } -}'`) - defer cmdSfdisk.Restore() - - _, err := gadget.OnDiskVolumeFromDevice("/dev/node") - c.Assert(err, ErrorMatches, "cannot position partitions: unknown unit .*") -} - -func (s *ondiskTestSuite) TestDeviceInfoSectorSizeNotMultiple512Unhappy(c *C) { - cmdSfdisk := testutil.MockCommand(c, "sfdisk", `echo '{ - "partitiontable": { - "label": "gpt", - "id": "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", - "device": "/dev/node", - "unit": "sectors", - "firstlba": 34, - "lastlba": 8388574, - "partitions": [ - {"node": "/dev/node1", "start": 2048, "size": 2048, "type": "21686148-6449-6E6F-744E-656564454649", "uuid": "2E59D969-52AB-430B-88AC-F83873519F6F", "name": "BIOS Boot"} - ] - } -}'`) - defer cmdSfdisk.Restore() - - cmdBlockdev := testutil.MockCommand(c, "blockdev", ` -if [ "$1" == --getss ]; then - # sector size - echo 513 - exit 0 -fi -echo "unexpected cmdline opts: $*" -exit 1 -`) - defer cmdBlockdev.Restore() - - _, err := gadget.OnDiskVolumeFromDevice("/dev/node") - c.Assert(err, ErrorMatches, `cannot calculate structure size: sector size \(513\) is not a multiple of 512`) - - c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "/dev/node"}, - }) - - c.Assert(cmdBlockdev.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getss", "/dev/node"}, - }) - -} - -func (s *ondiskTestSuite) TestDeviceInfoFilesystemInfoError(c *C) { - cmdSfdisk := testutil.MockCommand(c, "sfdisk", `echo '{ - "partitiontable": { - "label": "gpt", - "id": "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", - "device": "/dev/node", - "unit": "sectors", - "firstlba": 34, - "lastlba": 8388574, - "partitions": [ - {"node": "/dev/node1", "start": 2048, "size": 2048, "type": "21686148-6449-6E6F-744E-656564454649", "uuid": "2E59D969-52AB-430B-88AC-F83873519F6F", "name": "BIOS Boot"} - ] - } -}'`) - defer cmdSfdisk.Restore() - - cmdBlockdev := testutil.MockCommand(c, "blockdev", ` -if [ "$1" == --getss ]; then - # sector size - echo 512 - exit 0 -fi -echo "unexpected cmdline opts: $*" -exit 1 -`) - defer cmdBlockdev.Restore() - - cmdLsblk := testutil.MockCommand(c, "lsblk", "echo lsblk error; exit 1") - defer cmdLsblk.Restore() - - _, err := gadget.OnDiskVolumeFromDevice("/dev/node") - c.Assert(err, ErrorMatches, "cannot obtain filesystem information: lsblk error") - - c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "/dev/node"}, - }) - - c.Assert(cmdBlockdev.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getss", "/dev/node"}, - }) - - c.Assert(cmdLsblk.Calls(), DeepEquals, [][]string{ - {"lsblk", "--fs", "--json", "/dev/node1"}, - }) -} - -func (s *ondiskTestSuite) TestDeviceInfoJsonError(c *C) { - cmd := testutil.MockCommand(c, "sfdisk", `echo 'This is not a json'`) - defer cmd.Restore() - - dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") - c.Assert(err, ErrorMatches, "cannot parse sfdisk output: invalid .*") - c.Assert(dl, IsNil) -} - -func (s *ondiskTestSuite) TestDeviceInfoError(c *C) { - cmd := testutil.MockCommand(c, "sfdisk", "echo 'sfdisk: not found'; exit 127") - defer cmd.Restore() - - dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") - c.Assert(err, ErrorMatches, "sfdisk: not found") - c.Assert(dl, IsNil) -} - -func (s *ondiskTestSuite) TestUpdatePartitionList(c *C) { - const mockSfdiskScriptBios = ` ->&2 echo "Some warning from sfdisk" -echo '{ - "partitiontable": { - "label": "gpt", - "id": "9151F25B-CDF0-48F1-9EDE-68CBD616E2CA", - "device": "/dev/node", - "unit": "sectors", - "firstlba": 34, - "lastlba": 8388574, - "partitions": [ - { - "node": "/dev/node1", - "start": 2048, - "size": 2048, - "type": "21686148-6449-6E6F-744E-656564454649", - "uuid": "2E59D969-52AB-430B-88AC-F83873519F6F", - "name": "BIOS Boot" - } - ] - } -}'` - - const mockLsblkScriptBios = ` -[ "$3" == "/dev/node1" ] && echo '{ - "blockdevices": [ {"name": "node1", "fstype": null, "label": null, "uuid": null, "mountpoint": null} ] -}' -exit 0` - - // sector size is same for all calls - cmdBlockdev := testutil.MockCommand(c, "blockdev", ` -if [ "$1" == --getss ]; then - # sector size - echo 512 - exit 0 -fi -echo "unexpected cmdline opts: $*" -exit 1 -`) - defer cmdBlockdev.Restore() - - // start with a single partition - cmdSfdisk := testutil.MockCommand(c, "sfdisk", mockSfdiskScriptBios) - defer cmdSfdisk.Restore() - - cmdLsblk := testutil.MockCommand(c, "lsblk", mockLsblkScriptBios) - defer cmdLsblk.Restore() - - dl, err := gadget.OnDiskVolumeFromDevice("/dev/node") - c.Assert(err, IsNil) - - c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "/dev/node"}, - }) - - c.Assert(cmdLsblk.Calls(), DeepEquals, [][]string{ - {"lsblk", "--fs", "--json", "/dev/node1"}, - }) - - c.Assert(len(dl.Structure), Equals, 1) - c.Assert(dl.Structure[0].Node, Equals, "/dev/node1") - - // add a partition - cmdSfdisk = testutil.MockCommand(c, "sfdisk", mockSfdiskScriptBiosSeed) - defer cmdSfdisk.Restore() - - cmdLsblk = testutil.MockCommand(c, "lsblk", mockLsblkScriptBiosSeed) - defer cmdLsblk.Restore() - - // update the partition list - err = gadget.UpdatePartitionList(dl) - c.Assert(err, IsNil) - - c.Assert(cmdBlockdev.Calls(), DeepEquals, [][]string{ - {"blockdev", "--getss", "/dev/node"}, - {"blockdev", "--getss", "/dev/node"}, - }) - - c.Assert(cmdSfdisk.Calls(), DeepEquals, [][]string{ - {"sfdisk", "--json", "/dev/node"}, - }) - - c.Assert(cmdLsblk.Calls(), DeepEquals, [][]string{ - {"lsblk", "--fs", "--json", "/dev/node1"}, - {"lsblk", "--fs", "--json", "/dev/node2"}, - }) - - // check if the partition list was updated - c.Assert(len(dl.Structure), Equals, 2) - c.Assert(dl.Structure[0].Node, Equals, "/dev/node1") - c.Assert(dl.Structure[1].Node, Equals, "/dev/node2") -} - -func (s *ondiskTestSuite) TestFilesystemInfo(c *C) { - cmd := testutil.MockCommand(c, "lsblk", `echo '{ - "blockdevices": [ - {"name": "loop8p2", "fstype": "vfat", "label": "ubuntu-seed", "uuid": "C1F4-CE43", "mountpoint": null} - ] -}'`) - defer cmd.Restore() - - info, err := gadget.FilesystemInfo("/dev/node") - c.Assert(cmd.Calls(), DeepEquals, [][]string{ - {"lsblk", "--fs", "--json", "/dev/node"}, - }) - c.Assert(err, IsNil) - c.Assert(len(info.BlockDevices), Equals, 1) - bd := info.BlockDevices[0] - c.Assert(bd.Name, Equals, "loop8p2") - c.Assert(bd.FSType, Equals, "vfat") - c.Assert(bd.Label, Equals, "ubuntu-seed") - c.Assert(bd.UUID, Equals, "C1F4-CE43") -} - -func (s *ondiskTestSuite) TestFilesystemInfoJsonError(c *C) { - cmd := testutil.MockCommand(c, "lsblk", `echo 'This is not a json'`) - defer cmd.Restore() - - info, err := gadget.FilesystemInfo("/dev/node") - c.Assert(err, ErrorMatches, "cannot parse lsblk output: invalid .*") - c.Assert(info, IsNil) -} - -func (s *ondiskTestSuite) TestFilesystemInfoError(c *C) { - cmd := testutil.MockCommand(c, "lsblk", "echo 'lsblk: not found'; exit 127") - defer cmd.Restore() - - info, err := gadget.FilesystemInfo("/dev/node") - c.Assert(err, ErrorMatches, "lsblk: not found") - c.Assert(info, IsNil) -} diff -Nru snapd-2.53+21.10ubuntu1/gadget/update.go snapd-2.54.2+21.10/gadget/update.go --- snapd-2.53+21.10ubuntu1/gadget/update.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/update.go 2022-01-06 21:25:16.000000000 +0000 @@ -123,6 +123,209 @@ Canceled() error } +// IsCreatableAtInstall returns whether the gadget structure would be created at +// install - currently that is only ubuntu-save, ubuntu-data, and ubuntu-boot +func IsCreatableAtInstall(gv *VolumeStructure) bool { + // a structure is creatable at install if it is one of the roles for + // system-save, system-data, or system-boot + switch gv.Role { + case SystemSave, SystemData, SystemBoot: + return true + default: + return false + } +} + +func isCompatibleSchema(gadgetSchema, diskSchema string) bool { + switch gadgetSchema { + // XXX: "mbr,gpt" is currently unsupported + case "", "gpt": + return diskSchema == "gpt" + case "mbr": + return diskSchema == "dos" + default: + return false + } +} + +type EnsureLayoutCompatibilityOptions struct { + AssumeCreatablePartitionsCreated bool +} + +func EnsureLayoutCompatibility(gadgetLayout *LaidOutVolume, diskLayout *OnDiskVolume, opts *EnsureLayoutCompatibilityOptions) error { + if opts == nil { + opts = &EnsureLayoutCompatibilityOptions{} + } + eq := func(ds OnDiskStructure, gs LaidOutStructure) (bool, string) { + dv := ds.VolumeStructure + gv := gs.VolumeStructure + nameMatch := gv.Name == dv.Name + if gadgetLayout.Schema == "mbr" { + // partitions have no names in MBR so bypass the name check + nameMatch = true + } + // Previous installation may have failed before filesystem creation or + // partition may be encrypted, so if the on disk offset matches the + // gadget offset, and the gadget structure is creatable during install, + // then they are equal + // otherwise, if they are not created during installation, the + // filesystem must be the same + check := nameMatch && ds.StartOffset == gs.StartOffset + // if we require creatable partitions to already exist, then the + // filesystems must also match for creatable partitions + if opts.AssumeCreatablePartitionsCreated || !IsCreatableAtInstall(gv) { + check = check && (dv.Filesystem == gv.Filesystem) + } + sizeMatches := dv.Size == gv.Size + if gv.Role == SystemData { + // system-data may have been expanded + sizeMatches = dv.Size >= gv.Size + } + if check && sizeMatches { + return true, "" + } + + switch { + case !nameMatch: + // don't return a reason if the names don't match + return false, "" + case ds.StartOffset != gs.StartOffset: + return false, fmt.Sprintf("start offsets do not match (disk: %d (%s) and gadget: %d (%s))", + ds.StartOffset, ds.StartOffset.IECString(), gs.StartOffset, gs.StartOffset.IECString()) + case opts.AssumeCreatablePartitionsCreated && IsCreatableAtInstall(gv) && dv.Filesystem != gv.Filesystem: + return false, "filesystems do not match" + case !IsCreatableAtInstall(gv) && dv.Filesystem != gv.Filesystem: + return false, "filesystems do not match and the partition is not creatable at install" + case dv.Size < gv.Size: + return false, fmt.Sprintf("on disk size %d (%s) is smaller than gadget size %d (%s)", + dv.Size, dv.Size.IECString(), gv.Size, gv.Size.IECString()) + case gv.Role != SystemData && dv.Size > gv.Size: + return false, fmt.Sprintf("on disk size %d (%s) is larger than gadget size %d (%s) (and the role should not be expanded)", + dv.Size, dv.Size.IECString(), gv.Size, gv.Size.IECString()) + default: + return false, "some other logic condition (should be impossible?)" + } + } + + laidOutContains := func(haystack []LaidOutStructure, needle OnDiskStructure) (bool, string) { + reasonAbsent := "" + for _, h := range haystack { + matches, reasonNotMatches := eq(needle, h) + if matches { + return true, "" + } + // TODO: handle multiple error cases for DOS disks and fail early + // for GPT disks since we should not have multiple non-empty reasons + // for not matching for GPT disks, as that would require two YAML + // structures with the same name to be considered as candidates for + // a given on disk structure, and we do not allow duplicated + // structure names in the YAML at all via ValidateVolume. + // + // For DOS, since we cannot check the partition names, there will + // always be a reason if there was not a match, in which case we + // only want to return an error after we have finished searching the + // full haystack and didn't find any matches whatsoever. Note that + // the YAML structure that "should" have matched the on disk one we + // are looking for but doesn't because of some problem like wrong + // size or wrong filesystem may not be the last one, so returning + // only the last error like we do here is wrong. We should include + // all reasons why so the user can see which structure was the + // "closest" to what we were searching for so they can fix their + // gadget.yaml or on disk partitions so that it matches. + if reasonNotMatches != "" { + reasonAbsent = reasonNotMatches + } + } + return false, reasonAbsent + } + + onDiskContains := func(haystack []OnDiskStructure, needle LaidOutStructure) (bool, string) { + reasonAbsent := "" + for _, h := range haystack { + matches, reasonNotMatches := eq(h, needle) + if matches { + return true, "" + } + // this has the effect of only returning the last non-empty reason + // string + if reasonNotMatches != "" { + reasonAbsent = reasonNotMatches + } + } + return false, reasonAbsent + } + + // check size of volumes + lastUsableByte := quantity.Size(diskLayout.UsableSectorsEnd) * diskLayout.SectorSize + if gadgetLayout.Size > lastUsableByte { + return fmt.Errorf("device %v (last usable byte at %s) is too small to fit the requested layout (%s)", diskLayout.Device, + lastUsableByte.IECString(), gadgetLayout.Size.IECString()) + } + + // check that the sizes of all structures in the gadget are multiples of + // the disk sector size (unless the structure is the MBR) + for _, ls := range gadgetLayout.LaidOutStructure { + if !IsRoleMBR(ls) { + if ls.Size%diskLayout.SectorSize != 0 { + return fmt.Errorf("gadget volume structure %v size is not a multiple of disk sector size %v", + ls, diskLayout.SectorSize) + } + } + } + + // Check if top level properties match + if !isCompatibleSchema(gadgetLayout.Volume.Schema, diskLayout.Schema) { + return fmt.Errorf("disk partitioning schema %q doesn't match gadget schema %q", diskLayout.Schema, gadgetLayout.Volume.Schema) + } + if gadgetLayout.Volume.ID != "" && gadgetLayout.Volume.ID != diskLayout.ID { + return fmt.Errorf("disk ID %q doesn't match gadget volume ID %q", diskLayout.ID, gadgetLayout.Volume.ID) + } + + // Check if all existing device partitions are also in gadget + for _, ds := range diskLayout.Structure { + present, reasonAbsent := laidOutContains(gadgetLayout.LaidOutStructure, ds) + if !present { + if reasonAbsent != "" { + // use the right format so that it can be + // appended to the error message + reasonAbsent = fmt.Sprintf(": %s", reasonAbsent) + } + return fmt.Errorf("cannot find disk partition %s (starting at %d) in gadget%s", ds.Node, ds.StartOffset, reasonAbsent) + } + } + + // check all structures in the layout are present in the gadget, or have a + // valid excuse for absence (i.e. mbr or creatable structures at install) + for _, gs := range gadgetLayout.LaidOutStructure { + // we ignore reasonAbsent here since if there was an extra on disk + // structure that didn't match something in the YAML, we would have + // caught it above, this loop can only ever identify structures in the + // YAML that are not on disk at all + if present, _ := onDiskContains(diskLayout.Structure, gs); present { + continue + } + + // otherwise not present, figure out if it has a valid excuse + + if !gs.IsPartition() { + // raw structures like mbr or other "bare" type will not be + // identified by linux and thus should be skipped as they will not + // show up on the disk + continue + } + + // allow structures that are creatable during install if we don't assume + // created partitions to already exist + if IsCreatableAtInstall(gs.VolumeStructure) && !opts.AssumeCreatablePartitionsCreated { + continue + } + + return fmt.Errorf("cannot find gadget structure %s on disk", gs.String()) + } + + return nil +} + // Update applies the gadget update given the gadget information and data from // old and new revisions. It errors out when the update is not possible or // illegal, or a failure occurs at any of the steps. When there is no update, a @@ -158,7 +361,7 @@ // new kernel (rule 1) // d. After step (c) is completed the kernel refresh will now also // work (no more violation of rule 1) -func Update(old, new GadgetData, rollbackDirPath string, updatePolicy UpdatePolicyFunc, observer ContentUpdateObserver) error { +func Update(model Model, old, new GadgetData, rollbackDirPath string, updatePolicy UpdatePolicyFunc, observer ContentUpdateObserver) error { // TODO: support multi-volume gadgets. But for now we simply // do not do any gadget updates on those. We cannot error // here because this would break refreshes of gadgets even diff -Nru snapd-2.53+21.10ubuntu1/gadget/update_test.go snapd-2.54.2+21.10/gadget/update_test.go --- snapd-2.53+21.10ubuntu1/gadget/update_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/gadget/update_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -756,7 +756,7 @@ defer restore() // go go go - err := gadget.Update(oldData, newData, rollbackDir, nil, muo) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, muo) c.Assert(err, IsNil) c.Assert(backupCalls, DeepEquals, map[string]bool{ "first": true, @@ -808,7 +808,7 @@ defer restore() // go go go - err := gadget.Update(oldData, newData, rollbackDir, nil, muo) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, muo) c.Assert(err, IsNil) c.Assert(muo.beforeWriteCalled, Equals, 1) @@ -855,13 +855,13 @@ // both old and new bare struct data is missing // cannot lay out the new volume when bare struct data is missing - err := gadget.Update(oldData, newData, rollbackDir, nil, nil) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, nil) c.Assert(err, ErrorMatches, `cannot lay out the new volume: cannot lay out structure #0 \("foo"\): content "first.img": .* no such file or directory`) makeSizedFile(c, filepath.Join(newRootDir, "first.img"), quantity.SizeMiB, nil) // Update does not error out when when the bare struct data of the old volume is missing - err = gadget.Update(oldData, newData, rollbackDir, nil, nil) + err = gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, nil) c.Assert(err, Equals, gadget.ErrNoUpdate) } @@ -908,7 +908,7 @@ makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), quantity.SizeMiB, nil) makeSizedFile(c, filepath.Join(newRootDir, "first.img"), 900*quantity.SizeKiB, nil) - err := gadget.Update(oldData, newData, rollbackDir, nil, nil) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, nil) c.Assert(err, ErrorMatches, `cannot apply update to volume: cannot change the number of structures within volume from 1 to 2`) } @@ -959,7 +959,7 @@ makeSizedFile(c, filepath.Join(oldRootDir, "first.img"), quantity.SizeMiB, nil) - err := gadget.Update(oldData, newData, rollbackDir, nil, nil) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, nil) c.Assert(err, ErrorMatches, `cannot update volume structure #0 \("foo"\): cannot change a bare structure to filesystem one`) } @@ -998,7 +998,7 @@ }) defer restore() - err := gadget.Update(oldData, newData, rollbackDir, nil, nil) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, nil) c.Assert(err, ErrorMatches, `cannot find entry for volume "foo" in updated gadget info`) } @@ -1044,7 +1044,7 @@ }) defer restore() - err := gadget.Update(oldData, newData, rollbackDir, nil, muo) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, muo) c.Assert(err, Equals, gadget.ErrNoUpdate) // nothing was updated @@ -1101,7 +1101,7 @@ defer restore() policySeen := map[string]int{} - err := gadget.Update(oldData, newData, rollbackDir, func(_, to *gadget.LaidOutStructure) (bool, gadget.ResolvedContentFilterFunc) { + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, func(_, to *gadget.LaidOutStructure) (bool, gadget.ResolvedContentFilterFunc) { policySeen[to.Name]++ return false, nil }, nil) @@ -1117,7 +1117,7 @@ // try with different policy policySeen = map[string]int{} - err = gadget.Update(oldData, newData, rollbackDir, func(_, to *gadget.LaidOutStructure) (bool, gadget.ResolvedContentFilterFunc) { + err = gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, func(_, to *gadget.LaidOutStructure) (bool, gadget.ResolvedContentFilterFunc) { policySeen[to.Name]++ return to.Name == "second", nil }, nil) @@ -1151,7 +1151,7 @@ }) defer restore() - err := gadget.Update(oldData, newData, rollbackDir, gadget.RemodelUpdatePolicy, nil) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, gadget.RemodelUpdatePolicy, nil) c.Assert(err, IsNil) c.Assert(toUpdate, DeepEquals, map[string]int{ "first": 1, @@ -1194,7 +1194,7 @@ defer restore() // go go go - err := gadget.Update(oldData, newData, rollbackDir, nil, muo) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, muo) c.Assert(err, ErrorMatches, `cannot backup volume structure #1 \("second"\): failed`) // update was canceled before backup pass completed @@ -1243,7 +1243,7 @@ defer restore() // go go go - err := gadget.Update(oldData, newData, rollbackDir, nil, muo) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, muo) c.Assert(err, ErrorMatches, `cannot update volume structure #1 \("second"\): failed`) c.Assert(backupCalls, DeepEquals, map[string]bool{ // all were backed up @@ -1318,7 +1318,7 @@ defer restore() // go go go - err := gadget.Update(oldData, newData, rollbackDir, nil, nil) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, nil) // preserves update error c.Assert(err, ErrorMatches, `cannot update volume structure #2 \("third"\): update error`) c.Assert(backupCalls, DeepEquals, map[string]bool{ @@ -1355,7 +1355,7 @@ defer restore() // go go go - err := gadget.Update(oldData, newData, rollbackDir, nil, nil) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, nil) c.Assert(err, ErrorMatches, `cannot prepare update for volume structure #0 \("first"\): bad updater for structure`) } @@ -1432,13 +1432,13 @@ } // a new multi volume gadget update gives no error - err := gadget.Update(singleVolume, multiVolume, "some-rollback-dir", nil, nil) + err := gadget.Update(&modelCharateristics{}, singleVolume, multiVolume, "some-rollback-dir", nil, nil) c.Assert(err, IsNil) // but it warns that nothing happens either c.Assert(logbuf.String(), testutil.Contains, "WARNING: gadget assests cannot be updated yet when multiple volumes are used") // same for old - err = gadget.Update(multiVolume, singleVolume, "some-rollback-dir", nil, nil) + err = gadget.Update(&modelCharateristics{}, multiVolume, singleVolume, "some-rollback-dir", nil, nil) c.Assert(err, IsNil) c.Assert(strings.Count(logbuf.String(), "WARNING: gadget assests cannot be updated yet when multiple volumes are used"), Equals, 2) } @@ -1472,7 +1472,7 @@ defer restore() // go go go - err := gadget.Update(oldData, newData, rollbackDir, nil, muo) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, muo) c.Assert(err, Equals, gadget.ErrNoUpdate) // update called for 2 structures c.Assert(updateCalls, Equals, 2) @@ -1513,7 +1513,7 @@ defer restore() // go go go - err := gadget.Update(oldData, newData, rollbackDir, nil, muo) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, muo) c.Assert(err, IsNil) // update called for 2 structures c.Assert(updateCalls, Equals, 2) @@ -1541,7 +1541,7 @@ muo := &mockUpdateProcessObserver{ beforeWriteErr: errors.New("before write fail"), } - err := gadget.Update(oldData, newData, rollbackDir, nil, muo) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, muo) c.Assert(err, ErrorMatches, `cannot observe prepared update: before write fail`) // update was canceled before backup pass completed c.Check(muo.canceledCalled, Equals, 0) @@ -1570,7 +1570,7 @@ muo := &mockUpdateProcessObserver{ canceledErr: errors.New("canceled fail"), } - err := gadget.Update(oldData, newData, rollbackDir, nil, muo) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, muo) c.Assert(err, ErrorMatches, `cannot backup volume structure #0 .*: backup fails`) // canceled called after backup pass c.Check(muo.canceledCalled, Equals, 1) @@ -1580,7 +1580,7 @@ // backup works, update fails, triggers another canceled call backupErr = nil - err = gadget.Update(oldData, newData, rollbackDir, nil, muo) + err = gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, nil, muo) c.Assert(err, ErrorMatches, `cannot update volume structure #0 .*: update fails`) // canceled called after backup pass c.Check(muo.canceledCalled, Equals, 2) @@ -1758,7 +1758,7 @@ defer restore() // exercise KernelUpdatePolicy here - err := gadget.Update(oldData, newData, rollbackDir, gadget.KernelUpdatePolicy, muo) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, gadget.KernelUpdatePolicy, muo) c.Assert(err, IsNil) // ensure update for kernel content happened @@ -1816,7 +1816,7 @@ defer restore() // exercise KernelUpdatePolicy here - err := gadget.Update(oldData, newData, rollbackDir, gadget.KernelUpdatePolicy, muo) + err := gadget.Update(&modelCharateristics{}, oldData, newData, rollbackDir, gadget.KernelUpdatePolicy, muo) c.Assert(err, ErrorMatches, `gadget does not consume any of the kernel assets needing synced update "ref"`) // ensure update for kernel content didn't happen diff -Nru snapd-2.53+21.10ubuntu1/.github/workflows/test.yaml snapd-2.54.2+21.10/.github/workflows/test.yaml --- snapd-2.53+21.10ubuntu1/.github/workflows/test.yaml 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/.github/workflows/test.yaml 2022-01-06 21:25:16.000000000 +0000 @@ -44,9 +44,19 @@ uses: snapcore/action-build@v1 with: snapcraft-channel: 4.x/candidate - - name: Cache built artifact + - name: Cache and check built artifact run: | mkdir -p $(dirname "$CACHE_RESULT_STAMP") + unsquashfs snapd*.snap meta/snap.yaml usr/lib/snapd/info + if cat squashfs-root/meta/snap.yaml | grep -q "version:.*dirty.*"; then + echo "PR produces dirty snapd snap version" + cat squashfs-root/usr/lib/snapd/dirty-git-tree-info.txt + exit 1 + elif cat squashfs-root/usr/lib/snapd/info | grep -q "VERSION=.*dirty.*"; then + echo "PR produces dirty internal snapd info version" + cat squashfs-root/usr/lib/snapd/info + exit 1 + fi cp -v *.snap "$(dirname $CACHE_RESULT_STAMP)/" - name: Uploading snapd snap artifact uses: actions/upload-artifact@v2 @@ -66,7 +76,7 @@ # to use the go snap automatically. Note that we install go from the # snap in a step below. Without this we get the GitHub-controlled latest # version of go. - PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games + PATH: /snap/bin:/usr/sbin:/usr/bin:/sbin:/bin:/usr/games:${{ github.workspace }}/bin GOROOT: "" GITHUB_PULL_REQUEST: ${{ github.event.number }} strategy: @@ -181,6 +191,10 @@ if: steps.cached-results.outputs.already-ran != 'true' run: | cd ${{ github.workspace }}/src/github.com/snapcore/snapd/cmd/ && make check + - name: Reset code coverage data + if: steps.cached-results.outputs.already-ran != 'true' + run: | + rm -rf ${{ github.workspace }}/.coverage/ - name: Test Go if: steps.cached-results.outputs.already-ran != 'true' run: | @@ -206,17 +220,23 @@ run: | cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 SKIP_DIRTY_CHECK=1 GO_BUILD_TAGS=faultinject ./run-checks --unit + - name: Merge coverage results + if: ${{ steps.cached-results.outputs.already-ran != 'true' && matrix.gochannel != 'latest/stable' }} + run: | + cd ${{ github.workspace }}/src/github.com/snapcore/snapd || exit 1 + go install github.com/makholm/covertool + covertool merge -o .coverage/all.cov .coverage/coverage*.cov - name: Upload coverage to Codecov uses: codecov/codecov-action@v2 # uploading to codecov occasionally fails, so continue running the test # workflow regardless of the upload continue-on-error: true - if: steps.cached-results.outputs.already-ran != 'true' + if: ${{ steps.cached-results.outputs.already-ran != 'true' && matrix.gochannel != 'latest/stable' }} with: fail_ci_if_error: true flags: unittests name: codecov-umbrella - files: .coverage/coverage.out + files: .coverage/all.cov verbose: true - name: Cache successful run run: | @@ -243,10 +263,10 @@ - centos-7-64 - centos-8-64 - debian-10-64 + - debian-11-64 - debian-sid-64 - - fedora-33-64 - fedora-34-64 - - opensuse-15.2-64 + - fedora-35-64 - opensuse-15.3-64 - opensuse-tumbleweed-64 - ubuntu-14.04-64 @@ -292,7 +312,10 @@ run: | # Register a problem matcher to highlight spread failures echo "::add-matcher::.github/spread-problem-matcher.json" - spread -abend google:${{ matrix.system }}:tests/... + # "pipefail" ensures that a non-zero status from the spread is + # propagated; and we use a subshell as this option could trigger + # undesired changes elsewhere + (set -o pipefail; spread -abend google:${{ matrix.system }}:tests/... | tee spread.log) - name: Cache successful run run: | mkdir -p $(dirname "$CACHE_RESULT_STAMP") @@ -304,6 +327,21 @@ for r in .spread-reuse.*.yaml; do spread -discard -reuse-pid="$(echo "$r" | grep -o -E '[0-9]+')"; done + - name: report spread errors + if: always() + run: | + if [ -e spread.log ]; then + echo "Running spread log analyzer" + ./tests/lib/tools/log-parser spread.log --print-detail error-debug --output spread-results.json --cut 100 + while IFS= read -r line; do + if [ ! -z "$line" ]; then + echo "Reporting spread error..." + ./tests/lib/tools/report-mongodb --db-name snapd --db-collection spread_errors "$line" + fi + done <<< $(jq -cr '.[] | select( .type == "info") | select( (.info_type == "Error") or (.info_type == "Debug"))' spread-results.json) + else + echo "No spread log found, skipping errors reporting" + fi spread-nested: needs: [unit-tests] @@ -357,7 +395,10 @@ echo "::add-matcher::.github/spread-problem-matcher.json" export NESTED_BUILD_SNAPD_FROM_CURRENT=true export NESTED_ENABLE_KVM=true - spread -abend google-nested:${{ matrix.system }}:tests/nested/... + # "pipefail" ensures that a non-zero status from the spread is + # propagated; and we use a subshell as this option could trigger + # undesired changes elsewhere + (set -o pipefail; spread -abend google-nested:${{ matrix.system }}:tests/nested/... | tee spread.log) - name: Cache successful run run: | mkdir -p $(dirname "$CACHE_RESULT_STAMP") @@ -369,3 +410,18 @@ for r in .spread-reuse.*.yaml; do spread -discard -reuse-pid="$(echo "$r" | grep -o -E '[0-9]+')"; done + - name: report spread errors + if: always() + run: | + if [ -e spread.log ]; then + echo "Running spread log analyzer" + ./tests/lib/tools/log-parser spread.log --print-detail error --output spread-results.json --cut 100 + while IFS= read -r line; do + if [ ! -z "$line" ]; then + echo "Reporting spread error..." + ./tests/lib/tools/report-mongodb --db-name snapd --db-collection spread_errors "$line" + fi + done <<< $(jq -cr '.[] | select( .type == "info") | select( .info_type == "Error")' spread-results.json) + else + echo "No spread log found, skipping errors reporting" + fi diff -Nru snapd-2.53+21.10ubuntu1/go.mod snapd-2.54.2+21.10/go.mod --- snapd-2.53+21.10ubuntu1/go.mod 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/go.mod 2022-01-06 21:25:16.000000000 +0000 @@ -15,10 +15,12 @@ github.com/juju/ratelimit v1.0.1 github.com/kr/pretty v0.2.2-0.20200810074440-814ac30b4b18 // indirect github.com/mvo5/goconfigparser v0.0.0-20200803085309-72e476556adb - github.com/mvo5/libseccomp-golang v0.9.1-0.20180308152521-f4de83b52afb + // if below two libseccomp-golang lines are updated, one must also update packaging/ubuntu-14.04/rules + github.com/mvo5/libseccomp-golang v0.9.1-0.20180308152521-f4de83b52afb // old trusty builds only + github.com/seccomp/libseccomp-golang v0.9.2-0.20210917151616-9da99da69b1b github.com/snapcore/bolt v1.3.2-0.20210908134111-63c8bfcf7af8 github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 - github.com/snapcore/secboot v0.0.0-20210909111405-e3a397e2da90 + github.com/snapcore/secboot v0.0.0-20211018143212-802bb19ca263 golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9 golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365 golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 diff -Nru snapd-2.53+21.10ubuntu1/go.sum snapd-2.54.2+21.10/go.sum --- snapd-2.53+21.10ubuntu1/go.sum 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/go.sum 2022-01-06 21:25:16.000000000 +0000 @@ -38,12 +38,16 @@ github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a h1:3QH7VyOaaiUHNrA9Se4YQIRkDTCw1EJls9xTUCaCeRM= github.com/rogpeppe/clock v0.0.0-20190514195947-2896927a307a/go.mod h1:4r5QyqhjIWCcK8DO4KMclc5Iknq5qVBAlbYYzAbUScQ= +github.com/seccomp/libseccomp-golang v0.9.2-0.20210917151616-9da99da69b1b h1:x1CNwq9AHu5YhO+igGGZ4ov+eKU0U3kaiZCymcVUdSk= +github.com/seccomp/libseccomp-golang v0.9.2-0.20210917151616-9da99da69b1b/go.mod h1:JA8cRccbGaA1s33RQf7Y1+q9gHmZX1yB/z9WDN1C6fg= github.com/snapcore/bolt v1.3.2-0.20210908134111-63c8bfcf7af8 h1:WmyDfH38e3MaMWrMCO5YpW96BANq5Ti2iwbliM/xTW0= github.com/snapcore/bolt v1.3.2-0.20210908134111-63c8bfcf7af8/go.mod h1:Z6z3sf12AMDjT/4tbT/PmzzdACAxkWGhkuKWiVpTWLM= github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785 h1:PaunR+BhraKSLxt2awQ42zofkP+NKh/VjQ0PjIMk/y4= github.com/snapcore/go-gettext v0.0.0-20191107141714-82bbea49e785/go.mod h1:D3SsWAXK7wCCBZu+Vk5hc1EuKj/L3XN1puEMXTU4LrQ= github.com/snapcore/secboot v0.0.0-20210909111405-e3a397e2da90 h1:XCbqRVVmFztGHPWN+wdPcRahQY7moJBRd/g/zMXLJmA= github.com/snapcore/secboot v0.0.0-20210909111405-e3a397e2da90/go.mod h1:72paVOkm4sJugXt+v9ItmnjXgO921D8xqsbH2OekouY= +github.com/snapcore/secboot v0.0.0-20211018143212-802bb19ca263 h1:cq2rG4JcNBCwHvo7iNdJL4nb8Ns7L/aOUd1EFs2toFs= +github.com/snapcore/secboot v0.0.0-20211018143212-802bb19ca263/go.mod h1:72paVOkm4sJugXt+v9ItmnjXgO921D8xqsbH2OekouY= github.com/snapcore/snapd v0.0.0-20201005140838-501d14ac146e/go.mod h1:3xrn7QDDKymcE5VO2rgWEQ5ZAUGb9htfwlXnoel6Io8= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1 h1:A/5uWzF44DlIgdm/PQFwfMkW0JX+cIcQi/SwLAmZP5M= go.mozilla.org/pkcs7 v0.0.0-20200128120323-432b2356ecb1/go.mod h1:SNgMg+EgDFwmvSmLRTNKC5fegJjB7v23qTQ0XLGUNHk= diff -Nru snapd-2.53+21.10ubuntu1/HACKING.md snapd-2.54.2+21.10/HACKING.md --- snapd-2.53+21.10ubuntu1/HACKING.md 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/HACKING.md 2022-01-06 21:25:16.000000000 +0000 @@ -1,40 +1,64 @@ # Hacking on snapd -Hacking on snapd is fun and straightforward. The code is extensively -unit tested and we use the [spread](https://github.com/snapcore/spread) +Hacking on `snapd` is fun and straightforward. The code is extensively unit +tested and we use the [spread](https://github.com/snapcore/spread) integration test framework for the integration/system level tests. ## Development -### Supported Go versions +### Supported Ubuntu distributions -From snapd 2.52, snapd supports Go 1.13 and onwards. From snapd 2.38 -to 2.52, snapd requires Go 1.9+. Versions before 2.38 support Go 1.6+. +Ubuntu 18.04 LTS or later is recommended for `snapd` development. -### Setting up a GOPATH +If you want to build or test on older versions of Ubuntu, additional steps +may be required when installing dependencies. + + +### Supported Go version + +Go 1.13 or later is required to build `snapd`. + +If you need to build older versions of snapd, please have a look at the file +`debian/control` to find out what dependencies were needed at the time +(including which version of the Go compiler). + +### Setting up your build environment + +If your Go environment (e.g. `GOPATH`) is already configured, you should skip +this step. The Go environment setup can be added to your shell login script +(e.g. `~/.bashrc` for bash) for automatic setup (after a terminal restart). + +``` +export GOPATH=${HOME}/work +mkdir -p $GOPATH +export PATH="$PATH:$GOPATH/bin" +``` + +### Further environment variable details When working with the source of Go programs, you should define a path within your home directory (or other workspace) which will be your `GOPATH`. `GOPATH` -is similar to Java's `CLASSPATH` or Python's `~/.local`. `GOPATH` is documented -[online](http://golang.org/pkg/go/build/) and inside the go tool itself +is similar to Java's `CLASSPATH` or Python's `~/.local`. `GOPATH` is +documented [online](http://golang.org/pkg/go/build/) and inside the `go` tool +itself. go help gopath Various conventions exist for naming the location of your `GOPATH`, but it -should exist, and be writable by you. For example +should exist, and be writable by you. For example: export GOPATH=${HOME}/work mkdir $GOPATH will define and create `$HOME/work` as your local `GOPATH`. The `go` tool itself will create three subdirectories inside your `GOPATH` when required; -`src`, `pkg` and `bin`, which hold the source of Go programs, compiled packages -and compiled binaries, respectively. +`src`, `pkg` and `bin`, which hold the source of Go programs, compiled +packages and compiled binaries, respectively. Setting `GOPATH` correctly is critical when developing Go programs. Set and export it as part of your login script. -Add `$GOPATH/bin` to your `PATH`, so you can run the go programs you install: +Add `$GOPATH/bin` to your `PATH`, so you can run the Go programs you install: PATH="$PATH:$GOPATH/bin" @@ -42,76 +66,319 @@ your `$GOPATH` is more complex than a single entry you'll need to adjust the above). -Note that if you are using go 1.16 or newer you need to disable the -go modules feature. Use: - export GO111MODULE=off +### Getting the snapd sources + +The easiest way to get the source for `snapd` is to clone the GitHub repository +in a directory where you have read-write permissions, such as your home +directory. -for this. + cd ~/ + git clone https://github.com/snapcore/snapd.git + cd snapd +This will allow you to build and test `snapd`. If you wish to contribute to +the `snapd` project, please see the Contributing section. -### Getting the snapd sources +### Installing build dependencies + +Build dependencies can automatically be resolved using `build-dep`on Ubuntu. + + cd ~/snapd + sudo apt-get build-dep . + +Package build dependencies for other distributions can be found under the +`packages\`directory. + +Go module dependencies are automatically resolved at build time. + +### Building the snap with snapcraft + +The easiest (though not the most efficient) way to test changes to snapd is to +build the snapd snap using _snapcraft_ and then install that snapd snap. The +snapcraft.yaml for the snapd snap is located at ./build-aux/snapcraft.yaml, and +can be built using snapcraft either in a LXD container or a multipass VM (or +natively with `--destructive-mode` on a Ubuntu 16.04 host). + +Note: Currently, snapcraft's default track of 5.x does not support building the +snapd snap, since the snapd snap uses `build-base: core`. Building with a +`build-base` of core uses Ubuntu 16.04 as the base operating system (and thus +root filesystem) for building and Ubuntu 16.04 is now in Extended Security +Maintenance (ESM - see https://ubuntu.com/blog/ubuntu-16-04-lts-transitions-to-extended-security-maintenance-esm), and as such only is buildable using snapcraft's 4.x channel. At some point in the future, +the snapd snap should be moved to a newer `build-base`, but until then `4.x` +needs to be used. + +Install snapcraft from the 4.x channel: + +``` +sudo snap install snapcraft --channel=4.x +``` + +Then run snapcraft: + +``` +snapcraft +``` + +Now the snapd snap that was just built can be installed with: + +``` +snap install --dangerous snapd_*.snap +``` + +To go back to using snapd from the store instead of the custom version we +installed (since it will not get updates as it was installed dangerously), you +can either use `snap revert snapd`, or you can refresh directly with +`snap refresh snapd --stable --amend`. + +Note: It is also sometimes useful to use snapcraft to build the snapd snap for +other architectures using the `remote-build` feature, however there is currently a +bug in snapcraft around using the `4.x` track and using `remote-build`, where the +Launchpad job created for the remote-build will attempt to use the `latest` track instead +of the `4.x` channel. This was recently fixed in snapcraft in https://github.com/snapcore/snapcraft/pull/3600 +which for now requires using the `latest/edge` channel of snapcraft instead of the +`4.x` track which is needed to build the snap locally. However, this fix does +then introduce a different problem due to one of the tests in the snapd git tree +which currently has circular symlinks in the tree, which due to a regression in +snapcraft is no longer usable with remote-build. Removing these files in the +snapd git tree is tracked at https://bugs.launchpad.net/snapd/+bug/1948838, but +in the meantime in order to build remotely with snapcraft, do the following: + +``` +snap refresh snapcraft --channel=latest/edge +``` + +And then get rid of the symlinks in question: + +``` +rm -r tests/main/validate-container-failures/ +``` + +Now you can use remote-build with snapcraft on the snapd tree for any desired +architectures: + +``` +snapcraft remote-build --build-on=armhf,s390x,arm64 +``` + +And to go back to building the snapd snap locally, just revert the channel back +to 4.x: + +``` +snap refresh snapcraft --channel=4.x/stable +``` + + +#### Splicing the snapd snap into the core snap -The easiest way to get the source for `snapd` is to use the `go get` command. +Sometimes while developing you may need to build a version of the _core_ snap +with a custom snapd version. The snapcraft.yaml for the core snap currently is +complex in that it assumes it is built inside Launchpad with the +`snappy-dev/image` PPA enabled, so it is difficult to inject a custom version of +snapd into this by rebuilding the core snap directly, so an easier way is to +actually first build the snapd snap and inject the binaries from the snapd snap +into the core snap. This currently works since both the snapd snap and the core +snap have the same `build-base` of Ubuntu 16.04. However, at some point in time +this trick will stop working when the snapd snap starts using a `build-base` other +than Ubuntu 16.04, but until then, you can use the following trick to more +easily get a custom version of snapd inside a core snap. - go get -d -v github.com/snapcore/snapd/... +First follow the steps above to build a full snapd snap. Then, extract the core +snap you wish to splice the custom snapd snap into: -This command will checkout the source of `snapd` and inspect it for any unmet -Go package dependencies, downloading those as well. `go get` will also build -and install `snapd` and its dependencies. To also build and install `snapd` -itself into `$GOPATH/bin`, omit the `-d` flag. More details on the `go get` -flags are available using +``` +sudo unsquashfs -d custom-core core_.snap +``` - go help get +`sudo` is important as the core snap has special permissions on various +directories and files that must be preserved as it is a boot base snap. -At this point you will have the git local repository of the `snapd` source at -`$GOPATH/src/github.com/snapcore/snapd`. The source for any -dependent packages will also be available inside `$GOPATH`. +Now, extract the snapd snap, again with sudo because there are `suid` binaries +which must retain their permission bits: -### Dependencies handling +``` +sudo unsquashfs -d custom-snapd snapd-custom.snap +``` -Go dependencies are handled via `go mod`. +Now, copy the meta directory from the core snap outside to keep it and prevent +it from being lost when we replace the files from the snapd snap: -Other dependencies are handled via distribution packages and you should ensure -that dependencies for your distribution are installed. For example, on Ubuntu, -run: +``` +sudo cp ./custom-core/meta meta-core-backup +``` - sudo apt-get build-dep ./ +Then copy all the files from the snapd snap into the core snap, and delete the +meta directory so we don't use any of the meta files from the snapd snap: -### Building +``` +sudo cp -r ./custom-snapd/* ./custom-core/ +sudo rm -r ./custom-core/meta/ +sudo cp ./meta-core-backup ./custom-core/ +``` -To build, once the sources are available and `GOPATH` is set, you can just run +Now we can repack the core snap: - go build -o /tmp/snap github.com/snapcore/snapd/cmd/snap +``` +sudo snap pack custom-core +``` -to get the `snap` binary in /tmp (or without -o to get it in the current -working directory). Alternatively: +Sometimes it is helpful to modify the snap version in +`./custom-core/meta/snap.yaml` before repacking with `snap pack` so it is easy +to identify which snap file is which. - go install github.com/snapcore/snapd/cmd/snap/... -to have it available in `$GOPATH/bin` +### Building (natively) -Similarly, to build the `snapd` REST API daemon, you can run +To build the `snap` command line client: - go build -o /tmp/snapd github.com/snapcore/snapd/cmd/snapd +``` +cd ~/snapd +mkdir -p /tmp/build +go build -o /tmp/build/snap ./cmd/snap +``` +To build the `snapd` REST API daemon: + +``` +cd ~/snapd +mkdir -p /tmp/build +go build -o /tmp/build/snapd ./cmd/snapd +``` + +To build all the`snapd` Go components: + +``` +cd ~/snapd +mkdir -p /tmp/build +go build -o /tmp/build ./... +``` + +### Cross-compiling (example: ARM v7 target) + +Install a suitable cross-compiler for the target architecture. + +``` +sudo apt-get install gcc-arm-linux-gnueabihf +``` + +Verify the default architecture version of your GCC cross-compiler. + +``` +arm-linux-gnueabihf-gcc -v +: +--with-arch=armv7-a +--with-fpu=vfpv3-d16 +--with-float=hard +--with-mode=thumb +``` + +Verify the supported Go cross-compile ARM targets [here]( +https://github.com/golang/go/wiki/GoArm). + +`Snapd` depends on libseccomp v2.3 or later. The following instructions can be +used to cross-compile the library: + +``` +cd ~/ +git clone https://github.com/seccomp/libseccomp +cd libseccomp +./autogen.sh +./configure --host=arm-linux-gnueabihf --prefix=${HOME}/libseccomp/build +make && make install +``` + +Setup the Go environment for cross-compiling. + +``` +export CC=arm-linux-gnueabihf-gcc +export CGO_ENABLED=1 +export CGO_LDFLAGS="-L${HOME}/libseccomp/build/lib" +export GOOS=linux +export GOARCH=arm +export GOARM=7 +``` + +The Go environment variables are now explicitly set to target the ARM v7 +architecture. + +Run the same build commands from the Building (natively) section above. + +Verify the target architecture by looking at the application ELF header. + +``` +readelf -h /tmp/build/snapd +: +Class: ELF32 +OS/ABI: UNIX - System V +Machine: ARM +``` + +CGO produced ELF binaries contain additional architecture attributes that +reflect the exact ARM architecture we targeted. + +``` +readelf -A /tmp/build/snap-seccomp +: +File Attributes + Tag_CPU_name: "7-A" + Tag_CPU_arch: v7 + Tag_FP_arch: VFPv3-D16 +``` ### Contributing -Contributions are always welcome! Please make sure that you sign the -Canonical contributor license agreement at -http://www.ubuntu.com/legal/contributors - -Snapd can be found on GitHub, so in order to fork the source and contribute, -go to https://github.com/snapcore/snapd. Check out [GitHub's help -pages](https://help.github.com/) to find out how to set up your local branch, -commit changes and create pull requests. +Contributions are always welcome! + +Please make sure that you sign the Canonical contributor agreement [here]( +http://www.ubuntu.com/legal/contributors). + +Complete requirements can be found in CONTRIBUTING.md. + +Contributions are submitted through a Pull Request created from a fork of the +`snapd` repository (under your GitHub account). + +Start by creating a fork of the `snapd` repository on GitHub. + +Add your fork as an additional remote to an already cloned `snapd` main +repository. Replace `` with your GitHub account username. + +``` +cd ~/snapd +git remote add fork git@github.com:/snapd.git +``` + +Create a working branch on which to commit your work. Replace +`` with a suitable branch name. + +``` +git checkout -b +``` + +Make changes to the repository and commit the changes to your +working branch. Push the changes to your forked `snapd` repository. + +``` +git commit -a -m "commit message" +git push fork +``` + +Create the Pull Request for your branch on GitHub. + +This complete process is outlined in the GitHub documentation [here]( +https://docs.github.com/en/github/collaborating-with-pull-requests). We value good tests, so when you fix a bug or add a new feature we highly -encourage you to create a test in `$source_test.go`. See also the section -about Testing. +encourage you to add tests. + +For more information on testing, please see the Testing section. ### Testing +Install the following package(s) to satisfy test dependencies. + +``` +sudo apt-get install python3-yamlordereddictloader +``` + To run the various tests that we have to ensure a high quality source just run: ./run-checks @@ -133,8 +400,6 @@ (or -check.v for less verbose output). -Note, the yamlordereddictloader python package is needed to carry out the tests format check. - There is more to read about the testing framework on the [website](https://labix.org/gocheck) ### Running spread tests diff -Nru snapd-2.53+21.10ubuntu1/interfaces/apparmor/backend.go snapd-2.54.2+21.10/interfaces/apparmor/backend.go --- snapd-2.53+21.10ubuntu1/interfaces/apparmor/backend.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/apparmor/backend.go 2022-01-06 21:25:16.000000000 +0000 @@ -55,6 +55,7 @@ "github.com/snapcore/snapd/release" apparmor_sandbox "github.com/snapcore/snapd/sandbox/apparmor" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/timings" ) @@ -137,6 +138,18 @@ logger.Noticef("snapd enabled root filesystem on overlay support, additional upperdir permissions granted") } + // Check whether apparmor_parser supports bpf capability. Some older + // versions do not, hence the capability cannot be part of the default + // profile of snap-confine as loading it would fail. + if features, err := apparmor_sandbox.ParserFeatures(); err != nil { + logger.Noticef("cannot determine apparmor_parser features: %v", err) + } else if strutil.ListContains(features, "cap-bpf") { + policy["cap-bpf"] = &osutil.MemoryFileState{ + Content: []byte(capabilityBPFSnippet), + Mode: 0644, + } + } + // Ensure that generated policy is what we computed above. created, removed, err := osutil.EnsureDirState(dirs.SnapConfineAppArmorDir, glob, policy) if err != nil { @@ -738,6 +751,12 @@ tagSnippets += ptraceTraceDenySnippet } + // Deny the sys_module capability unless it has been explicitly + // requested + if spec.SuppressSysModuleCapability() && !spec.UsesSysModuleCapability() { + tagSnippets += sysModuleCapabilityDenySnippet + } + // Use 'ix' rules in the home interface unless an // interface asked to suppress them repl := "ix" diff -Nru snapd-2.53+21.10ubuntu1/interfaces/apparmor/backend_test.go snapd-2.54.2+21.10/interfaces/apparmor/backend_test.go --- snapd-2.53+21.10ubuntu1/interfaces/apparmor/backend_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/apparmor/backend_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -171,6 +171,9 @@ apparmor.MockRuntimeNumCPU(func() int { return 99 }) restore := release.MockReleaseInfo(&release.OS{ID: "ubuntu"}) s.AddCleanup(restore) + + restore = apparmor_sandbox.MockFeatures(nil, nil, nil, nil) + s.AddCleanup(restore) } func (s *backendSuite) TearDownTest(c *C) { @@ -1844,6 +1847,126 @@ c.Assert(cmd.Calls(), HasLen, 0) } +func (s *backendSuite) testSetupSnapConfineGeneratedPolicyWithBPFCapability(c *C, reexec bool) { + restore := apparmor.MockIsRootWritableOverlay(func() (string, error) { return "", nil }) + defer restore() + restore = apparmor.MockIsHomeUsingNFS(func() (bool, error) { return false, nil }) + defer restore() + // Pretend apparmor_parser supports bpf capability + apparmor_sandbox.MockFeatures(nil, nil, []string{"cap-bpf"}, nil) + + // Hijack interaction with apparmor_parser + cmd := testutil.MockCommand(c, "apparmor_parser", "") + defer cmd.Restore() + + fakeExe := filepath.Join(s.RootDir, "fake-proc-self-exe") + restore = apparmor.MockProcSelfExe(fakeExe) + defer restore() + if reexec { + // Pretend snapd is reexecuted from the core snap + err := os.Symlink(filepath.Join(dirs.SnapMountDir, "/core/1234/usr/lib/snapd/snapd"), fakeExe) + c.Assert(err, IsNil) + } else { + // Pretend snapd is executing from the native package + err := os.Symlink("/usr/lib/snapd/snapd", fakeExe) + c.Assert(err, IsNil) + } + + profilePath := filepath.Join(apparmor_sandbox.ConfDir, "usr.lib.snapd.snap-confine") + // Create the directory where system apparmor profiles are stored and write + // the system apparmor profile of snap-confine. + c.Assert(os.MkdirAll(apparmor_sandbox.ConfDir, 0755), IsNil) + c.Assert(ioutil.WriteFile(profilePath, []byte(""), 0644), IsNil) + + // Setup generated policy for snap-confine. + err := (&apparmor.Backend{}).Initialize(nil) + c.Assert(err, IsNil) + + // Capability bpf is supported by the parser, so an extra policy file + // for snap-confine is present + files, err := ioutil.ReadDir(dirs.SnapConfineAppArmorDir) + c.Assert(err, IsNil) + c.Assert(files, HasLen, 1) + c.Assert(files[0].Name(), Equals, "cap-bpf") + c.Assert(files[0].Mode(), Equals, os.FileMode(0644)) + c.Assert(files[0].IsDir(), Equals, false) + + c.Assert(filepath.Join(dirs.SnapConfineAppArmorDir, files[0].Name()), + testutil.FileContains, "capability bpf,") + + if reexec { + // The distribution policy was not reloaded because snap-confine executes + // from core snap. This is handled separately by per-profile Setup. + c.Assert(cmd.Calls(), HasLen, 0) + } else { + c.Assert(cmd.Calls(), DeepEquals, [][]string{{ + "apparmor_parser", "--replace", + "--write-cache", + "-O", "no-expr-simplify", + "--cache-loc=" + apparmor_sandbox.SystemCacheDir, + "--skip-read-cache", + "--quiet", + profilePath, + }}) + } +} + +// snap-confine policy when apparmor_parser supports BPF capability and snapd reexec +func (s *backendSuite) TestSetupSnapConfineGeneratedPolicyWithBPFCapabilityReexec(c *C) { + const reexecd = true + s.testSetupSnapConfineGeneratedPolicyWithBPFCapability(c, reexecd) +} + +// snap-confine policy when apparmor_parser supports BPF capability but no reexec +func (s *backendSuite) TestSetupSnapConfineGeneratedPolicyWithBPFCapabilityNoReexec(c *C) { + const reexecd = false + s.testSetupSnapConfineGeneratedPolicyWithBPFCapability(c, reexecd) +} + +func (s *backendSuite) TestSetupSnapConfineGeneratedPolicyWithBPFProbeError(c *C) { + log, restore := logger.MockLogger() + defer restore() + restore = apparmor.MockIsRootWritableOverlay(func() (string, error) { return "", nil }) + defer restore() + restore = apparmor.MockIsHomeUsingNFS(func() (bool, error) { return false, nil }) + defer restore() + // Probing for apparmor_parser features failed + apparmor_sandbox.MockFeatures(nil, nil, nil, fmt.Errorf("mock probe error")) + + // Hijack interaction with apparmor_parser + cmd := testutil.MockCommand(c, "apparmor_parser", "") + defer cmd.Restore() + + fakeExe := filepath.Join(s.RootDir, "fake-proc-self-exe") + restore = apparmor.MockProcSelfExe(fakeExe) + defer restore() + // Pretend snapd is executing from the native package + err := os.Symlink("/usr/lib/snapd/snapd", fakeExe) + c.Assert(err, IsNil) + + profilePath := filepath.Join(apparmor_sandbox.ConfDir, "usr.lib.snapd.snap-confine") + // Create the directory where system apparmor profiles are stored and write + // the system apparmor profile of snap-confine. + c.Assert(os.MkdirAll(apparmor_sandbox.ConfDir, 0755), IsNil) + c.Assert(ioutil.WriteFile(profilePath, []byte(""), 0644), IsNil) + + // Setup generated policy for snap-confine. + err = (&apparmor.Backend{}).Initialize(nil) + c.Assert(err, IsNil) + + // Probing apparmor_parser capabilities failed, so nothing gets written + // to the snap-confine policy directory + files, err := ioutil.ReadDir(dirs.SnapConfineAppArmorDir) + c.Assert(err, IsNil) + c.Assert(files, HasLen, 0) + + // No calls to apparmor_parser + c.Assert(cmd.Calls(), HasLen, 0) + + // But an error was logged + c.Assert(log.String(), testutil.Contains, "cannot determine apparmor_parser features: mock probe error") +} + type nfsAndOverlaySnippetsScenario struct { opts interfaces.ConfinementOptions overlaySnippet string diff -Nru snapd-2.53+21.10ubuntu1/interfaces/apparmor/spec.go snapd-2.54.2+21.10/interfaces/apparmor/spec.go --- snapd-2.53+21.10ubuntu1/interfaces/apparmor/spec.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/apparmor/spec.go 2022-01-06 21:25:16.000000000 +0000 @@ -78,6 +78,10 @@ suppressPtraceTrace bool usesPtraceTrace bool + // Same as the above, but for the sys_module capability + suppressSysModuleCapability bool + usesSysModuleCapability bool + // The home interface typically should have 'ix' as part of its rules, // but specifying certain change_profile rules with these rules cases // a 'conflicting x modifiers' parser error. Allow interfaces that @@ -620,6 +624,30 @@ return spec.suppressPtraceTrace } +// SetUsesSysModuleCapability records that some interface has granted the +// sys_module capability +func (spec *Specification) SetUsesSysModuleCapability() { + spec.usesSysModuleCapability = true +} + +// UsesSysModuleCapability returns whether the sys_module capability is being +// used by any of the interfaces in the spec. +func (spec *Specification) UsesSysModuleCapability() bool { + return spec.usesSysModuleCapability +} + +// SetSuppressSysModuleCapability to request explicit denial of the sys_module +// capability +func (spec *Specification) SetSuppressSysModuleCapability() { + spec.suppressSysModuleCapability = true +} + +// SuppressSysModuleCapability returns whether any interface has asked the +// sys_module capability to be explicitly denied +func (spec *Specification) SuppressSysModuleCapability() bool { + return spec.suppressSysModuleCapability +} + // SetSuppressHomeIx records suppression of the ix rules for the home // interface. func (spec *Specification) SetSuppressHomeIx() { diff -Nru snapd-2.53+21.10ubuntu1/interfaces/apparmor/template.go snapd-2.54.2+21.10/interfaces/apparmor/template.go --- snapd-2.53+21.10ubuntu1/interfaces/apparmor/template.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/apparmor/template.go 2022-01-06 21:25:16.000000000 +0000 @@ -336,6 +336,12 @@ owner @{HOME}/snap/@{SNAP_INSTANCE_NAME}/ r, owner @{HOME}/snap/@{SNAP_INSTANCE_NAME}/** mrkix, + # Experimental snap folder changes + owner @{HOME}/.snap/data/@{SNAP_INSTANCE_NAME}/ r, + owner @{HOME}/.snap/data/@{SNAP_INSTANCE_NAME}/** mrkix, + owner @{HOME}/.snap/data/@{SNAP_INSTANCE_NAME}/@{SNAP_REVISION}/** wl, + owner @{HOME}/.snap/data/@{SNAP_INSTANCE_NAME}/common/** wl, + # Writable home area for this version. # bind mount *not* used here (see 'parallel installs', above) owner @{HOME}/snap/@{SNAP_INSTANCE_NAME}/@{SNAP_REVISION}/** wl, @@ -405,7 +411,7 @@ signal (receive) peer=unconfined, # for 'udevadm trigger --verbose --dry-run --tag-match=snappy-assign' - /{,s}bin/udevadm ixr, + /{,usr/}{,s}bin/udevadm ixr, /etc/udev/udev.conf r, /{,var/}run/udev/tags/snappy-assign/ r, @{PROC}/cmdline r, @@ -871,6 +877,12 @@ "###UPPERDIR###/{,**/}" r, ` +// capabilityBPFSnippet contains extra permissions for snap-confine to execute +// bpf() syscall and set up or modify cgroupv2 device access filtering +var capabilityBPFSnippet = ` +capability bpf, +` + var ptraceTraceDenySnippet = ` # While commands like 'ps', 'ip netns identify ', 'ip netns pids foo', etc # trigger a 'ptrace (trace)' denial, they aren't actually tracing other @@ -885,6 +897,17 @@ deny capability sys_ptrace, ` +var sysModuleCapabilityDenySnippet = ` +# The rtnetlink kernel interface can trigger the loading of kernel modules, +# first attempting to operate on a network module (this requires the net_admin +# capability) and falling back to loading ordinary modules (and this requires +# the sys_module capability). For reference, see the dev_load() function in: +# https://kernel.ubuntu.com/git/ubuntu/ubuntu-focal.git/tree/net/core/dev_ioctl.c?h=v5.13#n354 +# The following rule is used to silence the denials for attempting to load +# generic kernel modules, while still allowing the loading of network modules. +deny capability sys_module, +` + // updateNSTemplate defines the apparmor profile for per-snap snap-update-ns. // // The per-snap snap-update-ns profiles are composed via a template and diff -Nru snapd-2.53+21.10ubuntu1/interfaces/backends/backends.go snapd-2.54.2+21.10/interfaces/backends/backends.go --- snapd-2.53+21.10ubuntu1/interfaces/backends/backends.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/backends/backends.go 2022-01-06 21:25:16.000000000 +0000 @@ -27,6 +27,7 @@ "github.com/snapcore/snapd/interfaces/dbus" "github.com/snapcore/snapd/interfaces/kmod" "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/interfaces/polkit" "github.com/snapcore/snapd/interfaces/seccomp" "github.com/snapcore/snapd/interfaces/systemd" "github.com/snapcore/snapd/interfaces/udev" @@ -45,6 +46,7 @@ &udev.Backend{}, &mount.Backend{}, &kmod.Backend{}, + &polkit.Backend{}, } // TODO use something like: diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/all_test.go snapd-2.54.2+21.10/interfaces/builtin/all_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/all_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/all_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -33,6 +33,7 @@ "github.com/snapcore/snapd/interfaces/ifacetest" "github.com/snapcore/snapd/interfaces/kmod" "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/interfaces/polkit" "github.com/snapcore/snapd/interfaces/seccomp" "github.com/snapcore/snapd/interfaces/systemd" "github.com/snapcore/snapd/interfaces/udev" @@ -101,6 +102,19 @@ MountPermanentSlot(spec *mount.Specification, slot *snap.SlotInfo) error } +type polkitDefiner1 interface { + PolkitConnectedPlug(spec *polkit.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error +} +type polkitDefiner2 interface { + PolkitConnectedSlot(spec *polkit.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error +} +type polkitDefiner3 interface { + PolkitPermanentPlug(spec *polkit.Specification, plug *snap.PlugInfo) error +} +type polkitDefiner4 interface { + PolkitPermanentSlot(spec *polkit.Specification, slot *snap.SlotInfo) error +} + type seccompDefiner1 interface { SecCompConnectedPlug(spec *seccomp.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error } @@ -162,6 +176,11 @@ reflect.TypeOf((*mountDefiner2)(nil)).Elem(), reflect.TypeOf((*mountDefiner3)(nil)).Elem(), reflect.TypeOf((*mountDefiner4)(nil)).Elem(), + // polkit + reflect.TypeOf((*polkitDefiner1)(nil)).Elem(), + reflect.TypeOf((*polkitDefiner2)(nil)).Elem(), + reflect.TypeOf((*polkitDefiner3)(nil)).Elem(), + reflect.TypeOf((*polkitDefiner4)(nil)).Elem(), // seccomp reflect.TypeOf((*seccompDefiner1)(nil)).Elem(), reflect.TypeOf((*seccompDefiner2)(nil)).Elem(), @@ -363,7 +382,7 @@ var sigs []funcSig // All the valid signatures from all the specification definers from all the backends. - for _, backend := range []string{"AppArmor", "SecComp", "UDev", "DBus", "Systemd", "KMod"} { + for _, backend := range []string{"AppArmor", "SecComp", "UDev", "DBus", "Systemd", "KMod", "Polkit"} { backendLower := strings.ToLower(backend) sigs = append(sigs, []funcSig{{ name: fmt.Sprintf("%sPermanentPlug", backend), diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/block_devices.go snapd-2.54.2+21.10/interfaces/builtin/block_devices.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/block_devices.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/block_devices.go 2022-01-06 21:25:16.000000000 +0000 @@ -99,6 +99,9 @@ # Allow /sys/block/sdX/device/state to be accessible to accept or reject the request from given the path. # To take the path offline will cause any subsequent access to fail immediately, vice versa. /sys/devices/**/host*/**/state rw, + +# Allow to use blkid to export key=value pairs such as UUID to get block device attributes +/{,usr/}sbin/blkid ixr, ` var blockDevicesConnectedPlugUDev = []string{ diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/browser_support.go snapd-2.54.2+21.10/interfaces/builtin/browser_support.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/browser_support.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/browser_support.go 2022-01-06 21:25:16.000000000 +0000 @@ -61,6 +61,7 @@ # the snap packaging adjusted to use LD_PRELOAD technique from LP: #1577514 owner /{dev,run}/shm/{,.}org.chromium.* mrw, owner /{dev,run}/shm/{,.}com.google.Chrome.* mrw, +owner /{dev,run}/shm/{,.}com.microsoft.Edge.* mrw, owner /{dev,run}/shm/.io.nwjs.* mrw, # Chrome's Singleton API sometimes causes an ouid/fsuid mismatch denial, so @@ -69,6 +70,7 @@ # parallel-installs: $XDG_RUNTIME_DIR is not remapped, need to use SNAP_INSTANCE_NAME /run/user/[0-9]*/snap.@{SNAP_INSTANCE_NAME}/{,.}org.chromium.*/SS r, /run/user/[0-9]*/snap.@{SNAP_INSTANCE_NAME}/{,.}com.google.Chrome.*/SS r, +/run/user/[0-9]*/snap.@{SNAP_INSTANCE_NAME}/{,.}com.microsoft.Edge.*/SS r, # Allow reading platform files /run/udev/data/+platform:* r, diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/common.go snapd-2.54.2+21.10/interfaces/builtin/common.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/common.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/common.go 2022-01-06 21:25:16.000000000 +0000 @@ -67,9 +67,12 @@ permanentPlugKModModules []string permanentSlotKModModules []string - usesPtraceTrace bool - suppressPtraceTrace bool - suppressHomeIx bool + usesPtraceTrace bool + suppressPtraceTrace bool + suppressHomeIx bool + usesSysModuleCapability bool + suppressSysModuleCapability bool + controlsDeviceCgroup bool serviceSnippets []string @@ -107,6 +110,11 @@ if iface.suppressHomeIx { spec.SetSuppressHomeIx() } + if iface.usesSysModuleCapability { + spec.SetUsesSysModuleCapability() + } else if iface.suppressSysModuleCapability { + spec.SetSuppressSysModuleCapability() + } if snippet := iface.connectedPlugAppArmor; snippet != "" { spec.AddSnippet(snippet) } diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/common_test.go snapd-2.54.2+21.10/interfaces/builtin/common_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/common_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/common_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -97,7 +97,7 @@ }) } -func (s *commonIfaceSuite) TestSuppressPtraceTrace(c *C) { +func (s *commonIfaceSuite) TestSuppressFeatures(c *C) { plug, _ := MockConnectedPlug(c, ` name: consumer version: 0 @@ -112,92 +112,110 @@ common: `, nil, "common") - // setting nothing - iface := &commonInterface{ - name: "common", - suppressPtraceTrace: false, - usesPtraceTrace: false, + type Checks []struct { + getter func(spec *apparmor.Specification) bool + expectedValue bool + } + + tests := []struct { + iface *commonInterface + checks Checks + }{ + // PtraceTrace + { + // setting nothing + &commonInterface{name: "common", suppressPtraceTrace: false, usesPtraceTrace: false}, + Checks{ + {(*apparmor.Specification).UsesPtraceTrace, false}, + {(*apparmor.Specification).SuppressPtraceTrace, false}, + }, + }, + { + // setting only uses + &commonInterface{name: "common", suppressPtraceTrace: false, usesPtraceTrace: true}, + Checks{ + {(*apparmor.Specification).UsesPtraceTrace, true}, + {(*apparmor.Specification).SuppressPtraceTrace, false}, + }, + }, + { + // setting only suppress + &commonInterface{name: "common", suppressPtraceTrace: true, usesPtraceTrace: false}, + Checks{ + {(*apparmor.Specification).UsesPtraceTrace, false}, + {(*apparmor.Specification).SuppressPtraceTrace, true}, + }, + }, + { + // setting both, only uses is set + &commonInterface{name: "common", suppressPtraceTrace: true, usesPtraceTrace: true}, + Checks{ + {(*apparmor.Specification).UsesPtraceTrace, true}, + {(*apparmor.Specification).SuppressPtraceTrace, false}, + }, + }, + // HomeIx + { + // setting nothing + &commonInterface{name: "common", suppressHomeIx: false}, + Checks{ + {(*apparmor.Specification).SuppressHomeIx, false}, + }, + }, + { + // setting suppress + &commonInterface{name: "common", suppressHomeIx: true}, + Checks{ + {(*apparmor.Specification).SuppressHomeIx, true}, + }, + }, + // sys_module capability + { + // setting nothing + &commonInterface{name: "common", suppressSysModuleCapability: false, usesSysModuleCapability: false}, + Checks{ + {(*apparmor.Specification).UsesSysModuleCapability, false}, + {(*apparmor.Specification).SuppressSysModuleCapability, false}, + }, + }, + { + // setting only uses + &commonInterface{name: "common", suppressSysModuleCapability: false, usesSysModuleCapability: true}, + Checks{ + {(*apparmor.Specification).UsesSysModuleCapability, true}, + {(*apparmor.Specification).SuppressSysModuleCapability, false}, + }, + }, + { + // setting only suppress + &commonInterface{name: "common", suppressSysModuleCapability: true, usesSysModuleCapability: false}, + Checks{ + {(*apparmor.Specification).UsesSysModuleCapability, false}, + {(*apparmor.Specification).SuppressSysModuleCapability, true}, + }, + }, + { + // setting both, only uses is set + &commonInterface{name: "common", suppressSysModuleCapability: true, usesSysModuleCapability: true}, + Checks{ + {(*apparmor.Specification).UsesSysModuleCapability, true}, + {(*apparmor.Specification).SuppressSysModuleCapability, false}, + }, + }, + } + + for _, test := range tests { + spec := &apparmor.Specification{} + iface := test.iface + // before connection, everything should be set to false + for _, check := range test.checks { + c.Check(check.getter(spec), Equals, false) + } + c.Check(spec.AddConnectedPlug(iface, plug, slot), IsNil) + for _, check := range test.checks { + c.Check(check.getter(spec), Equals, check.expectedValue) + } } - spec := &apparmor.Specification{} - c.Assert(spec.UsesPtraceTrace(), Equals, false) - c.Assert(spec.SuppressPtraceTrace(), Equals, false) - c.Assert(spec.AddConnectedPlug(iface, plug, slot), IsNil) - c.Assert(spec.UsesPtraceTrace(), Equals, false) - c.Assert(spec.SuppressPtraceTrace(), Equals, false) - - // setting only uses - iface = &commonInterface{ - name: "common", - suppressPtraceTrace: false, - usesPtraceTrace: true, - } - spec = &apparmor.Specification{} - c.Assert(spec.UsesPtraceTrace(), Equals, false) - c.Assert(spec.SuppressPtraceTrace(), Equals, false) - c.Assert(spec.AddConnectedPlug(iface, plug, slot), IsNil) - c.Assert(spec.UsesPtraceTrace(), Equals, true) - c.Assert(spec.SuppressPtraceTrace(), Equals, false) - - // setting only suppress - iface = &commonInterface{ - name: "common", - suppressPtraceTrace: true, - usesPtraceTrace: false, - } - spec = &apparmor.Specification{} - c.Assert(spec.UsesPtraceTrace(), Equals, false) - c.Assert(spec.SuppressPtraceTrace(), Equals, false) - c.Assert(spec.AddConnectedPlug(iface, plug, slot), IsNil) - c.Assert(spec.UsesPtraceTrace(), Equals, false) - c.Assert(spec.SuppressPtraceTrace(), Equals, true) - - // setting both, only uses is set - iface = &commonInterface{ - name: "common", - suppressPtraceTrace: true, - usesPtraceTrace: true, - } - spec = &apparmor.Specification{} - c.Assert(spec.UsesPtraceTrace(), Equals, false) - c.Assert(spec.SuppressPtraceTrace(), Equals, false) - c.Assert(spec.AddConnectedPlug(iface, plug, slot), IsNil) - c.Assert(spec.UsesPtraceTrace(), Equals, true) - c.Assert(spec.SuppressPtraceTrace(), Equals, false) -} - -func (s *commonIfaceSuite) TestSuppressHomeIx(c *C) { - plug, _ := MockConnectedPlug(c, ` -name: consumer -version: 0 -apps: - app: - plugs: [common] -`, nil, "common") - slot, _ := MockConnectedSlot(c, ` -name: producer -version: 0 -slots: - common: -`, nil, "common") - - // setting nothing - iface := &commonInterface{ - name: "common", - suppressHomeIx: false, - } - spec := &apparmor.Specification{} - c.Assert(spec.SuppressHomeIx(), Equals, false) - c.Assert(spec.AddConnectedPlug(iface, plug, slot), IsNil) - c.Assert(spec.SuppressHomeIx(), Equals, false) - - iface = &commonInterface{ - name: "common", - suppressHomeIx: true, - } - spec = &apparmor.Specification{} - c.Assert(spec.SuppressHomeIx(), Equals, false) - c.Assert(spec.AddConnectedPlug(iface, plug, slot), IsNil) - c.Assert(spec.SuppressHomeIx(), Equals, true) } func (s *commonIfaceSuite) TestControlsDeviceCgroup(c *C) { diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/dsp.go snapd-2.54.2+21.10/interfaces/builtin/dsp.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/dsp.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/dsp.go 2022-01-06 21:25:16.000000000 +0000 @@ -60,8 +60,24 @@ # another DSP device node /dev/lens rw, -# also needed for interfacing with the DSP -/proc/ambarella/vin0_idsp rw, +# various ambarella specific DSP control parameters +/proc/ambarella/iav r, +/proc/ambarella/ambnl/** rw, +/proc/ambarella/udc r, +/proc/ambarella/clock r, +/proc/ambarella/dsp_print rw, +/proc/ambarella/hdmi_edid r, +/proc/ambarella/cma r, +/proc/ambarella/ambarella_hwtimer rw, +/proc/ambarella/ambarella_hwtimer_outfreq rw, +/proc/ambarella/vapi_sync r, +/proc/ambarella/dsp_state r, + +# to match vin0_idsp, vin1_idsp, vin2_idsp, etc. +/proc/ambarella/vin[0-9]_idsp r, + +# to match e0021000.dma and e0020000.dma +/proc/ambarella/[0-9a-e][0-9a-e][0-9a-e][0-9a-e][0-9a-e][0-9a-e][0-9a-e][0-9a-e].dma rw, # needed to control the usb device attached to the DSP /proc/ambarella/usbphy0 rw, diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/dsp_test.go snapd-2.54.2+21.10/interfaces/builtin/dsp_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/dsp_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/dsp_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -98,7 +98,7 @@ spec := &apparmor.Specification{} err := spec.AddConnectedPlug(s.iface, s.plug, s.ambarellaSlot) c.Assert(err, IsNil) - c.Assert(spec.SnippetForTag("snap.my-device.svc"), testutil.Contains, "/proc/ambarella/vin0_idsp rw,\n") + c.Assert(spec.SnippetForTag("snap.my-device.svc"), testutil.Contains, "/proc/ambarella/vin[0-9]_idsp r,\n") } func (s *dspSuite) TestUDevConnectedPlugAmbarella(c *C) { diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/hardware_observe.go snapd-2.54.2+21.10/interfaces/builtin/hardware_observe.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/hardware_observe.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/hardware_observe.go 2022-01-06 21:25:16.000000000 +0000 @@ -45,6 +45,9 @@ /etc/modprobe.d/{,*} r, /{,usr/}lib/modprobe.d/{,*} r, +# for reading the available input devices on the system +/proc/bus/input/devices r, + # files in /sys pertaining to hardware (eg, 'lspci -A linux-sysfs') /sys/{block,bus,class,devices,firmware}/{,**} r, diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/kernel_module_control.go snapd-2.54.2+21.10/interfaces/builtin/kernel_module_control.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/kernel_module_control.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/kernel_module_control.go 2022-01-06 21:25:16.000000000 +0000 @@ -80,5 +80,7 @@ connectedPlugAppArmor: kernelModuleControlConnectedPlugAppArmor, connectedPlugSecComp: kernelModuleControlConnectedPlugSecComp, connectedPlugUDev: kernelModuleControlConnectedPlugUDev, + + usesSysModuleCapability: true, }) } diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/kernel_module_control_test.go snapd-2.54.2+21.10/interfaces/builtin/kernel_module_control_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/kernel_module_control_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/kernel_module_control_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -80,6 +80,8 @@ func (s *KernelModuleControlInterfaceSuite) TestAppArmorSpec(c *C) { spec := &apparmor.Specification{} c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Check(spec.SuppressSysModuleCapability(), Equals, false) + c.Check(spec.UsesSysModuleCapability(), Equals, true) c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "capability sys_module,") } diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/kernel_module_load.go snapd-2.54.2+21.10/interfaces/builtin/kernel_module_load.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/kernel_module_load.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/kernel_module_load.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,229 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin + +import ( + "errors" + "fmt" + "regexp" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/kmod" + "github.com/snapcore/snapd/snap" +) + +const kernelModuleLoadSummary = `allows constrained control over kernel module loading` + +const kernelModuleLoadBaseDeclarationPlugs = ` + kernel-module-load: + allow-installation: false + deny-auto-connection: true +` + +const kernelModuleLoadBaseDeclarationSlots = ` + kernel-module-load: + allow-installation: + slot-snap-type: + - core + deny-connection: true +` + +var modulesAttrTypeError = errors.New(`kernel-module-load "modules" attribute must be a list of dictionaries`) + +// kernelModuleLoadInterface allows creating transient and persistent modules +type kernelModuleLoadInterface struct { + commonInterface +} + +type loadOption int + +const ( + loadNone loadOption = iota + loadDenied + loadOnBoot +) + +type ModuleInfo struct { + name string + load loadOption + options string +} + +var kernelModuleNameRegexp = regexp.MustCompile(`^[-a-zA-Z0-9_]+$`) +var kernelModuleOptionsRegexp = regexp.MustCompile(`^([a-zA-Z][a-zA-Z0-9_]*(=[[:graph:]]+)? *)+$`) + +func enumerateModules(plug interfaces.Attrer, handleModule func(moduleInfo *ModuleInfo) error) error { + modulesAttr, ok := plug.Lookup("modules") + if !ok { + return nil + } + modules, ok := modulesAttr.([]interface{}) + if !ok { + return modulesAttrTypeError + } + + for _, m := range modules { + module, ok := m.(map[string]interface{}) + if !ok { + return modulesAttrTypeError + } + + name, ok := module["name"].(string) + if !ok { + return errors.New(`kernel-module-load "name" must be a string`) + } + + var load loadOption + if loadAttr, found := module["load"]; found { + loadString, ok := loadAttr.(string) + if !ok { + return errors.New(`kernel-module-load "load" must be a string`) + } + + switch loadString { + case "denied": + load = loadDenied + case "on-boot": + load = loadOnBoot + default: + return fmt.Errorf(`kernel-module-load "load" value is unrecognized: %q`, loadString) + } + } + + var options string + if optionsAttr, found := module["options"]; found { + options, ok = optionsAttr.(string) + if !ok { + return errors.New(`kernel-module-load "options" must be a string`) + } + } + + moduleInfo := &ModuleInfo{ + name: name, + load: load, + options: options, + } + + if err := handleModule(moduleInfo); err != nil { + return err + } + } + + return nil +} + +func validateNameAttr(name string) error { + if !kernelModuleNameRegexp.MatchString(name) { + return errors.New(`kernel-module-load "name" attribute is not a valid module name`) + } + + return nil +} + +func validateOptionsAttr(moduleInfo *ModuleInfo) error { + if moduleInfo.options == "" { + return nil + } + + if moduleInfo.load == loadDenied { + return errors.New(`kernel-module-load "options" attribute incompatible with "load: denied"`) + } + + if !kernelModuleOptionsRegexp.MatchString(moduleInfo.options) { + return fmt.Errorf(`kernel-module-load "options" attribute contains invalid characters: %q`, moduleInfo.options) + } + + return nil +} + +func validateModuleInfo(moduleInfo *ModuleInfo) error { + if err := validateNameAttr(moduleInfo.name); err != nil { + return err + } + + if err := validateOptionsAttr(moduleInfo); err != nil { + return err + } + + if moduleInfo.options == "" && moduleInfo.load == loadNone { + return errors.New(`kernel-module-load: must specify at least "load" or "options"`) + } + + return nil +} + +func (iface *kernelModuleLoadInterface) BeforeConnectPlug(plug *interfaces.ConnectedPlug) error { + numModulesEntries := 0 + err := enumerateModules(plug, func(moduleInfo *ModuleInfo) error { + numModulesEntries++ + return validateModuleInfo(moduleInfo) + }) + if err != nil { + return err + } + + if numModulesEntries == 0 { + return modulesAttrTypeError + } + + return nil +} + +func (iface *kernelModuleLoadInterface) KModConnectedPlug(spec *kmod.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + err := enumerateModules(plug, func(moduleInfo *ModuleInfo) error { + var err error + switch moduleInfo.load { + case loadDenied: + err = spec.DisallowModule(moduleInfo.name) + case loadOnBoot: + err = spec.AddModule(moduleInfo.name) + if err != nil { + break + } + fallthrough + case loadNone: + if len(moduleInfo.options) > 0 { + err = spec.SetModuleOptions(moduleInfo.name, moduleInfo.options) + } + default: + // we can panic, this will be catched on validation + panic("Unsupported module load option") + } + return err + }) + return err +} + +func (iface *kernelModuleLoadInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool { + return true +} + +func init() { + registerIface(&kernelModuleLoadInterface{ + commonInterface: commonInterface{ + name: "kernel-module-load", + summary: kernelModuleLoadSummary, + baseDeclarationPlugs: kernelModuleLoadBaseDeclarationPlugs, + baseDeclarationSlots: kernelModuleLoadBaseDeclarationSlots, + implicitOnCore: true, + implicitOnClassic: true, + }, + }) +} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/kernel_module_load_test.go snapd-2.54.2+21.10/interfaces/builtin/kernel_module_load_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/kernel_module_load_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/kernel_module_load_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,193 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin_test + +import ( + "fmt" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/interfaces/kmod" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +type KernelModuleLoadInterfaceSuite struct { + testutil.BaseTest + + iface interfaces.Interface + slotInfo *snap.SlotInfo + slot *interfaces.ConnectedSlot + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug +} + +var _ = Suite(&KernelModuleLoadInterfaceSuite{ + iface: builtin.MustInterface("kernel-module-load"), +}) + +const kernelModuleLoadConsumerYaml = `name: consumer +version: 0 +plugs: + kmod: + interface: kernel-module-load + modules: + - name: forbidden + load: denied + - name: mymodule1 + load: on-boot + options: p1=3 p2=true p3 + - name: mymodule2 + options: param_1=ok param_2=false +apps: + app: + plugs: [kmod] +` + +const kernelModuleLoadCoreYaml = `name: core +version: 0 +type: os +slots: + kernel-module-load: +` + +func (s *KernelModuleLoadInterfaceSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + s.plug, s.plugInfo = MockConnectedPlug(c, kernelModuleLoadConsumerYaml, nil, "kmod") + s.slot, s.slotInfo = MockConnectedSlot(c, kernelModuleLoadCoreYaml, nil, "kernel-module-load") +} + +func (s *KernelModuleLoadInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "kernel-module-load") +} + +func (s *KernelModuleLoadInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) +} + +func (s *KernelModuleLoadInterfaceSuite) TestSanitizePlug(c *C) { + c.Check(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) + c.Check(interfaces.BeforeConnectPlug(s.iface, s.plug), IsNil) +} + +func (s *KernelModuleLoadInterfaceSuite) TestSanitizePlugUnhappy(c *C) { + var kernelModuleLoadYaml = `name: consumer +version: 0 +plugs: + kmod: + interface: kernel-module-load + %s +apps: + app: + plugs: [kmod] +` + data := []struct { + plugYaml string + expectedError string + }{ + { + "", // missing "modules" attribute + `kernel-module-load "modules" attribute must be a list of dictionaries`, + }, + { + "modules: a string", + `kernel-module-load "modules" attribute must be a list of dictionaries`, + }, + { + "modules: [this, is, a, list]", + `kernel-module-load "modules" attribute must be a list of dictionaries`, + }, + { + "modules:\n - name: [this, is, a, list]", + `kernel-module-load "name" must be a string`, + }, + { + "modules:\n - name: pcspkr", + `kernel-module-load: must specify at least "load" or "options"`, + }, + { + "modules:\n - name: pcspkr\n load: [yes, no]", + `kernel-module-load "load" must be a string`, + }, + { + "modules:\n - name: pcspkr\n load: maybe", + `kernel-module-load "load" value is unrecognized: "maybe"`, + }, + { + "modules:\n - name: pcspkr\n options: [one, two]", + `kernel-module-load "options" must be a string`, + }, + { + "modules:\n - name: pcspkr\n options: \"a\\nnewline\"", + `kernel-module-load "options" attribute contains invalid characters: "a\\nnewline"`, + }, + { + "modules:\n - name: pcspkr\n options: \"5tartWithNumber=1\"", + `kernel-module-load "options" attribute contains invalid characters: "5tartWithNumber=1"`, + }, + { + "modules:\n - name: pcspkr\n options: \"no-dashes\"", + `kernel-module-load "options" attribute contains invalid characters: "no-dashes"`, + }, + { + "modules:\n - name: pcspkr\n load: denied\n options: p1=true", + `kernel-module-load "options" attribute incompatible with "load: denied"`, + }, + } + + for _, testData := range data { + snapYaml := fmt.Sprintf(kernelModuleLoadYaml, testData.plugYaml) + plug, _ := MockConnectedPlug(c, snapYaml, nil, "kmod") + err := interfaces.BeforeConnectPlug(s.iface, plug) + c.Check(err, ErrorMatches, testData.expectedError, Commentf("yaml: %s", testData.plugYaml)) + } +} + +func (s *KernelModuleLoadInterfaceSuite) TestKModSpec(c *C) { + spec := &kmod.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Check(spec.Modules(), DeepEquals, map[string]bool{ + "mymodule1": true, + }) + c.Check(spec.ModuleOptions(), DeepEquals, map[string]string{ + "mymodule1": "p1=3 p2=true p3", + "mymodule2": "param_1=ok param_2=false", + }) + c.Check(spec.DisallowedModules(), DeepEquals, []string{"forbidden"}) +} + +func (s *KernelModuleLoadInterfaceSuite) TestStaticInfo(c *C) { + si := interfaces.StaticInfoOf(s.iface) + c.Assert(si.ImplicitOnCore, Equals, true) + c.Assert(si.ImplicitOnClassic, Equals, true) + c.Assert(si.Summary, Equals, `allows constrained control over kernel module loading`) + c.Assert(si.BaseDeclarationSlots, testutil.Contains, "kernel-module-load") +} + +func (s *KernelModuleLoadInterfaceSuite) TestAutoConnect(c *C) { + c.Assert(s.iface.AutoConnect(s.plugInfo, s.slotInfo), Equals, true) +} + +func (s *KernelModuleLoadInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/kubernetes_support.go snapd-2.54.2+21.10/interfaces/builtin/kubernetes_support.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/kubernetes_support.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/kubernetes_support.go 2022-01-06 21:25:16.000000000 +0000 @@ -63,6 +63,11 @@ capability dac_override, +# Lock file used by Calico's IPAM plugin. This is configurable via the +# (undocumented) "ipam_lock_file" configuration key: +# https://github.com/projectcalico/cni-plugin/blob/master/pkg/types/types.go +/{,var/}run/calico/ipam.lock rwk, + /{,usr/}bin/systemd-run Cxr -> systemd_run, /run/systemd/private r, profile systemd_run (attach_disconnected,mediate_deleted) { diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/log_observe.go snapd-2.54.2+21.10/interfaces/builtin/log_observe.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/log_observe.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/log_observe.go 2022-01-06 21:25:16.000000000 +0000 @@ -35,6 +35,8 @@ /var/log/ r, /var/log/** r, +# for accessing dmesg +/dev/kmsg r, # for accessing journald and journalctl /run/log/journal/ r, diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/microstack_support.go snapd-2.54.2+21.10/interfaces/builtin/microstack_support.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/microstack_support.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/microstack_support.go 2022-01-06 21:25:16.000000000 +0000 @@ -27,6 +27,10 @@ * as an auxiliary daemon. tun/tap kernel module is used for creating virtual interfaces. * Virtual machines rely on KVM for virtualization acceleration and on vhost * framework in the kernel (vhost_net, vhost_scsi, vhost_vsock). + * + * This interface uses the controlsDeviceCgroup flag (which implies + * `Delegate=true` on the systemd unit) since the snap already manages the + * cgroup configuration of its containers. */ const microStackSupportSummary = `allows operating as the MicroStack service` @@ -216,19 +220,6 @@ mknodat - - |S_IFBLK - ` -var microStackConnectedPlugUDev = []string{ - `KERNEL=="vhost-net"`, - `KERNEL=="vhost-scsi"`, - `KERNEL=="vhost-vsock"`, - `SUBSYSTEM=="block", KERNEL=="nbd[0-9]*"`, - `SUBSYSTEM=="misc", KERNEL=="vfio"`, - `SUBSYSTEM=="vfio", KERNEL=="[0-9]*"`, - `SUBSYSTEM=="block", KERNEL=="loop[0-9]*"`, - `SUBSYSTEM=="misc", KERNEL=="loop-control"`, - `SUBSYSTEM=="misc", KERNEL=="device-mapper"`, - `SUBSYSTEM=="block", KERNEL=="dm-[0-9]*"`, -} - type microStackInterface struct { commonInterface } @@ -255,11 +246,11 @@ summary: microStackSupportSummary, implicitOnCore: true, implicitOnClassic: true, + controlsDeviceCgroup: true, baseDeclarationSlots: microStackSupportBaseDeclarationSlots, baseDeclarationPlugs: microStackSupportBaseDeclarationPlugs, connectedPlugAppArmor: microStackSupportConnectedPlugAppArmor, connectedPlugSecComp: microStackSupportConnectedPlugSecComp, - connectedPlugUDev: microStackConnectedPlugUDev, connectedPlugKModModules: microStackSupportConnectedPlugKmod, serviceSnippets: []string{`Delegate=true`}, }}) diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/microstack_support_test.go snapd-2.54.2+21.10/interfaces/builtin/microstack_support_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/microstack_support_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/microstack_support_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -20,11 +20,8 @@ package builtin_test import ( - "fmt" - . "gopkg.in/check.v1" - "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/apparmor" "github.com/snapcore/snapd/interfaces/builtin" @@ -132,31 +129,10 @@ func (s *microStackSupportInterfaceSuite) TestUDevConnectedPlug(c *C) { spec := &udev.Specification{} + // no udev specs because the interface controls it's own device cgroups err := spec.AddConnectedPlug(s.iface, s.plug, s.slot) c.Assert(err, IsNil) - c.Assert(spec.Snippets(), HasLen, 11) - c.Assert(spec.Snippets(), testutil.Contains, `# microstack-support -KERNEL=="vhost-net", TAG+="snap_microstack_app"`) - c.Assert(spec.Snippets(), testutil.Contains, `# microstack-support -KERNEL=="vhost-scsi", TAG+="snap_microstack_app"`) - c.Assert(spec.Snippets(), testutil.Contains, `# microstack-support -KERNEL=="vhost-vsock", TAG+="snap_microstack_app"`) - c.Assert(spec.Snippets(), testutil.Contains, `# microstack-support -SUBSYSTEM=="block", KERNEL=="nbd[0-9]*", TAG+="snap_microstack_app"`) - c.Assert(spec.Snippets(), testutil.Contains, `# microstack-support -SUBSYSTEM=="misc", KERNEL=="vfio", TAG+="snap_microstack_app"`) - c.Assert(spec.Snippets(), testutil.Contains, `# microstack-support -SUBSYSTEM=="vfio", KERNEL=="[0-9]*", TAG+="snap_microstack_app"`) - c.Assert(spec.Snippets(), testutil.Contains, `# microstack-support -SUBSYSTEM=="block", KERNEL=="loop[0-9]*", TAG+="snap_microstack_app"`) - c.Assert(spec.Snippets(), testutil.Contains, `# microstack-support -SUBSYSTEM=="misc", KERNEL=="loop-control", TAG+="snap_microstack_app"`) - c.Assert(spec.Snippets(), testutil.Contains, `# microstack-support -SUBSYSTEM=="misc", KERNEL=="device-mapper", TAG+="snap_microstack_app"`) - c.Assert(spec.Snippets(), testutil.Contains, `# microstack-support -SUBSYSTEM=="block", KERNEL=="dm-[0-9]*", TAG+="snap_microstack_app"`) - c.Assert(spec.Snippets(), testutil.Contains, - fmt.Sprintf(`TAG=="snap_microstack_app", RUN+="%s/snap-device-helper $env{ACTION} snap_microstack_app $devpath $major:$minor"`, dirs.DistroLibExecDir)) + c.Assert(spec.Snippets(), HasLen, 0) } func (s *microStackSupportInterfaceSuite) TestInterfaces(c *C) { diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/modem_manager.go snapd-2.54.2+21.10/interfaces/builtin/modem_manager.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/modem_manager.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/modem_manager.go 2022-01-06 21:25:16.000000000 +0000 @@ -154,6 +154,12 @@ path=/org/freedesktop/ModemManager1{,/**} interface=org.freedesktop.DBus.* peer=(label=###PLUG_SECURITY_TAGS###), + +# allow communicating with the mbim and qmi proxy servers, which provide +# support for talking to WWAN modems and devices which speak the Mobile +# Interface Broadband Model (MBIM) and Qualcomm MSM Interface (QMI) +# protocols respectively +unix (accept, receive, send) type=stream peer=(label=###PLUG_SECURITY_TAGS###), ` const modemManagerConnectedPlugAppArmor = ` diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/mount_control.go snapd-2.54.2+21.10/interfaces/builtin/mount_control.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/mount_control.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/mount_control.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,472 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin + +import ( + "bytes" + "errors" + "fmt" + "regexp" + "strings" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/utils" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/strutil" + "github.com/snapcore/snapd/systemd" +) + +const mountControlSummary = `allows creating transient and persistent mounts` + +const mountControlBaseDeclarationPlugs = ` + mount-control: + allow-installation: false + deny-auto-connection: true +` + +const mountControlBaseDeclarationSlots = ` + mount-control: + allow-installation: + slot-snap-type: + - core + deny-connection: true +` + +var mountAttrTypeError = errors.New(`mount-control "mount" attribute must be a list of dictionaries`) + +const mountControlConnectedPlugSecComp = ` +# Description: Allow mount and umount syscall access. No filtering here, as we +# rely on AppArmor to filter the mount operations. +mount +umount +umount2 +` + +// The reason why this list is not shared with osutil.MountOptsToCommonFlags or +// other parts of the codebase is that this one only contains the options which +// have been deemed safe and have been vetted by the security team. +var allowedMountOptions = []string{ + "async", + "atime", + "bind", + "diratime", + "dirsync", + "iversion", + "lazytime", + "nofail", + "noiversion", + "nomand", + "noatime", + "nodev", + "nodiratime", + "noexec", + "nolazytime", + "norelatime", + "nosuid", + "nostrictatime", + "nouser", + "relatime", + "strictatime", + "sync", + "ro", + "rw", +} + +// A few mount flags are special in that if they are specified, the filesystem +// type is ignored. We list them here, and we will ensure that the plug +// declaration does not specify a type, if any of them is present among the +// options. +var optionsWithoutFsType = []string{ + "bind", + // Note: the following flags should also fall into this list, but we are + // not currently allowing them (and don't plan to): + // - "make-private" + // - "make-shared" + // - "make-slave" + // - "make-unbindable" + // - "move" + // - "remount" +} + +// List of allowed filesystem types. This can be extended, keeping in mind that +// the filesystems in the following list were considered either dangerous or +// not relevant for this interface: +// bpf +// cgroup +// cgroup2 +// debugfs +// devpts +// ecryptfs +// hugetlbfs +// overlayfs +// proc +// securityfs +// sysfs +// tracefs +var allowedFSTypes = []string{ + "aufs", + "autofs", + "btrfs", + "ext2", + "ext3", + "ext4", + "hfs", + "iso9660", + "jfs", + "msdos", + "ntfs", + "ramfs", + "reiserfs", + "squashfs", + "tmpfs", + "ubifs", + "udf", + "ufs", + "vfat", + "zfs", + "xfs", +} + +// mountControlInterface allows creating transient and persistent mounts +type mountControlInterface struct { + commonInterface +} + +// The "what" and "where" attributes end up in the AppArmor profile, surrounded +// by double quotes; to ensure that a malicious snap cannot inject arbitrary +// rules by specifying something like +// where: $SNAP_DATA/foo", /** rw, # +// which would generate a profile line like: +// mount options=() "$SNAP_DATA/foo", /** rw, #" +// (which would grant read-write access to the whole filesystem), it's enough +// to exclude the `"` character: without it, whatever is written in the +// attribute will not be able to escape being treated like a pattern. +// +// To be safe, there's more to be done: the pattern also needs to be valid, as +// a malformed one (for example, a pattern having an unmatched `}`) would cause +// apparmor_parser to fail loading the profile. For this situation, we use the +// PathPattern interface to validate the pattern. +// +// Besides that, we are also excluding the `@` character, which is used to mark +// AppArmor variables (tunables): when generating the profile we lack the +// knowledge of which variables have been defined, so it's safer to exclude +// them. +// The what attribute regular expression here is intentionally permissive of +// nearly any path, and due to the super-privileged nature of this interface it +// is expected that sensible values of what are enforced by the store manual +// review queue and security teams. +var ( + whatRegexp = regexp.MustCompile(`^/[^"@]*$`) + whereRegexp = regexp.MustCompile(`^(\$SNAP_COMMON|\$SNAP_DATA)?/[^\$"@]+$`) +) + +// Excluding spaces and other characters which might allow constructing a +// malicious string like +// auto) options=() /malicious/content /var/lib/snapd/hostfs/...,\n mount fstype=( +var typeRegexp = regexp.MustCompile(`^[a-z0-9]+$`) + +type MountInfo struct { + what string + where string + persistent bool + types []string + options []string +} + +func parseStringList(mountEntry map[string]interface{}, fieldName string) ([]string, error) { + var list []string + value, ok := mountEntry[fieldName] + if ok { + interfaceList, ok := value.([]interface{}) + if !ok { + return nil, fmt.Errorf(`mount-control "%s" must be an array of strings (got %q)`, fieldName, value) + } + for i, iface := range interfaceList { + valueString, ok := iface.(string) + if !ok { + return nil, fmt.Errorf(`mount-control "%s" element %d not a string (%q)`, fieldName, i+1, iface) + } + list = append(list, valueString) + } + } + return list, nil +} + +func enumerateMounts(plug interfaces.Attrer, fn func(mountInfo *MountInfo) error) error { + mountAttr, ok := plug.Lookup("mount") + if !ok { + return nil + } + mounts, ok := mountAttr.([]interface{}) + if !ok { + return mountAttrTypeError + } + + for _, m := range mounts { + mount, ok := m.(map[string]interface{}) + if !ok { + return mountAttrTypeError + } + + what, ok := mount["what"].(string) + if !ok { + return fmt.Errorf(`mount-control "what" must be a string`) + } + + where, ok := mount["where"].(string) + if !ok { + return fmt.Errorf(`mount-control "where" must be a string`) + } + + persistent := false + persistentValue, ok := mount["persistent"] + if ok { + if persistent, ok = persistentValue.(bool); !ok { + return fmt.Errorf(`mount-control "persistent" must be a boolean`) + } + } + + types, err := parseStringList(mount, "type") + if err != nil { + return err + } + + options, err := parseStringList(mount, "options") + if err != nil { + return err + } + + mountInfo := &MountInfo{ + what: what, + where: where, + persistent: persistent, + types: types, + options: options, + } + + if err := fn(mountInfo); err != nil { + return err + } + } + + return nil +} + +func validateWhatAttr(what string) error { + if !whatRegexp.MatchString(what) { + return fmt.Errorf(`mount-control "what" attribute is invalid: must start with / and not contain special characters`) + } + + if !cleanSubPath(what) { + return fmt.Errorf(`mount-control "what" pattern is not clean: %q`, what) + } + + if _, err := utils.NewPathPattern(what); err != nil { + return fmt.Errorf(`mount-control "what" setting cannot be used: %v`, err) + } + + return nil +} + +func validateWhereAttr(where string) error { + if !whereRegexp.MatchString(where) { + return fmt.Errorf(`mount-control "where" attribute must start with $SNAP_COMMON, $SNAP_DATA or / and not contain special characters`) + } + + if !cleanSubPath(where) { + return fmt.Errorf(`mount-control "where" pattern is not clean: %q`, where) + } + + if _, err := utils.NewPathPattern(where); err != nil { + return fmt.Errorf(`mount-control "where" setting cannot be used: %v`, err) + } + + return nil +} + +func validateMountTypes(types []string) error { + for _, t := range types { + if !typeRegexp.MatchString(t) { + return fmt.Errorf(`mount-control filesystem type invalid: %q`, t) + } + if !strutil.ListContains(allowedFSTypes, t) { + return fmt.Errorf(`mount-control forbidden filesystem type: %q`, t) + } + } + return nil +} + +func validateMountOptions(options []string) error { + if len(options) == 0 { + return errors.New(`mount-control "options" cannot be empty`) + } + for _, o := range options { + if !strutil.ListContains(allowedMountOptions, o) { + return fmt.Errorf(`mount-control option unrecognized or forbidden: %q`, o) + } + } + return nil +} + +// Find the first option which is incompatible with a FS type declaration +func optionIncompatibleWithFsType(options []string) string { + for _, o := range options { + if strutil.ListContains(optionsWithoutFsType, o) { + return o + } + } + return "" +} + +func validateMountInfo(mountInfo *MountInfo) error { + if err := validateWhatAttr(mountInfo.what); err != nil { + return err + } + + if err := validateWhereAttr(mountInfo.where); err != nil { + return err + } + + if err := validateMountTypes(mountInfo.types); err != nil { + return err + } + + if err := validateMountOptions(mountInfo.options); err != nil { + return err + } + + // Check if any options are incompatible with specifying a FS type + fsExclusiveOption := optionIncompatibleWithFsType(mountInfo.options) + if fsExclusiveOption != "" && len(mountInfo.types) > 0 { + return fmt.Errorf(`mount-control option %q is incompatible with specifying filesystem type`, fsExclusiveOption) + } + + return nil +} + +func (iface *mountControlInterface) BeforeConnectPlug(plug *interfaces.ConnectedPlug) error { + // The systemd.ListMountUnits() method works by issuing the command + // "systemctl show *.mount", but globbing was only added in systemd v209. + if err := systemd.EnsureAtLeast(209); err != nil { + return err + } + + hasMountEntries := false + err := enumerateMounts(plug, func(mountInfo *MountInfo) error { + hasMountEntries = true + return validateMountInfo(mountInfo) + }) + if err != nil { + return err + } + + if !hasMountEntries { + return mountAttrTypeError + } + + return nil +} + +func (iface *mountControlInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + mountControlSnippet := bytes.NewBuffer(nil) + emit := func(f string, args ...interface{}) { + fmt.Fprintf(mountControlSnippet, f, args...) + } + snapInfo := plug.Snap() + + emit(` + # Rules added by the mount-control interface + capability sys_admin, # for mount + + owner @{PROC}/@{pid}/mounts r, + owner @{PROC}/@{pid}/mountinfo r, + owner @{PROC}/self/mountinfo r, + + /{,usr/}bin/mount ixr, + /{,usr/}bin/umount ixr, + # mount/umount (via libmount) track some mount info in these files + /run/mount/utab* wrlk, +`) + + // No validation is occurring here, as it was already performed in + // BeforeConnectPlug() + enumerateMounts(plug, func(mountInfo *MountInfo) error { + + source := mountInfo.what + target := mountInfo.where + if target[0] == '$' { + matches := whereRegexp.FindStringSubmatchIndex(target) + if matches == nil || len(matches) < 4 { + // This cannot really happen, as the string wouldn't pass the validation + return fmt.Errorf(`internal error: "where" fails to match regexp: %q`, mountInfo.where) + } + // the first two elements in "matches" are the boundaries of the whole + // string; the next two are the boundaries of the first match, which is + // what we care about as it contains the environment variable we want + // to expand: + variableStart, variableEnd := matches[2], matches[3] + variable := target[variableStart:variableEnd] + expanded := snapInfo.ExpandSnapVariables(variable) + target = expanded + target[variableEnd:] + } + + var typeRule string + if optionIncompatibleWithFsType(mountInfo.options) != "" { + // In this rule the FS type will not match unless it's empty + typeRule = "" + } else { + var types []string + if len(mountInfo.types) > 0 { + types = mountInfo.types + } else { + types = allowedFSTypes + } + typeRule = "fstype=(" + strings.Join(types, ",") + ")" + } + + options := strings.Join(mountInfo.options, ",") + + emit(" mount %s options=(%s) \"%s\" -> \"%s\",\n", typeRule, options, source, target) + emit(" umount \"%s\",\n", target) + return nil + }) + + spec.AddSnippet(mountControlSnippet.String()) + return nil +} + +func (iface *mountControlInterface) AutoConnect(*snap.PlugInfo, *snap.SlotInfo) bool { + return true +} + +func init() { + registerIface(&mountControlInterface{ + commonInterface: commonInterface{ + name: "mount-control", + summary: mountControlSummary, + baseDeclarationPlugs: mountControlBaseDeclarationPlugs, + baseDeclarationSlots: mountControlBaseDeclarationSlots, + implicitOnCore: true, + implicitOnClassic: true, + connectedPlugSecComp: mountControlConnectedPlugSecComp, + }, + }) +} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/mount_control_test.go snapd-2.54.2+21.10/interfaces/builtin/mount_control_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/mount_control_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/mount_control_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,300 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin_test + +import ( + "fmt" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/interfaces/seccomp" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/systemd" + "github.com/snapcore/snapd/testutil" +) + +type MountControlInterfaceSuite struct { + testutil.BaseTest + + iface interfaces.Interface + slotInfo *snap.SlotInfo + slot *interfaces.ConnectedSlot + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug +} + +var _ = Suite(&MountControlInterfaceSuite{ + iface: builtin.MustInterface("mount-control"), +}) + +const mountControlConsumerYaml = `name: consumer +version: 0 +plugs: + mntctl: + interface: mount-control + mount: + - what: /dev/sd* + where: /media/** + type: [ext2, ext3, ext4] + options: [rw, sync] + - what: /usr/** + where: $SNAP_COMMON/** + options: [bind] + - what: /dev/sda{0,1} + where: $SNAP_COMMON/** + options: [ro] + - what: /dev/sda[0-1] + where: $SNAP_COMMON/{foo,other,**} + options: [sync] +apps: + app: + plugs: [mntctl] +` + +const mountControlCoreYaml = `name: core +version: 0 +type: os +slots: + mount-control: +` + +func (s *MountControlInterfaceSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + s.plug, s.plugInfo = MockConnectedPlug(c, mountControlConsumerYaml, nil, "mntctl") + s.slot, s.slotInfo = MockConnectedSlot(c, mountControlCoreYaml, nil, "mount-control") + + s.AddCleanup(systemd.MockSystemdVersion(210, nil)) +} + +func (s *MountControlInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "mount-control") +} + +func (s *MountControlInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) +} + +func (s *MountControlInterfaceSuite) TestSanitizePlug(c *C) { + c.Check(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) + c.Check(interfaces.BeforeConnectPlug(s.iface, s.plug), IsNil) +} + +func (s *MountControlInterfaceSuite) TestSanitizePlugOldSystemd(c *C) { + restore := systemd.MockSystemdVersion(208, nil) + defer restore() + err := interfaces.BeforeConnectPlug(s.iface, s.plug) + c.Assert(err, ErrorMatches, `systemd version 208 is too old \(expected at least 209\)`) +} + +func (s *MountControlInterfaceSuite) TestSanitizePlugUnhappy(c *C) { + var mountControlYaml = `name: consumer +version: 0 +plugs: + mntctl: + interface: mount-control + %s +apps: + app: + plugs: [mntctl] +` + data := []struct { + plugYaml string + expectedError string + }{ + { + "", // missing "mount" attribute + `mount-control "mount" attribute must be a list of dictionaries`, + }, + { + "mount: a string", + `mount-control "mount" attribute must be a list of dictionaries`, + }, + { + "mount: [this, is, a, list]", + `mount-control "mount" attribute must be a list of dictionaries`, + }, + { + "mount:\n - what: [this, is, a, list]\n where: /media/**", + `mount-control "what" must be a string`, + }, + { + "mount:\n - what: /path/\n where: [this, is, a, list]", + `mount-control "where" must be a string`, + }, + { + "mount:\n - what: /\n where: /\n persistent: string", + `mount-control "persistent" must be a boolean`, + }, + { + "mount:\n - what: /\n where: /\n type: string", + `mount-control "type" must be an array of strings.*`, + }, + { + "mount:\n - what: /\n where: /\n type: [true, false]", + `mount-control "type" element 1 not a string.*`, + }, + { + "mount:\n - what: /\n where: /media/*\n type: [auto)]", + `mount-control filesystem type invalid.*`, + }, + { + "mount:\n - what: /\n where: /media/*\n type: [upperCase]", + `mount-control filesystem type invalid.*`, + }, + { + "mount:\n - what: /\n where: /media/*\n type: [two words]", + `mount-control filesystem type invalid.*`, + }, + { + "mount:\n - what: /\n where: /media/*\n", + `mount-control "options" cannot be empty`, + }, + { + "mount:\n - what: /\n where: /\n options: string", + `mount-control "options" must be an array of strings.*`, + }, + { + "mount:\n - what: /\n where: /media/*\n options: []", + `mount-control "options" cannot be empty`, + }, + { + "mount:\n - what: here\n where: /mnt", + `mount-control "what" attribute is invalid: must start with / and not contain special characters`, + }, + { + "mount:\n - what: /double\"quote\n where: /mnt", + `mount-control "what" attribute is invalid: must start with / and not contain special characters`, + }, + { + "mount:\n - what: /variables/are/not/@{allowed}\n where: /mnt", + `mount-control "what" attribute is invalid: must start with / and not contain special characters`, + }, + { + "mount:\n - what: /invalid}pattern\n where: /mnt", + `mount-control "what" setting cannot be used: invalid closing brace, no matching open.*`, + }, + { + "mount:\n - what: /\n where: /\n options: [ro]", + `mount-control "where" attribute must start with \$SNAP_COMMON, \$SNAP_DATA or / and not contain special characters`, + }, + { + "mount:\n - what: /\n where: /media/no\"quotes", + `mount-control "where" attribute must start with \$SNAP_COMMON, \$SNAP_DATA or / and not contain special characters`, + }, + { + "mount:\n - what: /\n where: /media/no@{variables}", + `mount-control "where" attribute must start with \$SNAP_COMMON, \$SNAP_DATA or / and not contain special characters`, + }, + { + "mount:\n - what: /\n where: $SNAP_DATA/$SNAP_DATA", + `mount-control "where" attribute must start with \$SNAP_COMMON, \$SNAP_DATA or / and not contain special characters`, + }, + { + "mount:\n - what: /\n where: /$SNAP_DATA", + `mount-control "where" attribute must start with \$SNAP_COMMON, \$SNAP_DATA or / and not contain special characters`, + }, + { + "mount:\n - what: /\n where: /media/invalid[path", + `mount-control "where" setting cannot be used: missing closing bracket ']'.*`, + }, + { + "mount:\n - what: /\n where: /media/*\n options: [sync,invalid]", + `mount-control option unrecognized or forbidden: "invalid"`, + }, + { + "mount:\n - what: /\n where: /media/*\n type: [ext4,debugfs]", + `mount-control forbidden filesystem type: "debugfs"`, + }, + { + "mount:\n - what: /\n where: /media/*\n type: [ext4]\n options: [rw,bind]", + `mount-control option "bind" is incompatible with specifying filesystem type`, + }, + { + "mount:\n - what: /tmp/..\n where: /media/*", + `mount-control "what" pattern is not clean:.*`, + }, + { + "mount:\n - what: /\n where: /media/../etc", + `mount-control "where" pattern is not clean:.*`, + }, + } + + for _, testData := range data { + snapYaml := fmt.Sprintf(mountControlYaml, testData.plugYaml) + plug, _ := MockConnectedPlug(c, snapYaml, nil, "mntctl") + err := interfaces.BeforeConnectPlug(s.iface, plug) + c.Check(err, ErrorMatches, testData.expectedError, Commentf("Yaml: %s", testData.plugYaml)) + } +} + +func (s *MountControlInterfaceSuite) TestSecCompSpec(c *C) { + spec := &seccomp.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "mount\n") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "umount\n") + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "umount2\n") +} + +func (s *MountControlInterfaceSuite) TestAppArmorSpec(c *C) { + spec := &apparmor.Specification{} + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `capability sys_admin,`) + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, `/{,usr/}bin/mount ixr,`) + + expectedMountLine1 := `mount fstype=(ext2,ext3,ext4) options=(rw,sync) "/dev/sd*" -> "/media/**",` + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine1) + + expectedMountLine2 := `mount options=(bind) "/usr/**" -> "/var/snap/consumer/common/**",` + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine2) + + expectedMountLine3 := `mount fstype=(` + + `aufs,autofs,btrfs,ext2,ext3,ext4,hfs,iso9660,jfs,msdos,ntfs,ramfs,` + + `reiserfs,squashfs,tmpfs,ubifs,udf,ufs,vfat,zfs,xfs` + + `) options=(ro) "/dev/sda{0,1}" -> "/var/snap/consumer/common/**",` + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine3) + + expectedMountLine4 := `mount fstype=(` + + `aufs,autofs,btrfs,ext2,ext3,ext4,hfs,iso9660,jfs,msdos,ntfs,ramfs,` + + `reiserfs,squashfs,tmpfs,ubifs,udf,ufs,vfat,zfs,xfs` + + `) options=(sync) "/dev/sda[0-1]" -> "/var/snap/consumer/common/{foo,other,**}",` + c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, expectedMountLine4) +} + +func (s *MountControlInterfaceSuite) TestStaticInfo(c *C) { + si := interfaces.StaticInfoOf(s.iface) + c.Assert(si.ImplicitOnCore, Equals, true) + c.Assert(si.ImplicitOnClassic, Equals, true) + c.Assert(si.Summary, Equals, `allows creating transient and persistent mounts`) + c.Assert(si.BaseDeclarationSlots, testutil.Contains, "mount-control") +} + +func (s *MountControlInterfaceSuite) TestAutoConnect(c *C) { + c.Assert(s.iface.AutoConnect(s.plugInfo, s.slotInfo), Equals, true) +} + +func (s *MountControlInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/mpris.go snapd-2.54.2+21.10/interfaces/builtin/mpris.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/mpris.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/mpris.go 2022-01-06 21:25:16.000000000 +0000 @@ -158,6 +158,14 @@ bus=session path=/org/mpris/MediaPlayer2 peer=(label=###SLOT_SECURITY_TAGS###), + +# receive signals for updated properties +dbus (receive) + bus=session + interface=org.freedesktop.DBus.Properties + path=/org/mpris/MediaPlayer2 + member=PropertiesChanged + peer=(label=###SLOT_SECURITY_TAGS###), ` type mprisInterface struct{} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/netlink_audit.go snapd-2.54.2+21.10/interfaces/builtin/netlink_audit.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/netlink_audit.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/netlink_audit.go 2022-01-06 21:25:16.000000000 +0000 @@ -19,6 +19,14 @@ package builtin +import ( + "errors" + + "github.com/snapcore/snapd/interfaces" + apparmor_sandbox "github.com/snapcore/snapd/sandbox/apparmor" + "github.com/snapcore/snapd/strutil" +) + const netlinkAuditSummary = `allows access to kernel audit system through netlink` const netlinkAuditBaseDeclarationSlots = ` @@ -51,8 +59,31 @@ capability audit_write, ` +type netlinkAuditInterface struct { + commonInterface +} + +func (iface *netlinkAuditInterface) BeforeConnectPlug(plug *interfaces.ConnectedPlug) error { + if apparmor_sandbox.ProbedLevel() == apparmor_sandbox.Unsupported { + // no apparmor means we don't have to deal with parser features + return nil + } + features, err := apparmor_sandbox.ParserFeatures() + if err != nil { + return err + } + + if !strutil.ListContains(features, "cap-audit-read") { + // the host system doesn't have the required feature to compile the + // policy (that happens in 14.04) + return errors.New("cannot connect plug on system without audit_read support") + } + + return nil +} + func init() { - registerIface(&commonInterface{ + registerIface(&netlinkAuditInterface{commonInterface{ name: "netlink-audit", summary: netlinkAuditSummary, implicitOnCore: true, @@ -60,5 +91,5 @@ baseDeclarationSlots: netlinkAuditBaseDeclarationSlots, connectedPlugSecComp: netlinkAuditConnectedPlugSecComp, connectedPlugAppArmor: netlinkAuditConnectedPlugAppArmor, - }) + }}) } diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/netlink_audit_test.go snapd-2.54.2+21.10/interfaces/builtin/netlink_audit_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/netlink_audit_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/netlink_audit_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -26,6 +26,7 @@ "github.com/snapcore/snapd/interfaces/apparmor" "github.com/snapcore/snapd/interfaces/builtin" "github.com/snapcore/snapd/interfaces/seccomp" + apparmor_sandbox "github.com/snapcore/snapd/sandbox/apparmor" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/snap/snaptest" "github.com/snapcore/snapd/testutil" @@ -75,6 +76,22 @@ c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) } +func (s *NetlinkAuditInterfaceSuite) TestSanitizePlugConnectionMissingAppArmorSandboxFeatures(c *C) { + r := apparmor_sandbox.MockLevel(apparmor_sandbox.Full) + defer r() + r = apparmor_sandbox.MockFeatures(nil, nil, nil, nil) + defer r() + err := interfaces.BeforeConnectPlug(s.iface, s.plug) + c.Assert(err, ErrorMatches, "cannot connect plug on system without audit_read support") +} + +func (s *NetlinkAuditInterfaceSuite) TestSanitizePlugConnectionMissingNoAppArmor(c *C) { + r := apparmor_sandbox.MockLevel(apparmor_sandbox.Unsupported) + defer r() + err := interfaces.BeforeConnectPlug(s.iface, s.plug) + c.Assert(err, IsNil) +} + func (s *NetlinkAuditInterfaceSuite) TestAppArmorSpec(c *C) { spec := &apparmor.Specification{} err := spec.AddConnectedPlug(s.iface, s.plug, s.slot) diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/network_control.go snapd-2.54.2+21.10/interfaces/builtin/network_control.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/network_control.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/network_control.go 2022-01-06 21:25:16.000000000 +0000 @@ -352,7 +352,8 @@ connectedPlugMount: networkControlConnectedPlugMount, connectedPlugUpdateNSAppArmor: networkControlConnectedPlugUpdateNSAppArmor, - suppressPtraceTrace: true, + suppressPtraceTrace: true, + suppressSysModuleCapability: true, // affects the plug snap because of mount backend affectsPlugOnRefresh: true, diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/network_control_test.go snapd-2.54.2+21.10/interfaces/builtin/network_control_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/network_control_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/network_control_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -82,6 +82,8 @@ func (s *NetworkControlInterfaceSuite) TestAppArmorSpec(c *C) { spec := &apparmor.Specification{} c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Check(spec.SuppressSysModuleCapability(), Equals, true) + c.Check(spec.UsesSysModuleCapability(), Equals, false) c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app"}) c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/run/netns/* rw,\n") c.Assert(spec.UpdateNS(), DeepEquals, []string{` diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/network_manager_observe.go snapd-2.54.2+21.10/interfaces/builtin/network_manager_observe.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/network_manager_observe.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/network_manager_observe.go 2022-01-06 21:25:16.000000000 +0000 @@ -127,11 +127,17 @@ interface="org.freedesktop.NetworkManager.Settings{,.Connection}" member="GetSettings" peer=(label=###SLOT_SECURITY_TAGS###), +dbus (send) + bus=system + path=/org/freedesktop + interface=org.freedesktop.DBus.ObjectManager + member="GetManagedObjects" + peer=(label=###SLOT_SECURITY_TAGS###), # receive signals for updated settings and properties dbus (receive) bus=system - path="/org/freedesktop/NetworkManager{,/{ActiveConnection,Devices}/*}" + path="/org/freedesktop/NetworkManager{,/**}" interface=org.freedesktop.DBus.Properties member=PropertiesChanged peer=(label=###SLOT_SECURITY_TAGS###), @@ -165,6 +171,12 @@ interface="org.freedesktop.NetworkManager.Settings.Connection" member=PropertiesChanged peer=(label=###SLOT_SECURITY_TAGS###), +dbus (receive) + bus=system + path=/org/freedesktop + interface=org.freedesktop.DBus.ObjectManager + member="Interfaces{Added,Removed}" + peer=(label=###SLOT_SECURITY_TAGS###), ` type networkManagerObserveInterface struct{} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/network_setup_control.go snapd-2.54.2+21.10/interfaces/builtin/network_setup_control.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/network_setup_control.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/network_setup_control.go 2022-01-06 21:25:16.000000000 +0000 @@ -61,6 +61,14 @@ #include +# Allow use of Netplan Generate API, used to generate network configuration +dbus (send) + bus=system + interface=io.netplan.Netplan + path=/io/netplan/Netplan + member=Generate + peer=(label=unconfined), + # Allow use of Netplan Apply API, used to apply network configuration dbus (send) bus=system diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/opengl.go snapd-2.54.2+21.10/interfaces/builtin/opengl.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/opengl.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/opengl.go 2022-01-06 21:25:16.000000000 +0000 @@ -127,6 +127,7 @@ # /sys/devices /sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/config r, /sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/revision r, +/sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/boot_vga r, /sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/{,subsystem_}class r, /sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/{,subsystem_}device r, /sys/devices/{,*pcie-controller/}pci[0-9a-f]*/**/{,subsystem_}vendor r, diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/qualcomm_ipc_router.go snapd-2.54.2+21.10/interfaces/builtin/qualcomm_ipc_router.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/qualcomm_ipc_router.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/qualcomm_ipc_router.go 2022-01-06 21:25:16.000000000 +0000 @@ -66,6 +66,10 @@ } func (iface *qualcomIPCRouterInterface) BeforeConnectPlug(plug *interfaces.ConnectedPlug) error { + if apparmor_sandbox.ProbedLevel() == apparmor_sandbox.Unsupported { + // no apparmor means we don't have to deal with parser features + return nil + } features, err := apparmor_sandbox.ParserFeatures() if err != nil { return err diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/qualcomm_ipc_router_test.go snapd-2.54.2+21.10/interfaces/builtin/qualcomm_ipc_router_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/qualcomm_ipc_router_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/qualcomm_ipc_router_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -81,12 +81,21 @@ } func (s *QrtrInterfaceSuite) TestSanitizePlugConnectionMissingAppArmorSandboxFeatures(c *C) { - r := apparmor_sandbox.MockFeatures(nil, nil, nil, nil) + r := apparmor_sandbox.MockLevel(apparmor_sandbox.Full) + defer r() + r = apparmor_sandbox.MockFeatures(nil, nil, nil, nil) defer r() err := interfaces.BeforeConnectPlug(s.iface, s.plug) c.Assert(err, ErrorMatches, "cannot connect plug on system without qipcrtr socket support") } +func (s *QrtrInterfaceSuite) TestSanitizePlugConnectionMissingNoAppArmor(c *C) { + r := apparmor_sandbox.MockLevel(apparmor_sandbox.Unsupported) + defer r() + err := interfaces.BeforeConnectPlug(s.iface, s.plug) + c.Assert(err, IsNil) +} + func (s *QrtrInterfaceSuite) TestAppArmorSpec(c *C) { spec := &apparmor.Specification{} c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/scsi_generic.go snapd-2.54.2+21.10/interfaces/builtin/scsi_generic.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/scsi_generic.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/scsi_generic.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,58 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin + +const scsiGenericSummary = `allows access to SCSI generic driver devices` + +const scsiGenericBaseDeclarationSlots = ` + scsi-generic: + allow-installation: + slot-snap-type: + - core + deny-auto-connection: true +` +const scsiGenericBaseDeclarationPlugs = ` + block-devices: + allow-installation: false + deny-auto-connection: true +` + +const scsiGenericConnectedPlugAppArmor = ` +# allow read,write access to generic scsi devices +# ref: https://www.kernel.org/doc/Documentation/scsi/scsi-generic.txt +/dev/sg[0-9]* rw, +` + +var scsiGenericConnectedPlugUDev = []string{ + // ref: https://www.kernel.org/doc/Documentation/scsi/scsi-generic.txt + `KERNEL=="sg[0-9]*"`, +} + +func init() { + registerIface(&commonInterface{ + name: "scsi-generic", + summary: scsiGenericSummary, + implicitOnCore: true, + implicitOnClassic: true, + baseDeclarationSlots: scsiGenericBaseDeclarationSlots, + connectedPlugAppArmor: scsiGenericConnectedPlugAppArmor, + connectedPlugUDev: scsiGenericConnectedPlugUDev, + }) +} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/scsi_generic_test.go snapd-2.54.2+21.10/interfaces/builtin/scsi_generic_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/scsi_generic_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/scsi_generic_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,108 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin_test + +import ( + "fmt" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/interfaces/udev" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" + "github.com/snapcore/snapd/testutil" +) + +type ScsiGenericInterfaceSuite struct { + iface interfaces.Interface + slotInfo *snap.SlotInfo + slot *interfaces.ConnectedSlot + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug +} + +var _ = Suite(&ScsiGenericInterfaceSuite{ + iface: builtin.MustInterface("scsi-generic"), +}) + +func (s *ScsiGenericInterfaceSuite) SetUpTest(c *C) { + var mockPlugSnapInfoYaml = `name: other +version: 1.0 +apps: + app: + command: foo + plugs: [scsi-generic] +` + s.slotInfo = &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "core", SnapType: snap.TypeOS}, + Name: "scsi-generic", + Interface: "scsi-generic", + } + s.slot = interfaces.NewConnectedSlot(s.slotInfo, nil, nil) + snapInfo := snaptest.MockInfo(c, mockPlugSnapInfoYaml, nil) + s.plugInfo = snapInfo.Plugs["scsi-generic"] + s.plug = interfaces.NewConnectedPlug(s.plugInfo, nil, nil) +} + +func (s *ScsiGenericInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "scsi-generic") +} + +func (s *ScsiGenericInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) +} + +func (s *ScsiGenericInterfaceSuite) TestSanitizePlug(c *C) { + c.Assert(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) +} + +func (s *ScsiGenericInterfaceSuite) TestUsedSecuritySystems(c *C) { + // connected plugs have a non-nil security snippet for apparmor + apparmorSpec := &apparmor.Specification{} + err := apparmorSpec.AddConnectedPlug(s.iface, s.plug, s.slot) + c.Assert(err, IsNil) + c.Assert(apparmorSpec.SecurityTags(), DeepEquals, []string{"snap.other.app"}) + c.Assert(apparmorSpec.SnippetForTag("snap.other.app"), testutil.Contains, "/dev/sg[0-9]* rw") +} + +func (s *ScsiGenericInterfaceSuite) TestUDevSpec(c *C) { + udevSpec := &udev.Specification{} + c.Assert(udevSpec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(udevSpec.Snippets(), HasLen, 2) + c.Assert(udevSpec.Snippets(), testutil.Contains, `# scsi-generic +KERNEL=="sg[0-9]*", TAG+="snap_other_app"`) + c.Assert(udevSpec.Snippets(), testutil.Contains, fmt.Sprintf(`TAG=="snap_other_app", RUN+="%v/snap-device-helper $env{ACTION} snap_other_app $devpath $major:$minor"`, dirs.DistroLibExecDir)) +} + +func (s *ScsiGenericInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} + +func (s *ScsiGenericInterfaceSuite) TestStaticInfo(c *C) { + si := interfaces.StaticInfoOf(s.iface) + c.Assert(si.ImplicitOnCore, Equals, true) + c.Assert(si.ImplicitOnClassic, Equals, true) + c.Assert(si.Summary, Equals, `allows access to SCSI generic driver devices`) + c.Assert(si.BaseDeclarationSlots, testutil.Contains, "scsi-generic") +} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/shared_memory.go snapd-2.54.2+21.10/interfaces/builtin/shared_memory.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/shared_memory.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/shared_memory.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,236 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin + +import ( + "bytes" + "errors" + "fmt" + "io" + "strings" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/snap" +) + +const sharedMemorySummary = `allows two snaps to use predefined shared memory objects` + +const sharedMemoryBaseDeclarationPlugs = ` + shared-memory: + allow-installation: true + allow-connection: + slot-attributes: + shared-memory: $PLUG(shared-memory) + allow-auto-connection: + slot-publisher-id: + - $PLUG_PUBLISHER_ID + slot-attributes: + shared-memory: $PLUG(shared-memory) +` + +const sharedMemoryBaseDeclarationSlots = ` + shared-memory: + allow-installation: false + deny-connection: true + deny-auto-connection: true +` + +func validateSharedMemoryPath(path string) error { + if len(path) == 0 { + return fmt.Errorf("shared-memory interface path is empty") + } + + if strings.TrimSpace(path) != path { + return fmt.Errorf("shared-memory interface path has leading or trailing spaces: %q", path) + } + + // TODO: allow "*" as a globbing character; figure out if more AARE should be allowed + if err := apparmor.ValidateNoAppArmorRegexp(path); err != nil { + return fmt.Errorf("shared-memory interface path is invalid: %v", err) + } + + // TODO: consider whether we should remove this check and allow full SHM path + if strings.Contains(path, "/") { + return fmt.Errorf("shared-memory interface path should not contain '/': %q", path) + } + + // The check above protects from most unclean paths, but one could still specify ".." + if !cleanSubPath(path) { + return fmt.Errorf("shared-memory interface path is not clean: %q", path) + } + + return nil +} + +func stringListAttribute(attrer interfaces.Attrer, key string) ([]string, error) { + parseError := func(key string, value interface{}) error { + return fmt.Errorf(`shared-memory %q attribute must be a list of strings, not "%v"`, key, value) + } + attr, ok := attrer.Lookup(key) + if !ok { + return nil, nil + } + + attrList, ok := attr.([]interface{}) + if !ok || len(attrList) == 0 { + return nil, parseError(key, attr) + } + + stringList := make([]string, 0, len(attrList)) + for _, value := range attrList { + s, ok := value.(string) + if !ok { + return nil, parseError(key, attrList) + } + stringList = append(stringList, s) + } + return stringList, nil +} + +// sharedMemoryInterface allows sharing sharedMemory between snaps +type sharedMemoryInterface struct{} + +func (iface *sharedMemoryInterface) Name() string { + return "shared-memory" +} + +func (iface *sharedMemoryInterface) StaticInfo() interfaces.StaticInfo { + return interfaces.StaticInfo{ + Summary: sharedMemorySummary, + BaseDeclarationPlugs: sharedMemoryBaseDeclarationPlugs, + BaseDeclarationSlots: sharedMemoryBaseDeclarationSlots, + AffectsPlugOnRefresh: true, + } +} + +func (iface *sharedMemoryInterface) BeforePrepareSlot(slot *snap.SlotInfo) error { + sharedMemoryAttr, isSet := slot.Attrs["shared-memory"] + sharedMemory, ok := sharedMemoryAttr.(string) + if isSet && !ok { + return fmt.Errorf(`shared-memory "shared-memory" attribute must be a string, not %v`, + slot.Attrs["shared-memory"]) + } + if sharedMemory == "" { + if slot.Attrs == nil { + slot.Attrs = make(map[string]interface{}) + } + // shared-memory defaults to "slot" name if unspecified + slot.Attrs["shared-memory"] = slot.Name + } + + readPaths, err := stringListAttribute(slot, "read") + if err != nil { + return err + } + + writePaths, err := stringListAttribute(slot, "write") + if err != nil { + return err + } + + // We perform the same validation for read-only and writable paths, so + // let's just put them all in the same array + allPaths := append(readPaths, writePaths...) + if len(allPaths) == 0 { + return errors.New(`shared memory interface requires at least a valid "read" or "write" attribute`) + } + + for _, path := range allPaths { + if err := validateSharedMemoryPath(path); err != nil { + return err + } + } + + return nil +} + +type sharedMemorySnippetType int + +const ( + snippetForSlot sharedMemorySnippetType = iota + snippetForPlug +) + +func writeSharedMemoryPaths(w io.Writer, slot *interfaces.ConnectedSlot, + snippetType sharedMemorySnippetType) { + emitWritableRule := func(path string) { + // Ubuntu 14.04 uses /run/shm instead of the most common /dev/shm + fmt.Fprintf(w, "\"/{dev,run}/shm/%s\" rwk,\n", path) + } + + // All checks were already done in BeforePrepare{Plug,Slot} + writePaths, _ := stringListAttribute(slot, "write") + for _, path := range writePaths { + emitWritableRule(path) + } + readPaths, _ := stringListAttribute(slot, "read") + for _, path := range readPaths { + if snippetType == snippetForPlug { + // grant read-only access + fmt.Fprintf(w, "\"/{dev,run}/shm/%s\" r,\n", path) + } else { + // the slot must still be granted write access, because the "read" + // and "write" attributes are meant to affect the plug only + emitWritableRule(path) + } + } +} + +func (iface *sharedMemoryInterface) BeforePreparePlug(plug *snap.PlugInfo) error { + sharedMemoryAttr, isSet := plug.Attrs["shared-memory"] + sharedMemory, ok := sharedMemoryAttr.(string) + if isSet && !ok { + return fmt.Errorf(`shared-memory "shared-memory" attribute must be a string, not %v`, + plug.Attrs["shared-memory"]) + } + if sharedMemory == "" { + if plug.Attrs == nil { + plug.Attrs = make(map[string]interface{}) + } + // shared-memory defaults to "plug" name if unspecified + plug.Attrs["shared-memory"] = plug.Name + } + + return nil +} + +func (iface *sharedMemoryInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + sharedMemorySnippet := &bytes.Buffer{} + writeSharedMemoryPaths(sharedMemorySnippet, slot, snippetForPlug) + spec.AddSnippet(sharedMemorySnippet.String()) + return nil +} + +func (iface *sharedMemoryInterface) AppArmorConnectedSlot(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + sharedMemorySnippet := &bytes.Buffer{} + writeSharedMemoryPaths(sharedMemorySnippet, slot, snippetForSlot) + spec.AddSnippet(sharedMemorySnippet.String()) + return nil +} + +func (iface *sharedMemoryInterface) AutoConnect(plug *snap.PlugInfo, slot *snap.SlotInfo) bool { + // allow what declarations allowed + return true +} + +func init() { + registerIface(&sharedMemoryInterface{}) +} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/shared_memory_test.go snapd-2.54.2+21.10/interfaces/builtin/shared_memory_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/shared_memory_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/shared_memory_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,304 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package builtin_test + +import ( + "fmt" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/apparmor" + "github.com/snapcore/snapd/interfaces/builtin" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +type SharedMemoryInterfaceSuite struct { + testutil.BaseTest + + iface interfaces.Interface + slotInfo *snap.SlotInfo + slot *interfaces.ConnectedSlot + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug +} + +var _ = Suite(&SharedMemoryInterfaceSuite{ + iface: builtin.MustInterface("shared-memory"), +}) + +const sharedMemoryConsumerYaml = `name: consumer +version: 0 +plugs: + shmem: + interface: shared-memory + shared-memory: foo +apps: + app: + plugs: [shmem] +` + +const sharedMemoryProviderYaml = `name: provider +version: 0 +slots: + shmem: + interface: shared-memory + shared-memory: foo + write: [ bar ] + read: [ bar-ro ] +apps: + app: + slots: [shmem] +` + +func (s *SharedMemoryInterfaceSuite) SetUpTest(c *C) { + s.BaseTest.SetUpTest(c) + + s.plug, s.plugInfo = MockConnectedPlug(c, sharedMemoryConsumerYaml, nil, "shmem") + s.slot, s.slotInfo = MockConnectedSlot(c, sharedMemoryProviderYaml, nil, "shmem") +} + +func (s *SharedMemoryInterfaceSuite) TestName(c *C) { + c.Assert(s.iface.Name(), Equals, "shared-memory") +} + +func (s *SharedMemoryInterfaceSuite) TestSanitizePlug(c *C) { + c.Check(interfaces.BeforePreparePlug(s.iface, s.plugInfo), IsNil) + c.Check(interfaces.BeforeConnectPlug(s.iface, s.plug), IsNil) +} + +func (s *SharedMemoryInterfaceSuite) TestSanitizePlugUnhappy(c *C) { + var sharedMemoryYaml = `name: consumer +version: 0 +plugs: + shmem: + interface: shared-memory + %s +apps: + app: + plugs: [shmem] +` + data := []struct { + plugYaml string + expectedError string + }{ + { + "shared-memory: [one two]", + `shared-memory "shared-memory" attribute must be a string, not \[one two\]`, + }, + } + + for _, testData := range data { + snapYaml := fmt.Sprintf(sharedMemoryYaml, testData.plugYaml) + _, plug := MockConnectedPlug(c, snapYaml, nil, "shmem") + err := interfaces.BeforePreparePlug(s.iface, plug) + c.Check(err, ErrorMatches, testData.expectedError, Commentf("yaml: %s", testData.plugYaml)) + } +} + +func (s *SharedMemoryInterfaceSuite) TestPlugShmAttribute(c *C) { + var plugYamlTemplate = `name: consumer +version: 0 +plugs: + shmem: + interface: shared-memory + %s +apps: + app: + plugs: [shmem] +` + + data := []struct { + plugYaml string + expectedName string + }{ + { + "", // missing "shared-memory" attribute + "shmem", // use the name of the plug + }, + { + "shared-memory: shmemFoo", + "shmemFoo", + }, + } + + for _, testData := range data { + snapYaml := fmt.Sprintf(plugYamlTemplate, testData.plugYaml) + _, plug := MockConnectedPlug(c, snapYaml, nil, "shmem") + err := interfaces.BeforePreparePlug(s.iface, plug) + c.Assert(err, IsNil) + c.Check(plug.Attrs["shared-memory"], Equals, testData.expectedName, + Commentf(`yaml: %q`, testData.plugYaml)) + } +} + +func (s *SharedMemoryInterfaceSuite) TestSanitizeSlot(c *C) { + c.Assert(interfaces.BeforePrepareSlot(s.iface, s.slotInfo), IsNil) +} + +func (s *SharedMemoryInterfaceSuite) TestSanitizeSlotUnhappy(c *C) { + var sharedMemoryYaml = `name: provider +version: 0 +slots: + shmem: + interface: shared-memory + %s +apps: + app: + slots: [shmem] +` + data := []struct { + slotYaml string + expectedError string + }{ + { + "shared-memory: 12", + `shared-memory "shared-memory" attribute must be a string, not 12`, + }, + { + "", // missing "write" attribute + `shared memory interface requires at least a valid "read" or "write" attribute`, + }, + { + "write: a string", + `shared-memory "write" attribute must be a list of strings, not "a string"`, + }, + { + "read: [Mixed, 12, False, list]", + `shared-memory "read" attribute must be a list of strings, not "\[Mixed 12 false list\]"`, + }, + { + `read: ["ok", "trailing-space "]`, + `shared-memory interface path has leading or trailing spaces: "trailing-space "`, + }, + { + `write: [" leading-space"]`, + `shared-memory interface path has leading or trailing spaces: " leading-space"`, + }, + { + `write: [""]`, + `shared-memory interface path is empty`, + }, + { + `write: [mem/**]`, + `shared-memory interface path is invalid: "mem/\*\*" contains a reserved apparmor char.*`, + }, + { + `read: [..]`, + `shared-memory interface path is not clean: ".."`, + }, + { + `write: [/dev/shm/bar]`, + `shared-memory interface path should not contain '/': "/dev/shm/bar"`, + }, + { + `write: [mem/../etc]`, + `shared-memory interface path should not contain '/': "mem/../etc"`, + }, + { + "write: [valid]\n read: [../invalid]", + `shared-memory interface path should not contain '/': "../invalid"`, + }, + { + "read: [valid]\n write: [../invalid]", + `shared-memory interface path should not contain '/': "../invalid"`, + }, + } + + for _, testData := range data { + snapYaml := fmt.Sprintf(sharedMemoryYaml, testData.slotYaml) + _, slot := MockConnectedSlot(c, snapYaml, nil, "shmem") + err := interfaces.BeforePrepareSlot(s.iface, slot) + c.Check(err, ErrorMatches, testData.expectedError, Commentf("yaml: %s", testData.slotYaml)) + } +} + +func (s *SharedMemoryInterfaceSuite) TestSlotShmAttribute(c *C) { + var slotYamlTemplate = `name: consumer +version: 0 +slots: + shmem: + interface: shared-memory + write: [foo] + %s +apps: + app: + slots: [shmem] +` + + data := []struct { + slotYaml string + expectedName string + }{ + { + "", // missing "shared-memory" attribute + "shmem", // use the name of the slot + }, + { + "shared-memory: shmemBar", + "shmemBar", + }, + } + + for _, testData := range data { + snapYaml := fmt.Sprintf(slotYamlTemplate, testData.slotYaml) + _, slot := MockConnectedSlot(c, snapYaml, nil, "shmem") + err := interfaces.BeforePrepareSlot(s.iface, slot) + c.Assert(err, IsNil) + c.Check(slot.Attrs["shared-memory"], Equals, testData.expectedName, + Commentf(`yaml: %q`, testData.slotYaml)) + } +} + +func (s *SharedMemoryInterfaceSuite) TestStaticInfo(c *C) { + si := interfaces.StaticInfoOf(s.iface) + c.Check(si.ImplicitOnCore, Equals, false) + c.Check(si.ImplicitOnClassic, Equals, false) + c.Check(si.Summary, Equals, `allows two snaps to use predefined shared memory objects`) + c.Check(si.BaseDeclarationSlots, testutil.Contains, "shared-memory") +} + +func (s *SharedMemoryInterfaceSuite) TestAppArmorSpec(c *C) { + spec := &apparmor.Specification{} + + c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + plugSnippet := spec.SnippetForTag("snap.consumer.app") + + c.Assert(spec.AddConnectedSlot(s.iface, s.plug, s.slot), IsNil) + slotSnippet := spec.SnippetForTag("snap.provider.app") + + c.Assert(spec.SecurityTags(), DeepEquals, []string{"snap.consumer.app", "snap.provider.app"}) + + c.Check(plugSnippet, testutil.Contains, `"/{dev,run}/shm/bar" rwk,`) + c.Check(plugSnippet, testutil.Contains, `"/{dev,run}/shm/bar-ro" r,`) + + // Slot has read-write permissions to all paths + c.Check(slotSnippet, testutil.Contains, `"/{dev,run}/shm/bar" rwk,`) + c.Check(slotSnippet, testutil.Contains, `"/{dev,run}/shm/bar-ro" rwk,`) +} + +func (s *SharedMemoryInterfaceSuite) TestAutoConnect(c *C) { + c.Assert(s.iface.AutoConnect(s.plugInfo, s.slotInfo), Equals, true) +} + +func (s *SharedMemoryInterfaceSuite) TestInterfaces(c *C) { + c.Check(builtin.Interfaces(), testutil.DeepContains, s.iface) +} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/time_control.go snapd-2.54.2+21.10/interfaces/builtin/time_control.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/time_control.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/time_control.go 2022-01-06 21:25:16.000000000 +0000 @@ -93,6 +93,11 @@ /sys/class/rtc/*/ rw, /sys/class/rtc/*/** rw, +# Allow access to pps +# https://www.kernel.org/doc/html/latest/driver-api/pps.html +/dev/pps[0-9]* rw, +/sys/devices/virtual/pps/*/** rw, + # As the core snap ships the hwclock utility we can also allow # clients to use it now that they have access to the relevant # device nodes. Note: some invocations of hwclock will try to @@ -123,7 +128,10 @@ socket AF_NETLINK - NETLINK_AUDIT ` -var timeControlConnectedPlugUDev = []string{`SUBSYSTEM=="rtc"`} +var timeControlConnectedPlugUDev = []string{ + `SUBSYSTEM=="rtc"`, + `KERNEL=="pps[0-9]*"`, +} func init() { registerIface(&commonInterface{ diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/time_control_test.go snapd-2.54.2+21.10/interfaces/builtin/time_control_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/time_control_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/time_control_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -106,9 +106,11 @@ func (s *TimeControlInterfaceSuite) TestUDevSpec(c *C) { spec := &udev.Specification{} c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) - c.Assert(spec.Snippets(), HasLen, 2) + c.Assert(spec.Snippets(), HasLen, 3) c.Assert(spec.Snippets(), testutil.Contains, `# time-control SUBSYSTEM=="rtc", TAG+="snap_consumer_app"`) + c.Assert(spec.Snippets(), testutil.Contains, `# time-control +KERNEL=="pps[0-9]*", TAG+="snap_consumer_app"`) c.Assert(spec.Snippets(), testutil.Contains, fmt.Sprintf(`TAG=="snap_consumer_app", RUN+="%v/snap-device-helper $env{ACTION} snap_consumer_app $devpath $major:$minor"`, dirs.DistroLibExecDir)) } diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/timezone_control.go snapd-2.54.2+21.10/interfaces/builtin/timezone_control.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/timezone_control.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/timezone_control.go 2022-01-06 21:25:16.000000000 +0000 @@ -59,6 +59,13 @@ member="SetTimezone" peer=(label=unconfined), +dbus (send) + bus=system + path=/org/freedesktop/timedate1 + interface=org.freedesktop.timedate1 + member="ListTimezones" + peer=(label=unconfined), + # Read all properties from timedate1 # do not use peer=(label=unconfined) here since this is DBus activated dbus (send) diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/u2f_devices.go snapd-2.54.2+21.10/interfaces/builtin/u2f_devices.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/u2f_devices.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/u2f_devices.go 2022-01-06 21:25:16.000000000 +0000 @@ -108,6 +108,11 @@ ProductIDPattern: "42b1", }, { + Name: "Nitrokey 3", + VendorIDPattern: "20a0", + ProductIDPattern: "42b2", + }, + { Name: "Google Titan U2F", VendorIDPattern: "18d1", ProductIDPattern: "5026", @@ -142,6 +147,16 @@ VendorIDPattern: "32a3", ProductIDPattern: "3201", }, + { + Name: "Trezor", + VendorIDPattern: "534c", + ProductIDPattern: "0001|0002", + }, + { + Name: "Trezor v2", + VendorIDPattern: "1209", + ProductIDPattern: "53c0|53c1", + }, } const u2fDevicesConnectedPlugAppArmor = ` diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/u2f_devices_test.go snapd-2.54.2+21.10/interfaces/builtin/u2f_devices_test.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/u2f_devices_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/u2f_devices_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -89,7 +89,7 @@ func (s *u2fDevicesInterfaceSuite) TestUDevSpec(c *C) { spec := &udev.Specification{} c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) - c.Assert(spec.Snippets(), HasLen, 21) + c.Assert(spec.Snippets(), HasLen, 24) c.Assert(spec.Snippets(), testutil.Contains, `# u2f-devices # Yubico YubiKey SUBSYSTEM=="hidraw", KERNEL=="hidraw*", ATTRS{idVendor}=="1050", ATTRS{idProduct}=="0113|0114|0115|0116|0120|0121|0200|0402|0403|0406|0407|0410", TAG+="snap_consumer_app"`) diff -Nru snapd-2.53+21.10ubuntu1/interfaces/builtin/unity7.go snapd-2.54.2+21.10/interfaces/builtin/unity7.go --- snapd-2.53+21.10ubuntu1/interfaces/builtin/unity7.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/builtin/unity7.go 2022-01-06 21:25:16.000000000 +0000 @@ -359,28 +359,28 @@ # dbusmenu dbus (send) bus=session - path=/{MenuBar{,/[0-9A-F]*},com/canonical/menu/[0-9A-F]*} + path=/{MenuBar{,/[0-9A-F]*},com/canonical/{menu/[0-9A-F]*,dbusmenu}} interface=com.canonical.dbusmenu member="{LayoutUpdated,ItemsPropertiesUpdated}" peer=(name=org.freedesktop.DBus, label=unconfined), dbus (receive) bus=session - path=/{MenuBar{,/[0-9A-F]*},com/canonical/menu/[0-9A-F]*} + path=/{MenuBar{,/[0-9A-F]*},com/canonical/{menu/[0-9A-F]*,dbusmenu}} interface="{com.canonical.dbusmenu,org.freedesktop.DBus.Properties}" member=Get* peer=(label=unconfined), dbus (receive) bus=session - path=/{MenuBar{,/[0-9A-F]*},com/canonical/menu/[0-9A-F]*} + path=/{MenuBar{,/[0-9A-F]*},com/canonical/{menu/[0-9A-F]*,dbusmenu}} interface=com.canonical.dbusmenu member="{AboutTo*,Event*}" peer=(label=unconfined), dbus (receive) bus=session - path=/{MenuBar{,/[0-9A-F]*},com/canonical/menu/[0-9A-F]*} + path=/{MenuBar{,/[0-9A-F]*},com/canonical/{menu/[0-9A-F]*,dbusmenu}} interface=org.freedesktop.DBus.Introspectable member=Introspect peer=(label=unconfined), diff -Nru snapd-2.53+21.10ubuntu1/interfaces/core.go snapd-2.54.2+21.10/interfaces/core.go --- snapd-2.53+21.10ubuntu1/interfaces/core.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/core.go 2022-01-06 21:25:16.000000000 +0000 @@ -295,6 +295,8 @@ SecurityKMod SecuritySystem = "kmod" // SecuritySystemd identifies the systemd services security system. SecuritySystemd SecuritySystem = "systemd" + // SecurityPolkit identifies the polkit security system. + SecurityPolkit SecuritySystem = "polkit" ) var isValidBusName = regexp.MustCompile(`^[a-zA-Z_-][a-zA-Z0-9_-]*(\.[a-zA-Z_-][a-zA-Z0-9_-]*)+$`).MatchString diff -Nru snapd-2.53+21.10ubuntu1/interfaces/ifacetest/testiface.go snapd-2.54.2+21.10/interfaces/ifacetest/testiface.go --- snapd-2.53+21.10ubuntu1/interfaces/ifacetest/testiface.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/ifacetest/testiface.go 2022-01-06 21:25:16.000000000 +0000 @@ -26,6 +26,7 @@ "github.com/snapcore/snapd/interfaces/hotplug" "github.com/snapcore/snapd/interfaces/kmod" "github.com/snapcore/snapd/interfaces/mount" + "github.com/snapcore/snapd/interfaces/polkit" "github.com/snapcore/snapd/interfaces/seccomp" "github.com/snapcore/snapd/interfaces/systemd" "github.com/snapcore/snapd/interfaces/udev" @@ -103,6 +104,13 @@ SystemdConnectedSlotCallback func(spec *systemd.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error SystemdPermanentPlugCallback func(spec *systemd.Specification, plug *snap.PlugInfo) error SystemdPermanentSlotCallback func(spec *systemd.Specification, slot *snap.SlotInfo) error + + // Support for interacting with the polkit backend. + + PolkitConnectedPlugCallback func(spec *polkit.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error + PolkitConnectedSlotCallback func(spec *polkit.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error + PolkitPermanentPlugCallback func(spec *polkit.Specification, plug *snap.PlugInfo) error + PolkitPermanentSlotCallback func(spec *polkit.Specification, slot *snap.SlotInfo) error } // TestHotplugInterface is an interface for various kinds of tests @@ -411,6 +419,36 @@ } return nil } + +// Support for interacting with the polkit backend. + +func (t *TestInterface) PolkitConnectedPlug(spec *polkit.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + if t.PolkitConnectedPlugCallback != nil { + return t.PolkitConnectedPlugCallback(spec, plug, slot) + } + return nil +} + +func (t *TestInterface) PolkitConnectedSlot(spec *polkit.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + if t.PolkitConnectedSlotCallback != nil { + return t.PolkitConnectedSlotCallback(spec, plug, slot) + } + return nil +} + +func (t *TestInterface) PolkitPermanentSlot(spec *polkit.Specification, slot *snap.SlotInfo) error { + if t.PolkitPermanentSlotCallback != nil { + return t.PolkitPermanentSlotCallback(spec, slot) + } + return nil +} + +func (t *TestInterface) PolkitPermanentPlug(spec *polkit.Specification, plug *snap.PlugInfo) error { + if t.PolkitPermanentPlugCallback != nil { + return t.PolkitPermanentPlugCallback(spec, plug) + } + return nil +} // Support for interacting with hotplug subsystem. diff -Nru snapd-2.53+21.10ubuntu1/interfaces/kmod/backend.go snapd-2.54.2+21.10/interfaces/kmod/backend.go --- snapd-2.53+21.10ubuntu1/interfaces/kmod/backend.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/kmod/backend.go 2022-01-06 21:25:16.000000000 +0000 @@ -67,9 +67,56 @@ return "kmod" } -// Setup creates a conf file with list of kernel modules required by given snap, -// writes it in /etc/modules-load.d/ directory and immediately loads the modules -// using /sbin/modprobe. The devMode is ignored. +// setupModules creates a conf file with list of kernel modules required by +// given snap, writes it in /etc/modules-load.d/ directory and immediately +// loads the modules using /sbin/modprobe. The devMode is ignored. +func (b *Backend) setupModules(snapInfo *snap.Info, spec *Specification) error { + content, modules := deriveContent(spec, snapInfo) + // synchronize the content with the filesystem + glob := interfaces.SecurityTagGlob(snapInfo.InstanceName()) + dir := dirs.SnapKModModulesDir + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("cannot create directory for kmod files %q: %s", dir, err) + } + + changed, _, err := osutil.EnsureDirState(dirs.SnapKModModulesDir, glob, content) + if err != nil { + return err + } + + if len(changed) > 0 { + b.loadModules(modules) + } + return nil +} + +// setupModprobe creates a configuration file under /etc/modprobe.d/ according +// to the specification: this allows to either specify the load parameters for +// a module, or prevent it from being loaded. +// TODO: consider whether +// - a newly blocklisted module should get unloaded +// - a module whose option change should get reloaded +func (b *Backend) setupModprobe(snapInfo *snap.Info, spec *Specification) error { + dir := dirs.SnapKModModprobeDir + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("cannot create directory for kmod files %q: %s", dir, err) + } + + glob := interfaces.SecurityTagGlob(snapInfo.InstanceName()) + dirContents := prepareModprobeDirContents(spec, snapInfo) + _, _, err := osutil.EnsureDirState(dirs.SnapKModModprobeDir, glob, dirContents) + if err != nil { + return err + } + + return nil +} + +// Setup will make the kmod backend generate the needed system files (such as +// those under /etc/modules-load.d/ and /etc/modprobe.d/) and call the +// appropriate system commands so that the desired kernel module configuration +// will be applied. +// The devMode is ignored. // // If the method fails it should be re-tried (with a sensible strategy) by the caller. func (b *Backend) Setup(snapInfo *snap.Info, confinement interfaces.ConfinementOptions, repo *interfaces.Repository, tm timings.Measurer) error { @@ -80,22 +127,16 @@ return fmt.Errorf("cannot obtain kmod specification for snap %q: %s", snapName, err) } - content, modules := deriveContent(spec.(*Specification), snapInfo) - // synchronize the content with the filesystem - glob := interfaces.SecurityTagGlob(snapName) - dir := dirs.SnapKModModulesDir - if err := os.MkdirAll(dir, 0755); err != nil { - return fmt.Errorf("cannot create directory for kmod files %q: %s", dir, err) + err = b.setupModprobe(snapInfo, spec.(*Specification)) + if err != nil { + return err } - changed, _, err := osutil.EnsureDirState(dirs.SnapKModModulesDir, glob, content) + err = b.setupModules(snapInfo, spec.(*Specification)) if err != nil { return err } - if len(changed) > 0 { - b.loadModules(modules) - } return nil } @@ -106,8 +147,20 @@ // If the method fails it should be re-tried (with a sensible strategy) by the caller. func (b *Backend) Remove(snapName string) error { glob := interfaces.SecurityTagGlob(snapName) - _, _, err := osutil.EnsureDirState(dirs.SnapKModModulesDir, glob, nil) - return err + var errors []error + if _, _, err := osutil.EnsureDirState(dirs.SnapKModModulesDir, glob, nil); err != nil { + errors = append(errors, err) + } + + if _, _, err := osutil.EnsureDirState(dirs.SnapKModModprobeDir, glob, nil); err != nil { + errors = append(errors, err) + } + + if len(errors) > 0 { + return fmt.Errorf("cannot remove kernel modules config files: %v", errors) + } + + return nil } func deriveContent(spec *Specification, snapInfo *snap.Info) (map[string]osutil.FileState, []string) { @@ -134,6 +187,31 @@ return content, modules } +func prepareModprobeDirContents(spec *Specification, snapInfo *snap.Info) map[string]osutil.FileState { + disallowedModules := spec.DisallowedModules() + if len(disallowedModules) == 0 && len(spec.moduleOptions) == 0 { + return nil + } + + contents := "# Generated by snapd. Do not edit\n\n" + // First, write down the list of disallowed modules + for _, module := range disallowedModules { + contents += fmt.Sprintf("blacklist %s\n", module) + } + // Then, write down the module options + for module, options := range spec.moduleOptions { + contents += fmt.Sprintf("options %s %s\n", module, options) + } + + fileName := fmt.Sprintf("%s.conf", snap.SecurityTag(snapInfo.InstanceName())) + return map[string]osutil.FileState{ + fileName: &osutil.MemoryFileState{ + Content: []byte(contents), + Mode: 0644, + }, + } +} + func (b *Backend) NewSpecification() interfaces.Specification { return &Specification{} } diff -Nru snapd-2.53+21.10ubuntu1/interfaces/kmod/backend_test.go snapd-2.54.2+21.10/interfaces/kmod/backend_test.go --- snapd-2.53+21.10ubuntu1/interfaces/kmod/backend_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/kmod/backend_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -118,6 +118,63 @@ } } +func (s *backendSuite) TestInstallingSnapCreatesModprobeConf(c *C) { + s.Iface.KModPermanentSlotCallback = func(spec *kmod.Specification, slot *snap.SlotInfo) error { + spec.AddModule("module1") + spec.SetModuleOptions("module1", "opt1=true opt2=2") + spec.DisallowModule("module2") + return nil + } + + modulesPath := filepath.Join(dirs.SnapKModModulesDir, "snap.samba.conf") + c.Assert(osutil.FileExists(modulesPath), Equals, false) + modprobePath := filepath.Join(dirs.SnapKModModprobeDir, "snap.samba.conf") + c.Assert(osutil.FileExists(modprobePath), Equals, false) + + for _, opts := range testedConfinementOpts { + s.modprobeCmd.ForgetCalls() + snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0) + + c.Assert(osutil.FileExists(modulesPath), Equals, true) + c.Assert(modulesPath, testutil.FileEquals, "# This file is automatically generated.\nmodule1\n") + + c.Assert(osutil.FileExists(modprobePath), Equals, true) + c.Assert(modprobePath, testutil.FileEquals, `# Generated by snapd. Do not edit + +blacklist module2 +options module1 opt1=true opt2=2 +`) + + c.Assert(s.modprobeCmd.Calls(), DeepEquals, [][]string{ + {"modprobe", "--syslog", "module1"}, + }) + s.RemoveSnap(c, snapInfo) + } +} + +func (s *backendSuite) TestRemovingSnapRemovesModprobeConf(c *C) { + s.Iface.KModPermanentSlotCallback = func(spec *kmod.Specification, slot *snap.SlotInfo) error { + spec.AddModule("module1") + spec.SetModuleOptions("module1", "opt1=true opt2=2") + spec.DisallowModule("module2") + return nil + } + + modulesPath := filepath.Join(dirs.SnapKModModulesDir, "snap.samba.conf") + c.Assert(osutil.FileExists(modulesPath), Equals, false) + modprobePath := filepath.Join(dirs.SnapKModModprobeDir, "snap.samba.conf") + c.Assert(osutil.FileExists(modprobePath), Equals, false) + + for _, opts := range testedConfinementOpts { + snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0) + c.Assert(osutil.FileExists(modulesPath), Equals, true) + c.Assert(osutil.FileExists(modprobePath), Equals, true) + s.RemoveSnap(c, snapInfo) + c.Assert(osutil.FileExists(modulesPath), Equals, false) + c.Assert(osutil.FileExists(modprobePath), Equals, false) + } +} + func (s *backendSuite) TestSecurityIsStable(c *C) { // NOTE: Hand out a permanent snippet so that .conf file is generated. s.Iface.KModPermanentSlotCallback = func(spec *kmod.Specification, slot *snap.SlotInfo) error { diff -Nru snapd-2.53+21.10ubuntu1/interfaces/kmod/spec.go snapd-2.54.2+21.10/interfaces/kmod/spec.go --- snapd-2.53+21.10ubuntu1/interfaces/kmod/spec.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/kmod/spec.go 2022-01-06 21:25:16.000000000 +0000 @@ -20,6 +20,7 @@ package kmod import ( + "sort" "strings" "github.com/snapcore/snapd/interfaces" @@ -33,6 +34,9 @@ // setup process. type Specification struct { modules map[string]bool + + moduleOptions map[string]string + disallowedModules map[string]bool } // AddModule adds a kernel module, trimming spaces and ignoring duplicated modules. @@ -57,6 +61,45 @@ return result } +// SetModuleOptions specifies which options to use when loading the given kernel module. +func (spec *Specification) SetModuleOptions(module, options string) error { + if spec.moduleOptions == nil { + spec.moduleOptions = make(map[string]string) + } + spec.moduleOptions[module] = options + return nil +} + +// moduleOptions returns the load options for each kernel module +func (spec *Specification) ModuleOptions() map[string]string { + return spec.moduleOptions +} + +// DisallowModule adds a kernel module to the list of disallowed modules. +func (spec *Specification) DisallowModule(module string) error { + m := strings.TrimSpace(module) + if m == "" { + return nil + } + if spec.disallowedModules == nil { + spec.disallowedModules = make(map[string]bool) + } + spec.disallowedModules[m] = true + return nil +} + +// DisallowedModules returns the list of disallowed modules. +func (spec *Specification) DisallowedModules() []string { + result := make([]string, 0, len(spec.disallowedModules)) + for k, v := range spec.disallowedModules { + if v { + result = append(result, k) + } + } + sort.Strings(result) + return result +} + // Implementation of methods required by interfaces.Specification // AddConnectedPlug records kmod-specific side-effects of having a connected plug. diff -Nru snapd-2.53+21.10ubuntu1/interfaces/policy/basedeclaration_test.go snapd-2.54.2+21.10/interfaces/policy/basedeclaration_test.go --- snapd-2.53+21.10ubuntu1/interfaces/policy/basedeclaration_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/policy/basedeclaration_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -627,6 +627,7 @@ "hidraw": {"core", "gadget"}, "i2c": {"core", "gadget"}, "iio": {"core", "gadget"}, + "kernel-module-load": {"core"}, "kubernetes-support": {"core"}, "location-control": {"app"}, "location-observe": {"app"}, @@ -636,6 +637,7 @@ "mir": {"app"}, "microstack-support": {"core"}, "modem-manager": {"app", "core"}, + "mount-control": {"core"}, "mpris": {"app"}, "netlink-driver": {"core", "gadget"}, "network-manager": {"app", "core"}, @@ -649,6 +651,7 @@ "pwm": {"core", "gadget"}, "qualcomm-ipc-router": {"core"}, "raw-volume": {"core", "gadget"}, + "scsi-generic": {"core"}, "sd-control": {"core"}, "serial-port": {"core", "gadget"}, "spi": {"core", "gadget"}, @@ -668,6 +671,7 @@ "classic-support": nil, "docker": nil, "lxd": nil, + "shared-memory": nil, } restrictedPlugInstallation = map[string][]string{ @@ -734,9 +738,11 @@ "gpio-control": true, "ion-memory-control": true, "kernel-module-control": true, + "kernel-module-load": true, "kubernetes-support": true, "lxd-support": true, "microstack-support": true, + "mount-control": true, "multipass-support": true, "packagekit-control": true, "personal-files": true, @@ -797,6 +803,7 @@ "mir": true, "online-accounts-service": true, "raw-volume": true, + "shared-memory": true, "storage-framework-service": true, "thumbnailer-service": true, "ubuntu-download-manager": true, @@ -974,13 +981,16 @@ "gpio-control": true, "ion-memory-control": true, "kernel-module-control": true, + "kernel-module-load": true, "kubernetes-support": true, "lxd-support": true, "microstack-support": true, + "mount-control": true, "multipass-support": true, "packagekit-control": true, "personal-files": true, "sd-control": true, + "shared-memory": true, "snap-refresh-control": true, "snap-themes-control": true, "snapd-control": true, diff -Nru snapd-2.53+21.10ubuntu1/interfaces/polkit/backend.go snapd-2.54.2+21.10/interfaces/polkit/backend.go --- snapd-2.53+21.10ubuntu1/interfaces/polkit/backend.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/polkit/backend.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,120 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// Package polkit implements interaction between snapd and polkit. +// +// Snapd installs polkitd policy files on behalf of snaps that +// describe administrative actions they can perform on behalf of +// clients. +// +// The policy files are XML files whose format is described here: +// https://www.freedesktop.org/software/polkit/docs/latest/polkit.8.html#polkit-declaring-actions +package polkit + +import ( + "fmt" + "os" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/timings" +) + +func polkitPolicyName(snapName, nameSuffix string) string { + return snap.ScopedSecurityTag(snapName, "interface", nameSuffix) + ".policy" +} + +// Backend is responsible for maintaining polkitd policy files. +type Backend struct{} + +// Initialize does nothing. +func (b *Backend) Initialize(*interfaces.SecurityBackendOptions) error { + return nil +} + +// Name returns the name of the backend. +func (b *Backend) Name() interfaces.SecuritySystem { + return interfaces.SecurityPolkit +} + +// Setup installs the polkit policy files specific to a given snap. +// +// Polkit has no concept of a complain mode so confinment type is ignored. +func (b *Backend) Setup(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository, tm timings.Measurer) error { + snapName := snapInfo.InstanceName() + // Get the policies that apply to this snap + spec, err := repo.SnapSpecification(b.Name(), snapName) + if err != nil { + return fmt.Errorf("cannot obtain polkit specification for snap %q: %s", snapName, err) + } + + // Get the files that this snap should have + glob := polkitPolicyName(snapName, "*") + content := deriveContent(spec.(*Specification), snapInfo) + dir := dirs.SnapPolkitPolicyDir + if err := os.MkdirAll(dir, 0755); err != nil { + return fmt.Errorf("cannot create directory for polkit policy files %q: %s", dir, err) + } + _, _, err = osutil.EnsureDirState(dir, glob, content) + if err != nil { + return fmt.Errorf("cannot synchronize polkit policy files for snap %q: %s", snapName, err) + } + return nil +} + +// Remove removes polkit policy files of a given snap. +// +// This method should be called after removing a snap. +func (b *Backend) Remove(snapName string) error { + glob := polkitPolicyName(snapName, "*") + _, _, err := osutil.EnsureDirState(dirs.SnapPolkitPolicyDir, glob, nil) + if err != nil { + return fmt.Errorf("cannot synchronize polkit files for snap %q: %s", snapName, err) + } + return nil +} + +// deriveContent combines security snippets collected from all the interfaces +// affecting a given snap into a content map applicable to EnsureDirState. +func deriveContent(spec *Specification, snapInfo *snap.Info) map[string]osutil.FileState { + policies := spec.Policies() + if len(policies) == 0 { + return nil + } + content := make(map[string]osutil.FileState, len(policies)+1) + for nameSuffix, policyContent := range policies { + filename := polkitPolicyName(snapInfo.InstanceName(), nameSuffix) + content[filename] = &osutil.MemoryFileState{ + Content: policyContent, + Mode: 0644, + } + } + return content +} + +func (b *Backend) NewSpecification() interfaces.Specification { + return &Specification{} +} + +// SandboxFeatures returns list of features supported by snapd for polkit policy. +func (b *Backend) SandboxFeatures() []string { + return nil +} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/polkit/backend_test.go snapd-2.54.2+21.10/interfaces/polkit/backend_test.go --- snapd-2.53+21.10ubuntu1/interfaces/polkit/backend_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/polkit/backend_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,131 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package polkit_test + +import ( + "io/ioutil" + "os" + "path/filepath" + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/interfaces/polkit" + "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/testutil" +) + +func Test(t *testing.T) { + TestingT(t) +} + +type backendSuite struct { + ifacetest.BackendSuite +} + +var _ = Suite(&backendSuite{}) + +var testedConfinementOpts = []interfaces.ConfinementOptions{ + {}, + {DevMode: true}, + {JailMode: true}, + {Classic: true}, +} + +func (s *backendSuite) SetUpTest(c *C) { + s.Backend = &polkit.Backend{} + s.BackendSuite.SetUpTest(c) + c.Assert(s.Repo.AddBackend(s.Backend), IsNil) + + // Prepare a directory for polkit policy files. + // NOTE: Normally this is a part of the OS snap. + err := os.MkdirAll(dirs.SnapPolkitPolicyDir, 0700) + c.Assert(err, IsNil) +} + +func (s *backendSuite) TearDownTest(c *C) { + s.BackendSuite.TearDownTest(c) +} + +// Tests for Setup() and Remove() +func (s *backendSuite) TestName(c *C) { + c.Check(s.Backend.Name(), Equals, interfaces.SecurityPolkit) +} + +func (s *backendSuite) TestInstallingSnapWritesPolicyFiles(c *C) { + // NOTE: Hand out a permanent policy so that .policy file is generated. + s.Iface.PolkitPermanentSlotCallback = func(spec *polkit.Specification, slot *snap.SlotInfo) error { + return spec.AddPolicy("foo", polkit.Policy("")) + } + for _, opts := range testedConfinementOpts { + snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0) + policy := filepath.Join(dirs.SnapPolkitPolicyDir, "snap.samba.interface.foo.policy") + // file called "snap.sambda.interface.foo.policy" was created + c.Check(policy, testutil.FileContains, "") + s.RemoveSnap(c, snapInfo) + } +} + +func (s *backendSuite) TestRemovingSnapRemovesPolicyFiles(c *C) { + // NOTE: Hand out a permanent snippet so that .policy file is generated. + s.Iface.PolkitPermanentSlotCallback = func(spec *polkit.Specification, slot *snap.SlotInfo) error { + return spec.AddPolicy("foo", polkit.Policy("")) + } + for _, opts := range testedConfinementOpts { + snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0) + s.RemoveSnap(c, snapInfo) + policy := filepath.Join(dirs.SnapPolkitPolicyDir, "snap.samba.interface.foo.policy") + // file called "snap.sambda.interface.foo.policy" was removed + c.Check(policy, testutil.FileAbsent) + } +} + +func (s *backendSuite) TestNoPolicyFiles(c *C) { + for _, opts := range testedConfinementOpts { + snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0) + policy := filepath.Join(dirs.SnapPolkitPolicyDir, "snap.samba.interface.foo.policy") + // Without any snippets, there the .conf file is not created. + c.Check(policy, testutil.FileAbsent) + s.RemoveSnap(c, snapInfo) + } +} + +func (s *backendSuite) TestUnexpectedPolicyFilesremoved(c *C) { + policyFile := filepath.Join(dirs.SnapPolkitPolicyDir, "snap.samba.interface.something.policy") + + for _, opts := range testedConfinementOpts { + c.Assert(ioutil.WriteFile(policyFile, []byte(""), 0644), IsNil) + // Installing snap removes unexpected policy files + snapInfo := s.InstallSnap(c, opts, "", ifacetest.SambaYamlV1, 0) + c.Check(policyFile, testutil.FileAbsent) + + c.Assert(ioutil.WriteFile(policyFile, []byte(""), 0644), IsNil) + // Removing snap also removes unexpected policy files + s.RemoveSnap(c, snapInfo) + c.Check(policyFile, testutil.FileAbsent) + } +} + +func (s *backendSuite) TestSandboxFeatures(c *C) { + c.Assert(s.Backend.SandboxFeatures(), HasLen, 0) +} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/polkit/spec.go snapd-2.54.2+21.10/interfaces/polkit/spec.go --- snapd-2.53+21.10ubuntu1/interfaces/polkit/spec.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/polkit/spec.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,106 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package polkit + +import ( + "bytes" + "fmt" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/snap" +) + +type Policy []byte + +// Specification keeps all the polkit policies. +type Specification struct { + policyFiles map[string]Policy +} + +// AddPolicy adds a polkit policy file to install. +func (spec *Specification) AddPolicy(nameSuffix string, content Policy) error { + if old, ok := spec.policyFiles[nameSuffix]; ok && !bytes.Equal(old, content) { + return fmt.Errorf("internal error: polkit policy content for %q re-defined with different content", nameSuffix) + } + if spec.policyFiles == nil { + spec.policyFiles = make(map[string]Policy) + } + spec.policyFiles[nameSuffix] = content + return nil +} + +// Policies returns a map of polkit policies added to the Specification. +func (spec *Specification) Policies() map[string]Policy { + if spec.policyFiles == nil { + return nil + } + result := make(map[string]Policy, len(spec.policyFiles)) + for k, v := range spec.policyFiles { + result[k] = make(Policy, len(v)) + copy(result[k], v) + } + return result +} + +// Implementation of methods required by interfaces.Specification + +// AddConnectedPlug records polkit-specific side-effects of having a connected plug. +func (spec *Specification) AddConnectedPlug(iface interfaces.Interface, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + type definer interface { + PolkitConnectedPlug(spec *Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error + } + if iface, ok := iface.(definer); ok { + return iface.PolkitConnectedPlug(spec, plug, slot) + } + return nil +} + +// AddConnectedSlot records polkit-specific side-effects of having a connected slot. +func (spec *Specification) AddConnectedSlot(iface interfaces.Interface, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + type definer interface { + PolkitConnectedSlot(spec *Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error + } + if iface, ok := iface.(definer); ok { + return iface.PolkitConnectedSlot(spec, plug, slot) + } + return nil +} + +// AddPermanentPlug records polkit-specific side-effects of having a plug. +func (spec *Specification) AddPermanentPlug(iface interfaces.Interface, plug *snap.PlugInfo) error { + type definer interface { + PolkitPermanentPlug(spec *Specification, plug *snap.PlugInfo) error + } + if iface, ok := iface.(definer); ok { + return iface.PolkitPermanentPlug(spec, plug) + } + return nil +} + +// AddPermanentSlot records polkit-specific side-effects of having a slot. +func (spec *Specification) AddPermanentSlot(iface interfaces.Interface, slot *snap.SlotInfo) error { + type definer interface { + PolkitPermanentSlot(spec *Specification, slot *snap.SlotInfo) error + } + if iface, ok := iface.(definer); ok { + return iface.PolkitPermanentSlot(spec, slot) + } + return nil +} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/polkit/spec_test.go snapd-2.54.2+21.10/interfaces/polkit/spec_test.go --- snapd-2.53+21.10ubuntu1/interfaces/polkit/spec_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/polkit/spec_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,99 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package polkit_test + +import ( + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces" + "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/interfaces/polkit" + "github.com/snapcore/snapd/snap" +) + +type specSuite struct { + iface *ifacetest.TestInterface + spec *polkit.Specification + plugInfo *snap.PlugInfo + plug *interfaces.ConnectedPlug + slotInfo *snap.SlotInfo + slot *interfaces.ConnectedSlot +} + +var _ = Suite(&specSuite{ + iface: &ifacetest.TestInterface{ + InterfaceName: "test", + PolkitConnectedPlugCallback: func(spec *polkit.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + return spec.AddPolicy("connected-plug", polkit.Policy("policy-connected-plug")) + }, + PolkitConnectedSlotCallback: func(spec *polkit.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + return spec.AddPolicy("connected-slot", polkit.Policy("policy-connected-slot")) + }, + PolkitPermanentPlugCallback: func(spec *polkit.Specification, plug *snap.PlugInfo) error { + return spec.AddPolicy("permanent-plug", polkit.Policy("policy-permanent-plug")) + }, + PolkitPermanentSlotCallback: func(spec *polkit.Specification, slot *snap.SlotInfo) error { + return spec.AddPolicy("permanent-slot", polkit.Policy("policy-permanent-slot")) + }, + }, + plugInfo: &snap.PlugInfo{ + Snap: &snap.Info{SuggestedName: "snap1"}, + Name: "name", + Interface: "test", + Apps: map[string]*snap.AppInfo{ + "app1": { + Snap: &snap.Info{ + SuggestedName: "snap1", + }, + Name: "app1"}}, + }, + slotInfo: &snap.SlotInfo{ + Snap: &snap.Info{SuggestedName: "snap2"}, + Name: "name", + Interface: "test", + Apps: map[string]*snap.AppInfo{ + "app2": { + Snap: &snap.Info{ + SuggestedName: "snap2", + }, + Name: "app2"}}, + }, +}) + +func (s *specSuite) SetUpTest(c *C) { + s.spec = &polkit.Specification{} + s.plug = interfaces.NewConnectedPlug(s.plugInfo, nil, nil) + s.slot = interfaces.NewConnectedSlot(s.slotInfo, nil, nil) +} + +// The spec.Specification can be used through the interfaces.Specification interface +func (s *specSuite) TestSpecificationIface(c *C) { + var r interfaces.Specification = s.spec + c.Assert(r.AddConnectedPlug(s.iface, s.plug, s.slot), IsNil) + c.Assert(r.AddConnectedSlot(s.iface, s.plug, s.slot), IsNil) + c.Assert(r.AddPermanentPlug(s.iface, s.plugInfo), IsNil) + c.Assert(r.AddPermanentSlot(s.iface, s.slotInfo), IsNil) + c.Assert(s.spec.Policies(), DeepEquals, map[string]polkit.Policy{ + "connected-plug": polkit.Policy("policy-connected-plug"), + "connected-slot": polkit.Policy("policy-connected-slot"), + "permanent-plug": polkit.Policy("policy-permanent-plug"), + "permanent-slot": polkit.Policy("policy-permanent-slot"), + }) +} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/udev/spec.go snapd-2.54.2+21.10/interfaces/udev/spec.go --- snapd-2.53+21.10ubuntu1/interfaces/udev/spec.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/udev/spec.go 2022-01-06 21:25:16.000000000 +0000 @@ -51,6 +51,8 @@ // SetControlsDeviceCgroup marks a specification as needing to control // its own device cgroup which prevents generation of any udev tagging rules // for this snap name +// TODO: this setting should also imply setting Delegates=true in the +// ServicePermanentPlug somehow, perhaps just for the commonInterface func (spec *Specification) SetControlsDeviceCgroup() { spec.controlsDeviceCgroup = true } diff -Nru snapd-2.53+21.10ubuntu1/interfaces/udev/udev.go snapd-2.54.2+21.10/interfaces/udev/udev.go --- snapd-2.53+21.10ubuntu1/interfaces/udev/udev.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/udev/udev.go 2022-01-06 21:25:16.000000000 +0000 @@ -24,6 +24,31 @@ "os/exec" ) +// udevadmTrigger runs "udevadm trigger" but ignores an non-zero exit codes. +// udevadm only started reporting errors in systemd 248 and in order to +// work correctly in LXD these errors need to be ignored. See +// https://github.com/systemd/systemd/pull/18684 for some more background +// (and https://github.com/lxc/lxd/issues/9526) +func udevadmTrigger(args ...string) error { + args = append([]string{"trigger"}, args...) + output, err := exec.Command("udevadm", args...).CombinedOutput() + // can happen when events for some of the devices or all of + // them could not be triggered, but we cannot distinguish which of + // those happened, in any case snapd invoked udevadm and tried its + // best + if exitErr, ok := err.(*exec.ExitError); ok { + // ignore "normal" exit codes but report e.g. segfaults + // that are reported as -1 + if exitErr.ExitCode() > 0 { + return nil + } + } + if err != nil { + return fmt.Errorf("%s\nudev output:\n%s", err, string(output)) + } + return nil +} + // reloadRules runs three commands that reload udev rule database. // // The commands are: udevadm control --reload-rules @@ -45,9 +70,8 @@ // By default, trigger for all events except the input subsystem since // it can cause noticeable blocked input on, for example, classic // desktop. - output, err = exec.Command("udevadm", "trigger", "--subsystem-nomatch=input").CombinedOutput() - if err != nil { - return fmt.Errorf("cannot run udev triggers: %s\nudev output:\n%s", err, string(output)) + if err = udevadmTrigger("--subsystem-nomatch=input"); err != nil { + return fmt.Errorf("cannot run udev triggers: %s", err) } // FIXME: track if also should trigger the joystick property if it @@ -63,9 +87,8 @@ // subsystem for joysticks, then trigger the joystick // events in a way that is specific to joysticks to not // block other inputs. - output, err = exec.Command("udevadm", "trigger", "--property-match=ID_INPUT_JOYSTICK=1").CombinedOutput() - if err != nil { - return fmt.Errorf("cannot run udev triggers for joysticks: %s\nudev output:\n%s", err, string(output)) + if err = udevadmTrigger("--property-match=ID_INPUT_JOYSTICK=1"); err != nil { + return fmt.Errorf("cannot run udev triggers for joysticks: %s", err) } inputJoystickTriggered = true } else if subsystem == "input/key" { @@ -73,16 +96,14 @@ // subsystem for input keys, then trigger the keys // events in a way that is specific to input keys // to not block other inputs. - output, err = exec.Command("udevadm", "trigger", "--property-match=ID_INPUT_KEY=1", "--property-match=ID_INPUT_KEYBOARD!=1").CombinedOutput() - if err != nil { - return fmt.Errorf("cannot run udev triggers for keys: %s\nudev output:\n%s", err, string(output)) + if err = udevadmTrigger("--property-match=ID_INPUT_KEY=1", "--property-match=ID_INPUT_KEYBOARD!=1"); err != nil { + return fmt.Errorf("cannot run udev triggers for keys: %s", err) } } else if subsystem != "" { // If one of the interfaces said it uses a subsystem, // then do it too. - output, err = exec.Command("udevadm", "trigger", "--subsystem-match="+subsystem).CombinedOutput() - if err != nil { - return fmt.Errorf("cannot run udev triggers for %s subsystem: %s\nudev output:\n%s", subsystem, err, string(output)) + if err := udevadmTrigger("--subsystem-match=" + subsystem); err != nil { + return fmt.Errorf("cannot run udev triggers for %s subsystem: %s", subsystem, err) } if subsystem == "input" { @@ -97,9 +118,8 @@ // this. Allows joysticks to be removed from the device cgroup on // interface disconnect. if !inputJoystickTriggered { - output, err = exec.Command("udevadm", "trigger", "--property-match=ID_INPUT_JOYSTICK=1").CombinedOutput() - if err != nil { - return fmt.Errorf("cannot run udev triggers for joysticks: %s\nudev output:\n%s", err, string(output)) + if err := udevadmTrigger("--property-match=ID_INPUT_JOYSTICK=1"); err != nil { + return fmt.Errorf("cannot run udev triggers for joysticks: %s", err) } } diff -Nru snapd-2.53+21.10ubuntu1/interfaces/udev/udev_test.go snapd-2.54.2+21.10/interfaces/udev/udev_test.go --- snapd-2.53+21.10ubuntu1/interfaces/udev/udev_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/udev/udev_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -79,7 +79,7 @@ }) } -func (s *uDevSuite) TestReloadUDevRulesReportsErrorsFromDefaultTrigger(c *C) { +func (s *uDevSuite) TestReloadUDevRulesIgnoresErrorsFromDefaultTrigger(c *C) { cmd := testutil.MockCommand(c, "udevadm", ` if [ "$1" = "trigger" ]; then echo "failure 2" @@ -88,10 +88,30 @@ `) defer cmd.Restore() err := s.backend.ReloadRules(nil) - c.Assert(err.Error(), Equals, ""+ - "cannot run udev triggers: exit status 2\n"+ - "udev output:\n"+ - "failure 2\n") + c.Assert(err, IsNil) + c.Assert(cmd.Calls(), DeepEquals, [][]string{ + {"udevadm", "control", "--reload-rules"}, + {"udevadm", "trigger", "--subsystem-nomatch=input"}, + // FIXME: temporary until spec.TriggerSubsystem() can be + // called during disconnect + {"udevadm", "trigger", "--property-match=ID_INPUT_JOYSTICK=1"}, + {"udevadm", "settle", "--timeout=10"}, + }) +} + +func (s *uDevSuite) TestReloadUDevRulesReportsErrorsFromDefaultTriggerSignals(c *C) { + cmd := testutil.MockCommand(c, "udevadm", ` +if [ "$1" = "trigger" ]; then + echo "failure 2" + kill -9 $$ +fi + `) + defer cmd.Restore() + err := s.backend.ReloadRules(nil) + c.Assert(err, ErrorMatches, `cannot run udev triggers: signal: killed +udev output: +failure 2 +`) c.Assert(cmd.Calls(), DeepEquals, [][]string{ {"udevadm", "control", "--reload-rules"}, {"udevadm", "trigger", "--subsystem-nomatch=input"}, @@ -111,7 +131,7 @@ }) } -func (s *uDevSuite) TestReloadUDevRulesReportsErrorsFromSubsystemTrigger(c *C) { +func (s *uDevSuite) TestReloadUDevRulesIgnoresErrorsFromSubsystemTrigger(c *C) { cmd := testutil.MockCommand(c, "udevadm", ` if [ "$2" = "--subsystem-match=input" ]; then echo "failure 2" @@ -120,14 +140,12 @@ `) defer cmd.Restore() err := s.backend.ReloadRules([]string{"input"}) - c.Assert(err.Error(), Equals, ""+ - "cannot run udev triggers for input subsystem: exit status 2\n"+ - "udev output:\n"+ - "failure 2\n") + c.Assert(err, IsNil) c.Assert(cmd.Calls(), DeepEquals, [][]string{ {"udevadm", "control", "--reload-rules"}, {"udevadm", "trigger", "--subsystem-nomatch=input"}, {"udevadm", "trigger", "--subsystem-match=input"}, + {"udevadm", "settle", "--timeout=10"}, }) } @@ -144,7 +162,7 @@ }) } -func (s *uDevSuite) TestReloadUDevRulesReportsErrorsFromJoystickTrigger(c *C) { +func (s *uDevSuite) TestReloadUDevRulesIgnoresErrorsFromJoystickTrigger(c *C) { cmd := testutil.MockCommand(c, "udevadm", ` if [ "$2" = "--property-match=ID_INPUT_JOYSTICK=1" ]; then echo "failure 2" @@ -153,14 +171,12 @@ `) defer cmd.Restore() err := s.backend.ReloadRules([]string{"input/joystick"}) - c.Assert(err.Error(), Equals, ""+ - "cannot run udev triggers for joysticks: exit status 2\n"+ - "udev output:\n"+ - "failure 2\n") + c.Assert(err, IsNil) c.Assert(cmd.Calls(), DeepEquals, [][]string{ {"udevadm", "control", "--reload-rules"}, {"udevadm", "trigger", "--subsystem-nomatch=input"}, {"udevadm", "trigger", "--property-match=ID_INPUT_JOYSTICK=1"}, + {"udevadm", "settle", "--timeout=10"}, }) } diff -Nru snapd-2.53+21.10ubuntu1/interfaces/utils/export_test.go snapd-2.54.2+21.10/interfaces/utils/export_test.go --- snapd-2.53+21.10ubuntu1/interfaces/utils/export_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/utils/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,26 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package utils + +var ( + CreateRegex = createRegex + GlobDefault = globDefault + GlobNull = globNull +) diff -Nru snapd-2.53+21.10ubuntu1/interfaces/utils/path_patterns.go snapd-2.54.2+21.10/interfaces/utils/path_patterns.go --- snapd-2.53+21.10ubuntu1/interfaces/utils/path_patterns.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/utils/path_patterns.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,177 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package utils + +import ( + "fmt" + "regexp" +) + +type PathPattern struct { + pattern string + regex *regexp.Regexp +} + +const maxGroupDepth = 50 + +type GlobFlags int + +const ( + globDefault GlobFlags = 1 << iota + globNull +) + +// createRegex converts the apparmor-like glob sequence into a regex. Loosely +// using this as reference: +// https://gitlab.com/apparmor/apparmor/-/blob/master/parser/parser_regex.c#L107 +func createRegex(pattern string, glob GlobFlags) (string, error) { + regex := "^" + + appendGlob := func(defaultGlob, nullGlob string) { + var pattern string + switch glob { + case globDefault: + pattern = defaultGlob + case globNull: + pattern = nullGlob + } + regex += pattern + } + + const ( + noSlashOrNull = `[^/\x00]` + noSlash = `[^/]` + ) + + escapeNext := false + currentGroupLevel := 0 + inCharClass := false + skipNext := false + itemCountInGroup := new([maxGroupDepth + 1]int) + for i, ch := range pattern { + if escapeNext { + regex += regexp.QuoteMeta(string(ch)) + escapeNext = false + continue + } + if skipNext { + skipNext = false + continue + } + if inCharClass && ch != '\\' && ch != ']' { + // no characters are special other than '\' and ']' + regex += string(ch) + continue + } + switch ch { + case '\\': + escapeNext = true + case '*': + if regex[len(regex)-1] == '/' { + // if the * is at the end of the pattern or is followed by a + // '/' we don't want it to match an empty string: + // /foo/* -> should not match /foo/ + // /foo/*bar -> should match /foo/bar + // /*/foo -> should not match //foo + pos := i + 1 + for len(pattern) > pos && pattern[pos] == '*' { + pos++ + } + if len(pattern) <= pos || pattern[pos] == '/' { + appendGlob(noSlashOrNull, noSlash) + } + } + + if len(pattern) > i+1 && pattern[i+1] == '*' { + // Handle ** + appendGlob("[^\\x00]*", ".*") + skipNext = true + } else { + appendGlob(noSlashOrNull+"*", noSlash+"*") + } + case '?': + appendGlob(noSlashOrNull, noSlash) + case '[': + inCharClass = true + regex += string(ch) + case ']': + if !inCharClass { + return "", fmt.Errorf("pattern contains unmatching ']': %q", pattern) + } + inCharClass = false + regex += string(ch) + case '{': + currentGroupLevel++ + if currentGroupLevel > maxGroupDepth { + return "", fmt.Errorf("maximum group depth exceeded: %q", pattern) + } + itemCountInGroup[currentGroupLevel] = 0 + regex += "(" + case '}': + if currentGroupLevel <= 0 { + return "", fmt.Errorf("invalid closing brace, no matching open { found: %q", pattern) + } + if itemCountInGroup[currentGroupLevel] == 0 { + return "", fmt.Errorf("invalid number of items between {}: %q", pattern) + } + currentGroupLevel-- + regex += ")" + case ',': + if currentGroupLevel > 0 { + itemCountInGroup[currentGroupLevel]++ + regex += "|" + } else { + return "", fmt.Errorf("cannot use ',' outside of group or character class") + } + default: + // take literal character (with quoting if needed) + regex += regexp.QuoteMeta(string(ch)) + } + } + + if currentGroupLevel > 0 { + return "", fmt.Errorf("missing %d closing brace(s): %q", currentGroupLevel, pattern) + } + if inCharClass { + return "", fmt.Errorf("missing closing bracket ']': %q", pattern) + } + if escapeNext { + return "", fmt.Errorf("expected character after '\\': %q", pattern) + } + + regex += "$" + return regex, nil +} + +func NewPathPattern(pattern string) (*PathPattern, error) { + regexPattern, err := createRegex(pattern, globDefault) + if err != nil { + return nil, err + } + + regex := regexp.MustCompile(regexPattern) + + pp := &PathPattern{pattern, regex} + return pp, nil +} + +func (pp *PathPattern) Matches(path string) bool { + return pp.regex.MatchString(path) +} diff -Nru snapd-2.53+21.10ubuntu1/interfaces/utils/path_patterns_test.go snapd-2.54.2+21.10/interfaces/utils/path_patterns_test.go --- snapd-2.53+21.10ubuntu1/interfaces/utils/path_patterns_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/interfaces/utils/path_patterns_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,133 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package utils_test + +import ( + "regexp" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/interfaces/utils" +) + +type pathPatternsSuite struct{} + +var _ = Suite(&pathPatternsSuite{}) + +func (s *pathPatternsSuite) TestRegexCreationHappy(c *C) { + // to save some typing: + d := utils.GlobDefault + n := utils.GlobNull + + data := []struct { + pattern string + glob utils.GlobFlags + expectedRegex string + }{ + {`/media/user/`, d, `^/media/user/$`}, + {`/dev/sd*`, d, `^/dev/sd[^/\x00]*$`}, + {`/dev/sd*`, n, `^/dev/sd[^/]*$`}, + {`/dev/sd?`, d, `^/dev/sd[^/\x00]$`}, + {`/dev/sd?`, n, `^/dev/sd[^/]$`}, + {`/etc/**`, d, `^/etc/[^/\x00][^\x00]*$`}, + {`/home/*/.bashrc`, d, `^/home/[^/\x00][^/\x00]*/\.bashrc$`}, + {`/home/*/.bashrc`, n, `^/home/[^/][^/]*/\.bashrc$`}, + {`/media/{user,loser}/`, d, `^/media/(user|loser)/$`}, + {`/nested/{a,b{c,d}}/`, d, `^/nested/(a|b(c|d))/$`}, + {`/media/\{in-braces\}/`, d, `^/media/\{in-braces\}/$`}, + {`/media/\[in-brackets\]/`, d, `^/media/\[in-brackets\]/$`}, + {`/dev/sd[abc][0-9]`, d, `^/dev/sd[abc][0-9]$`}, + {`/quoted/bracket/[ab\]c]`, d, `^/quoted/bracket/[ab\]c]$`}, + {`{[,],}`, d, `^([,]|)$`}, + {`/path/with/comma[,]`, d, `^/path/with/comma[,]$`}, + {`/$pecial/c^aracters`, d, `^/\$pecial/c\^aracters$`}, + {`/in/char/class[^$]`, d, `^/in/char/class[^$]$`}, + } + + for _, testData := range data { + pattern := testData.pattern + expectedRegex := testData.expectedRegex + regex, err := utils.CreateRegex(pattern, testData.glob) + c.Assert(err, IsNil, Commentf("%s", pattern)) + c.Assert(regex, Equals, expectedRegex, Commentf("%s", pattern)) + // Also, make sure that the obtained regex is valid + _, err = regexp.Compile(regex) + c.Assert(err, IsNil, Commentf("%s", pattern)) + } +} + +func (s *pathPatternsSuite) TestRegexCreationUnhappy(c *C) { + data := []struct { + pattern string + expectedError string + }{ + {`/media/{}/`, `invalid number of items between {}:.*`}, + {`/media/{some/things`, `missing 1 closing brace\(s\):.*`}, + {`/media/}`, `invalid closing brace, no matching open { found:.*`}, + {`/media/[abc`, `missing closing bracket ']':.*`}, + {`/media/]`, `pattern contains unmatching ']':.*`}, + {`/media\`, `expected character after '\\':.*`}, + // 123456789012345678901234567890123456789012345678901, 51 of them + {`/{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{{`, `maximum group depth exceeded:.*`}, + {`/comma/not/in/group/a,b`, `cannot use ',' outside of group or character class`}, + } + + for _, testData := range data { + pattern := testData.pattern + expectedError := testData.expectedError + pathPattern, err := utils.NewPathPattern(pattern) + c.Assert(pathPattern, IsNil, Commentf("%s", pattern)) + c.Assert(err, ErrorMatches, expectedError, Commentf("%s", pattern)) + } +} + +func (s *pathPatternsSuite) TestPatternMatches(c *C) { + data := []struct { + pattern string + testPath string + expectedMatch bool + }{ + {`/same/path/`, `/same/path/`, true}, + {`/path/*`, `/path/here`, true}, + {`/path/*`, `/path/too/deep`, false}, + {`/path/**`, `/path/here`, true}, + {`/path/**`, `/path/here/too`, true}, + {`/dev/sd?`, `/dev/sda`, true}, + {`/dev/sd?`, `/dev/sdb1`, false}, + {`/media/{user,loser}/`, `/media/user/`, true}, + {`/media/{user,loser}/`, `/media/other/`, false}, + {`/nested/{a,b{c,d}}/`, `/nested/a/`, true}, + {`/nested/{a,b{c,d}}/`, `/nested/bd/`, true}, + {`/nested/{a,b{c,d}}/`, `/nested/ad/`, false}, + {`/dev/sd[abc][0-9]`, `/dev/sda0`, true}, + {`/dev/sd[abc][0-9]`, `/dev/sdb4`, true}, + {`/dev/sd[abc][0-9]`, `/dev/sda10`, false}, + {`/dev/sd[abc][0-9]`, `/dev/sdd0`, false}, + } + + for _, testData := range data { + pattern := testData.pattern + testPath := testData.testPath + expectedMatch := testData.expectedMatch + pathPattern, err := utils.NewPathPattern(pattern) + c.Assert(err, IsNil, Commentf("%s", pattern)) + c.Assert(pathPattern.Matches(testPath), Equals, expectedMatch, Commentf("%s", pattern)) + } +} diff -Nru snapd-2.53+21.10ubuntu1/kernel/fde/fde.go snapd-2.54.2+21.10/kernel/fde/fde.go --- snapd-2.53+21.10ubuntu1/kernel/fde/fde.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/kernel/fde/fde.go 2022-01-06 21:25:16.000000000 +0000 @@ -29,6 +29,8 @@ "encoding/json" "fmt" "os/exec" + + "github.com/snapcore/snapd/osutil" ) // HasRevealKey return true if the current system has a "fde-reveal-key" @@ -80,16 +82,25 @@ return &res, nil } +// TODO: unexport this because how the hook is driven is an implemenation +// detail. It creates quite a bit of churn unfortunately, see +// https://github.com/snapcore/snapd/compare/master...mvo5:ice/refactor-fde?expand=1 +// // SetupRequest carries the operation and parameters for the fde-setup hooks // made available to them via the snapctl fde-setup-request command. type SetupRequest struct { - // XXX: make "op" a type: "features", "initial-setup", "update" ? Op string `json:"op"` // This needs to be a []byte so that Go's standard library will base64 // encode it automatically for us - Key []byte `json:"key,omitempty"` + Key []byte `json:"key,omitempty"` + + // Only used when called with "initial-setup" KeyName string `json:"key-name,omitempty"` + + // The part of the device kernel path for a "setup-device" call. + // Only used when called with "device-setup" + Device string `json:"device,omitempty"` } // A RunSetupHookFunc implements running the fde-setup kernel hook. @@ -126,3 +137,52 @@ } return res, nil } + +// CheckFeatures returns the features of fde-setup hook. +func CheckFeatures(runSetupHook RunSetupHookFunc) ([]string, error) { + req := &SetupRequest{ + Op: "features", + } + output, err := runSetupHook(req) + if err != nil { + return nil, err + } + var res struct { + Features []string `json:"features"` + Error string `json:"error"` + } + if err := json.Unmarshal(output, &res); err != nil { + return nil, fmt.Errorf("cannot parse hook output %q: %v", output, err) + } + if res.Features == nil && res.Error == "" { + return nil, fmt.Errorf(`cannot use hook: neither "features" nor "error" returned`) + } + if res.Error != "" { + return nil, fmt.Errorf("cannot use hook: it returned error: %v", res.Error) + } + return res.Features, nil +} + +// DeviceSetupParams contains the inputs for the fde-setup hook. +// The encryption key and the device (partition) are passed in. +type DeviceSetupParams struct { + Key []byte + Device string +} + +// DeviceSetup invokes the "device-setup" op running the fde-setup +// hook via runSetupHook. This can be used to e.g. initialize +// inline crypto hardware. +func DeviceSetup(runSetupHook RunSetupHookFunc, params *DeviceSetupParams) error { + req := &SetupRequest{ + Op: "device-setup", + Key: params.Key, + Device: params.Device, + } + hookOutput, err := runSetupHook(req) + if err != nil { + return fmt.Errorf("device setup failed with: %v", osutil.OutputErr(hookOutput, err)) + } + + return nil +} diff -Nru snapd-2.53+21.10ubuntu1/kernel/fde/fde_test.go snapd-2.54.2+21.10/kernel/fde/fde_test.go --- snapd-2.53+21.10ubuntu1/kernel/fde/fde_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/kernel/fde/fde_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -528,6 +528,52 @@ c.Check(osutil.FileExists(filepath.Join(dirs.GlobalRootDir, "/run/fde-reveal-key")), Equals, false) } +func (s *fdeSuite) TestDeviceSetupHappy(c *C) { + mockKey := []byte{1, 2, 3, 4} + mockDevice := "/dev/sda2" + + runSetupHook := func(req *fde.SetupRequest) ([]byte, error) { + c.Check(req, DeepEquals, &fde.SetupRequest{ + Op: "device-setup", + Key: mockKey, + Device: mockDevice, + }) + // empty reply: no error + mockJSON := `{}` + return []byte(mockJSON), nil + } + + params := &fde.DeviceSetupParams{ + Key: mockKey, + Device: mockDevice, + } + err := fde.DeviceSetup(runSetupHook, params) + c.Assert(err, IsNil) +} + +func (s *fdeSuite) TestDeviceSetupError(c *C) { + mockKey := []byte{1, 2, 3, 4} + mockDevice := "/dev/sda2" + + runSetupHook := func(req *fde.SetupRequest) ([]byte, error) { + c.Check(req, DeepEquals, &fde.SetupRequest{ + Op: "device-setup", + Key: mockKey, + Device: mockDevice, + }) + // empty reply: no error + mockJSON := `something failed badly` + return []byte(mockJSON), fmt.Errorf("exit status 1") + } + + params := &fde.DeviceSetupParams{ + Key: mockKey, + Device: mockDevice, + } + err := fde.DeviceSetup(runSetupHook, params) + c.Check(err, ErrorMatches, "device setup failed with: something failed badly") +} + func (s *fdeSuite) TestHasDeviceUnlock(c *C) { oldPath := os.Getenv("PATH") defer func() { os.Setenv("PATH", oldPath) }() @@ -552,3 +598,27 @@ c.Check(fde.HasDeviceUnlock(), Equals, true) } + +func (s *fdeSuite) TestIsEncryptedDeviceMapperName(c *C) { + // matches + for _, t := range []string{ + "something-device-locked", + "foo23-device-locked", + "device-device-locked", + "WE-DON'T-CARE-WHAT-THE-PREFIX-IS-AND-YOU-CAN'T-MAKE-US-device-locked", + } { + c.Assert(fde.IsHardwareEncryptedDeviceMapperName(t), Equals, true) + } + + // doesn't match + for _, t := range []string{ + "", + "-device-locked", + "device-locked", + "-device-locked-foo", + "some-device", + "CRYPT-LUKS2-5a522809c87e4dfa81a88dc5667d1304-ubuntu-data-3776bab4-8bcc-46b7-9da2-6a84ce7f93b4", + } { + c.Assert(fde.IsHardwareEncryptedDeviceMapperName(t), Equals, false) + } +} diff -Nru snapd-2.53+21.10ubuntu1/kernel/fde/mapper.go snapd-2.54.2+21.10/kernel/fde/mapper.go --- snapd-2.53+21.10ubuntu1/kernel/fde/mapper.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/kernel/fde/mapper.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,52 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package fde + +import ( + "path/filepath" + "strings" + + "github.com/snapcore/snapd/osutil/disks" +) + +// IsEncryptedDevice returns true when the provided device mapper name indicates +// that it is encrypted using FDE hooks. +func IsHardwareEncryptedDeviceMapperName(dmName string) bool { + // TODO: is there anything more we can use to limit the prefix of the + // dmName? + return dmName != "-device-locked" && strings.HasSuffix(dmName, "-device-locked") +} + +// DeviceUnlockKernelHookDeviceMapperBackResolver is a back resolver to be used +// with disks.RegisterDeviceMapperBackResolver for devices that implement full +// disk encryption via hardware devices with kernel snap hooks. +func DeviceUnlockKernelHookDeviceMapperBackResolver(dmUUID, dmName []byte) (dev string, ok bool) { + if !IsHardwareEncryptedDeviceMapperName(string(dmName)) { + return "", false + } + // this is a device encrypted using FDE hooks + + // the uuid of the mapper device is the same as the partuuid + return filepath.Join("/dev/disk/by-partuuid", string(dmUUID)), true +} + +func init() { + disks.RegisterDeviceMapperBackResolver("device-unlock-kernel-fde", DeviceUnlockKernelHookDeviceMapperBackResolver) +} diff -Nru snapd-2.53+21.10ubuntu1/mkversion.sh snapd-2.54.2+21.10/mkversion.sh --- snapd-2.53+21.10ubuntu1/mkversion.sh 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/mkversion.sh 2022-01-06 21:25:16.000000000 +0000 @@ -130,4 +130,5 @@ cat < "$PKG_BUILDDIR/data/info" VERSION=$v +SNAPD_APPARMOR_REEXEC=0 EOF diff -Nru snapd-2.53+21.10ubuntu1/osutil/buildid_test.go snapd-2.54.2+21.10/osutil/buildid_test.go --- snapd-2.53+21.10ubuntu1/osutil/buildid_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/buildid_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -147,6 +147,7 @@ // set custom homedir to ensure tests work in an sbuild environment // that force a non-existing homedir cmd.Env = append(os.Environ(), "HOME="+tmpdir) + cmd.Dir = tmpdir output, err := cmd.CombinedOutput() c.Assert(string(output), Equals, "") c.Assert(err, IsNil) diff -Nru snapd-2.53+21.10ubuntu1/osutil/chattr_32.go snapd-2.54.2+21.10/osutil/chattr_32.go --- snapd-2.53+21.10ubuntu1/osutil/chattr_32.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/chattr_32.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build arm || 386 || ppc // +build arm 386 ppc /* diff -Nru snapd-2.53+21.10ubuntu1/osutil/chattr_64.go snapd-2.54.2+21.10/osutil/chattr_64.go --- snapd-2.53+21.10ubuntu1/osutil/chattr_64.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/chattr_64.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build amd64 || arm64 || ppc64le || riscv64 || s390x // +build amd64 arm64 ppc64le riscv64 s390x /* diff -Nru snapd-2.53+21.10ubuntu1/osutil/cp_other.go snapd-2.54.2+21.10/osutil/cp_other.go --- snapd-2.53+21.10ubuntu1/osutil/cp_other.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/cp_other.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !linux // +build !linux /* diff -Nru snapd-2.53+21.10ubuntu1/osutil/disks/blockdev.go snapd-2.54.2+21.10/osutil/disks/blockdev.go --- snapd-2.53+21.10ubuntu1/osutil/disks/blockdev.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/disks/blockdev.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,71 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package disks + +import ( + "fmt" + "os/exec" + "strconv" + "strings" + + "github.com/snapcore/snapd/osutil" +) + +func blockdevSizeCmd(cmd, devpath string) (uint64, error) { + out, err := exec.Command("blockdev", cmd, devpath).CombinedOutput() + if err != nil { + return 0, osutil.OutputErr(out, err) + } + nospace := strings.TrimSpace(string(out)) + sz, err := strconv.Atoi(nospace) + if err != nil { + return 0, fmt.Errorf("cannot parse blockdev %s result size %q: %v", cmd, nospace, err) + } + return uint64(sz), nil +} + +func blockDeviceSizeInSectors(devpath string) (uint64, error) { + // the size is always reported in 512-byte sectors, even if the device does + // not have a physical sector size of 512 + // XXX: consider using /sys/block//size directly + return blockdevSizeCmd("--getsz", devpath) +} + +func blockDeviceSectorSize(devpath string) (uint64, error) { + // the size is reported in raw bytes + sz, err := blockdevSizeCmd("--getss", devpath) + if err != nil { + return 0, err + } + + // ensure that the sector size is a multiple of 512, since we rely on that + // when we calculate the size in sectors, as blockdev --getsz always returns + // the size in 512-byte sectors + if sz%512 != 0 { + return 0, fmt.Errorf("sector size (%d) is not a multiple of 512", sz) + } + if sz == 0 { + // in some other places we are using the sector size as a divisor (to + // convert from bytes to sectors), so it's essential that 0 is treated + // as an error + return 0, fmt.Errorf("internal error: sector size returned as 0") + } + return sz, nil +} diff -Nru snapd-2.53+21.10ubuntu1/osutil/disks/disks_darwin.go snapd-2.54.2+21.10/osutil/disks/disks_darwin.go --- snapd-2.53+21.10ubuntu1/osutil/disks/disks_darwin.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/disks/disks_darwin.go 2022-01-06 21:25:16.000000000 +0000 @@ -28,7 +28,16 @@ return nil, osutil.ErrDarwin } -var diskFromDeviceName = func(mountpoint string) (Disk, error) { +var diskFromDeviceName = func(deviceName string) (Disk, error) { + return nil, osutil.ErrDarwin +} + +// DiskFromDevicePath is not implemented on darwin +func DiskFromDevicePath(devicePath string) (Disk, error) { + return nil, osutil.ErrDarwin +} + +var diskFromDevicePath = func(devicePath string) (Disk, error) { return nil, osutil.ErrDarwin } @@ -44,3 +53,7 @@ var mountPointsForPartitionRoot = func(p Partition, opts map[string]string) ([]string, error) { return nil, osutil.ErrDarwin } + +var diskFromPartitionDeviceNode = func(node string) (Disk, error) { + return nil, osutil.ErrDarwin +} diff -Nru snapd-2.53+21.10ubuntu1/osutil/disks/disks.go snapd-2.54.2+21.10/osutil/disks/disks.go --- snapd-2.53+21.10ubuntu1/osutil/disks/disks.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/disks/disks.go 2022-01-06 21:25:16.000000000 +0000 @@ -76,6 +76,10 @@ // does not have partitions for example. HasPartitions() bool + // DiskID returns the partition table ID, which is either a hexadecimal + // number for DOS disks, or a UUID for GPT disks. + DiskID() string + // Partitions returns all partitions found on a physical disk device. Note // that this method, and all others that require discovering partitions on // the disk, caches the partitions once first found and does not re-discover @@ -89,6 +93,36 @@ // KernelDevicePath returns the full device path in /sys/devices for the // disk such as /sys/devices/pci0000:00/0000:00:03.0/virtio1/block/vda/. KernelDevicePath() string + + // Schema returns the schema for the disk, either DOS or GPT in lowercase. + Schema() string + + // SectorSize returns the sector size for the disk in bytes, usually 512, + // sometimes 4096, possibly even 8196 some day. + // TODO: make this return a quantity.Size when that is doable without + // importing gadget + SectorSize() (uint64, error) + + // SizeInBytes returns the overall size of the disk in bytes. Not all of the + // bytes may be usable for partitions, as some space on disks is reserved + // for metadata such as the MBR on DOS disks or the GPT headers (and backup) + // on GPT disks. + // TODO: make this return a quantity.Size when that is doable without + // importing gadget + SizeInBytes() (uint64, error) + + // UsableSectorsEnd returns the exclusive end of usable sectors on the disk + // where partitions may occupy and be created. Specifically, the end is not + // itself usable, it is the region immediately after usable space; this sort + // of measurement is used when partitioning disks to indicate where a given + // partition ends. + // The sector unit is the in the native size for the disk, either 512 or + // 4096 bytes typically. + // This measurement is distinct from the size of the disk, though for some + // disks, this measurement may be the size of the disk in sectors - this is + // the case for DOS disks, but not for GPT disks. GPT disks have a backup + // header section at the end of the disk that is not usable for partitions. + UsableSectorsEnd() (uint64, error) } // Partition represents a partition on a Disk device. @@ -104,6 +138,10 @@ PartitionLabel string // the partition UUID PartitionUUID string + + // TODO: Major and Minor should be uints, they are required to be uints by + // the kernel, so it makes sense to match that here + // Major is the major number for this partition. Major int // Minor is the minor number for this partition. @@ -113,6 +151,20 @@ KernelDevicePath string // KernelDeviceNode is the kernel device node in /dev. KernelDeviceNode string + // PartitionType is the type of structure, for example 0C in the case of a + // vfat partition on a DOS disk, or 0FC63DAF-8483-4772-8E79-3D69D8477DE4, + // which is ext4 on a GPT disk. This is always upper case. + PartitionType string + // FilesystemType is the type of filesystem i.e. ext4 or vfat, etc. + FilesystemType string + // StructureIndex is the index of the structure on the disk, where the first + // partition/structure has index of 1. + StructureIndex uint64 + // StartInBytes is the beginning of the partition/structure in bytes. + StartInBytes uint64 + // SizeInBytes is the overall size of the partition/structure in bytes. + SizeInBytes uint64 + // TODO: also include a Disk field for finding what Disk this partition came // from? } @@ -153,3 +205,24 @@ var ( _ = error(PartitionNotFoundError{}) ) + +var deviceMapperBackResolvers = map[string]func(dmUUID, dmName []byte) (dev string, ok bool){} + +// RegisterDeviceMapperBackResolver takes a callback function which is used when +// the disks package through some of it's various methods to locate/create a +// disk needs to trace a device mapper node back to the original device node +// location such as /dev/mapper/something -> /dev/sda1 -> /dev/sda. The +// parameters the callback is provided are the device mapper UUID and name +// parameters from the kernel. If and only if the device mapper handler matches +// this device mapper node, the callback should return the source device node +// for the mapper device and true. If the handler does not match a provided +// device mapper, the function should return "ok" as false. +// The name of the handler is currently only used in tests. +func RegisterDeviceMapperBackResolver(name string, f func(dmUUID, dmName []byte) (dev string, ok bool)) { + deviceMapperBackResolvers[name] = f +} + +// mainly for tests to un-register and re-register handlers +func unregisterDeviceMapperBackResolver(name string) { + delete(deviceMapperBackResolvers, name) +} diff -Nru snapd-2.53+21.10ubuntu1/osutil/disks/disks_linux.go snapd-2.54.2+21.10/osutil/disks/disks_linux.go --- snapd-2.53+21.10ubuntu1/osutil/disks/disks_linux.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/disks/disks_linux.go 2022-01-06 21:25:16.000000000 +0000 @@ -22,12 +22,13 @@ import ( "bufio" "bytes" + "encoding/json" + "errors" "fmt" "io" "io/ioutil" "os/exec" "path/filepath" - "regexp" "sort" "strconv" "strings" @@ -38,18 +39,6 @@ var _ = Disk(&disk{}) -var ( - // this regexp is for the DM_UUID udev property, or equivalently the dm/uuid - // sysfs entry for a luks2 device mapper volume dynamically created by - // systemd-cryptsetup when unlocking - // the actual value that is returned also has "-some-name" appended to this - // pattern, but we delete that from the string before matching with this - // regexp to prevent issues like a mapper volume name that has CRYPT-LUKS2- - // in the name and thus we might accidentally match it - // see also the comments in DiskFromMountPoint about this value - luksUUIDPatternRe = regexp.MustCompile(`^CRYPT-LUKS2-([0-9a-f]{32})$`) -) - // diskFromMountPoint is exposed for mocking from other tests via // MockMountPointDisksToPartitionMapping, but we can't just assign // diskFromMountPointImpl to diskFromMountPoint due to signature differences, @@ -115,7 +104,40 @@ return m, scanner.Err() } -func diskFromUdevProps(deviceIdentifier string, deviceIDType string, props map[string]string) (Disk, error) { +type errNonPhysicalDisk struct { + err string +} + +func (e errNonPhysicalDisk) Error() string { return e.err } + +const propNotFoundErrFmt = "property %q not found" + +func requiredUDevPropUint(props map[string]string, name string) (uint64, error) { + partIndex, ok := props[name] + if !ok { + return 0, fmt.Errorf(propNotFoundErrFmt, name) + } + v, err := strconv.ParseUint(partIndex, 10, 64) + if err != nil { + return 0, fmt.Errorf("cannot parse %q: %v", partIndex, err) + } + return v, nil +} + +func diskFromUDevProps(deviceIdentifier string, deviceIDType string, props map[string]string) (Disk, error) { + // all physical disks must have ID_PART_TABLE_TYPE defined as the schema for + // the disk, so check for that first and if it's missing then we return a + // specific NotAPhysicalDisk error + schema := strings.ToLower(props["ID_PART_TABLE_TYPE"]) + if schema == "" { + return nil, errNonPhysicalDisk{fmt.Sprintf("device with %s %q is not a physical disk", deviceIDType, deviceIdentifier)} + } + + // for now we only support DOS and GPT schema disks + if schema != "dos" && schema != "gpt" { + return nil, fmt.Errorf("unsupported disk schema %q", props["ID_PART_TABLE_TYPE"]) + } + major, err := strconv.Atoi(props["MAJOR"]) if err != nil { return nil, fmt.Errorf("cannot find disk with %s %q: malformed udev output", deviceIDType, deviceIdentifier) @@ -144,11 +166,27 @@ // create the full path by pre-pending /sys, since udev doesn't include /sys devpath = filepath.Join(dirs.SysfsDir, devpath) + partTableID := props["ID_PART_TABLE_UUID"] + if partTableID == "" { + return nil, fmt.Errorf("cannot find disk with %s %q: malformed udev output missing property \"ID_PART_TABLE_UUID\"", deviceIDType, deviceIdentifier) + } + + // check if the device has partitions by attempting to actually search for + // them in /sys with the DEVPATH and DEVNAME + + paths, err := filepath.Glob(filepath.Join(devpath, filepath.Base(devname)+"*")) + if err != nil { + return nil, fmt.Errorf("internal error with glob pattern: %v", err) + } + return &disk{ - major: major, - minor: minor, - devname: devname, - devpath: devpath, + schema: schema, + diskID: partTableID, + major: major, + minor: minor, + devname: devname, + devpath: devpath, + hasPartitions: len(paths) != 0, }, nil } @@ -159,7 +197,7 @@ } // diskFromDevicePath is exposed for mocking from other tests via -// MockDeviceNameDisksToPartitionMapping. +// MockDevicePathToDiskMapping (which is yet to be added). var diskFromDevicePath = func(devicePath string) (Disk, error) { // query for the disk props using udev with --path props, err := udevPropertiesForPath(devicePath) @@ -167,7 +205,7 @@ return nil, err } - return diskFromUdevProps(devicePath, "path", props) + return diskFromUDevProps(devicePath, "path", props) } // DiskFromDeviceName finds a matching Disk using the specified name, such as @@ -177,7 +215,7 @@ } // diskFromDeviceName is exposed for mocking from other tests via -// MockDeviceNameDisksToPartitionMapping. +// MockDeviceNameToDiskMapping. var diskFromDeviceName = func(deviceName string) (Disk, error) { // query for the disk props using udev with --name props, err := udevPropertiesForName(deviceName) @@ -185,7 +223,7 @@ return nil, err } - return diskFromUdevProps(deviceName, "name", props) + return diskFromUDevProps(deviceName, "name", props) } func mountPointsForPartitionRoot(part Partition, mountOptsMatching map[string]string) ([]string, error) { @@ -214,28 +252,41 @@ return mountpoints, nil } -func (d *disk) Partitions() ([]Partition, error) { - if !d.hasPartitions { - // for i.e. device mapper disks which don't have partitions - return nil, nil - } - if err := d.populatePartitions(); err != nil { - return nil, err - } - - return d.partitions, nil -} - // DiskFromMountPoint finds a matching Disk for the specified mount point. func DiskFromMountPoint(mountpoint string, opts *Options) (Disk, error) { // call the unexported version that may be mocked by tests return diskFromMountPoint(mountpoint, opts) } +// DiskFromPartitionDeviceNode finds a matching Disk that the specified +// partition node resides on. +func DiskFromPartitionDeviceNode(node string) (Disk, error) { + // TODO: support options such as IsDecryptedDevice + return diskFromPartitionDeviceNode(node) +} + +var diskFromPartitionDeviceNode = func(node string) (Disk, error) { + // get the udev properties for this device node + props, err := udevPropertiesForName(node) + if err != nil { + return nil, err + } + + disk, err := diskFromPartUDevProps(props, nil) + if err != nil { + return nil, fmt.Errorf("cannot find disk from partition device node %s: %v", node, err) + } + return disk, nil +} + type disk struct { major int minor int + schema string + + diskID string + // devname is the DEVNAME property for the disk device like /dev/sda devname string @@ -261,51 +312,109 @@ return d.devpath } -// diskFromMountPointImpl returns a Disk for the underlying mount source of the -// specified mount point. For mount points which have sources that are not -// partitions, and thus are a part of a disk, the returned Disk refers to the -// volume/disk of the mount point itself. -func diskFromMountPointImpl(mountpoint string, opts *Options) (*disk, error) { - // first get the mount entry for the mountpoint - mounts, err := osutil.LoadMountInfo() - if err != nil { +func (d *disk) DiskID() string { + return d.diskID +} + +func (d *disk) Partitions() ([]Partition, error) { + if !d.hasPartitions { + // for i.e. device mapper disks which don't have partitions + return nil, nil + } + if err := d.populatePartitions(); err != nil { return nil, err } - var d *disk - var partMountPointSource string - // loop over the mount entries in reverse order to prevent shadowing of a - // particular mount on top of another one - for i := len(mounts) - 1; i >= 0; i-- { - if mounts[i].MountDir == mountpoint { - d = &disk{ - major: mounts[i].DevMajor, - minor: mounts[i].DevMinor, - } - partMountPointSource = mounts[i].MountSource - break + + return d.partitions, nil +} + +// okay to use in initrd, uses blockdev command +func (d *disk) SectorSize() (uint64, error) { + return blockDeviceSectorSize(d.devname) +} + +// sfdiskDeviceDump represents the sfdisk --dump JSON output format. +type sfdiskDeviceDump struct { + PartitionTable sfdiskPartitionTable `json:"partitiontable"` +} + +type sfdiskPartitionTable struct { + Unit string `json:"unit"` + FirstLBA uint64 `json:"firstlba"` + LastLBA uint64 `json:"lastlba"` +} + +func (d *disk) SizeInBytes() (uint64, error) { + // TODO: this could be implemented by reading the "size" file in sysfs + // instead of using blockdev + + // The size of the disk is always given by using blockdev, but blockdev + // returns the size in 512-byte blocks, so for bytes we have to multiply by + // 512. + num512Sectors, err := blockDeviceSizeInSectors(d.devname) + // if err is non-nil, numSectors will be 0 and thus 0*512 will still be + // zero + return num512Sectors * 512, err +} + +// okay to use in initrd for dos disks since it uses blockdev command, but for +// gpt disks, we need to use sfdisk, which is not okay to use in the initrd +func (d *disk) UsableSectorsEnd() (uint64, error) { + if d.schema == "dos" { + // for DOS disks, it is sufficient to just get the size in bytes and + // divide by the sector size + byteSz, err := d.SizeInBytes() + if err != nil { + return 0, err + } + sectorSz, err := d.SectorSize() + if err != nil { + return 0, err } + return byteSz / sectorSz, nil } - if d == nil { - return nil, fmt.Errorf("cannot find mountpoint %q", mountpoint) + + // TODO: this could also be accomplished by reading from the GPT headers + // directly to get the last logical block address (LBA) instead of using + // sfdisk, in which case this function could then be used in the initrd + + // otherwise for GPT, we need to use sfdisk on the device node + output, err := exec.Command("sfdisk", "--json", d.devname).Output() + if err != nil { + return 0, err } - // now we have the partition for this mountpoint, we need to tie that back - // to a disk with a major minor, so query udev with the mount source path - // of the mountpoint for properties - props, err := udevPropertiesForName(partMountPointSource) - if err != nil && props == nil { - // only fail here if props is nil, if it's available we validate it - // below - return nil, fmt.Errorf("cannot find disk for partition %s: %v", partMountPointSource, err) + var dump sfdiskDeviceDump + if err := json.Unmarshal(output, &dump); err != nil { + return 0, fmt.Errorf("cannot parse sfdisk output: %v", err) } + // check that the unit is sectors + if dump.PartitionTable.Unit != "sectors" { + return 0, fmt.Errorf("cannot get size in sectors, sfdisk reported unknown unit %s", dump.PartitionTable.Unit) + } + + // the last logical block address (LBA) is the location of the last + // occupiable sector, so the end is 1 further (the end itself is not + // included) + + // sfdisk always returns the sectors in native sector size, so we don't need + // to do any conversion here + return (dump.PartitionTable.LastLBA + 1), nil +} + +func (d *disk) Schema() string { + return d.schema +} + +func diskFromPartUDevProps(props map[string]string, opts *Options) (*disk, error) { if opts != nil && opts.IsDecryptedDevice { // verify that the mount point is indeed a mapper device, it should: // 1. have DEVTYPE == disk from udev // 2. have dm files in the sysfs entry for the maj:min of the device if props["DEVTYPE"] != "disk" { // not a decrypted device - return nil, fmt.Errorf("mountpoint source %s is not a decrypted device: devtype is not disk (is %s)", partMountPointSource, props["DEVTYPE"]) + return nil, fmt.Errorf("not a decrypted device: devtype is not disk (is %s)", props["DEVTYPE"]) } // TODO:UC20: currently, we effectively parse the DM_UUID env variable @@ -329,113 +438,159 @@ // or not, given that these variables have been observed to // be missing from the initrd previously, and are not // available at all during userspace on UC20 for some reason - errFmt := "mountpoint source %s is not a decrypted device: could not read device mapper metadata: %v" + errFmt := "not a decrypted device: could not read device mapper metadata: %v" - dmDir := filepath.Join(dirs.SysfsDir, "dev", "block", d.Dev(), "dm") + if props["MAJOR"] == "" { + return nil, fmt.Errorf("incomplete udev output missing required property \"MAJOR\"") + } + if props["MINOR"] == "" { + return nil, fmt.Errorf("incomplete udev output missing required property \"MAJOR\"") + } + + majmin := props["MAJOR"] + ":" + props["MINOR"] + + dmDir := filepath.Join(dirs.SysfsDir, "dev", "block", majmin, "dm") dmUUID, err := ioutil.ReadFile(filepath.Join(dmDir, "uuid")) if err != nil { - return nil, fmt.Errorf(errFmt, partMountPointSource, err) + return nil, fmt.Errorf(errFmt, err) } + dmUUID = bytes.TrimSpace(dmUUID) dmName, err := ioutil.ReadFile(filepath.Join(dmDir, "name")) if err != nil { - return nil, fmt.Errorf(errFmt, partMountPointSource, err) + return nil, fmt.Errorf(errFmt, err) } + dmName = bytes.TrimSpace(dmName) - // trim the suffix of the dm name from the dm uuid to safely match the - // regex - the dm uuid contains the dm name, and the dm name is user - // controlled, so we want to remove that and just use the luks pattern - // to match the device uuid - // we are extra safe here since the dm name could be hypothetically user - // controlled via an external USB disk with LVM partition names, etc. - dmUUIDSafe := bytes.TrimSuffix( - bytes.TrimSpace(dmUUID), - append([]byte("-"), bytes.TrimSpace(dmName)...), - ) - matches := luksUUIDPatternRe.FindSubmatch(dmUUIDSafe) - if len(matches) != 2 { - // the format of the uuid is different - different luks version - // maybe? - return nil, fmt.Errorf("cannot verify disk: partition %s does not have a valid luks uuid format: %s", d.Dev(), dmUUIDSafe) - } - - // the uuid is the first and only submatch, but it is not in the same - // format exactly as we want to use, namely it is missing all of the "-" - // characters in a typical uuid, i.e. it is of the form: - // ae6e79de00a9406f80ee64ba7c1966bb but we want it to be like: - // ae6e79de-00a9-406f-80ee-64ba7c1966bb so we need to add in 4 "-" - // characters - compactUUID := string(matches[1]) - canonicalUUID := fmt.Sprintf( - "%s-%s-%s-%s-%s", - compactUUID[0:8], - compactUUID[8:12], - compactUUID[12:16], - compactUUID[16:20], - compactUUID[20:], - ) - - // now finally, we need to use this uuid, which is the device uuid of - // the actual physical encrypted partition to get the path, which will - // be something like /dev/vda4, etc. - byUUIDPath := filepath.Join("/dev/disk/by-uuid", canonicalUUID) - props, err = udevPropertiesForName(byUUIDPath) - if err != nil { - return nil, fmt.Errorf("cannot get udev properties for encrypted partition %s: %v", byUUIDPath, err) + matchedHandler := false + for _, resolver := range deviceMapperBackResolvers { + if dev, ok := resolver(dmUUID, dmName); ok { + props, err = udevPropertiesForName(dev) + if err != nil { + return nil, fmt.Errorf("cannot get udev properties for encrypted partition %s: %v", dev, err) + } + + matchedHandler = true + break + } + } + + if !matchedHandler { + return nil, fmt.Errorf("internal error: no back resolver supports decrypted device mapper with UUID %q and name %q", dmUUID, dmName) } } // ID_PART_ENTRY_DISK will give us the major and minor of the disk that this // partition originated from if this mount point is indeed for a partition - if majorMinor, ok := props["ID_PART_ENTRY_DISK"]; ok { - maj, min, err := parseDeviceMajorMinor(majorMinor) - if err != nil { - // bad udev output? - return nil, fmt.Errorf("cannot find disk for partition %s, bad udev output: %v", partMountPointSource, err) - } - d.major = maj - d.minor = min + if props["ID_PART_ENTRY_DISK"] == "" { + // TODO: there may be valid use cases for ID_PART_ENTRY_DISK being + // missing, like where a mountpoint is for a decrypted mapper device, + // and the physical backing device is a full disk and not a partition, + // but we don't have such use cases right now so just error, this can + // be revisited later on + return nil, fmt.Errorf("incomplete udev output missing required property \"ID_PART_ENTRY_DISK\"") + } - // now go find the devname and devpath for this major/minor pair since - // we will need that later - note that the props variable at this point - // is for the partition, not the parent disk itself, hence the - // additional lookup - realDiskProps, err := udevPropertiesForName(filepath.Join("/dev/block/", majorMinor)) - if err != nil { - return nil, fmt.Errorf("cannot find disk for partition %s: %v", partMountPointSource, err) - } + majorMinor := props["ID_PART_ENTRY_DISK"] + maj, min, err := parseDeviceMajorMinor(majorMinor) + if err != nil { + // bad udev output? + return nil, fmt.Errorf("bad udev output: %v", err) + } - if realDiskProps["DEVNAME"] == "" { - return nil, fmt.Errorf("cannot find disk for partition %s: incomplete udev output missing required property \"DEVNAME\"", partMountPointSource) - } + d := &disk{ + major: maj, + minor: min, + } - d.devname = realDiskProps["DEVNAME"] + // now go find the devname and devpath for this major/minor pair since + // we will need that later - note that the props variable at this point + // is for the partition, not the parent disk itself, hence the + // additional lookup + realDiskProps, err := udevPropertiesForName(filepath.Join("/dev/block/", majorMinor)) + if err != nil { + return nil, err + } - if realDiskProps["DEVPATH"] == "" { - return nil, fmt.Errorf("cannot find disk for partition %s: incomplete udev output missing required property \"DEVPATH\"", partMountPointSource) - } - // the DEVPATH is given as relative to /sys, so for simplicity's sake - // add /sys to the path we save as we return it later - d.devpath = filepath.Join(dirs.SysfsDir, realDiskProps["DEVPATH"]) + if devtype := realDiskProps["DEVTYPE"]; devtype != "disk" { + return nil, fmt.Errorf("unsupported DEVTYPE %q", devtype) + } + + if realDiskProps["DEVNAME"] == "" { + return nil, fmt.Errorf("incomplete udev output missing required property \"DEVNAME\"") + } + + d.devname = realDiskProps["DEVNAME"] + + if realDiskProps["DEVPATH"] == "" { + return nil, fmt.Errorf("incomplete udev output missing required property \"DEVPATH\"") + } + + // the DEVPATH is given as relative to /sys, so for simplicity's sake + // add /sys to the path we save as we return it later + d.devpath = filepath.Join(dirs.SysfsDir, realDiskProps["DEVPATH"]) - // since the mountpoint device has a disk, the mountpoint source itself - // must be a partition from a disk, thus the disk has partitions - d.hasPartitions = true - return d, nil + partTableID := realDiskProps["ID_PART_TABLE_UUID"] + if partTableID == "" { + return nil, fmt.Errorf("incomplete udev output missing required property \"ID_PART_TABLE_UUID\"") } - // if we don't have ID_PART_ENTRY_DISK, the partition is probably a mapped - // volume or other non-physical disk, so confirm that DEVTYPE == disk and - // return the maj/min for it - if devType, ok := props["DEVTYPE"]; ok { - if devType == "disk" { - return d, nil + schema := strings.ToLower(realDiskProps["ID_PART_TABLE_TYPE"]) + if schema == "" { + return nil, fmt.Errorf("incomplete udev output missing required property \"ID_PART_TABLE_TYPE\"") + } + + if schema != "gpt" && schema != "dos" { + return nil, fmt.Errorf("unsupported disk schema %q", realDiskProps["ID_PART_TABLE_TYPE"]) + } + + d.schema = schema + d.diskID = partTableID + + // since the mountpoint device has a disk, the mountpoint source itself + // must be a partition from a disk, thus the disk has partitions + d.hasPartitions = true + return d, nil +} + +// diskFromMountPointImpl returns a Disk for the underlying mount source of the +// specified mount point. For mount points which have sources that are not +// partitions, and thus are a part of a disk, the returned Disk refers to the +// volume/disk of the mount point itself. +func diskFromMountPointImpl(mountpoint string, opts *Options) (*disk, error) { + // first get the mount entry for the mountpoint + mounts, err := osutil.LoadMountInfo() + if err != nil { + return nil, err + } + var partMountPointSource string + // loop over the mount entries in reverse order to prevent shadowing of a + // particular mount on top of another one + for i := len(mounts) - 1; i >= 0; i-- { + if mounts[i].MountDir == mountpoint { + partMountPointSource = mounts[i].MountSource + break } - // unclear what other DEVTYPE's we should support for this function - return nil, fmt.Errorf("unsupported DEVTYPE %q for mount point source %s", devType, partMountPointSource) + } + if partMountPointSource == "" { + return nil, fmt.Errorf("cannot find mountpoint %q", mountpoint) } - return nil, fmt.Errorf("cannot find disk for partition %s, incomplete udev output", partMountPointSource) + // now we have the partition for this mountpoint, we need to tie that back + // to a disk with a major minor, so query udev with the mount source path + // of the mountpoint for properties + props, err := udevPropertiesForName(partMountPointSource) + if err != nil && props == nil { + // only fail here if props is nil, if it's available we validate it + // below + return nil, fmt.Errorf("cannot find disk for partition %s: %v", partMountPointSource, err) + } + + disk, err := diskFromPartUDevProps(props, opts) + if err != nil { + return nil, fmt.Errorf("cannot find disk from mountpoint source %s of %s: %v", partMountPointSource, mountpoint, err) + } + return disk, nil } func (d *disk) populatePartitions() error { @@ -487,19 +642,71 @@ continue } - // the devpath should always be available + emitUDevPropErr := func(e error) error { + return fmt.Errorf("cannot get required udev property for device %s (a partition of %s): %v", partDev, d.Dev(), e) + } + + // the devpath and devname should always be available devpath, ok := udevProps["DEVPATH"] if !ok { - return fmt.Errorf("cannot get udev properties for device %s (a partition of %s), missing required udev property \"DEVPATH\"", partDev, d.Dev()) + return emitUDevPropErr(fmt.Errorf(propNotFoundErrFmt, "DEVPATH")) } part.KernelDevicePath = filepath.Join(dirs.SysfsDir, devpath) devname, ok := udevProps["DEVNAME"] if !ok { - return fmt.Errorf("cannot get udev properties for device %s (a partition of %s), missing required udev property \"DEVNAME\"", partDev, d.Dev()) + return emitUDevPropErr(fmt.Errorf(propNotFoundErrFmt, "DEVNAME")) } part.KernelDeviceNode = devname + // we should always have the partition type + partType, ok := udevProps["ID_PART_ENTRY_TYPE"] + if !ok { + return emitUDevPropErr(fmt.Errorf(propNotFoundErrFmt, "ID_PART_ENTRY_TYPE")) + } + + // on dos disks, the type is formatted like "0xc", when we prefer to + // always use "0C" so fix the formatting + if d.schema == "dos" { + partType = strings.TrimPrefix(strings.ToLower(partType), "0x") + v, err := strconv.ParseUint(partType, 16, 8) + if err != nil { + return emitUDevPropErr(fmt.Errorf("cannot convert MBR partition type %q: %v", partType, err)) + } + partType = fmt.Sprintf("%02X", v) + } else { + // on GPT disks, just capitalize the partition type since it's a + // UUID + partType = strings.ToUpper(partType) + } + + part.PartitionType = partType + + // the partition may not have a filesystem, in which case this might + // be the empty string + part.FilesystemType = udevProps["ID_FS_TYPE"] + + part.SizeInBytes, err = requiredUDevPropUint(udevProps, "ID_PART_ENTRY_SIZE") + if err != nil { + return emitUDevPropErr(err) + } + + part.StructureIndex, err = requiredUDevPropUint(udevProps, "ID_PART_ENTRY_NUMBER") + if err != nil { + return emitUDevPropErr(err) + } + + part.StartInBytes, err = requiredUDevPropUint(udevProps, "ID_PART_ENTRY_OFFSET") + if err != nil { + return emitUDevPropErr(err) + } + + // udev always reports the size and offset in 512 byte blocks, + // regardless of sector size, so multiply the size in 512 byte + // blocks by 512 to get the size/offset in bytes + part.StartInBytes *= 512 + part.SizeInBytes *= 512 + // we should always have the partition uuid, and we may not have // either the partition label or the filesystem label, on GPT disks // the partition label is optional, and may or may not have a @@ -508,19 +715,21 @@ // the partition part.PartitionUUID = udevProps["ID_PART_ENTRY_UUID"] if part.PartitionUUID == "" { - return fmt.Errorf("cannot get udev properties for device %s (a partition of %s), missing udev property \"ID_PART_ENTRY_UUID\"", partDev, d.Dev()) + return emitUDevPropErr(fmt.Errorf(propNotFoundErrFmt, "ID_PART_ENTRY_UUID")) } // we should also always have the device major/minor for this device - part.Major, err = strconv.Atoi(udevProps["MAJOR"]) + maj, err := requiredUDevPropUint(udevProps, "MAJOR") if err != nil { - return fmt.Errorf("cannot parse device major number format: %v", err) + return emitUDevPropErr(err) } + part.Major = int(maj) - part.Minor, err = strconv.Atoi(udevProps["MINOR"]) + min, err := requiredUDevPropUint(udevProps, "MINOR") if err != nil { - return fmt.Errorf("cannot parse device major number format: %v", err) + return emitUDevPropErr(err) } + part.Minor = int(min) // on MBR disks we may not have a partition label, so this may be // the empty string. Note that this value is encoded similarly to @@ -648,3 +857,36 @@ // d.partitions is empty or not return d.hasPartitions } + +func AllPhysicalDisks() ([]Disk, error) { + // get disks for every block device in /sys/block/ + blockDir := filepath.Join(dirs.SysfsDir, "block") + + files, err := ioutil.ReadDir(blockDir) + if err != nil { + return nil, err + } + + disks := make([]Disk, 0, len(files)) + + for _, f := range files { + if f.IsDir() { + // unexpected to have a directory here and not a symlink, but for + // now just silently ignore it + continue + } + + // get a disk by path with the name of the file and /block/ + fullpath := filepath.Join(blockDir, f.Name()) + + disk, err := DiskFromDevicePath(fullpath) + if err != nil { + if errors.As(err, &errNonPhysicalDisk{}) { + continue + } + return nil, err + } + disks = append(disks, disk) + } + return disks, nil +} diff -Nru snapd-2.53+21.10ubuntu1/osutil/disks/disks_linux_test.go snapd-2.54.2+21.10/osutil/disks/disks_linux_test.go --- snapd-2.53+21.10ubuntu1/osutil/disks/disks_linux_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/disks/disks_linux_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -28,6 +28,8 @@ . "gopkg.in/check.v1" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget/quantity" + "github.com/snapcore/snapd/kernel/fde" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/osutil/disks" "github.com/snapcore/snapd/testutil" @@ -39,12 +41,16 @@ // typical real-world values for tests diskUdevPropMap = map[string]string{ "ID_PART_ENTRY_DISK": "42:0", + "ID_PART_TABLE_UUID": "foobaruuid", + "ID_PART_TABLE_TYPE": "gpt", "DEVNAME": "/dev/vda", "DEVPATH": virtioDiskDevPath, + "DEVTYPE": "disk", } biosBootUdevPropMap = map[string]string{ "ID_PART_ENTRY_UUID": "bios-boot-partuuid", + "ID_PART_ENTRY_TYPE": "21686148-6449-6e6f-744e-656564454649", // the udev prop for bios-boot has no fs label, which is typical of the // real bios-boot partition on a amd64 pc gadget system, and so we should // safely just ignore and skip this partition in the fs label @@ -53,44 +59,64 @@ // we will however still have a partition label of "BIOS Boot" "ID_PART_ENTRY_NAME": "BIOS\\x20Boot", - "DEVNAME": "/dev/vda1", - "DEVPATH": "/devices/bios-boot-device", - "MAJOR": "42", - "MINOR": "1", + "DEVNAME": "/dev/vda1", + "DEVPATH": "/devices/bios-boot-device", + "MAJOR": "42", + "MINOR": "1", + "ID_PART_ENTRY_OFFSET": "2048", + "ID_PART_ENTRY_SIZE": "2048", + "ID_PART_ENTRY_NUMBER": "1", } // all the ubuntu- partitions have fs labels ubuntuSeedUdevPropMap = map[string]string{ - "ID_PART_ENTRY_UUID": "ubuntu-seed-partuuid", - "ID_FS_LABEL_ENC": "ubuntu-seed", - "ID_PART_ENTRY_NAME": "ubuntu-seed", - "DEVNAME": "/dev/vda2", - "DEVPATH": "/devices/ubuntu-seed-device", - "MAJOR": "42", - "MINOR": "2", + "ID_PART_ENTRY_UUID": "ubuntu-seed-partuuid", + "ID_FS_LABEL_ENC": "ubuntu-seed", + "ID_PART_ENTRY_NAME": "ubuntu-seed", + "ID_PART_ENTRY_TYPE": "c12a7328-f81f-11d2-ba4b-00a0c93ec93b", + "DEVNAME": "/dev/vda2", + "DEVPATH": "/devices/ubuntu-seed-device", + "MAJOR": "42", + "MINOR": "2", + "ID_PART_ENTRY_OFFSET": "4096", + "ID_PART_ENTRY_SIZE": "2457600", + "ID_PART_ENTRY_NUMBER": "2", } ubuntuBootUdevPropMap = map[string]string{ - "ID_PART_ENTRY_UUID": "ubuntu-boot-partuuid", - "ID_FS_LABEL_ENC": "ubuntu-boot", - "ID_PART_ENTRY_NAME": "ubuntu-boot", - "DEVNAME": "/dev/vda3", - "DEVPATH": "/devices/ubuntu-boot-device", - "MAJOR": "42", - "MINOR": "3", + "ID_PART_ENTRY_UUID": "ubuntu-boot-partuuid", + "ID_FS_LABEL_ENC": "ubuntu-boot", + "ID_PART_ENTRY_NAME": "ubuntu-boot", + "ID_PART_ENTRY_TYPE": "0fc63daf-8483-4772-8e79-3d69d8477de4", + "DEVNAME": "/dev/vda3", + "DEVPATH": "/devices/ubuntu-boot-device", + "MAJOR": "42", + "MINOR": "3", + "ID_PART_ENTRY_OFFSET": "2461696", + "ID_PART_ENTRY_SIZE": "1536000", + "ID_PART_ENTRY_NUMBER": "3", } ubuntuDataUdevPropMap = map[string]string{ "ID_PART_ENTRY_UUID": "ubuntu-data-partuuid", "ID_FS_LABEL_ENC": "ubuntu-data", "ID_PART_ENTRY_NAME": "ubuntu-data", + "ID_PART_ENTRY_TYPE": "0fc63daf-8483-4772-8e79-3d69d8477de4", "DEVNAME": "/dev/vda4", "DEVPATH": "/devices/ubuntu-data-device", "MAJOR": "42", "MINOR": "4", + // meh this doesn't line up because I used output from a real uc20 dev + // with ubuntu-save too, but none of the tests here assume ubuntu-save + "ID_PART_ENTRY_OFFSET": "3997696", + "ID_PART_ENTRY_SIZE": "8552415", + "ID_PART_ENTRY_NUMBER": "3", } ) -func createVirtioDevicesInSysfs(c *C, devsToPartition map[string]bool) { - diskDir := filepath.Join(dirs.SysfsDir, virtioDiskDevPath) +func createVirtioDevicesInSysfs(c *C, path string, devsToPartition map[string]bool) { + if path == "" { + path = virtioDiskDevPath + } + diskDir := filepath.Join(dirs.SysfsDir, path) for dev, isPartition := range devsToPartition { err := os.MkdirAll(filepath.Join(diskDir, dev), 0755) c.Assert(err, IsNil) @@ -111,17 +137,19 @@ dirs.SetRootDir(c.MkDir()) } -func (s *diskSuite) TestDiskFromNameHappy(c *C) { +func (s *diskSuite) TestDiskFromDeviceNameHappy(c *C) { const sdaSysfsPath = "/devices/pci0000:00/0000:00:01.1/0000:01:00.1/ata1/host0/target0:0:0/0:0:0:0/block/sda" restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { c.Assert(typeOpt, Equals, "--name") c.Assert(dev, Equals, "sda") return map[string]string{ - "MAJOR": "1", - "MINOR": "2", - "DEVTYPE": "disk", - "DEVNAME": "/dev/sda", - "DEVPATH": sdaSysfsPath, + "MAJOR": "1", + "MINOR": "2", + "DEVTYPE": "disk", + "DEVNAME": "/dev/sda", + "ID_PART_TABLE_UUID": "foo", + "ID_PART_TABLE_TYPE": "gpt", + "DEVPATH": sdaSysfsPath, }, nil }) defer restore() @@ -129,41 +157,207 @@ d, err := disks.DiskFromDeviceName("sda") c.Assert(err, IsNil) c.Assert(d.Dev(), Equals, "1:2") + c.Assert(d.DiskID(), Equals, "foo") + c.Assert(d.Schema(), Equals, "gpt") c.Assert(d.KernelDeviceNode(), Equals, "/dev/sda") c.Assert(d.KernelDevicePath(), Equals, filepath.Join(dirs.SysfsDir, sdaSysfsPath)) + // it doesn't have any partitions since we didn't mock any in sysfs + c.Assert(d.HasPartitions(), Equals, false) + + // if we mock some sysfs partitions then it has partitions when we it has + // some partitions on it it + createVirtioDevicesInSysfs(c, sdaSysfsPath, map[string]bool{ + "sda1": true, + "sda2": true, + }) + + d, err = disks.DiskFromDeviceName("sda") + c.Assert(err, IsNil) + c.Assert(d.Dev(), Equals, "1:2") + c.Assert(d.KernelDeviceNode(), Equals, "/dev/sda") + c.Assert(d.HasPartitions(), Equals, true) } -func (s *diskSuite) TestDiskFromPathHappy(c *C) { +func (s *diskSuite) TestDiskFromDevicePathHappy(c *C) { const vdaSysfsPath = "/devices/pci0000:00/0000:00:04.0/virtio2/block/vdb" + fullSysPath := filepath.Join("/sys", vdaSysfsPath) + n := 0 restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { - c.Assert(typeOpt, Equals, "--path") - c.Assert(dev, Equals, filepath.Join("/sys", vdaSysfsPath)) - return map[string]string{ - "MAJOR": "1", - "MINOR": "2", - "DEVTYPE": "disk", - "DEVNAME": "/dev/vdb", - "DEVPATH": vdaSysfsPath, - }, nil + n++ + switch n { + case 1: + // for getting the disk itself the first time + c.Assert(typeOpt, Equals, "--path") + c.Assert(dev, Equals, fullSysPath) + return map[string]string{ + "MAJOR": "1", + "MINOR": "2", + "DEVTYPE": "disk", + "DEVNAME": "/dev/vdb", + "ID_PART_TABLE_UUID": "bar", + "ID_PART_TABLE_TYPE": "dos", + "DEVPATH": vdaSysfsPath, + }, nil + case 2: + // getting the disk again when there are partitions defined + c.Assert(typeOpt, Equals, "--path") + c.Assert(dev, Equals, fullSysPath) + return map[string]string{ + "MAJOR": "1", + "MINOR": "2", + "DEVTYPE": "disk", + "DEVNAME": "/dev/vdb", + "ID_PART_TABLE_UUID": "bar", + "ID_PART_TABLE_TYPE": "dos", + "DEVPATH": vdaSysfsPath, + }, nil + case 3: + // getting the first partition + c.Assert(typeOpt, Equals, "--name") + c.Assert(dev, Equals, "vdb1") + return map[string]string{ + "DEVPATH": vdaSysfsPath + "1", + "DEVNAME": "/dev/vdb1", + // upper case 0X + "ID_PART_ENTRY_TYPE": "0Xc", + "ID_PART_ENTRY_SIZE": "524288", + "ID_PART_ENTRY_NUMBER": "1", + "ID_PART_ENTRY_OFFSET": "2048", + "ID_PART_ENTRY_UUID": "1212e868-01", + "MAJOR": "1", + "MINOR": "3", + }, nil + case 4: + // getting the second partition + c.Assert(typeOpt, Equals, "--name") + c.Assert(dev, Equals, "vdb2") + return map[string]string{ + "DEVPATH": vdaSysfsPath + "2", + "DEVNAME": "/dev/vdb2", + // lower case 0x + "ID_PART_ENTRY_TYPE": "0x83", + "ID_PART_ENTRY_SIZE": "124473665", + "ID_PART_ENTRY_NUMBER": "2", + "ID_PART_ENTRY_OFFSET": "526336", + "ID_PART_ENTRY_UUID": "1212e868-02", + "MAJOR": "1", + "MINOR": "4", + }, nil + default: + c.Errorf("test broken unexpected call to udevPropertiesForDevice for type %q on dev %q", typeOpt, dev) + return nil, fmt.Errorf("test broken, unexpected call for type %q on dev %q", typeOpt, dev) + } }) defer restore() - d, err := disks.DiskFromDevicePath(filepath.Join("/sys", vdaSysfsPath)) + d, err := disks.DiskFromDevicePath(fullSysPath) c.Assert(err, IsNil) c.Assert(d.Dev(), Equals, "1:2") + c.Assert(d.DiskID(), Equals, "bar") + c.Assert(d.Schema(), Equals, "dos") c.Assert(d.KernelDeviceNode(), Equals, "/dev/vdb") // note that we don't always prepend exactly /sys, we use dirs.SysfsDir c.Assert(d.KernelDevicePath(), Equals, filepath.Join(dirs.SysfsDir, vdaSysfsPath)) + + // it doesn't have any partitions since we didn't mock any in sysfs + c.Assert(d.HasPartitions(), Equals, false) + + // if we mock some sysfs partitions then it has partitions when we it has + // some partitions on it it + createVirtioDevicesInSysfs(c, vdaSysfsPath, map[string]bool{ + "vdb1": true, + "vdb2": true, + }) + + d, err = disks.DiskFromDevicePath(fullSysPath) + c.Assert(err, IsNil) + c.Assert(d.Dev(), Equals, "1:2") + c.Assert(d.KernelDeviceNode(), Equals, "/dev/vdb") + c.Assert(d.HasPartitions(), Equals, true) + + parts, err := d.Partitions() + c.Assert(err, IsNil) + c.Assert(parts, DeepEquals, []disks.Partition{ + { + Major: 1, + Minor: 4, + PartitionUUID: "1212e868-02", + PartitionType: "83", + KernelDevicePath: filepath.Join(dirs.SysfsDir, vdaSysfsPath) + "2", + KernelDeviceNode: "/dev/vdb2", + SizeInBytes: 124473665 * 512, + StartInBytes: uint64(257 * quantity.SizeMiB), + StructureIndex: 2, + }, + { + Major: 1, + Minor: 3, + PartitionUUID: "1212e868-01", + PartitionType: "0C", + KernelDevicePath: filepath.Join(dirs.SysfsDir, vdaSysfsPath) + "1", + KernelDeviceNode: "/dev/vdb1", + SizeInBytes: uint64(256 * quantity.SizeMiB), + StartInBytes: uint64(quantity.SizeMiB), + StructureIndex: 1, + }, + }) + + c.Assert(n, Equals, 4) } -func (s *diskSuite) TestDiskFromNameUnhappyPartition(c *C) { +func (s *diskSuite) TestDiskFromPartitionDeviceNodeHappy(c *C) { + restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + switch dev { + // first is for the partition itself, only relevant info is the + // ID_PART_ENTRY_DISK which is the parent disk + case "/dev/sda1": + return map[string]string{ + "ID_PART_ENTRY_DISK": "42:0", + }, nil + // next up is the disk itself identified by the major/minor + case "/dev/block/42:0": + return map[string]string{ + "DEVTYPE": "disk", + "DEVNAME": "/dev/sda", + "DEVPATH": "/devices/foo/sda", + "ID_PART_TABLE_UUID": "foo-id", + "ID_PART_TABLE_TYPE": "gpt", + }, nil + default: + c.Errorf("unexpected udev device properties requested: %s", dev) + return nil, fmt.Errorf("unexpected udev device: %s", dev) + } + }) + defer restore() + + // create the partition device node too + createVirtioDevicesInSysfs(c, "/devices/foo/sda", map[string]bool{ + "sda1": true, + }) + + d, err := disks.DiskFromPartitionDeviceNode("/dev/sda1") + c.Assert(err, IsNil) + c.Assert(d.Dev(), Equals, "42:0") + c.Assert(d.DiskID(), Equals, "foo-id") + c.Assert(d.Schema(), Equals, "gpt") + c.Assert(d.KernelDeviceNode(), Equals, "/dev/sda") + // note that we don't always prepend exactly /sys, we use dirs.SysfsDir + c.Assert(d.KernelDevicePath(), Equals, filepath.Join(dirs.SysfsDir, "/devices/foo/sda")) + + // it doesn't have any partitions since we didn't mock any in sysfs + c.Assert(d.HasPartitions(), Equals, true) +} + +func (s *diskSuite) TestDiskFromDeviceNameUnhappyPartition(c *C) { restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { c.Assert(typeOpt, Equals, "--name") c.Assert(dev, Equals, "sda1") return map[string]string{ - "MAJOR": "1", - "MINOR": "3", - "DEVTYPE": "partition", + "ID_PART_TABLE_TYPE": "dos", + "MAJOR": "1", + "MINOR": "3", + "DEVTYPE": "partition", }, nil }) defer restore() @@ -172,14 +366,52 @@ c.Assert(err, ErrorMatches, "device \"sda1\" is not a disk, it has DEVTYPE of \"partition\"") } -func (s *diskSuite) TestDiskFromNameUnhappyBadUdevOutput(c *C) { +func (s *diskSuite) TestDiskFromDeviceNameUnhappyNonPhysicalDisk(c *C) { + restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + c.Assert(dev, Equals, "loop1") + return map[string]string{ + // missing ID_PART_TABLE_TYPE and thus not a physical disk with a + // partition table + "MAJOR": "1", + "MINOR": "3", + // even though DEVTYPE is disk, it is not a physical disk + "DEVTYPE": "disk", + }, nil + }) + defer restore() + + _, err := disks.DiskFromDeviceName("loop1") + c.Assert(err, ErrorMatches, "device with name \"loop1\" is not a physical disk") +} + +func (s *diskSuite) TestDiskFromDeviceNameUnhappyUnknownDiskSchema(c *C) { + restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + c.Assert(dev, Equals, "loop1") + return map[string]string{ + // unsupported disk schema + "ID_PART_TABLE_TYPE": "foobar", + "MAJOR": "1", + "MINOR": "3", + "DEVTYPE": "disk", + }, nil + }) + defer restore() + + _, err := disks.DiskFromDeviceName("loop1") + c.Assert(err, ErrorMatches, "unsupported disk schema \"foobar\"") +} + +func (s *diskSuite) TestDiskFromDeviceNameUnhappyBadUdevOutput(c *C) { restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { c.Assert(typeOpt, Equals, "--name") c.Assert(dev, Equals, "sda") // udev should always return the major/minor but if it doesn't we should // fail return map[string]string{ - "MAJOR": "blah blah blah", + "ID_PART_TABLE_TYPE": "gpt", + "MAJOR": "blah blah blah", }, nil }) defer restore() @@ -212,7 +444,7 @@ defer restore() _, err := disks.DiskFromMountPoint("/run/mnt/point", nil) - c.Assert(err, ErrorMatches, "cannot find disk for partition /dev/vda4, incomplete udev output") + c.Assert(err, ErrorMatches, "cannot find disk from mountpoint source /dev/vda4 of /run/mnt/point: incomplete udev output missing required property \"ID_PART_ENTRY_DISK\"") } func (s *diskSuite) TestDiskFromMountPointUnhappyBadUdevPropsMountpointPartition(c *C) { @@ -224,13 +456,14 @@ c.Assert(typeOpt, Equals, "--name") c.Assert(dev, Equals, "/dev/vda4") return map[string]string{ + "DEVTYPE": "disk", "ID_PART_ENTRY_DISK": "not-a-number", }, nil }) defer restore() _, err := disks.DiskFromMountPoint("/run/mnt/point", nil) - c.Assert(err, ErrorMatches, `cannot find disk for partition /dev/vda4, bad udev output: invalid device number format: \(expected :\)`) + c.Assert(err, ErrorMatches, `cannot find disk from mountpoint source /dev/vda4 of /run/mnt/point: bad udev output: invalid device number format: \(expected :\)`) } func (s *diskSuite) TestDiskFromMountPointUnhappyIsDecryptedDeviceNotDiskDevice(c *C) { @@ -258,7 +491,7 @@ opts := &disks.Options{IsDecryptedDevice: true} _, err := disks.DiskFromMountPoint("/run/mnt/point", opts) - c.Assert(err, ErrorMatches, `mountpoint source /dev/vda4 is not a decrypted device: devtype is not disk \(is partition\)`) + c.Assert(err, ErrorMatches, `cannot find disk from mountpoint source /dev/vda4 of /run/mnt/point: not a decrypted device: devtype is not disk \(is partition\)`) } func (s *diskSuite) TestDiskFromMountPointUnhappyIsDecryptedDeviceNoSysfs(c *C) { @@ -272,6 +505,8 @@ case "/dev/mapper/something": return map[string]string{ "DEVTYPE": "disk", + "MAJOR": "252", + "MINOR": "0", }, nil default: c.Errorf("unexpected udev device properties requested: %s", dev) @@ -284,7 +519,7 @@ opts := &disks.Options{IsDecryptedDevice: true} _, err := disks.DiskFromMountPoint("/run/mnt/point", opts) - c.Assert(err, ErrorMatches, fmt.Sprintf(`mountpoint source /dev/mapper/something is not a decrypted device: could not read device mapper metadata: open %s/dev/block/252:0/dm/uuid: no such file or directory`, dirs.SysfsDir)) + c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot find disk from mountpoint source /dev/mapper/something of /run/mnt/point: not a decrypted device: could not read device mapper metadata: open %s/dev/block/252:0/dm/uuid: no such file or directory`, dirs.SysfsDir)) } func (s *diskSuite) TestDiskFromMountPointHappySinglePartitionIgnoresNonPartitionsInSysfs(c *C) { @@ -306,14 +541,20 @@ // verify that the /run/mnt/point is from the same disk return map[string]string{ "ID_PART_ENTRY_DISK": "42:0", + "DEVTYPE": "disk", + "ID_PART_TABLE_UUID": "foobar", + "ID_PART_TABLE_TYPE": "gpt", }, nil - case 2: + case 2, 5: // after we get the disk itself, we query it to get the // DEVPATH and DEVNAME specifically for the disk c.Assert(dev, Equals, "/dev/block/42:0") return map[string]string{ - "DEVNAME": "/dev/vda", - "DEVPATH": virtioDiskDevPath, + "DEVNAME": "/dev/vda", + "DEVPATH": virtioDiskDevPath, + "DEVTYPE": "disk", + "ID_PART_TABLE_UUID": "some-gpt-uuid", + "ID_PART_TABLE_TYPE": "gpt", }, nil case 3: c.Assert(dev, Equals, "vda4") @@ -322,20 +563,16 @@ // this is essentially the same as /dev/block/42:1 in actuality, but // we search for it differently return map[string]string{ - "ID_FS_LABEL_ENC": "some-label", - "ID_PART_ENTRY_UUID": "some-uuid", - "DEVPATH": "/devices/some-device", - "DEVNAME": "/dev/vda4", - "MAJOR": "42", - "MINOR": "4", - }, nil - case 5: - // after we get the disk itself, we query it to get the - // DEVNAME specifically for the disk - c.Assert(dev, Equals, "/dev/block/42:0") - return map[string]string{ - "DEVNAME": "/dev/vda", - "DEVPATH": virtioDiskDevPath, + "ID_FS_LABEL_ENC": "some-label", + "ID_PART_ENTRY_UUID": "some-uuid", + "ID_PART_ENTRY_TYPE": "some-gpt-uuid-type", + "ID_PART_ENTRY_SIZE": "3000", + "ID_PART_ENTRY_OFFSET": "2500", + "ID_PART_ENTRY_NUMBER": "4", + "DEVPATH": "/devices/some-device", + "DEVNAME": "/dev/vda4", + "MAJOR": "42", + "MINOR": "4", }, nil default: c.Errorf("unexpected udev device properties requested: %s", dev) @@ -346,7 +583,7 @@ // create just the single valid partition in sysfs, and an invalid // non-partition device that we should ignore - createVirtioDevicesInSysfs(c, map[string]bool{ + createVirtioDevicesInSysfs(c, "", map[string]bool{ "vda4": true, "vda5": false, }) @@ -370,6 +607,10 @@ KernelDeviceNode: "/dev/vda4", Major: 42, Minor: 4, + PartitionType: "SOME-GPT-UUID-TYPE", + SizeInBytes: 3000 * 512, + StructureIndex: 4, + StartInBytes: 2500 * 512, }, }) @@ -397,11 +638,16 @@ elif [ "$*" = "info --query property --name /dev/block/42:0" ]; then echo "DEVNAME=/dev/vda" echo "DEVPATH=%s" + echo "DEVTYPE=disk" + echo "ID_PART_TABLE_UUID=some-gpt-uuid" + # GPT is upper case, it gets turned into lower case + echo "ID_PART_TABLE_TYPE=GPT" else echo "unexpected arguments $*" exit 1 fi `, virtioDiskDevPath)) + defer udevadmCmd.Restore() d, err := disks.DiskFromMountPoint("/run/mnt/point", nil) c.Assert(err, IsNil) @@ -410,6 +656,7 @@ c.Assert(d.HasPartitions(), Equals, true) c.Assert(d.KernelDeviceNode(), Equals, "/dev/vda") c.Assert(d.KernelDevicePath(), Equals, filepath.Join(dirs.SysfsDir, virtioDiskDevPath)) + c.Assert(d.Schema(), Equals, "gpt") c.Assert(udevadmCmd.Calls(), DeepEquals, [][]string{ {"udevadm", "info", "--query", "property", "--name", "/dev/vda1"}, @@ -417,32 +664,27 @@ }) } -func (s *diskSuite) TestDiskFromMountPointVolumeHappy(c *C) { +func (s *diskSuite) TestDiskFromMountPointVolumeUnhappyWithoutPartEntryDisk(c *C) { restore := osutil.MockMountInfo(`130 30 42:1 / /run/mnt/point rw,relatime shared:54 - ext4 /dev/mapper/something rw `) defer restore() udevadmCmd := testutil.MockCommand(c, "udevadm", ` if [ "$*" = "info --query property --name /dev/mapper/something" ]; then - # not a partition, so no ID_PART_ENTRY_DISK, but we will have DEVTYPE=disk + # no ID_PART_ENTRY_DISK, so not a disk from this mount point echo "DEVTYPE=disk" else echo "unexpected arguments $*" exit 1 fi `) + defer udevadmCmd.Restore() - d, err := disks.DiskFromMountPoint("/run/mnt/point", nil) - c.Assert(err, IsNil) - c.Assert(d.Dev(), Equals, "42:1") - c.Assert(d.HasPartitions(), Equals, false) - - c.Assert(udevadmCmd.Calls(), DeepEquals, [][]string{ - {"udevadm", "info", "--query", "property", "--name", "/dev/mapper/something"}, - }) + _, err := disks.DiskFromMountPoint("/run/mnt/point", nil) + c.Assert(err, ErrorMatches, "cannot find disk from mountpoint source /dev/mapper/something of /run/mnt/point: incomplete udev output missing required property \"ID_PART_ENTRY_DISK\"") } -func (s *diskSuite) TestDiskFromMountPointIsDecryptedDeviceVolumeHappy(c *C) { +func (s *diskSuite) TestDiskFromMountPointIsDecryptedLUKSDeviceVolumeHappy(c *C) { restore := osutil.MockMountInfo(`130 30 242:1 / /run/mnt/point rw,relatime shared:54 - ext4 /dev/mapper/something rw `) defer restore() @@ -453,10 +695,20 @@ case "/dev/mapper/something": return map[string]string{ "DEVTYPE": "disk", + "MAJOR": "242", + "MINOR": "1", }, nil case "/dev/disk/by-uuid/5a522809-c87e-4dfa-81a8-8dc5667d1304": return map[string]string{ - "DEVTYPE": "disk", + "ID_PART_ENTRY_DISK": "42:0", + }, nil + case "/dev/block/42:0": + return map[string]string{ + "DEVTYPE": "disk", + "DEVNAME": "foo", + "DEVPATH": "/devices/foo", + "ID_PART_TABLE_UUID": "foo-uuid", + "ID_PART_TABLE_TYPE": "DOS", }, nil default: c.Errorf("unexpected udev device properties requested: %s", dev) @@ -479,13 +731,133 @@ c.Assert(err, IsNil) opts := &disks.Options{IsDecryptedDevice: true} + + // when the handler is not available, we can't handle the mapper + disks.UnregisterDeviceMapperBackResolver("crypt-luks2") + defer func() { + // re-register it at the end, since it's registered by default + disks.RegisterDeviceMapperBackResolver("crypt-luks2", disks.CryptLuks2DeviceMapperBackResolver) + }() + + _, err = disks.DiskFromMountPoint("/run/mnt/point", opts) + c.Assert(err, ErrorMatches, `cannot find disk from mountpoint source /dev/mapper/something of /run/mnt/point: internal error: no back resolver supports decrypted device mapper with UUID "CRYPT-LUKS2-5a522809c87e4dfa81a88dc5667d1304-something" and name "something"`) + + // but when it is available it works + disks.RegisterDeviceMapperBackResolver("crypt-luks2", disks.CryptLuks2DeviceMapperBackResolver) + d, err := disks.DiskFromMountPoint("/run/mnt/point", opts) c.Assert(err, IsNil) - c.Assert(d.Dev(), Equals, "242:1") - c.Assert(d.HasPartitions(), Equals, false) + c.Assert(d.Dev(), Equals, "42:0") + c.Assert(d.HasPartitions(), Equals, true) + c.Assert(d.Schema(), Equals, "dos") +} + +func (s *diskSuite) TestDiskFromMountPointIsDecryptedUnlockedDeviceVolumeHappy(c *C) { + restore := osutil.MockMountInfo(`130 30 242:1 / /run/mnt/point rw,relatime shared:54 - ext4 /dev/mapper/something-device-locked rw +`) + defer restore() + + // un-register the fde handler so we can make sure that the default + // handler(s) don't match this device mapper - this name needs to be kept in + // sync with what's in kernel/fde/fde.go + disks.UnregisterDeviceMapperBackResolver("device-unlock-kernel-fde") + defer func() { + // this is registered by default in this file since we import the fde + // package so make sure it is re-registered at the end + disks.RegisterDeviceMapperBackResolver("device-unlock-kernel-fde", fde.DeviceUnlockKernelHookDeviceMapperBackResolver) + }() + + restore = disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + switch dev { + case "/dev/mapper/something-device-locked": + return map[string]string{ + "DEVTYPE": "disk", + "MAJOR": "242", + "MINOR": "1", + }, nil + case "/dev/disk/by-partuuid/5a522809-c87e-4dfa-81a8-8dc5667d1304": + return map[string]string{ + "DEVTYPE": "disk", + "ID_PART_ENTRY_DISK": "41:3", + }, nil + case "/dev/block/41:3": + return map[string]string{ + "DEVTYPE": "disk", + "DEVNAME": "/dev/sda", + "DEVPATH": "/block/foo", + "ID_PART_TABLE_UUID": "foo-foo-foo-foo", + "ID_PART_TABLE_TYPE": "gpt", + }, nil + case "sda1": + return map[string]string{ + "DEVPATH": "/block/foo/sda1", + "DEVNAME": "/dev/sda1", + "ID_PART_ENTRY_UUID": "foo-foo-foo-foo-1", + "ID_PART_ENTRY_TYPE": "gooooooo", + "ID_PART_ENTRY_SIZE": "500000", + "ID_PART_ENTRY_NUMBER": "1", + "ID_PART_ENTRY_OFFSET": "2048", + "MAJOR": "41", + "MINOR": "4", + }, nil + default: + c.Errorf("unexpected udev device properties requested: %s", dev) + return nil, fmt.Errorf("unexpected udev device: %s", dev) + } + }) + defer restore() + + // mock the sysfs dm uuid and name files + dmDir := filepath.Join(filepath.Join(dirs.SysfsDir, "dev", "block"), "242:1", "dm") + err := os.MkdirAll(dmDir, 0755) + c.Assert(err, IsNil) + + // name expected by fde + b := []byte("something-device-locked") + err = ioutil.WriteFile(filepath.Join(dmDir, "name"), b, 0644) + c.Assert(err, IsNil) + + b = []byte("5a522809-c87e-4dfa-81a8-8dc5667d1304") + err = ioutil.WriteFile(filepath.Join(dmDir, "uuid"), b, 0644) + c.Assert(err, IsNil) + + opts := &disks.Options{IsDecryptedDevice: true} + + // first try without the handler fails + _, err = disks.DiskFromMountPoint("/run/mnt/point", opts) + c.Assert(err, ErrorMatches, `cannot find disk from mountpoint source /dev/mapper/something-device-locked of /run/mnt/point: internal error: no back resolver supports decrypted device mapper with UUID "5a522809-c87e-4dfa-81a8-8dc5667d1304" and name "something-device-locked"`) + + // next try with the FDE package handler works + disks.RegisterDeviceMapperBackResolver("device-unlock-kernel-fde", fde.DeviceUnlockKernelHookDeviceMapperBackResolver) + + d, err := disks.DiskFromMountPoint("/run/mnt/point", opts) + c.Assert(err, IsNil) + c.Assert(d.Dev(), Equals, "41:3") + c.Assert(d.HasPartitions(), Equals, true) + + // mock a partition + partFile := filepath.Join(dirs.SysfsDir, "/block/foo/sda1/partition") + err = os.MkdirAll(filepath.Dir(partFile), 0755) + c.Assert(err, IsNil) + err = ioutil.WriteFile(partFile, nil, 0644) + c.Assert(err, IsNil) + parts, err := d.Partitions() c.Assert(err, IsNil) - c.Assert(parts, HasLen, 0) + c.Assert(parts, DeepEquals, []disks.Partition{ + { + PartitionType: "GOOOOOOO", + StructureIndex: 1, + StartInBytes: 512 * 2048, + SizeInBytes: 500000 * 512, + PartitionUUID: "foo-foo-foo-foo-1", + Major: 41, + Minor: 4, + KernelDevicePath: filepath.Join(dirs.SysfsDir, "/block/foo/sda1"), + KernelDeviceNode: "/dev/sda1", + }, + }) } func (s *diskSuite) TestDiskFromMountPointNotDiskUnsupported(c *C) { @@ -495,18 +867,54 @@ udevadmCmd := testutil.MockCommand(c, "udevadm", ` if [ "$*" = "info --query property --name /dev/not-a-disk" ]; then + echo "ID_PART_ENTRY_DISK=43:0" +elif [ "$*" = "info --query property --name /dev/block/43:0" ]; then echo "DEVTYPE=not-a-disk" else echo "unexpected arguments $*" exit 1 fi `) + defer udevadmCmd.Restore() _, err := disks.DiskFromMountPoint("/run/mnt/point", nil) - c.Assert(err, ErrorMatches, "unsupported DEVTYPE \"not-a-disk\" for mount point source /dev/not-a-disk") + c.Assert(err, ErrorMatches, "cannot find disk from mountpoint source /dev/not-a-disk of /run/mnt/point: unsupported DEVTYPE \"not-a-disk\"") c.Assert(udevadmCmd.Calls(), DeepEquals, [][]string{ {"udevadm", "info", "--query", "property", "--name", "/dev/not-a-disk"}, + {"udevadm", "info", "--query", "property", "--name", "/dev/block/43:0"}, + }) +} + +func (s *diskSuite) TestDiskFromMountPointUnsupportedSchema(c *C) { + restore := osutil.MockMountInfo(`130 30 42:1 / /run/mnt/point rw,relatime shared:54 - ext4 /dev/not-a-supported-schema-disk rw +`) + defer restore() + + udevadmCmd := testutil.MockCommand(c, "udevadm", ` +if [ "$*" = "info --query property --name /dev/not-a-supported-schema-disk" ]; then + echo "DEVTYPE=disk" + echo "ID_PART_ENTRY_DISK=42:0" +elif [ "$*" = "info --query property --name /dev/block/42:0" ]; then + echo "DEVTYPE=disk" + echo "DEVNAME=/dev/foo" + echo "DEVPATH=/block/32" + echo "ID_PART_TABLE_UUID=something" + echo "ID_PART_ENTRY_DISK=42:0" + echo "ID_PART_TABLE_TYPE=foo" +else + echo "unexpected arguments $*" + exit 1 +fi +`) + defer udevadmCmd.Restore() + + _, err := disks.DiskFromMountPoint("/run/mnt/point", nil) + c.Assert(err, ErrorMatches, "cannot find disk from mountpoint source /dev/not-a-supported-schema-disk of /run/mnt/point: unsupported disk schema \"foo\"") + + c.Assert(udevadmCmd.Calls(), DeepEquals, [][]string{ + {"udevadm", "info", "--query", "property", "--name", "/dev/not-a-supported-schema-disk"}, + {"udevadm", "info", "--query", "property", "--name", "/dev/block/42:0"}, }) } @@ -601,7 +1009,7 @@ defer restore() // create all 4 partitions as device nodes in sysfs - createVirtioDevicesInSysfs(c, map[string]bool{ + createVirtioDevicesInSysfs(c, "", map[string]bool{ "vda1": true, "vda2": true, "vda3": true, @@ -685,6 +1093,10 @@ `) defer restore() + // the order is reversed so that Find... functions working on the list of + // partitions can easily implement the same logic that udev uses when + // choosing which partition to use as /dev/disk/by-label when there exist + // multiple disks with that label, which is "last seen" partsOnDisk := map[string]disks.Partition{ "ubuntu-data-enc": { FilesystemLabel: "ubuntu-data-enc", @@ -693,6 +1105,10 @@ Minor: 4, KernelDevicePath: fmt.Sprintf("%s/devices/ubuntu-data-enc-device", dirs.SysfsDir), KernelDeviceNode: "/dev/vda4", + PartitionType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + SizeInBytes: 8552415 * 512, + StructureIndex: 4, + StartInBytes: 3997696 * 512, }, "ubuntu-boot": { FilesystemLabel: "ubuntu-boot", @@ -702,6 +1118,10 @@ Minor: 3, KernelDevicePath: fmt.Sprintf("%s/devices/ubuntu-boot-device", dirs.SysfsDir), KernelDeviceNode: "/dev/vda3", + PartitionType: "0FC63DAF-8483-4772-8E79-3D69D8477DE4", + SizeInBytes: 1536000 * 512, + StructureIndex: 3, + StartInBytes: 2461696 * 512, }, "ubuntu-seed": { FilesystemLabel: "ubuntu-seed", @@ -711,6 +1131,10 @@ Minor: 2, KernelDevicePath: fmt.Sprintf("%s/devices/ubuntu-seed-device", dirs.SysfsDir), KernelDeviceNode: "/dev/vda2", + PartitionType: "C12A7328-F81F-11D2-BA4B-00A0C93EC93B", + SizeInBytes: 2457600 * 512, + StructureIndex: 2, + StartInBytes: 4096 * 512, }, "bios-boot": { PartitionLabel: "BIOS\\x20Boot", @@ -719,16 +1143,24 @@ Minor: 1, KernelDevicePath: fmt.Sprintf("%s/devices/bios-boot-device", dirs.SysfsDir), KernelDeviceNode: "/dev/vda1", + PartitionType: "21686148-6449-6E6F-744E-656564454649", + SizeInBytes: 2048 * 512, + StructureIndex: 1, + StartInBytes: 2048 * 512, }, } ubuntuDataEncUdevPropMap := map[string]string{ - "ID_FS_LABEL_ENC": "ubuntu-data-enc", - "ID_PART_ENTRY_UUID": "ubuntu-data-enc-partuuid", - "DEVPATH": "/devices/ubuntu-data-enc-device", - "DEVNAME": "/dev/vda4", - "MAJOR": "42", - "MINOR": "4", + "ID_FS_LABEL_ENC": "ubuntu-data-enc", + "ID_PART_ENTRY_UUID": "ubuntu-data-enc-partuuid", + "DEVPATH": "/devices/ubuntu-data-enc-device", + "ID_PART_ENTRY_TYPE": "0fc63daf-8483-4772-8e79-3d69d8477de4", + "DEVNAME": "/dev/vda4", + "MAJOR": "42", + "MINOR": "4", + "ID_PART_ENTRY_OFFSET": "3997696", + "ID_PART_ENTRY_SIZE": "8552415", + "ID_PART_ENTRY_NUMBER": "4", } n := 0 @@ -742,6 +1174,8 @@ // the mapper device is a disk/volume return map[string]string{ "DEVTYPE": "disk", + "MAJOR": "252", + "MINOR": "0", }, nil case 2: // next we find the physical disk by the dm uuid @@ -803,6 +1237,8 @@ // the mapper device is a disk/volume return map[string]string{ "DEVTYPE": "disk", + "MAJOR": "252", + "MINOR": "0", }, nil case 17: // then we find the physical disk by the dm uuid @@ -833,7 +1269,7 @@ c.Assert(err, IsNil) // mock the dev nodes in sysfs for the partitions - createVirtioDevicesInSysfs(c, map[string]bool{ + createVirtioDevicesInSysfs(c, "", map[string]bool{ "vda1": true, "vda2": true, "vda3": true, @@ -1004,3 +1440,441 @@ restore() } } + +func (s *diskSuite) TestDiskSizeRelatedMethodsGPT(c *C) { + restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + c.Assert(dev, Equals, "sda") + return map[string]string{ + "MAJOR": "1", + "MINOR": "2", + "DEVTYPE": "disk", + "DEVNAME": "/dev/sda", + "ID_PART_TABLE_UUID": "foo", + "ID_PART_TABLE_TYPE": "gpt", + "DEVPATH": "/devices/foo/sda", + }, nil + }) + defer restore() + + sfdiskCmd := testutil.MockCommand(c, "sfdisk", ` +echo '{ + "partitiontable": { + "unit": "sectors", + "lastlba": 42 + } +}' +`) + defer sfdiskCmd.Restore() + + d, err := disks.DiskFromDeviceName("sda") + c.Assert(err, IsNil) + c.Assert(d.Schema(), Equals, "gpt") + c.Assert(d.KernelDeviceNode(), Equals, "/dev/sda") + + endSectors, err := d.UsableSectorsEnd() + c.Assert(err, IsNil) + c.Assert(endSectors, Equals, uint64(43)) + c.Assert(sfdiskCmd.Calls(), DeepEquals, [][]string{ + {"sfdisk", "--json", "/dev/sda"}, + }) + sfdiskCmd.ForgetCalls() + + blockDevCmd := testutil.MockCommand(c, "blockdev", ` +if [ "$1" = "--getsz" ]; then + echo 10000 +elif [ "$1" = "--getss" ]; then + echo 512 +else + echo "fail, test broken" + exit 1 +fi +`) + defer blockDevCmd.Restore() + + sz, err := d.SizeInBytes() + c.Assert(err, IsNil) + c.Assert(sz, Equals, uint64(10000*512)) + c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ + {"blockdev", "--getsz", "/dev/sda"}, + }) + + blockDevCmd.ForgetCalls() + + sectorSz, err := d.SectorSize() + c.Assert(err, IsNil) + c.Assert(sectorSz, Equals, uint64(512)) + c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ + {"blockdev", "--getss", "/dev/sda"}, + }) + + // we didn't use sfdisk again at all + c.Assert(sfdiskCmd.Calls(), HasLen, 0) +} + +func (s *diskSuite) TestDiskUsableSectorsEndGPTUnexpectedSfdiskUnit(c *C) { + restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + c.Assert(dev, Equals, "sda") + return map[string]string{ + "MAJOR": "1", + "MINOR": "2", + "DEVTYPE": "disk", + "DEVNAME": "/dev/sda", + "ID_PART_TABLE_UUID": "foo", + "ID_PART_TABLE_TYPE": "gpt", + "DEVPATH": "/devices/foo/sda", + }, nil + }) + defer restore() + + cmd := testutil.MockCommand(c, "sfdisk", ` +echo '{ + "partitiontable": { + "unit": "not-sectors", + "lastlba": 42 + } +}' +`) + defer cmd.Restore() + + d, err := disks.DiskFromDeviceName("sda") + c.Assert(err, IsNil) + c.Assert(d.Schema(), Equals, "gpt") + c.Assert(d.KernelDeviceNode(), Equals, "/dev/sda") + + _, err = d.UsableSectorsEnd() + c.Assert(err, ErrorMatches, "cannot get size in sectors, sfdisk reported unknown unit not-sectors") + + c.Assert(cmd.Calls(), DeepEquals, [][]string{ + {"sfdisk", "--json", "/dev/sda"}, + }) +} + +func (s *diskSuite) TestDiskSizeRelatedMethodsGPTSectorSize4K(c *C) { + restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + c.Assert(dev, Equals, "sda") + return map[string]string{ + "MAJOR": "1", + "MINOR": "2", + "DEVTYPE": "disk", + "DEVNAME": "/dev/sda", + "ID_PART_TABLE_UUID": "foo", + "ID_PART_TABLE_TYPE": "gpt", + "DEVPATH": "/devices/foo/sda", + }, nil + }) + defer restore() + + sfdiskCmd := testutil.MockCommand(c, "sfdisk", ` +echo '{ + "partitiontable": { + "unit": "sectors", + "lastlba": 42 + } +}' +`) + defer sfdiskCmd.Restore() + + d, err := disks.DiskFromDeviceName("sda") + c.Assert(err, IsNil) + c.Assert(d.Schema(), Equals, "gpt") + c.Assert(d.KernelDeviceNode(), Equals, "/dev/sda") + + endSectors, err := d.UsableSectorsEnd() + c.Assert(err, IsNil) + c.Assert(endSectors, Equals, uint64(43)) + c.Assert(sfdiskCmd.Calls(), DeepEquals, [][]string{ + {"sfdisk", "--json", "/dev/sda"}, + }) + + sfdiskCmd.ForgetCalls() + + blockDevCmd := testutil.MockCommand(c, "blockdev", ` +if [ "$1" = "--getsz" ]; then + echo 10000 +elif [ "$1" = "--getss" ]; then + echo 4096 +else + echo "fail, test broken" + exit 1 +fi +`) + defer blockDevCmd.Restore() + + sz, err := d.SizeInBytes() + c.Assert(err, IsNil) + // the size is still the result of --getsz * 512, we don't use the native + // sector size at all here since blockdev doesn't use the native sector size + // at all + c.Assert(sz, Equals, uint64(10000*512)) + c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ + {"blockdev", "--getsz", "/dev/sda"}, + }) + + blockDevCmd.ForgetCalls() + + sectorSz, err := d.SectorSize() + c.Assert(err, IsNil) + c.Assert(sectorSz, Equals, uint64(4096)) + c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ + {"blockdev", "--getss", "/dev/sda"}, + }) + + // we didn't use sfdisk again at all after UsableSectorsEnd + c.Assert(sfdiskCmd.Calls(), HasLen, 0) +} + +func (s *diskSuite) TestDiskSizeRelatedMethodsGPTNon512MultipleSectorSizeError(c *C) { + restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + c.Assert(dev, Equals, "sda") + return map[string]string{ + "MAJOR": "1", + "MINOR": "2", + "DEVTYPE": "disk", + "DEVNAME": "/dev/sda", + "ID_PART_TABLE_UUID": "foo", + "ID_PART_TABLE_TYPE": "gpt", + "DEVPATH": "/devices/foo/sda", + }, nil + }) + defer restore() + + sfdiskCmd := testutil.MockCommand(c, "sfdisk", ` +echo '{ + "partitiontable": { + "unit": "sectors", + "lastlba": 42 + } +}' +`) + defer sfdiskCmd.Restore() + + d, err := disks.DiskFromDeviceName("sda") + c.Assert(err, IsNil) + c.Assert(d.Schema(), Equals, "gpt") + c.Assert(d.KernelDeviceNode(), Equals, "/dev/sda") + + // the end of sectors end does not query the size of the sectors + endSectors, err := d.UsableSectorsEnd() + c.Assert(err, IsNil) + c.Assert(endSectors, Equals, uint64(43)) + + sfdiskCmd.ForgetCalls() + + blockDevCmd := testutil.MockCommand(c, "blockdev", ` +echo 513 +`) + defer blockDevCmd.Restore() + + // but getting the sector size itself fails + _, err = d.SectorSize() + c.Assert(err, ErrorMatches, `sector size \(513\) is not a multiple of 512`) + + c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ + {"blockdev", "--getss", "/dev/sda"}, + }) + + c.Assert(sfdiskCmd.Calls(), HasLen, 0) +} + +func (s *diskSuite) TestDiskSizeRelatedMethodsDOS(c *C) { + restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + c.Assert(dev, Equals, "sda") + return map[string]string{ + "MAJOR": "1", + "MINOR": "2", + "DEVTYPE": "disk", + "DEVNAME": "/dev/sda", + "ID_PART_TABLE_UUID": "foo", + "ID_PART_TABLE_TYPE": "dos", + "DEVPATH": "/devices/foo/sda", + }, nil + }) + defer restore() + + sfdiskCmd := testutil.MockCommand(c, "sfdisk", "echo broken test; exit 1") + defer sfdiskCmd.Restore() + + blockDevCmd := testutil.MockCommand(c, "blockdev", ` +if [ "$1" = "--getsz" ]; then + echo 10000 +elif [ "$1" = "--getss" ]; then + echo 512 +else + echo "fail, test broken" + exit 1 +fi +`) + defer blockDevCmd.Restore() + + d, err := disks.DiskFromDeviceName("sda") + c.Assert(err, IsNil) + c.Assert(d.Schema(), Equals, "dos") + c.Assert(d.KernelDeviceNode(), Equals, "/dev/sda") + + // the usable sectors ends up being exactly what blockdev gave us, but only + // because the sector size is exactly what blockdev naturally assumes + endSectors, err := d.UsableSectorsEnd() + c.Assert(err, IsNil) + c.Assert(endSectors, Equals, uint64(10000)) + + c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ + {"blockdev", "--getsz", "/dev/sda"}, + {"blockdev", "--getss", "/dev/sda"}, + }) + + blockDevCmd.ForgetCalls() + + // the size of the disk does not depend on querying the sector size + sz, err := d.SizeInBytes() + c.Assert(err, IsNil) + c.Assert(sz, Equals, uint64(10000*512)) + + c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ + {"blockdev", "--getsz", "/dev/sda"}, + }) + + // we never used sfdisk + c.Assert(sfdiskCmd.Calls(), HasLen, 0) +} + +func (s *diskSuite) TestDiskSizeRelatedMethodsDOS4096SectorSize(c *C) { + restore := disks.MockUdevPropertiesForDevice(func(typeOpt, dev string) (map[string]string, error) { + c.Assert(typeOpt, Equals, "--name") + c.Assert(dev, Equals, "sda") + return map[string]string{ + "MAJOR": "1", + "MINOR": "2", + "DEVTYPE": "disk", + "DEVNAME": "/dev/sda", + "ID_PART_TABLE_UUID": "foo", + "ID_PART_TABLE_TYPE": "dos", + "DEVPATH": "/devices/foo/sda", + }, nil + }) + defer restore() + + sfdiskCmd := testutil.MockCommand(c, "sfdisk", "echo broken test; exit 1") + defer sfdiskCmd.Restore() + + blockDevCmd := testutil.MockCommand(c, "blockdev", ` +if [ "$1" = "--getsz" ]; then + echo 10000 + elif [ "$1" = "--getss" ]; then + echo 4096 +else + echo "fail, test broken" + exit 1 +fi +`) + defer blockDevCmd.Restore() + + d, err := disks.DiskFromDeviceName("sda") + c.Assert(err, IsNil) + c.Assert(d.Schema(), Equals, "dos") + c.Assert(d.KernelDeviceNode(), Equals, "/dev/sda") + + endSectors, err := d.UsableSectorsEnd() + c.Assert(err, IsNil) + c.Assert(endSectors, Equals, uint64(10000*512/4096)) + + c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ + {"blockdev", "--getsz", "/dev/sda"}, + {"blockdev", "--getss", "/dev/sda"}, + }) + + blockDevCmd.ForgetCalls() + + // the size of the disk does not depend on querying the sector size + sz, err := d.SizeInBytes() + c.Assert(err, IsNil) + c.Assert(sz, Equals, uint64(10000*512)) + + c.Assert(blockDevCmd.Calls(), DeepEquals, [][]string{ + {"blockdev", "--getsz", "/dev/sda"}, + }) + + // we never used sfdisk + c.Assert(sfdiskCmd.Calls(), HasLen, 0) +} + +func (s *diskSuite) TestAllPhysicalDisks(c *C) { + // mock some devices in /sys/block + + blockDir := filepath.Join(dirs.SysfsDir, "block") + err := os.MkdirAll(blockDir, 0755) + c.Assert(err, IsNil) + devsToCreate := []string{"sda", "loop1", "loop2", "sdb", "nvme0n1", "mmcblk0"} + for _, dev := range devsToCreate { + err := ioutil.WriteFile(filepath.Join(blockDir, dev), nil, 0644) + c.Assert(err, IsNil) + } + + restore := disks.MockUdevPropertiesForDevice(func(typ, dev string) (map[string]string, error) { + c.Assert(typ, Equals, "--path") + c.Assert(filepath.Dir(dev), Equals, blockDir) + switch filepath.Base(dev) { + case "sda": + return map[string]string{ + "ID_PART_TABLE_TYPE": "gpt", + "MAJOR": "42", + "MINOR": "0", + "DEVTYPE": "disk", + "DEVNAME": "/dev/sda", + "DEVPATH": "/devices/foo/sda", + "ID_PART_TABLE_UUID": "foo-sda-uuid", + }, nil + case "loop1": + return map[string]string{}, nil + case "loop2": + return map[string]string{}, nil + case "sdb": + return map[string]string{ + "ID_PART_TABLE_TYPE": "gpt", + "MAJOR": "43", + "MINOR": "0", + "DEVTYPE": "disk", + "DEVNAME": "/dev/sdb", + "DEVPATH": "/devices/foo/sdb", + "ID_PART_TABLE_UUID": "foo-sdb-uuid", + }, nil + case "nvme0n1": + return map[string]string{ + "ID_PART_TABLE_TYPE": "gpt", + "MAJOR": "44", + "MINOR": "0", + "DEVTYPE": "disk", + "DEVNAME": "/dev/nvme0n1", + "DEVPATH": "/devices/foo/nvme0n1", + "ID_PART_TABLE_UUID": "foo-nvme-uuid", + }, nil + case "mmcblk0": + return map[string]string{ + "ID_PART_TABLE_TYPE": "gpt", + "MAJOR": "45", + "MINOR": "0", + "DEVTYPE": "disk", + "DEVNAME": "/dev/mmcblk0", + "DEVPATH": "/devices/foo/mmcblk0", + "ID_PART_TABLE_UUID": "foo-mmc-uuid", + }, nil + default: + c.Errorf("unexpected udev device properties requested: %s", dev) + return nil, fmt.Errorf("unexpected udev device: %s", dev) + } + }) + defer restore() + + d, err := disks.AllPhysicalDisks() + c.Assert(err, IsNil) + c.Assert(d, HasLen, 4) + + c.Assert(d[0].KernelDeviceNode(), Equals, "/dev/mmcblk0") + c.Assert(d[1].KernelDeviceNode(), Equals, "/dev/nvme0n1") + c.Assert(d[2].KernelDeviceNode(), Equals, "/dev/sda") + c.Assert(d[3].KernelDeviceNode(), Equals, "/dev/sdb") +} diff -Nru snapd-2.53+21.10ubuntu1/osutil/disks/export_test.go snapd-2.54.2+21.10/osutil/disks/export_test.go --- snapd-2.53+21.10ubuntu1/osutil/disks/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/disks/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -41,3 +41,7 @@ udevadmProperties = old } } + +var UnregisterDeviceMapperBackResolver = unregisterDeviceMapperBackResolver + +var CryptLuks2DeviceMapperBackResolver = cryptLuks2DeviceMapperBackResolver diff -Nru snapd-2.53+21.10ubuntu1/osutil/disks/labels.go snapd-2.54.2+21.10/osutil/disks/labels.go --- snapd-2.53+21.10ubuntu1/osutil/disks/labels.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/disks/labels.go 2022-01-06 21:25:16.000000000 +0000 @@ -22,6 +22,8 @@ import ( "bytes" "fmt" + "regexp" + "strconv" "strings" "unicode/utf8" ) @@ -39,10 +41,42 @@ case utf8.RuneLen(r) > 1: buf.WriteRune(r) case !strings.ContainsRune(allowed, r): - fmt.Fprintf(buf, `\x%x`, r) + fmt.Fprintf(buf, `\x%02x`, r) default: buf.WriteRune(r) } } return buf.String() } + +var hexCode = regexp.MustCompile(`\\x[0-9a-f]{2}`) + +// BlkIDDecodeLabel decodes a string such as a filesystem or partition label +// encoded by udev in BlkIDEncodeLabel for normal comparison, i.e. +// "BIOS\x20Boot" becomes "BIOS Boot" +func BlkIDDecodeLabel(in string) (string, error) { + out := strings.Builder{} + pos := 0 + for _, m := range hexCode.FindAllStringIndex(in, -1) { + start := m[0] + beforeMatch := in[pos:start] + if i := strings.IndexRune(beforeMatch, '\\'); i >= 0 { + return "", fmt.Errorf(`string is malformed, unparsable escape sequence at "%s"`, beforeMatch[i:]) + } + out.WriteString(beforeMatch) + hex := in[start+2 : start+4] + n, err := strconv.ParseUint(hex, 16, 8) + if err != nil { + // This cannot really happen, since the regexp wouldn't match otherwise + return "", fmt.Errorf("internal error: cannot parse hex %q despite matching regexp", hex) + } + out.WriteRune(rune(n)) + pos = m[1] + } + remaining := in[pos:] + if i := strings.IndexRune(remaining, '\\'); i >= 0 { + return "", fmt.Errorf(`string is malformed, unparsable escape sequence at "%s"`, remaining[i:]) + } + out.WriteString(remaining) + return out.String(), nil +} diff -Nru snapd-2.53+21.10ubuntu1/osutil/disks/labels_test.go snapd-2.54.2+21.10/osutil/disks/labels_test.go --- snapd-2.53+21.10ubuntu1/osutil/disks/labels_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/disks/labels_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -33,7 +33,7 @@ var _ = Suite(&diskLabelSuite{}) -func (ts *diskLabelSuite) TestEncodeHexBlkIDFormat(c *C) { +func (ts *diskLabelSuite) TestBlkIDEncodeDecodeLabelHappy(c *C) { // Test output obtained with the following program: // // #include @@ -72,8 +72,9 @@ // these are "unsafe" chars, so they get encoded {"ubuntu data", `ubuntu\x20data`}, - {"ubuntu\ttab", `ubuntu\x9tab`}, - {"ubuntu\nnewline", `ubuntu\xanewline`}, + {"ubuntu\ttab", `ubuntu\x09tab`}, + {"ubuntu\t9tab", `ubuntu\x099tab`}, + {"ubuntu\nnewline", `ubuntu\x0anewline`}, {"foo bar", `foo\x20bar`}, {"foo/bar", `foo\x2fbar`}, {"foo/../bar", `foo\x2f..\x2fbar`}, @@ -81,9 +82,72 @@ {"pinkié pie", `pinkié\x20pie`}, {"(EFI Boot)", `\x28EFI\x20Boot\x29`}, {"[System Boot]", `\x5bSystem\x20Boot\x5d`}, + // 0x7e is just a 1-rune long character that is not in the allowed set + // to demonstrate that these two input strings are encoded/decoded + // properly with the constant double width + {"ubuntu\x7etab", `ubuntu\x7etab`}, + {"ubuntu\x07" + "etab", `ubuntu\x07etab`}, + // works when the only character is an escaped one too + {"\t", `\x09`}, } for _, t := range tt { c.Logf("tc: %v %q", t.in, t.out) c.Assert(disks.BlkIDEncodeLabel(t.in), Equals, t.out) + + // make sure the other way around works too + expin, err := disks.BlkIDDecodeLabel(t.out) + c.Assert(err, IsNil) + + c.Assert(expin, Equals, t.in) + } +} + +func (ts *diskLabelSuite) TestBlkIDDecodeLabelUnhappy(c *C) { + tt := []struct { + in string + experr string + }{ + { + `\x7z`, + `string is malformed, unparsable escape sequence at "\\x7z"`, + }, + { + `\x09\x7y`, + `string is malformed, unparsable escape sequence at "\\x7y"`, + }, + { + `\z`, + `string is malformed, unparsable escape sequence at "\\z"`, + }, + { + `\`, + `string is malformed, unparsable escape sequence at "\\"`, + }, + { + `\x40\`, + `string is malformed, unparsable escape sequence at "\\"`, + }, + { + `\x`, + `string is malformed, unparsable escape sequence at "\\x"`, + }, + { + `\x40\x`, + `string is malformed, unparsable escape sequence at "\\x"`, + }, + { + `\x0`, + `string is malformed, unparsable escape sequence at "\\x0"`, + }, + { + `\x40\x4`, + `string is malformed, unparsable escape sequence at "\\x4"`, + }, + } + + for _, t := range tt { + c.Logf("input: %q", t.in) + _, err := disks.BlkIDDecodeLabel(t.in) + c.Assert(err, ErrorMatches, t.experr) } } diff -Nru snapd-2.53+21.10ubuntu1/osutil/disks/luks.go snapd-2.54.2+21.10/osutil/disks/luks.go --- snapd-2.53+21.10ubuntu1/osutil/disks/luks.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/disks/luks.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,91 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package disks + +import ( + "bytes" + "fmt" + "path/filepath" + "regexp" + "strings" +) + +var ( + // this regexp is for the DM_UUID udev property, or equivalently the dm/uuid + // sysfs entry for a luks2 device mapper volume dynamically created by + // systemd-cryptsetup when unlocking + // the actual value that is returned also has "-some-name" appended to this + // pattern, but we delete that from the string before matching with this + // regexp to prevent issues like a mapper volume name that has CRYPT-LUKS2- + // in the name and thus we might accidentally match it + // see also the comments in DiskFromMountPoint about this value + luksUUIDPatternRe = regexp.MustCompile(`^CRYPT-LUKS2-([0-9a-f]{32})$`) +) + +func cryptLuks2DeviceMapperBackResolver(dmUUID, dmName []byte) (dev string, ok bool) { + if !strings.HasPrefix(string(dmUUID), "CRYPT-LUKS") { + return "", false + } + + // this is a LUKS encrypted device + + // trim the suffix of the dm name from the dm uuid to safely match the + // regex - the dm uuid contains the dm name, and the dm name is user + // controlled, so we want to remove that and just use the luks pattern + // to match the device uuid + // we are extra safe here since the dm name could be hypothetically user + // controlled via an external USB disk with LVM partition names, etc. + dmUUIDSafe := bytes.TrimSuffix( + bytes.TrimSpace(dmUUID), + append([]byte("-"), bytes.TrimSpace(dmName)...), + ) + matches := luksUUIDPatternRe.FindSubmatch(dmUUIDSafe) + if len(matches) != 2 { + // the format of the uuid is different - different luks version + // maybe? + return "", false + } + + // the uuid is the first and only submatch, but it is not in the same + // format exactly as we want to use, namely it is missing all of the "-" + // characters in a typical uuid, i.e. it is of the form: + // ae6e79de00a9406f80ee64ba7c1966bb but we want it to be like: + // ae6e79de-00a9-406f-80ee-64ba7c1966bb so we need to add in 4 "-" + // characters + compactUUID := string(matches[1]) + canonicalUUID := fmt.Sprintf( + "%s-%s-%s-%s-%s", + compactUUID[0:8], + compactUUID[8:12], + compactUUID[12:16], + compactUUID[16:20], + compactUUID[20:], + ) + + // now finally, we need to use this uuid, which is the device uuid of + // the actual physical encrypted partition to get the path, which will + // be something like /dev/vda4, etc. + byUUIDPath := filepath.Join("/dev/disk/by-uuid", canonicalUUID) + return byUUIDPath, true +} + +func init() { + RegisterDeviceMapperBackResolver("crypt-luks2", cryptLuks2DeviceMapperBackResolver) +} diff -Nru snapd-2.53+21.10ubuntu1/osutil/disks/mockdisk.go snapd-2.54.2+21.10/osutil/disks/mockdisk.go --- snapd-2.53+21.10ubuntu1/osutil/disks/mockdisk.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/disks/mockdisk.go 2022-01-06 21:25:16.000000000 +0000 @@ -34,23 +34,28 @@ // DevNum must be a unique string per unique mocked disk, if only one disk is // being mocked it can be left empty. type MockDiskMapping struct { - // TODO: eliminate these manual mappings and instead switch all the users - // over to providing the full list of Partitions instead, but in the - // interest of smaller PR's we are doing that in a separate PR. - // FilesystemLabelToPartUUID is a mapping of the udev encoded filesystem - // labels to the expected partition uuids. - FilesystemLabelToPartUUID map[string]string - // PartitionLabelToPartUUID is a mapping of the udev encoded partition - // labels to the expected partition uuids. - PartitionLabelToPartUUID map[string]string - DiskHasPartitions bool - - // TODO: add an exported list of Partitions here - - // static variables for the disk + // TODO: should this be automatically determined if Structure has non-zero + // len instead? + DiskHasPartitions bool + + // Structure is the set of partitions or structures on the disk. These + // partitions are used with Partitions() as well as + // FindMatchingPartitionWith{Fs,Part}Label + Structure []Partition + + // static variables for the disk that must be unique for different disks, + // but note that there are potentially multiple DevNode values that could + // map to a single disk, but it's not worth encoding that complexity here + // by making DevNodes a list DevNum string DevNode string DevPath string + + ID string + DiskSchema string + SectorSizeBytes uint64 + DiskUsableSectorEnd uint64 + DiskSizeInBytes uint64 } // FindMatchingPartitionUUIDWithFsLabel returns a matching PartitionUUID @@ -58,19 +63,13 @@ func (d *MockDiskMapping) FindMatchingPartitionWithFsLabel(label string) (Partition, error) { // TODO: this should just iterate over the static list when that is a thing osutil.MustBeTestBinary("mock disks only to be used in tests") - if partuuid, ok := d.FilesystemLabelToPartUUID[label]; ok { - part := Partition{ - PartitionUUID: partuuid, - FilesystemLabel: label, - } - // add the partition label too if we have one for this partition uuid - for partlabel, partuuid2 := range d.PartitionLabelToPartUUID { - if partuuid2 == partuuid { - part.PartitionLabel = partlabel - } + + for _, p := range d.Structure { + if p.FilesystemLabel == label { + return p, nil } - return part, nil } + return Partition{}, PartitionNotFoundError{ SearchType: "filesystem-label", SearchQuery: label, @@ -80,21 +79,14 @@ // FindMatchingPartitionUUIDWithPartLabel returns a matching PartitionUUID // for the specified filesystem label if it exists. Part of the Disk interface. func (d *MockDiskMapping) FindMatchingPartitionWithPartLabel(label string) (Partition, error) { - // TODO: this should just iterate over the static list when that is a thing osutil.MustBeTestBinary("mock disks only to be used in tests") - if partuuid, ok := d.PartitionLabelToPartUUID[label]; ok { - part := Partition{ - PartitionUUID: partuuid, - PartitionLabel: label, - } - // add the filesystem label too if we have one for this partition uuid - for fsLabel, partuuid2 := range d.FilesystemLabelToPartUUID { - if partuuid2 == partuuid { - part.FilesystemLabel = fsLabel - } + + for _, p := range d.Structure { + if p.PartitionLabel == label { + return p, nil } - return part, nil } + return Partition{}, PartitionNotFoundError{ SearchType: "partition-label", SearchQuery: label, @@ -118,39 +110,7 @@ } func (d *MockDiskMapping) Partitions() ([]Partition, error) { - // TODO: this should just return the static list that was in the mapping - // when that is a thing - - // dynamically build up a list of partitions with the mappings we were - // provided - parts := make([]Partition, 0, len(d.PartitionLabelToPartUUID)) - - partUUIDToPart := map[string]Partition{} - - // first populate with all the partition labels - for partLabel, partuuid := range d.PartitionLabelToPartUUID { - part := Partition{ - PartitionLabel: partLabel, - PartitionUUID: partuuid, - } - - partUUIDToPart[partuuid] = part - } - - for fsLabel, partuuid := range d.FilesystemLabelToPartUUID { - existingPart, ok := partUUIDToPart[partuuid] - if !ok { - parts = append(parts, Partition{ - FilesystemLabel: fsLabel, - PartitionUUID: partuuid, - }) - continue - } - existingPart.FilesystemLabel = fsLabel - parts = append(parts, existingPart) - } - - return parts, nil + return d.Structure, nil } // HasPartitions returns if the mock disk has partitions or not. Part of the @@ -193,6 +153,26 @@ return d.DevPath } +func (d *MockDiskMapping) DiskID() string { + return d.ID +} + +func (d *MockDiskMapping) Schema() string { + return d.DiskSchema +} + +func (d *MockDiskMapping) SectorSize() (uint64, error) { + return d.SectorSizeBytes, nil +} + +func (d *MockDiskMapping) UsableSectorsEnd() (uint64, error) { + return d.DiskUsableSectorEnd, nil +} + +func (d *MockDiskMapping) SizeInBytes() (uint64, error) { + return d.DiskSizeInBytes, nil +} + // Mountpoint is a combination of a mountpoint location and whether that // mountpoint is a decrypted device. It is only used in identifying mount points // with MountPointIsFromDisk and DiskFromMountPoint with @@ -202,19 +182,139 @@ IsDecryptedDevice bool } -// MockDeviceNameDisksToPartitionMapping will mock DiskFromDeviceName such that -// the provided map of device names to mock disks is used instead of the actual +func checkMockDiskMappingsForDuplicates(mockedDisks map[string]*MockDiskMapping) { + // we do the minimal amount of validation here, where if things are + // specified as non-zero value we check that they make sense, but we don't + // require that every field is set for every partition since many tests + // don't care about every field + + // check partition uuid's and partition labels for duplication inter-disk + // we could have valid cloned disks where the same partition uuid/label + // appears on two disks, but never on the same disk + for _, disk := range mockedDisks { + seenPartUUID := make(map[string]bool, len(disk.Structure)) + seenPartLabel := make(map[string]bool, len(disk.Structure)) + for _, p := range disk.Structure { + if p.PartitionUUID != "" { + if seenPartUUID[p.PartitionUUID] { + panic("mock error: disk has duplicated partition uuids in its structure") + } + seenPartUUID[p.PartitionUUID] = true + } + + if p.PartitionLabel != "" { + if seenPartLabel[p.PartitionLabel] { + panic("mock error: disk has duplicated partition labels in its structure") + } + seenPartLabel[p.PartitionLabel] = true + } + } + } + + // check major/minors across each disk + for _, disk := range mockedDisks { + type majmin struct{ maj, min int } + seenMajorMinors := map[majmin]bool{} + for _, p := range disk.Structure { + if p.Major == 0 && p.Minor == 0 { + continue + } + + m := majmin{maj: p.Major, min: p.Minor} + if seenMajorMinors[m] { + panic("mock error: duplicated major minor numbers for partitions in disk mapping") + } + seenMajorMinors[m] = true + } + } + + // check StructureIndex across each disk + for _, disk := range mockedDisks { + seenIndices := map[uint64]bool{} + for _, p := range disk.Structure { + if p.StructureIndex == 0 { + continue + } + + if seenIndices[p.StructureIndex] { + panic("mock error: duplicated structure indices for partitions in disk mapping") + } + seenIndices[p.StructureIndex] = true + } + } + + // check device paths across each disk + for _, disk := range mockedDisks { + seenDevPaths := map[string]bool{} + for _, p := range disk.Structure { + if p.KernelDevicePath == "" { + continue + } + if seenDevPaths[p.KernelDevicePath] { + panic("mock error: duplicated kernel device paths for partitions in disk mapping") + } + seenDevPaths[p.KernelDevicePath] = true + } + } + + // check device nodes across each disk + for _, disk := range mockedDisks { + sendDevNodes := map[string]bool{} + for _, p := range disk.Structure { + if p.KernelDevicePath == "" { + continue + } + + if sendDevNodes[p.KernelDeviceNode] { + panic("mock error: duplicated kernel device nodes for partitions in disk mapping") + } + sendDevNodes[p.KernelDeviceNode] = true + } + } + + // no checking of filesystem label/uuid since those could be duplicated as + // they exist independent of any other structure +} + +// MockPartitionDeviceNodeToDiskMapping will mock DiskFromPartitionDeviceNode +// such that the provided map of device names to mock disks is used instead of +// the actual implementation using udev. +func MockPartitionDeviceNodeToDiskMapping(mockedDisks map[string]*MockDiskMapping) (restore func()) { + osutil.MustBeTestBinary("mock disks only to be used in tests") + + checkMockDiskMappingsForDuplicates(mockedDisks) + + // note that there can be multiple partitions that map to the same disk, so + // we don't really validate the keys of the provided mapping + + old := diskFromPartitionDeviceNode + diskFromPartitionDeviceNode = func(node string) (Disk, error) { + disk, ok := mockedDisks[node] + if !ok { + return nil, fmt.Errorf("partition device node %q not mocked", node) + } + return disk, nil + } + return func() { + diskFromPartitionDeviceNode = old + } +} + +// MockDeviceNameToDiskMapping will mock DiskFromDeviceName such that the +// provided map of device names to mock disks is used instead of the actual // implementation using udev. -func MockDeviceNameDisksToPartitionMapping(mockedMountPoints map[string]*MockDiskMapping) (restore func()) { +func MockDeviceNameToDiskMapping(mockedDisks map[string]*MockDiskMapping) (restore func()) { osutil.MustBeTestBinary("mock disks only to be used in tests") - // note that devices can have many names that are recognized by + checkMockDiskMappingsForDuplicates(mockedDisks) + + // note that devices can have multiple names that are recognized by // udev/kernel, so we don't do any validation of the mapping here like we do // for MockMountPointDisksToPartitionMapping old := diskFromDeviceName diskFromDeviceName = func(deviceName string) (Disk, error) { - disk, ok := mockedMountPoints[deviceName] + disk, ok := mockedDisks[deviceName] if !ok { return nil, fmt.Errorf("device name %q not mocked", deviceName) } @@ -226,6 +326,32 @@ } } +// MockDevicePathToDiskMapping will mock DiskFromDevicePath such that the +// provided map of device names to mock disks is used instead of the actual +// implementation using udev. +func MockDevicePathToDiskMapping(mockedDisks map[string]*MockDiskMapping) (restore func()) { + osutil.MustBeTestBinary("mock disks only to be used in tests") + + checkMockDiskMappingsForDuplicates(mockedDisks) + + // note that devices can have multiple paths that are recognized by + // udev/kernel, so we don't do any validation of the mapping here like we do + // for MockMountPointDisksToPartitionMapping + + old := diskFromDevicePath + diskFromDevicePath = func(devicePath string) (Disk, error) { + disk, ok := mockedDisks[devicePath] + if !ok { + return nil, fmt.Errorf("device path %q not mocked", devicePath) + } + return disk, nil + } + + return func() { + diskFromDevicePath = old + } +} + // MockMountPointDisksToPartitionMapping will mock DiskFromMountPoint such that // the specified mapping is returned/used. Specifically, keys in the provided // map are mountpoints, and the values for those keys are the disks that will diff -Nru snapd-2.53+21.10ubuntu1/osutil/disks/mockdisk_test.go snapd-2.54.2+21.10/osutil/disks/mockdisk_test.go --- snapd-2.53+21.10ubuntu1/osutil/disks/mockdisk_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/disks/mockdisk_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -39,11 +39,14 @@ dirs.SetRootDir(c.MkDir()) } -func (s *mockDiskSuite) TestMockDeviceNameDisksToPartitionMapping(c *C) { +func (s *mockDiskSuite) TestMockDeviceNameToDiskMapping(c *C) { // one disk with different device names d1 := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "label1": "part1", + Structure: []disks.Partition{ + { + FilesystemLabel: "label1", + PartitionUUID: "part1", + }, }, DiskHasPartitions: true, DevNum: "d1", @@ -52,8 +55,11 @@ } d2 := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "label2": "part2", + Structure: []disks.Partition{ + { + FilesystemLabel: "label2", + PartitionUUID: "part2", + }, }, DiskHasPartitions: true, DevNum: "d2", @@ -67,7 +73,7 @@ "other-disk": d2, } - r := disks.MockDeviceNameDisksToPartitionMapping(m) + r := disks.MockDeviceNameToDiskMapping(m) defer r() res, err := disks.DiskFromDeviceName("devName1") @@ -101,19 +107,166 @@ c.Assert(res3, DeepEquals, d2) } +func (s *mockDiskSuite) TestMockDevicePathToDiskMapping(c *C) { + // one disk with different device paths + d1 := &disks.MockDiskMapping{ + Structure: []disks.Partition{ + { + FilesystemLabel: "label1", + PartitionUUID: "part1", + }, + }, + DiskHasPartitions: true, + DevNum: "d1", + DevNode: "/dev/vda", + DevPath: "/sys/devices/pci/foo/dev1", + } + + d2 := &disks.MockDiskMapping{ + Structure: []disks.Partition{ + { + FilesystemLabel: "label2", + PartitionUUID: "part2", + }, + }, + DiskHasPartitions: true, + DevNum: "d2", + DevNode: "/dev/vdb", + DevPath: "/sys/devices/foo2", + } + + m := map[string]*disks.MockDiskMapping{ + "/sys/devices/pci/foo/dev1": d1, + // this simulates a symlink in /sys/block which points to the above path + "/sys/block/dev1": d1, + + // a totally different disk + "/sys/device/mmc/bar/dev2": d2, + } + + r := disks.MockDevicePathToDiskMapping(m) + defer r() + + res, err := disks.DiskFromDevicePath("/sys/devices/pci/foo/dev1") + c.Assert(err, IsNil) + c.Assert(res.KernelDeviceNode(), Equals, "/dev/vda") + c.Assert(res.KernelDevicePath(), Equals, "/sys/devices/pci/foo/dev1") + parts, err := res.Partitions() + c.Assert(err, IsNil) + c.Assert(parts, DeepEquals, []disks.Partition{{FilesystemLabel: "label1", PartitionUUID: "part1"}}) + c.Assert(res, DeepEquals, d1) + + res2, err := disks.DiskFromDevicePath("/sys/block/dev1") + c.Assert(err, IsNil) + c.Assert(res2.KernelDeviceNode(), Equals, "/dev/vda") + c.Assert(res2.KernelDevicePath(), Equals, "/sys/devices/pci/foo/dev1") + parts, err = res.Partitions() + c.Assert(err, IsNil) + c.Assert(parts, DeepEquals, []disks.Partition{{FilesystemLabel: "label1", PartitionUUID: "part1"}}) + c.Assert(res2, DeepEquals, d1) + + _, err = disks.DiskFromDevicePath("/sys/device/nvme/foo/dev3") + c.Assert(err, ErrorMatches, fmt.Sprintf("device path %q not mocked", "/sys/device/nvme/foo/dev3")) + + res3, err := disks.DiskFromDevicePath("/sys/device/mmc/bar/dev2") + c.Assert(err, IsNil) + c.Assert(res3.KernelDeviceNode(), Equals, "/dev/vdb") + c.Assert(res3.KernelDevicePath(), Equals, "/sys/devices/foo2") + parts, err = res3.Partitions() + c.Assert(err, IsNil) + c.Assert(parts, DeepEquals, []disks.Partition{{FilesystemLabel: "label2", PartitionUUID: "part2"}}) + c.Assert(res3, DeepEquals, d2) +} + +func (s *mockDiskSuite) TestMockPartitionDeviceNodeToDiskMapping(c *C) { + // two disks + d1 := &disks.MockDiskMapping{ + Structure: []disks.Partition{ + { + FilesystemLabel: "label1", + PartitionUUID: "part1", + }, + }, + DiskHasPartitions: true, + DevNum: "d1", + DevNode: "/dev/vda", + DevPath: "/sys/devices/foo1", + } + + d2 := &disks.MockDiskMapping{ + Structure: []disks.Partition{ + { + FilesystemLabel: "label2", + PartitionUUID: "part2", + }, + }, + DiskHasPartitions: true, + DevNum: "d2", + DevNode: "/dev/vdb", + DevPath: "/sys/devices/foo2", + } + + m := map[string]*disks.MockDiskMapping{ + // two partitions on vda + "/dev/vda1": d1, + "/dev/vda2": d1, + // one partition on vdb + "/dev/vdb1": d2, + } + + r := disks.MockPartitionDeviceNodeToDiskMapping(m) + defer r() + + res, err := disks.DiskFromPartitionDeviceNode("/dev/vda1") + c.Assert(err, IsNil) + c.Assert(res.KernelDeviceNode(), Equals, "/dev/vda") + c.Assert(res.KernelDevicePath(), Equals, "/sys/devices/foo1") + parts, err := res.Partitions() + c.Assert(err, IsNil) + c.Assert(parts, DeepEquals, []disks.Partition{{FilesystemLabel: "label1", PartitionUUID: "part1"}}) + c.Assert(res, DeepEquals, d1) + + res2, err := disks.DiskFromPartitionDeviceNode("/dev/vda2") + c.Assert(err, IsNil) + c.Assert(res2.KernelDeviceNode(), Equals, "/dev/vda") + c.Assert(res2.KernelDevicePath(), Equals, "/sys/devices/foo1") + parts, err = res.Partitions() + c.Assert(err, IsNil) + c.Assert(parts, DeepEquals, []disks.Partition{{FilesystemLabel: "label1", PartitionUUID: "part1"}}) + c.Assert(res2, DeepEquals, d1) + + _, err = disks.DiskFromPartitionDeviceNode("/dev/vda3") + c.Assert(err, ErrorMatches, fmt.Sprintf("partition device node %q not mocked", "/dev/vda3")) + + res3, err := disks.DiskFromPartitionDeviceNode("/dev/vdb1") + c.Assert(err, IsNil) + c.Assert(res3.KernelDeviceNode(), Equals, "/dev/vdb") + c.Assert(res3.KernelDevicePath(), Equals, "/sys/devices/foo2") + parts, err = res3.Partitions() + c.Assert(err, IsNil) + c.Assert(parts, DeepEquals, []disks.Partition{{FilesystemLabel: "label2", PartitionUUID: "part2"}}) + c.Assert(res3, DeepEquals, d2) +} + func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMappingVerifiesUniqueness(c *C) { // two different disks with different DevNum's d1 := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "label1": "part1", + Structure: []disks.Partition{ + { + FilesystemLabel: "label1", + PartitionUUID: "part1", + }, }, DiskHasPartitions: true, DevNum: "d1", } d2 := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "label1": "part1", + Structure: []disks.Partition{ + { + FilesystemLabel: "label1", + PartitionUUID: "part1", + }, }, DiskHasPartitions: false, DevNum: "d2", @@ -152,8 +305,11 @@ func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMappingVerifiesConsistency(c *C) { d1 := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "label1": "part1", + Structure: []disks.Partition{ + { + FilesystemLabel: "label1", + PartitionUUID: "part1", + }, }, DiskHasPartitions: true, DevNum: "d1", @@ -177,22 +333,24 @@ func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMapping(c *C) { d1 := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "label1": "part1", - }, - PartitionLabelToPartUUID: map[string]string{ - "part-label1": "part1", + Structure: []disks.Partition{ + { + FilesystemLabel: "label1", + PartitionUUID: "part1", + PartitionLabel: "part-label1", + }, }, DiskHasPartitions: true, DevNum: "d1", } d2 := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "label2": "part2", - }, - PartitionLabelToPartUUID: map[string]string{ - "part-label2": "part2", + Structure: []disks.Partition{ + { + FilesystemLabel: "label2", + PartitionUUID: "part2", + PartitionLabel: "part-label2", + }, }, DiskHasPartitions: true, DevNum: "d2", @@ -322,10 +480,19 @@ func (s *mockDiskSuite) TestMockMountPointDisksToPartitionMappingDecryptedDevices(c *C) { d1 := &disks.MockDiskMapping{ - FilesystemLabelToPartUUID: map[string]string{ - "ubuntu-seed": "ubuntu-seed-part", - "ubuntu-boot": "ubuntu-boot-part", - "ubuntu-data-enc": "ubuntu-data-enc-part", + Structure: []disks.Partition{ + { + FilesystemLabel: "ubuntu-seed", + PartitionUUID: "ubuntu-seed-part", + }, + { + FilesystemLabel: "ubuntu-boot", + PartitionUUID: "ubuntu-boot-part", + }, + { + FilesystemLabel: "ubuntu-data-enc", + PartitionUUID: "ubuntu-data-enc-part", + }, }, DiskHasPartitions: true, DevNum: "d1", diff -Nru snapd-2.53+21.10ubuntu1/osutil/export_fault_test.go snapd-2.54.2+21.10/osutil/export_fault_test.go --- snapd-2.53+21.10ubuntu1/osutil/export_fault_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/export_fault_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build faultinject // +build faultinject /* diff -Nru snapd-2.53+21.10ubuntu1/osutil/faultinject_dummy.go snapd-2.54.2+21.10/osutil/faultinject_dummy.go --- snapd-2.53+21.10ubuntu1/osutil/faultinject_dummy.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/faultinject_dummy.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !faultinject // +build !faultinject /* diff -Nru snapd-2.53+21.10ubuntu1/osutil/faultinject_dummy_test.go snapd-2.54.2+21.10/osutil/faultinject_dummy_test.go --- snapd-2.53+21.10ubuntu1/osutil/faultinject_dummy_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/faultinject_dummy_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !faultinject // +build !faultinject /* diff -Nru snapd-2.53+21.10ubuntu1/osutil/faultinject.go snapd-2.54.2+21.10/osutil/faultinject.go --- snapd-2.53+21.10ubuntu1/osutil/faultinject.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/faultinject.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build faultinject // +build faultinject /* diff -Nru snapd-2.53+21.10ubuntu1/osutil/faultinject_test.go snapd-2.54.2+21.10/osutil/faultinject_test.go --- snapd-2.53+21.10ubuntu1/osutil/faultinject_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/faultinject_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build faultinject // +build faultinject /* diff -Nru snapd-2.53+21.10ubuntu1/osutil/group_cgo.go snapd-2.54.2+21.10/osutil/group_cgo.go --- snapd-2.53+21.10ubuntu1/osutil/group_cgo.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/group_cgo.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build cgo // +build cgo /* diff -Nru snapd-2.53+21.10ubuntu1/osutil/group_no_cgo.go snapd-2.54.2+21.10/osutil/group_no_cgo.go --- snapd-2.53+21.10ubuntu1/osutil/group_no_cgo.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/group_no_cgo.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !cgo // +build !cgo package osutil diff -Nru snapd-2.53+21.10ubuntu1/osutil/io.go snapd-2.54.2+21.10/osutil/io.go --- snapd-2.53+21.10ubuntu1/osutil/io.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/io.go 2022-01-06 21:25:16.000000000 +0000 @@ -255,6 +255,11 @@ // snapdUnsafeIO controls the ability to ignore expensive disk // synchronization. It is only used inside tests. if !snapdUnsafeIO { + // if called with a path with trailing '/', filepath.Dir + // returns the dir itself instead of the parent + oldName = filepath.Clean(oldName) + newName = filepath.Clean(newName) + oldDirPath := filepath.Dir(oldName) newDirPath := filepath.Dir(newName) diff -Nru snapd-2.53+21.10ubuntu1/osutil/io_test.go snapd-2.54.2+21.10/osutil/io_test.go --- snapd-2.53+21.10ubuntu1/osutil/io_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/io_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -401,7 +401,7 @@ var _ = Suite(&AtomicRenameTestSuite{}) -func (ts *AtomicRenameTestSuite) TestAtomicRename(c *C) { +func (ts *AtomicRenameTestSuite) TestAtomicRenameFile(c *C) { d := c.MkDir() err := ioutil.WriteFile(filepath.Join(d, "foo"), []byte("foobar"), 0644) @@ -470,3 +470,39 @@ func (s *SafeIoAtomicTestSuite) TearDownSuite(c *C) { s.restoreUnsafeIO() } + +func (ts *AtomicWriteTestSuite) TestAtomicRenameDir(c *C) { + // create a source directory + srcParentDir := c.MkDir() + src := filepath.Join(srcParentDir, "foo") + + err := os.MkdirAll(src, 0755) + c.Assert(err, IsNil) + + // put a file in the source directory + srcFile := filepath.Join(src, "file") + contents := []byte("contents") + err = osutil.AtomicWriteFile(srcFile, contents, 0644, 0) + c.Assert(err, IsNil) + + // the parent dir of the destination + dstParentDir := c.MkDir() + dst := filepath.Join(dstParentDir, "bar") + + // ensure it works even with trailing '/' + err = osutil.AtomicRename(src+"/", dst+"/") + c.Assert(err, IsNil) + + d, err := ioutil.ReadDir(dst) + c.Assert(err, IsNil) + c.Assert(len(d), Equals, 1) + c.Assert(d[0].Name(), Equals, "file") + + data, err := ioutil.ReadFile(filepath.Join(dst, "file")) + c.Assert(err, IsNil) + c.Assert(data, DeepEquals, contents) + + exists, _, err := osutil.DirExists(src) + c.Assert(err, IsNil) + c.Assert(exists, Equals, false) +} diff -Nru snapd-2.53+21.10ubuntu1/osutil/settime_32bit.go snapd-2.54.2+21.10/osutil/settime_32bit.go --- snapd-2.53+21.10ubuntu1/osutil/settime_32bit.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/settime_32bit.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,5 +1,6 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build (386 || arm) && linux // +build 386 arm // +build linux diff -Nru snapd-2.53+21.10ubuntu1/osutil/settime_64bit.go snapd-2.54.2+21.10/osutil/settime_64bit.go --- snapd-2.53+21.10ubuntu1/osutil/settime_64bit.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/settime_64bit.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,8 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- -// +build !386 -// +build !arm -// +build linux +//go:build !386 && !arm && linux +// +build !386,!arm,linux /* * Copyright (C) 2021 Canonical Ltd diff -Nru snapd-2.53+21.10ubuntu1/osutil/strace/export_test.go snapd-2.54.2+21.10/osutil/strace/export_test.go --- snapd-2.53+21.10ubuntu1/osutil/strace/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/strace/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -19,10 +19,6 @@ package strace -var ( - ExcludedSyscalls = excludedSyscalls -) - func (stt *ExecveTiming) ExeRuntimes() []ExeRuntime { return stt.exeRuntimes } diff -Nru snapd-2.53+21.10ubuntu1/osutil/strace/strace.go snapd-2.54.2+21.10/osutil/strace/strace.go --- snapd-2.53+21.10ubuntu1/osutil/strace/strace.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/strace/strace.go 2022-01-06 21:25:16.000000000 +0000 @@ -24,6 +24,7 @@ "os/exec" "os/user" "path/filepath" + "runtime" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" @@ -31,7 +32,21 @@ // These syscalls are excluded because they make strace hang on all or // some architectures (gettimeofday on arm64). -var excludedSyscalls = "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep" +// Furthermore not all syscalls exist on all arches, and for some +// arches strace has a mapping for some of the non-existent +// syscalls. Thus there is no universal list of syscalls to exclude. +func getExcludedSyscalls() string { + switch runtime.GOARCH { + case "arm", "arm64", "riscv64": + return "!pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep" + default: + return "!select,pselect6,_newselect,clock_gettime,sigaltstack,gettid,gettimeofday,nanosleep" + } +} + +// Export excluded syscalls as used by this package and multiple +// testsuites +var ExcludedSyscalls = getExcludedSyscalls() // Command returns how to run strace in the users context with the // right set of excluded system calls. @@ -73,7 +88,7 @@ stracePath, "-u", current.Username, "-f", - "-e", excludedSyscalls, + "-e", ExcludedSyscalls, } args = append(args, extraStraceOpts...) args = append(args, traceeCmd...) diff -Nru snapd-2.53+21.10ubuntu1/osutil/sys/sysnum_16_linux.go snapd-2.54.2+21.10/osutil/sys/sysnum_16_linux.go --- snapd-2.53+21.10ubuntu1/osutil/sys/sysnum_16_linux.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/sys/sysnum_16_linux.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build arm || 386 // +build arm 386 /* diff -Nru snapd-2.53+21.10ubuntu1/osutil/sys/sysnum_32_linux.go snapd-2.54.2+21.10/osutil/sys/sysnum_32_linux.go --- snapd-2.53+21.10ubuntu1/osutil/sys/sysnum_32_linux.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/sys/sysnum_32_linux.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build amd64 || arm64 || ppc || ppc64le || riscv64 || s390x // +build amd64 arm64 ppc ppc64le riscv64 s390x /* diff -Nru snapd-2.53+21.10ubuntu1/osutil/udev/netlink/rawsockstop_other.go snapd-2.54.2+21.10/osutil/udev/netlink/rawsockstop_other.go --- snapd-2.53+21.10ubuntu1/osutil/udev/netlink/rawsockstop_other.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/osutil/udev/netlink/rawsockstop_other.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,6 @@ +//go:build !arm64 // +build !arm64 + // don't remove the newline between the above statement and the package statement // or else the build constraint will be ignored and assumed to be part of the package comment! diff -Nru snapd-2.53+21.10ubuntu1/overlord/assertstate/assertstate.go snapd-2.54.2+21.10/overlord/assertstate/assertstate.go --- snapd-2.53+21.10ubuntu1/overlord/assertstate/assertstate.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/assertstate/assertstate.go 2022-01-06 21:25:16.000000000 +0000 @@ -344,6 +344,10 @@ snapstate.AutoAliases = AutoAliases // hook the helper for getting enforced validation sets snapstate.EnforcedValidationSets = EnforcedValidationSets + // hook the helper for saving current validation sets to the stack + snapstate.AddCurrentTrackingToValidationSetsStack = addCurrentTrackingToValidationSetsHistory + // hook the helper for restoring validation sets tracking from the stack + snapstate.RestoreValidationSetsTracking = RestoreValidationSetsTracking } // AutoRefreshAssertions tries to refresh all assertions @@ -463,11 +467,11 @@ return err } - snaps, err := snapstate.InstalledSnaps(s) + snaps, ignoreValidation, err := snapstate.InstalledSnaps(s) if err != nil { return err } - err = vsets.CheckInstalledSnaps(snaps) + err = vsets.CheckInstalledSnaps(snaps, ignoreValidation) if verr, ok := err.(*snapasserts.ValidationSetsValidationError); ok { if len(verr.InvalidSnaps) > 0 || len(verr.MissingSnaps) > 0 { return verr @@ -501,11 +505,11 @@ AllowLocalFallback bool } -// ValidationSetAssertionForMonitor tries to fetch or refresh the validation +// validationSetAssertionForMonitor tries to fetch or refresh the validation // set assertion with accountID/name/sequence (sequence is optional) using pool. // If assertion cannot be fetched but exists locally and opts.AllowLocalFallback // is set then the local one is returned -func ValidationSetAssertionForMonitor(st *state.State, accountID, name string, sequence int, pinned bool, userID int, opts *ResolveOptions) (as *asserts.ValidationSet, local bool, err error) { +func validationSetAssertionForMonitor(st *state.State, accountID, name string, sequence int, pinned bool, userID int, opts *ResolveOptions) (as *asserts.ValidationSet, local bool, err error) { if opts == nil { opts = &ResolveOptions{} } @@ -591,12 +595,12 @@ return as, false, err } -// ValidationSetAssertionForEnforce tries to fetch the validation set assertion +// validationSetAssertionForEnforce tries to fetch the validation set assertion // with the given accountID/name/sequence (sequence is optional) using pool and // checks if it's not in conflict with existing validation sets in enforcing mode // (all currently tracked validation set assertions get refreshed), and if they // are valid for installed snaps. -func ValidationSetAssertionForEnforce(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap) (vs *asserts.ValidationSet, err error) { +func validationSetAssertionForEnforce(st *state.State, accountID, name string, sequence int, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) (vs *asserts.ValidationSet, err error) { deviceCtx, err := snapstate.DevicePastSeeding(st, nil) if err != nil { return nil, err @@ -661,7 +665,7 @@ if err := valsets.Conflict(); err != nil { return err } - if err := valsets.CheckInstalledSnaps(snaps); err != nil { + if err := valsets.CheckInstalledSnaps(snaps, ignoreValidation); err != nil { return err } return nil @@ -726,6 +730,52 @@ return vs, err } +// EnforceValidationSet tries to fetch the given validation set and enforce it. +// If all validation sets constrains are satisfied, the current validation sets +// tracking state is saved in validation sets history. +func EnforceValidationSet(st *state.State, accountID, name string, sequence, userID int, snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool) error { + vs, err := validationSetAssertionForEnforce(st, accountID, name, sequence, userID, snaps, ignoreValidation) + if err != nil { + return err + } + + tr := ValidationSetTracking{ + AccountID: accountID, + Name: name, + Mode: Enforce, + // note, sequence may be 0, meaning not pinned. + PinnedAt: sequence, + Current: vs.Sequence(), + } + + UpdateValidationSet(st, &tr) + return addCurrentTrackingToValidationSetsHistory(st) +} + +// MonitorValidationSet tries to fetch the given validation set and monitor it. +// The current validation sets tracking state is saved in validation sets history. +func MonitorValidationSet(st *state.State, accountID, name string, sequence int, userID int) error { + pinned := sequence > 0 + opts := ResolveOptions{AllowLocalFallback: true} + as, local, err := validationSetAssertionForMonitor(st, accountID, name, sequence, pinned, userID, &opts) + if err != nil { + return fmt.Errorf("cannot get validation set assertion for %v: %v", ValidationSetKey(accountID, name), err) + } + + tr := ValidationSetTracking{ + AccountID: accountID, + Name: name, + Mode: Monitor, + // note, Sequence may be 0, meaning not pinned. + PinnedAt: sequence, + Current: as.Sequence(), + LocalOnly: local, + } + + UpdateValidationSet(st, &tr) + return addCurrentTrackingToValidationSetsHistory(st) +} + // TemporaryDB returns a temporary database stacked on top of the assertions // database. Writing to it will not affect the assertions database. func TemporaryDB(st *state.State) *asserts.Database { diff -Nru snapd-2.53+21.10ubuntu1/overlord/assertstate/assertstate_test.go snapd-2.54.2+21.10/overlord/assertstate/assertstate_test.go --- snapd-2.53+21.10ubuntu1/overlord/assertstate/assertstate_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/assertstate/assertstate_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -3023,7 +3023,7 @@ } sequence := 0 - vs, err := assertstate.ValidationSetAssertionForEnforce(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps) + vs, err := assertstate.ValidationSetAssertionForEnforce(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) c.Assert(err, IsNil) c.Check(vs.Revision(), Equals, 2) c.Check(vs.Sequence(), Equals, 2) @@ -3060,7 +3060,7 @@ } sequence := 2 - vs, err := assertstate.ValidationSetAssertionForEnforce(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps) + vs, err := assertstate.ValidationSetAssertionForEnforce(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) c.Assert(err, IsNil) c.Check(vs.Revision(), Equals, 2) c.Check(vs.Sequence(), Equals, 2) @@ -3095,7 +3095,7 @@ snaps := []*snapasserts.InstalledSnap{} sequence := 0 - _, err := assertstate.ValidationSetAssertionForEnforce(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps) + _, err := assertstate.ValidationSetAssertionForEnforce(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) c.Assert(err, NotNil) verr, ok := err.(*snapasserts.ValidationSetsValidationError) c.Assert(ok, Equals, true) @@ -3147,7 +3147,7 @@ snaps := []*snapasserts.InstalledSnap{} sequence := 0 - _, err := assertstate.ValidationSetAssertionForEnforce(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps) + _, err := assertstate.ValidationSetAssertionForEnforce(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) c.Check(err, ErrorMatches, fmt.Sprintf(`validation sets are in conflict:\n- cannot constrain snap "foo" as both invalid \(%s/boo\) and required at revision 1 \(%s/bar\)`, s.dev1Acct.AccountID(), s.dev1Acct.AccountID())) // and it hasn't been committed @@ -3187,7 +3187,7 @@ } sequence := 0 - vs, err := assertstate.ValidationSetAssertionForEnforce(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps) + vs, err := assertstate.ValidationSetAssertionForEnforce(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) c.Assert(err, IsNil) // new assertion got fetched c.Check(vs.Revision(), Equals, 5) @@ -3238,7 +3238,7 @@ } sequence := 0 - vs, err := assertstate.ValidationSetAssertionForEnforce(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps) + vs, err := assertstate.ValidationSetAssertionForEnforce(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) c.Assert(err, IsNil) // new assertion got fetched c.Check(vs.Revision(), Equals, 5) @@ -3331,3 +3331,294 @@ c.Assert(err, IsNil) c.Assert(fromDB.(*asserts.Model), DeepEquals, model) } + +func (s *assertMgrSuite) TestEnforceValidationSetAssertion(c *C) { + st := s.state + + st.Lock() + defer st.Unlock() + + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + c.Assert(s.storeSigning.Add(storeAs), IsNil) + c.Assert(assertstate.Add(st, s.storeSigning.StoreAccountKey("")), IsNil) + c.Assert(assertstate.Add(st, s.dev1Acct), IsNil) + c.Assert(assertstate.Add(st, s.dev1AcctKey), IsNil) + + // add sequence to the store + vsetAs := s.validationSetAssert(c, "bar", "2", "2", "required", "1") + c.Assert(s.storeSigning.Add(vsetAs), IsNil) + + snaps := []*snapasserts.InstalledSnap{ + snapasserts.NewInstalledSnap("foo", "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", snap.Revision{N: 1}), + } + + sequence := 2 + err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) + c.Assert(err, IsNil) + + // and it has been committed + _, err = assertstate.DB(s.state).Find(asserts.ValidationSetType, map[string]string{ + "series": "16", + "account-id": s.dev1Acct.AccountID(), + "name": "bar", + "sequence": "2", + }) + c.Assert(err, IsNil) + c.Check(s.fakeStore.(*fakeStore).opts.IsAutoRefresh, Equals, false) + + var tr assertstate.ValidationSetTracking + c.Assert(assertstate.GetValidationSet(s.state, s.dev1Acct.AccountID(), "bar", &tr), IsNil) + + c.Check(tr, DeepEquals, assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "bar", + Mode: assertstate.Enforce, + PinnedAt: 2, + Current: 2, + }) + + // and it was added to the history + vshist, err := assertstate.ValidationSetsHistory(st) + c.Assert(err, IsNil) + c.Check(vshist, DeepEquals, []map[string]*assertstate.ValidationSetTracking{{ + fmt.Sprintf("%s/bar", s.dev1Acct.AccountID()): { + AccountID: s.dev1Acct.AccountID(), + Name: "bar", + Mode: assertstate.Enforce, + PinnedAt: 2, + Current: 2, + }, + }}) +} + +func (s *assertMgrSuite) TestEnforceValidationSetAssertionAfterMonitor(c *C) { + st := s.state + + st.Lock() + defer st.Unlock() + + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + c.Assert(s.storeSigning.Add(storeAs), IsNil) + c.Assert(assertstate.Add(st, s.storeSigning.StoreAccountKey("")), IsNil) + c.Assert(assertstate.Add(st, s.dev1Acct), IsNil) + c.Assert(assertstate.Add(st, s.dev1AcctKey), IsNil) + + // add and old assertion to local database + vsetAs1 := s.validationSetAssert(c, "bar", "1", "1", "required", "1") + c.Assert(assertstate.Add(st, vsetAs1), IsNil) + + monitor := assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "bar", + Mode: assertstate.Monitor, + Current: 1, + } + assertstate.UpdateValidationSet(st, &monitor) + + snaps := []*snapasserts.InstalledSnap{ + snapasserts.NewInstalledSnap("foo", "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", snap.Revision{N: 1}), + } + + // add a newer sequence to the store + vsetAs := s.validationSetAssert(c, "bar", "2", "2", "required", "1") + c.Assert(s.storeSigning.Add(vsetAs), IsNil) + + sequence := 2 + err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, nil) + c.Assert(err, IsNil) + + // and it has been committed + _, err = assertstate.DB(s.state).Find(asserts.ValidationSetType, map[string]string{ + "series": "16", + "account-id": s.dev1Acct.AccountID(), + "name": "bar", + "sequence": "2", + }) + c.Assert(err, IsNil) + c.Check(s.fakeStore.(*fakeStore).opts.IsAutoRefresh, Equals, false) + + var tr assertstate.ValidationSetTracking + c.Assert(assertstate.GetValidationSet(s.state, s.dev1Acct.AccountID(), "bar", &tr), IsNil) + + c.Check(tr, DeepEquals, assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "bar", + Mode: assertstate.Enforce, + PinnedAt: 2, + Current: 2, + }) +} + +func (s *assertMgrSuite) TestEnforceValidationSetAssertionIgnoreValidation(c *C) { + st := s.state + + st.Lock() + defer st.Unlock() + + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + c.Assert(s.storeSigning.Add(storeAs), IsNil) + c.Assert(assertstate.Add(st, s.storeSigning.StoreAccountKey("")), IsNil) + c.Assert(assertstate.Add(st, s.dev1Acct), IsNil) + c.Assert(assertstate.Add(st, s.dev1AcctKey), IsNil) + + // add sequence to the store + vsetAs := s.validationSetAssert(c, "bar", "2", "2", "required", "1") + c.Assert(s.storeSigning.Add(vsetAs), IsNil) + + snaps := []*snapasserts.InstalledSnap{ + snapasserts.NewInstalledSnap("foo", "qOqKhntON3vR7kwEbVPsILm7bUViPDzz", snap.Revision{N: 3}), + } + + sequence := 2 + ignoreValidation := map[string]bool{} + err := assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, ignoreValidation) + wrongRevErr, ok := err.(*snapasserts.ValidationSetsValidationError) + c.Assert(ok, Equals, true) + c.Check(wrongRevErr.WrongRevisionSnaps["foo"], NotNil) + + ignoreValidation["foo"] = true + err = assertstate.EnforceValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0, snaps, ignoreValidation) + c.Assert(err, IsNil) + + // and it has been committed + _, err = assertstate.DB(s.state).Find(asserts.ValidationSetType, map[string]string{ + "series": "16", + "account-id": s.dev1Acct.AccountID(), + "name": "bar", + "sequence": "2", + }) + c.Assert(err, IsNil) + c.Check(s.fakeStore.(*fakeStore).opts.IsAutoRefresh, Equals, false) + + var tr assertstate.ValidationSetTracking + c.Assert(assertstate.GetValidationSet(s.state, s.dev1Acct.AccountID(), "bar", &tr), IsNil) + + c.Check(tr, DeepEquals, assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "bar", + Mode: assertstate.Enforce, + PinnedAt: 2, + Current: 2, + }) +} + +func (s *assertMgrSuite) TestMonitorValidationSet(c *C) { + st := s.state + + st.Lock() + defer st.Unlock() + + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + c.Assert(s.storeSigning.Add(storeAs), IsNil) + c.Assert(assertstate.Add(st, s.storeSigning.StoreAccountKey("")), IsNil) + c.Assert(assertstate.Add(st, s.dev1Acct), IsNil) + c.Assert(assertstate.Add(st, s.dev1AcctKey), IsNil) + + // add to the store + vsetAs := s.validationSetAssert(c, "bar", "2", "2", "required", "1") + c.Assert(s.storeSigning.Add(vsetAs), IsNil) + + sequence := 2 + err := assertstate.MonitorValidationSet(st, s.dev1Acct.AccountID(), "bar", sequence, 0) + c.Assert(err, IsNil) + + // and it has been committed + _, err = assertstate.DB(s.state).Find(asserts.ValidationSetType, map[string]string{ + "series": "16", + "account-id": s.dev1Acct.AccountID(), + "name": "bar", + "sequence": "2", + }) + c.Assert(err, IsNil) + c.Check(s.fakeStore.(*fakeStore).opts.IsAutoRefresh, Equals, false) + + var tr assertstate.ValidationSetTracking + c.Assert(assertstate.GetValidationSet(s.state, s.dev1Acct.AccountID(), "bar", &tr), IsNil) + + c.Check(tr, DeepEquals, assertstate.ValidationSetTracking{ + AccountID: s.dev1Acct.AccountID(), + Name: "bar", + Mode: assertstate.Monitor, + PinnedAt: 2, + Current: 2, + }) + + // and it was added to the history + vshist, err := assertstate.ValidationSetsHistory(st) + c.Assert(err, IsNil) + c.Check(vshist, DeepEquals, []map[string]*assertstate.ValidationSetTracking{{ + fmt.Sprintf("%s/bar", s.dev1Acct.AccountID()): { + AccountID: s.dev1Acct.AccountID(), + Name: "bar", + Mode: assertstate.Monitor, + PinnedAt: 2, + Current: 2, + }, + }}) +} + +func (s *assertMgrSuite) TestForgetValidationSet(c *C) { + st := s.state + + st.Lock() + defer st.Unlock() + + // have a model and the store assertion available + storeAs := s.setupModelAndStore(c) + c.Assert(s.storeSigning.Add(storeAs), IsNil) + c.Assert(assertstate.Add(st, s.storeSigning.StoreAccountKey("")), IsNil) + c.Assert(assertstate.Add(st, s.dev1Acct), IsNil) + c.Assert(assertstate.Add(st, s.dev1AcctKey), IsNil) + + // add to the store + vsetAs1 := s.validationSetAssert(c, "bar", "2", "2", "required", "1") + c.Assert(s.storeSigning.Add(vsetAs1), IsNil) + + vsetAs2 := s.validationSetAssert(c, "baz", "2", "2", "required", "1") + c.Assert(s.storeSigning.Add(vsetAs2), IsNil) + + c.Assert(assertstate.MonitorValidationSet(st, s.dev1Acct.AccountID(), "bar", 2, 0), IsNil) + c.Assert(assertstate.MonitorValidationSet(st, s.dev1Acct.AccountID(), "baz", 2, 0), IsNil) + + c.Assert(assertstate.ForgetValidationSet(st, s.dev1Acct.AccountID(), "bar"), IsNil) + + // and it was added to the history + vshist, err := assertstate.ValidationSetsHistory(st) + c.Assert(err, IsNil) + c.Check(vshist, DeepEquals, []map[string]*assertstate.ValidationSetTracking{{ + fmt.Sprintf("%s/bar", s.dev1Acct.AccountID()): { + AccountID: s.dev1Acct.AccountID(), + Name: "bar", + Mode: assertstate.Monitor, + PinnedAt: 2, + Current: 2, + }, + }, { + fmt.Sprintf("%s/bar", s.dev1Acct.AccountID()): { + AccountID: s.dev1Acct.AccountID(), + Name: "bar", + Mode: assertstate.Monitor, + PinnedAt: 2, + Current: 2, + }, + fmt.Sprintf("%s/baz", s.dev1Acct.AccountID()): { + AccountID: s.dev1Acct.AccountID(), + Name: "baz", + Mode: assertstate.Monitor, + PinnedAt: 2, + Current: 2, + }, + }, { + fmt.Sprintf("%s/baz", s.dev1Acct.AccountID()): { + AccountID: s.dev1Acct.AccountID(), + Name: "baz", + Mode: assertstate.Monitor, + PinnedAt: 2, + Current: 2, + }, + }}) +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/assertstate/export_test.go snapd-2.54.2+21.10/overlord/assertstate/export_test.go --- snapd-2.53+21.10ubuntu1/overlord/assertstate/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/assertstate/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -21,7 +21,11 @@ // expose for testing var ( - DoFetch = doFetch + DoFetch = doFetch + ValidationSetAssertionForEnforce = validationSetAssertionForEnforce + ValidationSetAssertionForMonitor = validationSetAssertionForMonitor + AddCurrentTrackingToValidationSetsHistory = addCurrentTrackingToValidationSetsHistory + ValidationSetsHistoryTop = validationSetsHistoryTop ) func MockMaxGroups(n int) (restore func()) { @@ -31,3 +35,11 @@ maxGroups = oldMaxGroups } } + +func MockMaxValidationSetsHistorySize(n int) (restore func()) { + oldMaxValidationSetsHistorySize := maxValidationSetsHistorySize + maxValidationSetsHistorySize = n + return func() { + maxValidationSetsHistorySize = oldMaxValidationSetsHistorySize + } +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/assertstate/validation_set_tracking.go snapd-2.54.2+21.10/overlord/assertstate/validation_set_tracking.go --- snapd-2.53+21.10ubuntu1/overlord/assertstate/validation_set_tracking.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/assertstate/validation_set_tracking.go 2022-01-06 21:25:16.000000000 +0000 @@ -29,6 +29,9 @@ "github.com/snapcore/snapd/release" ) +// maximum number of entries kept in validation-sets-history in the state +var maxValidationSetsHistorySize = 50 + // ValidationSetMode reflects the mode of respective validation set, which is // either monitoring or enforcing. type ValidationSetMode int @@ -57,6 +60,12 @@ LocalOnly bool `json:"local-only,omitempty"` } +func (vs *ValidationSetTracking) sameAs(tr *ValidationSetTracking) bool { + return vs.AccountID == tr.AccountID && vs.Current == tr.Current && + vs.LocalOnly == tr.LocalOnly && vs.Mode == tr.Mode && + vs.Name == tr.Name && vs.PinnedAt == tr.PinnedAt +} + // ValidationSetKey formats the given account id and name into a validation set key. func ValidationSetKey(accountID, name string) string { return fmt.Sprintf("%s/%s", accountID, name) @@ -83,19 +92,21 @@ st.Set("validation-sets", vsmap) } -// DeleteValidationSet deletes a validation set for the given accoundID and name. +// ForgetValidationSet deletes a validation set for the given accountID and name. // It is not an error to delete a non-existing one. -func DeleteValidationSet(st *state.State, accountID, name string) { +func ForgetValidationSet(st *state.State, accountID, name string) error { var vsmap map[string]*json.RawMessage err := st.Get("validation-sets", &vsmap) if err != nil && err != state.ErrNoState { panic("internal error: cannot unmarshal validation set tracking state: " + err.Error()) } if len(vsmap) == 0 { - return + return nil } delete(vsmap, ValidationSetKey(accountID, name)) st.Set("validation-sets", vsmap) + + return addCurrentTrackingToValidationSetsHistory(st) } // GetValidationSet retrieves the ValidationSetTracking for the given account and name. @@ -172,3 +183,97 @@ return sets, err } + +// addCurrentTrackingToValidationSetsHistory stores the current state of validation-sets +// tracking on top of the validation sets history. +func addCurrentTrackingToValidationSetsHistory(st *state.State) error { + current, err := ValidationSets(st) + if err != nil { + return err + } + return addToValidationSetsHistory(st, current) +} + +func addToValidationSetsHistory(st *state.State, validationSets map[string]*ValidationSetTracking) error { + vshist, err := ValidationSetsHistory(st) + if err != nil { + return err + } + + // if nothing is being tracked and history is empty (meaning nothing was + // tracked before), then don't store anything. + // if nothing is being tracked but history is not empty, then we want to + // store empty tracking - this means snap validate --forget was used and + // we need to remember such empty state in the history. + if len(validationSets) == 0 && len(vshist) == 0 { + return nil + } + + var matches bool + if len(vshist) > 0 { + // only add to the history if it's different than topmost entry + top := vshist[len(vshist)-1] + if len(top) == len(validationSets) { + matches = true + for vskey, vset := range validationSets { + prev, ok := top[vskey] + if !ok || !prev.sameAs(vset) { + matches = false + break + } + } + } + } + if !matches { + vshist = append(vshist, validationSets) + } + if len(vshist) > maxValidationSetsHistorySize { + vshist = vshist[len(vshist)-maxValidationSetsHistorySize:] + } + st.Set("validation-sets-history", &vshist) + return nil +} + +// validationSetsHistoryTop returns the topmost validation sets tracking state from +// the validations sets tracking history. +func validationSetsHistoryTop(st *state.State) (map[string]*ValidationSetTracking, error) { + var vshist []*json.RawMessage + if err := st.Get("validation-sets-history", &vshist); err != nil && err != state.ErrNoState { + return nil, err + } + if len(vshist) == 0 { + return nil, nil + } + // decode just the topmost entry + raw := vshist[len(vshist)-1] + var top map[string]*ValidationSetTracking + if err := json.Unmarshal([]byte(*raw), &top); err != nil { + return nil, fmt.Errorf("cannot unmarshal validation set tracking state: %v", err) + } + return top, nil +} + +// ValidationSetsHistory returns the complete history of validation sets tracking. +func ValidationSetsHistory(st *state.State) ([]map[string]*ValidationSetTracking, error) { + var vshist []map[string]*ValidationSetTracking + if err := st.Get("validation-sets-history", &vshist); err != nil && err != state.ErrNoState { + return nil, err + } + return vshist, nil +} + +// RestoreValidationSetsTracking restores validation-sets state to the last state +// stored in the validation-sets-stack. It should only be called when the stack +// is not empty, otherwise an error is returned. +func RestoreValidationSetsTracking(st *state.State) error { + trackingState, err := validationSetsHistoryTop(st) + if err != nil { + return err + } + if len(trackingState) == 0 { + // we should never be called when there is nothing in the stack + return state.ErrNoState + } + st.Set("validation-sets", trackingState) + return nil +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/assertstate/validation_set_tracking_test.go snapd-2.54.2+21.10/overlord/assertstate/validation_set_tracking_test.go --- snapd-2.53+21.10ubuntu1/overlord/assertstate/validation_set_tracking_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/assertstate/validation_set_tracking_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -30,8 +30,7 @@ ) type validationSetTrackingSuite struct { - st *state.State - //storeSigning *assertstest.StoreStack + st *state.State dev1Signing *assertstest.SigningDB dev1acct *asserts.Account } @@ -134,12 +133,13 @@ c.Check(gotSecond, Equals, true) } -func (s *validationSetTrackingSuite) TestDelete(c *C) { +// there is a more extensive test for forget in assertstate_test.go. +func (s *validationSetTrackingSuite) TestForget(c *C) { s.st.Lock() defer s.st.Unlock() // delete non-existing one is fine - assertstate.DeleteValidationSet(s.st, "foo", "bar") + assertstate.ForgetValidationSet(s.st, "foo", "bar") all, err := assertstate.ValidationSets(s.st) c.Assert(err, IsNil) c.Assert(all, HasLen, 0) @@ -155,8 +155,8 @@ c.Assert(err, IsNil) c.Assert(all, HasLen, 1) - // deletes existing one - assertstate.DeleteValidationSet(s.st, "foo", "bar") + // forget existing one + assertstate.ForgetValidationSet(s.st, "foo", "bar") all, err = assertstate.ValidationSets(s.st) c.Assert(err, IsNil) c.Assert(all, HasLen, 0) @@ -255,3 +255,210 @@ err = valsets.Conflict() c.Check(err, ErrorMatches, `validation sets are in conflict:\n- cannot constrain snap "snap-b" as both invalid \(.*/bar\) and required at any revision \(.*/foo\)`) } + +func (s *validationSetTrackingSuite) TestAddToValidationSetsHistory(c *C) { + s.st.Lock() + defer s.st.Unlock() + + all, err := assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + c.Assert(all, HasLen, 0) + + tr1 := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + PinnedAt: 1, + Current: 2, + } + assertstate.UpdateValidationSet(s.st, &tr1) + tr2 := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "baz", + Mode: assertstate.Monitor, + Current: 4, + } + assertstate.UpdateValidationSet(s.st, &tr2) + + c.Assert(assertstate.AddCurrentTrackingToValidationSetsHistory(s.st), IsNil) + top, err := assertstate.ValidationSetsHistoryTop(s.st) + c.Assert(err, IsNil) + c.Check(top, DeepEquals, map[string]*assertstate.ValidationSetTracking{ + "foo/bar": { + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + PinnedAt: 1, + Current: 2, + }, + "foo/baz": { + AccountID: "foo", + Name: "baz", + Mode: assertstate.Monitor, + Current: 4, + }, + }) + + // adding unchanged validation set tracking doesn't create another entry + c.Assert(assertstate.AddCurrentTrackingToValidationSetsHistory(s.st), IsNil) + top2, err := assertstate.ValidationSetsHistoryTop(s.st) + c.Assert(err, IsNil) + c.Check(top, DeepEquals, top2) + vshist, err := assertstate.ValidationSetsHistory(s.st) + c.Assert(err, IsNil) + c.Check(vshist, HasLen, 1) + + tr3 := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "boo", + Mode: assertstate.Enforce, + Current: 2, + } + assertstate.UpdateValidationSet(s.st, &tr3) + c.Assert(assertstate.AddCurrentTrackingToValidationSetsHistory(s.st), IsNil) + + vshist, err = assertstate.ValidationSetsHistory(s.st) + c.Assert(err, IsNil) + // the history now has 2 entries + c.Check(vshist, HasLen, 2) + + top3, err := assertstate.ValidationSetsHistoryTop(s.st) + c.Assert(err, IsNil) + c.Check(top3, DeepEquals, map[string]*assertstate.ValidationSetTracking{ + "foo/bar": { + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + PinnedAt: 1, + Current: 2, + }, + "foo/baz": { + AccountID: "foo", + Name: "baz", + Mode: assertstate.Monitor, + Current: 4, + }, + "foo/boo": { + AccountID: "foo", + Name: "boo", + Mode: assertstate.Enforce, + Current: 2, + }, + }) +} + +func (s *validationSetTrackingSuite) TestAddToValidationSetsHistoryRemovesOldEntries(c *C) { + restore := assertstate.MockMaxValidationSetsHistorySize(4) + defer restore() + + s.st.Lock() + defer s.st.Unlock() + + all, err := assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + c.Assert(all, HasLen, 0) + + for i := 1; i <= 6; i++ { + tr := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + Current: i, + } + assertstate.UpdateValidationSet(s.st, &tr) + + c.Assert(assertstate.AddCurrentTrackingToValidationSetsHistory(s.st), IsNil) + } + + vshist, err := assertstate.ValidationSetsHistory(s.st) + c.Assert(err, IsNil) + + // two first entries got dropped + c.Check(vshist, DeepEquals, []map[string]*assertstate.ValidationSetTracking{ + { + "foo/bar": { + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + Current: 3, + }, + }, + { + "foo/bar": { + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + Current: 4, + }, + }, + { + "foo/bar": { + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + Current: 5, + }, + }, + { + "foo/bar": { + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + Current: 6, + }, + }, + }) +} + +func (s *validationSetTrackingSuite) TestRestoreValidationSetsTrackingNoHistory(c *C) { + s.st.Lock() + defer s.st.Unlock() + + c.Assert(assertstate.RestoreValidationSetsTracking(s.st), Equals, state.ErrNoState) +} + +func (s *validationSetTrackingSuite) TestRestoreValidationSetsTracking(c *C) { + s.st.Lock() + defer s.st.Unlock() + + tr1 := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + PinnedAt: 1, + Current: 2, + } + assertstate.UpdateValidationSet(s.st, &tr1) + + c.Assert(assertstate.AddCurrentTrackingToValidationSetsHistory(s.st), IsNil) + + all, err := assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + c.Assert(all, HasLen, 1) + + tr2 := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "baz", + Mode: assertstate.Enforce, + Current: 5, + } + assertstate.UpdateValidationSet(s.st, &tr2) + + all, err = assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + // two validation sets are now tracked + c.Check(all, DeepEquals, map[string]*assertstate.ValidationSetTracking{ + "foo/bar": &tr1, + "foo/baz": &tr2, + }) + + // restore + c.Assert(assertstate.RestoreValidationSetsTracking(s.st), IsNil) + + // and we're back at one validation set being tracked + all, err = assertstate.ValidationSets(s.st) + c.Assert(err, IsNil) + c.Check(all, DeepEquals, map[string]*assertstate.ValidationSetTracking{ + "foo/bar": &tr1, + }) +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/backend.go snapd-2.54.2+21.10/overlord/backend.go --- snapd-2.53+21.10ubuntu1/overlord/backend.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/backend.go 2022-01-06 21:25:16.000000000 +0000 @@ -23,13 +23,11 @@ "time" "github.com/snapcore/snapd/osutil" - "github.com/snapcore/snapd/overlord/state" ) type overlordStateBackend struct { - path string - ensureBefore func(d time.Duration) - requestRestart func(t state.RestartType) + path string + ensureBefore func(d time.Duration) } func (osb *overlordStateBackend) Checkpoint(data []byte) error { @@ -39,7 +37,3 @@ func (osb *overlordStateBackend) EnsureBefore(d time.Duration) { osb.ensureBefore(d) } - -func (osb *overlordStateBackend) RequestRestart(t state.RestartType) { - osb.requestRestart(t) -} diff -Nru snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/certs.go snapd-2.54.2+21.10/overlord/configstate/configcore/certs.go --- snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/certs.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/configstate/configcore/certs.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff -Nru snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/cloud.go snapd-2.54.2+21.10/overlord/configstate/configcore/cloud.go --- snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/cloud.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/configstate/configcore/cloud.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff -Nru snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/handlers.go snapd-2.54.2+21.10/overlord/configstate/configcore/handlers.go --- snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/handlers.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/configstate/configcore/handlers.go 2022-01-06 21:25:16.000000000 +0000 @@ -97,8 +97,12 @@ // system.timezone addFSOnlyHandler(validateTimezoneSettings, handleTimezoneConfiguration, coreOnly) - // system.hostname - addFSOnlyHandler(validateHostnameSettings, handleHostnameConfiguration, coreOnly) + // system.hostname - note that the validation is done via hostnamectl + // when applying so there is no validation handler, see LP:1952740 + addFSOnlyHandler(nil, handleHostnameConfiguration, coreOnly) + + // tmpfs.size + addFSOnlyHandler(validateTmpfsSettings, handleTmpfsConfiguration, coreOnly) sysconfig.ApplyFilesystemOnlyDefaultsImpl = filesystemOnlyApply } diff -Nru snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/hostname.go snapd-2.54.2+21.10/overlord/configstate/configcore/hostname.go --- snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/hostname.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/configstate/configcore/hostname.go 2022-01-06 21:25:16.000000000 +0000 @@ -38,22 +38,23 @@ config.RegisterExternalConfig("core", "system.hostname", getHostnameFromSystemHelper) } -// We are conservative here and follow hostname(7). The hostnamectl -// binary is more liberal but let's err on the side of caution for -// now. -var validHostname = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{1,62}$`).MatchString - -func validateHostnameSettings(tr config.ConfGetter) error { - hostname, err := coreCfg(tr, "system.hostname") - if err != nil { - return err - } - if hostname == "" { - return nil - } - if !validHostname(hostname) { +// The hostname can also be set via hostnamectl so we cannot be more strict +// than hostnamectl itself. +// See: systemd/src/basic/hostname-util.c:ostname_is_valid +var validHostnameRegexp = regexp.MustCompile(`^[a-zA-Z0-9][a-zA-Z0-9-]{0,62}(\.[a-zA-Z0-9-]{1,63})*$`).MatchString + +// Note that HOST_NAME_MAX is 64 on Linux, but DNS allows domain names +// up to 255 characters +const HOST_NAME_MAX = 64 + +func validateHostname(hostname string) error { + validHostname := validHostnameRegexp(hostname) + if !validHostname { return fmt.Errorf("cannot set hostname %q: name not valid", hostname) } + if len(hostname) > HOST_NAME_MAX { + return fmt.Errorf("cannot set hostname %q: name too long", hostname) + } return nil } @@ -81,6 +82,10 @@ return fmt.Errorf("cannot set hostname: %v", osutil.OutputErr(output, err)) } } else { + if err := validateHostname(hostname); err != nil { + return err + } + // On the UC16/UC18/UC20 images the file /etc/hostname is a // symlink to /etc/writable/hostname. The /etc/hostname is // not part of the "writable-path" so we must set the file @@ -103,7 +108,18 @@ } func getHostnameFromSystem() (string, error) { - output, err := exec.Command("hostname").CombinedOutput() + // try pretty hostname first + output, err := exec.Command("hostnamectl", "status", "--pretty").CombinedOutput() + if err != nil { + return "", fmt.Errorf("cannot get hostname (pretty): %v", osutil.OutputErr(output, err)) + } + prettyHostname := strings.TrimSpace(string(output)) + if len(prettyHostname) > 0 { + return prettyHostname, nil + } + + // then static hostname + output, err = exec.Command("hostnamectl", "status", "--static").CombinedOutput() if err != nil { return "", fmt.Errorf("cannot get hostname: %v", osutil.OutputErr(output, err)) } diff -Nru snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/hostname_test.go snapd-2.54.2+21.10/overlord/configstate/configcore/hostname_test.go --- snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/hostname_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/configstate/configcore/hostname_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -46,48 +46,84 @@ err := os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "/etc/"), 0755) c.Assert(err, IsNil) - s.mockedHostnamectl = testutil.MockCommand(c, "hostnamectl", "") + script := `if [ "$1" = "status" ]; then echo bar; fi` + s.mockedHostnamectl = testutil.MockCommand(c, "hostnamectl", script) s.AddCleanup(s.mockedHostnamectl.Restore) + + restore := release.MockOnClassic(false) + s.AddCleanup(restore) } -func (s *hostnameSuite) TestConfigureHostnameInvalid(c *C) { +func (s *hostnameSuite) TestConfigureHostnameFsOnlyInvalid(c *C) { + tmpdir := c.MkDir() + + filler := strings.Repeat("x", 60) invalidHostnames := []string{ - "-no-start-with-dash", "no-upper-A", "no-ä", "no/slash", - "ALL-CAPS-IS-NEVER-OKAY", "no-SHOUTING-allowed", + "-no-start-with-dash", "no-ä", "no/slash", "foo..bar", strings.Repeat("x", 64), + strings.Join([]string{filler, filler, filler, filler, filler}, "."), + // systemd testcases, see test-hostname-util.c + "foobar.com.", "fooBAR.", "fooBAR.com.", "fööbar", + ".", "..", "foobar.", ".foobar", "foo..bar", "foo.bar..", + "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx", + "au-xph5-rvgrdsb5hcxc-47et3a5vvkrc-server-wyoz4elpdpe3.openstack.local", } for _, name := range invalidHostnames { - err := configcore.Run(coreDev, &mockConf{ - state: s.state, - conf: map[string]interface{}{ - "system.hostname": name, - }, + conf := configcore.PlainCoreConfig(map[string]interface{}{ + "system.hostname": name, }) - c.Assert(err, ErrorMatches, `cannot set hostname.*`) + err := configcore.FilesystemOnlyApply(coreDev, tmpdir, conf) + c.Assert(err, ErrorMatches, `cannot set hostname.*`, Commentf("%v", name)) } c.Check(s.mockedHostnamectl.Calls(), HasLen, 0) } -func (s *hostnameSuite) TestConfigureHostnameIntegration(c *C) { - restore := release.MockOnClassic(false) - defer restore() - - mockedHostname := testutil.MockCommand(c, "hostname", "echo bar") - defer mockedHostname.Restore() +func (s *hostnameSuite) TestConfigureHostnameFsOnlyHappy(c *C) { + tmpdir := c.MkDir() + filler := strings.Repeat("x", 16) validHostnames := []string{ + "a", "foo", strings.Repeat("x", 63), "foo-bar", "foo-------bar", "foo99", "99foo", + "localhost.localdomain", + "foo.-bar.com", "can-end-with-a-dash-", + // can look like a serial + "C253432146-00214", + "C253432146-00214UPPERATTHEENDTOO", + // FQDN is ok too + "CS1.lse.ac.uk.edu", + // 3*16 + 12 + 3 dots = 63 + strings.Join([]string{filler, filler, filler, strings.Repeat("x", 12)}, "."), + // systemd testcases, see test-hostname-util.c + "foobar", "foobar.com", "fooBAR", "fooBAR.com", + } + + for _, name := range validHostnames { + conf := configcore.PlainCoreConfig(map[string]interface{}{ + "system.hostname": name, + }) + err := configcore.FilesystemOnlyApply(coreDev, tmpdir, conf) + c.Assert(err, IsNil) + } + + c.Check(s.mockedHostnamectl.Calls(), HasLen, 0) +} + +func (s *hostnameSuite) TestConfigureHostnameWithStateOnlyHostnamectlValidates(c *C) { + hostnames := []string{ + "good", + "bäd-hostname-is-only-validated-by-hostnamectl", } - for _, hostname := range validHostnames { + for _, hostname := range hostnames { err := configcore.Run(coreDev, &mockConf{ state: s.state, conf: map[string]interface{}{ @@ -95,39 +131,82 @@ }, }) c.Assert(err, IsNil) - c.Check(mockedHostname.Calls(), DeepEquals, [][]string{ - {"hostname"}, - }) c.Check(s.mockedHostnamectl.Calls(), DeepEquals, [][]string{ + {"hostnamectl", "status", "--pretty"}, {"hostnamectl", "set-hostname", hostname}, }) s.mockedHostnamectl.ForgetCalls() - mockedHostname.ForgetCalls() } } +func (s *hostnameSuite) TestConfigureHostnameWithStateOnlyHostnamectlUnhappy(c *C) { + script := ` +if [ "$1" = "status" ]; then + echo bar; +else + echo "some error" + exit 1 +fi` + mockedHostnamectl := testutil.MockCommand(c, "hostnamectl", script) + defer mockedHostnamectl.Restore() + + hostname := "simulated-invalid-hostname" + err := configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "system.hostname": hostname, + }, + }) + c.Assert(err, ErrorMatches, "cannot set hostname: some error") + c.Check(mockedHostnamectl.Calls(), DeepEquals, [][]string{ + {"hostnamectl", "status", "--pretty"}, + {"hostnamectl", "set-hostname", hostname}, + }) +} + func (s *hostnameSuite) TestConfigureHostnameIntegrationSameHostname(c *C) { - restore := release.MockOnClassic(false) - defer restore() + // and set new hostname to "bar" but the "s.mockedHostnamectl" is + // already returning "bar" + err := configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + // hostname is already "bar" + "system.hostname": "bar", + }, + }) + c.Assert(err, IsNil) + c.Check(s.mockedHostnamectl.Calls(), DeepEquals, [][]string{ + {"hostnamectl", "status", "--pretty"}, + }) +} - // pretent current hostname is "foo" - mockedHostname := testutil.MockCommand(c, "hostname", "echo foo") - defer mockedHostname.Restore() - // and set new hostname to "foo" +func (s *hostnameSuite) TestConfigureHostnameIntegrationSameHostnameNoPretty(c *C) { + script := ` +if [ "$1" = "status" ] && [ "$2" = "--pretty" ]; then + # no pretty hostname, only a static one + exit 0; +elif [ "$1" = "status" ] && [ "$2" = "--static" ]; then + echo bar; +fi` + mockedHostnamectl := testutil.MockCommand(c, "hostnamectl", script) + defer mockedHostnamectl.Restore() + + // and set new hostname to "bar" err := configcore.Run(coreDev, &mockConf{ state: s.state, conf: map[string]interface{}{ - "system.hostname": "foo", + // hostname is already "bar" + "system.hostname": "bar", }, }) c.Assert(err, IsNil) - c.Check(mockedHostname.Calls(), DeepEquals, [][]string{ - {"hostname"}, + c.Check(mockedHostnamectl.Calls(), DeepEquals, [][]string{ + {"hostnamectl", "status", "--pretty"}, + {"hostnamectl", "status", "--static"}, }) - c.Check(s.mockedHostnamectl.Calls(), HasLen, 0) } -func (s *hostnameSuite) TestFilesystemOnlyApply(c *C) { +func (s *hostnameSuite) TestFilesystemOnlyApplyHappy(c *C) { conf := configcore.PlainCoreConfig(map[string]interface{}{ "system.hostname": "bar", }) diff -Nru snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/proxy.go snapd-2.54.2+21.10/overlord/configstate/configcore/proxy.go --- snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/proxy.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/configstate/configcore/proxy.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff -Nru snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/refresh.go snapd-2.54.2+21.10/overlord/configstate/configcore/refresh.go --- snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/refresh.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/configstate/configcore/refresh.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff -Nru snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/runwithstate.go snapd-2.54.2+21.10/overlord/configstate/configcore/runwithstate.go --- snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/runwithstate.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/configstate/configcore/runwithstate.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff -Nru snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/services_test.go snapd-2.54.2+21.10/overlord/configstate/configcore/services_test.go --- snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/services_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/configstate/configcore/services_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -50,9 +50,9 @@ output = []byte("ActiveState=inactive") } else { if s.serviceInstalled { - output = []byte(fmt.Sprintf("Id=%s\nType=daemon\nActiveState=inactive\nUnitFileState=enabled\n", args[2])) + output = []byte(fmt.Sprintf("Id=%s\nType=daemon\nActiveState=inactive\nUnitFileState=enabled\nNames=%[1]s\n", args[2])) } else { - output = []byte(fmt.Sprintf("Id=%s\nType=\nActiveState=inactive\nUnitFileState=\n", args[2])) + output = []byte(fmt.Sprintf("Id=%s\nType=\nActiveState=inactive\nUnitFileState=\nNames=%[1]s\n", args[2])) } } } @@ -79,7 +79,7 @@ err := configcore.SwitchDisableService("sshd.service", false, nil) c.Assert(err, IsNil) c.Check(s.systemctlArgs, DeepEquals, [][]string{ - {"show", "--property=Id,ActiveState,UnitFileState,Type", "sshd.service"}, + {"show", "--property=Id,ActiveState,UnitFileState,Type,Names", "sshd.service"}, {"unmask", "sshd.service"}, {"enable", "sshd.service"}, {"start", "sshd.service"}, @@ -90,7 +90,7 @@ err := configcore.SwitchDisableService("sshd.service", true, nil) c.Assert(err, IsNil) c.Check(s.systemctlArgs, DeepEquals, [][]string{ - {"show", "--property=Id,ActiveState,UnitFileState,Type", "sshd.service"}, + {"show", "--property=Id,ActiveState,UnitFileState,Type,Names", "sshd.service"}, {"disable", "sshd.service"}, {"mask", "sshd.service"}, {"stop", "sshd.service"}, @@ -136,7 +136,7 @@ default: if service.installed { c.Check(s.systemctlArgs, DeepEquals, [][]string{ - {"show", "--property=Id,ActiveState,UnitFileState,Type", srv}, + {"show", "--property=Id,ActiveState,UnitFileState,Type,Names", srv}, {"disable", srv}, {"mask", srv}, {"stop", srv}, @@ -144,7 +144,7 @@ }) } else { c.Check(s.systemctlArgs, DeepEquals, [][]string{ - {"show", "--property=Id,ActiveState,UnitFileState,Type", srv}, + {"show", "--property=Id,ActiveState,UnitFileState,Type,Names", srv}, }) } } @@ -294,14 +294,14 @@ default: if service.installed { c.Check(s.systemctlArgs, DeepEquals, [][]string{ - {"show", "--property=Id,ActiveState,UnitFileState,Type", srv}, + {"show", "--property=Id,ActiveState,UnitFileState,Type,Names", srv}, {"unmask", srv}, {"enable", srv}, {"start", srv}, }) } else { c.Check(s.systemctlArgs, DeepEquals, [][]string{ - {"show", "--property=Id,ActiveState,UnitFileState,Type", srv}, + {"show", "--property=Id,ActiveState,UnitFileState,Type,Names", srv}, }) } } diff -Nru snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/snapshots.go snapd-2.54.2+21.10/overlord/configstate/configcore/snapshots.go --- snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/snapshots.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/configstate/configcore/snapshots.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff -Nru snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/tmp.go snapd-2.54.2+21.10/overlord/configstate/configcore/tmp.go --- snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/tmp.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/configstate/configcore/tmp.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,145 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package configcore + +import ( + "errors" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/gadget/quantity" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/sysconfig" +) + +const ( + mntStaticOptions = "mode=1777,strictatime,nosuid,nodev" + tmpfsMountPoint = "/tmp" + tmpMntServOverrideSubDir = "tmp.mount.d" + tmpMntServOverrideFile = "override.conf" +) + +func init() { + // add supported configuration of this module + supportedConfigurations["core.tmp.size"] = true +} + +func validTmpfsSize(sizeStr string) error { + if sizeStr == "" { + return nil + } + + // TODO allow also percentages. That is allowed for CPU quotas so + // it is probably fine to allow that for tmp.size too. + size, err := quantity.ParseSize(sizeStr) + if err != nil { + return err + } + + // Do not allow less than 16mb + // 0 is special and means unlimited + if size > 0 && size < 16*quantity.SizeMiB { + return fmt.Errorf("size is less than 16Mb") + } + + return nil +} + +func validateTmpfsSettings(tr config.ConfGetter) error { + tmpfsSz, err := coreCfg(tr, "tmp.size") + if err != nil { + return err + } + + return validTmpfsSize(tmpfsSz) +} + +func handleTmpfsConfiguration(_ sysconfig.Device, tr config.ConfGetter, opts *fsOnlyContext) error { + tmpfsSz, err := coreCfg(tr, "tmp.size") + if err != nil { + return err + } + + // Create override configuration file for tmp.mount service + + // Create /etc/systemd/system/tmp.mount.d if needed + var overrDir string + if opts == nil { + // runtime system + overrDir = dirs.SnapServicesDir + } else { + overrDir = dirs.SnapServicesDirUnder(opts.RootDir) + } + overrDir = filepath.Join(overrDir, tmpMntServOverrideSubDir) + + // Write service config override if needed + options := mntStaticOptions + dirContent := make(map[string]osutil.FileState, 1) + cfgFilePath := filepath.Join(overrDir, tmpMntServOverrideFile) + modify := true + if tmpfsSz != "" { + if err := os.MkdirAll(overrDir, 0755); err != nil { + return err + } + options = fmt.Sprintf("%s,size=%s", options, tmpfsSz) + content := fmt.Sprintf("[Mount]\nOptions=%s\n", options) + dirContent[tmpMntServOverrideFile] = &osutil.MemoryFileState{ + Content: []byte(content), + Mode: 0644, + } + oldContent, err := ioutil.ReadFile(cfgFilePath) + if err == nil && content == string(oldContent) { + modify = false + } + } else { + // Use default tmpfs size if empty setting (50%, see tmpfs(5)) + options = fmt.Sprintf("%s,size=50%%", options) + // In this case, we are removing the file, so we will + // not do anything if the file is not there alreay. + if _, err := os.Stat(cfgFilePath); errors.Is(err, os.ErrNotExist) { + modify = false + } + } + + // Re-starting the tmp.mount service will fail if some process + // is using a file in /tmp, so instead of doing that we use + // the remount option for the mount command, which will not + // fail in that case. There is however the possibility of a + // failure in case we are reducing the size to something + // smaller than the currently used space in the mount. We + // return an error in that case. + if opts == nil && modify { + if output, err := exec.Command("mount", "-o", "remount,"+options, tmpfsMountPoint).CombinedOutput(); err != nil { + return fmt.Errorf("cannot remount tmpfs with new size: %s (%s)", err.Error(), output) + } + } + + glob := tmpMntServOverrideFile + if _, _, err = osutil.EnsureDirState(overrDir, glob, dirContent); err != nil { + return err + } + + return nil +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/tmp_test.go snapd-2.54.2+21.10/overlord/configstate/configcore/tmp_test.go --- snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/tmp_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/configstate/configcore/tmp_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,224 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package configcore_test + +import ( + "fmt" + "io/ioutil" + "os" + "path/filepath" + "time" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/configstate/configcore" + "github.com/snapcore/snapd/testutil" +) + +type tmpfsSuite struct { + configcoreSuite + + servOverridePath string + servOverrideDir string +} + +var _ = Suite(&tmpfsSuite{}) + +func (s *tmpfsSuite) SetUpTest(c *C) { + s.configcoreSuite.SetUpTest(c) + + s.servOverrideDir = filepath.Join(dirs.SnapServicesDir, "tmp.mount.d") + s.servOverridePath = filepath.Join(s.servOverrideDir, "override.conf") +} + +// Configure with different valid values +func (s *tmpfsSuite) TestConfigureTmpfsGoodVals(c *C) { + expectedMountCalls := [][]string{} + mountCmd := testutil.MockCommand(c, "mount", "") + defer mountCmd.Restore() + + for _, size := range []string{"104857600", "16M", "7G", "0"} { + + err := configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "tmp.size": size, + }, + }) + c.Assert(err, IsNil) + + c.Check(s.servOverridePath, testutil.FileEquals, + fmt.Sprintf("[Mount]\nOptions=mode=1777,strictatime,nosuid,nodev,size=%s\n", size)) + mntOpts := fmt.Sprintf("remount,mode=1777,strictatime,nosuid,nodev,size=%s", size) + expectedMountCalls = append(expectedMountCalls, []string{"mount", "-o", mntOpts, "/tmp"}) + } + + c.Check(s.systemctlArgs, HasLen, 0) + c.Check(mountCmd.Calls(), DeepEquals, expectedMountCalls) +} + +// Configure with different invalid values +func (s *tmpfsSuite) TestConfigureTmpfsBadVals(c *C) { + for _, size := range []string{"100p", "0x123", "10485f7600", "20%%", + "20%", "100m", "10k", "10K", "10g"} { + + err := configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "tmp.size": size, + }, + }) + c.Assert(err, ErrorMatches, `invalid suffix .*`) + + _, err = os.Stat(s.servOverridePath) + c.Assert(os.IsNotExist(err), Equals, true) + } + + c.Assert(s.systemctlArgs, IsNil) +} + +func (s *tmpfsSuite) TestConfigureTmpfsTooSmall(c *C) { + for _, size := range []string{"1", "16777215"} { + + err := configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "tmp.size": size, + }, + }) + c.Assert(err, ErrorMatches, `size is less than 16Mb`) + + _, err = os.Stat(s.servOverridePath) + c.Assert(os.IsNotExist(err), Equals, true) + } + + c.Assert(s.systemctlArgs, IsNil) +} + +// Ensure things are fine if destination folder already existed +func (s *tmpfsSuite) TestConfigureTmpfsAllConfDirExistsAlready(c *C) { + mountCmd := testutil.MockCommand(c, "mount", "") + defer mountCmd.Restore() + + // make tmp.mount.d directory already + err := os.MkdirAll(s.servOverrideDir, 0755) + c.Assert(err, IsNil) + + size := "100M" + err = configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "tmp.size": size, + }, + }) + c.Assert(err, IsNil) + c.Check(s.servOverridePath, testutil.FileEquals, + fmt.Sprintf("[Mount]\nOptions=mode=1777,strictatime,nosuid,nodev,size=%s\n", size)) + + c.Check(s.systemctlArgs, HasLen, 0) + c.Check(mountCmd.Calls(), DeepEquals, + [][]string{{"mount", "-o", "remount,mode=1777,strictatime,nosuid,nodev,size=100M", "/tmp"}}) +} + +// Test cfg file is not updated if we set the same size that is already set +func (s *tmpfsSuite) TestConfigureTmpfsNoFileUpdate(c *C) { + err := os.MkdirAll(s.servOverrideDir, 0755) + c.Assert(err, IsNil) + size := "100M" + content := "[Mount]\nOptions=mode=1777,strictatime,nosuid,nodev,size=" + size + "\n" + err = ioutil.WriteFile(s.servOverridePath, []byte(content), 0644) + c.Assert(err, IsNil) + + info, err := os.Stat(s.servOverridePath) + c.Assert(err, IsNil) + + fileModTime := info.ModTime() + + // To make sure the times will differ if the file is newly written + time.Sleep(100 * time.Millisecond) + + err = configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "tmp.size": size, + }, + }) + c.Assert(err, IsNil) + c.Check(s.servOverridePath, testutil.FileEquals, content) + + info, err = os.Stat(s.servOverridePath) + c.Assert(err, IsNil) + c.Assert(info.ModTime(), Equals, fileModTime) + + c.Check(s.systemctlArgs, HasLen, 0) +} + +// Test that config file is removed when unsetting +func (s *tmpfsSuite) TestConfigureTmpfsRemovesIfUnset(c *C) { + mountCmd := testutil.MockCommand(c, "mount", "") + defer mountCmd.Restore() + + err := os.MkdirAll(s.servOverrideDir, 0755) + c.Assert(err, IsNil) + + // add canary to ensure we don't touch other files + canary := filepath.Join(s.servOverrideDir, "05-canary.conf") + err = ioutil.WriteFile(canary, nil, 0644) + c.Assert(err, IsNil) + + content := "[Mount]\nOptions=mode=1777,strictatime,nosuid,nodev,size=1G\n" + err = ioutil.WriteFile(s.servOverridePath, []byte(content), 0644) + c.Assert(err, IsNil) + + err = configcore.Run(coreDev, &mockConf{ + state: s.state, + conf: map[string]interface{}{ + "tmp.size": "", + }, + }) + c.Assert(err, IsNil) + + // ensure the file got deleted + c.Check(osutil.FileExists(s.servOverridePath), Equals, false) + // but the canary is still here + c.Check(osutil.FileExists(canary), Equals, true) + + // the default was applied + c.Check(s.systemctlArgs, HasLen, 0) + c.Check(mountCmd.Calls(), DeepEquals, + [][]string{{"mount", "-o", "remount,mode=1777,strictatime,nosuid,nodev,size=50%", "/tmp"}}) +} + +// Test applying on image preparation +func (s *tmpfsSuite) TestFilesystemOnlyApply(c *C) { + conf := configcore.PlainCoreConfig(map[string]interface{}{ + "tmp.size": "16777216", + }) + + tmpDir := c.MkDir() + c.Assert(configcore.FilesystemOnlyApply(coreDev, tmpDir, conf), IsNil) + + tmpfsOverrCfg := filepath.Join(tmpDir, + "/etc/systemd/system/tmp.mount.d/override.conf") + c.Check(tmpfsOverrCfg, testutil.FileEquals, + "[Mount]\nOptions=mode=1777,strictatime,nosuid,nodev,size=16777216\n") +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/vitality.go snapd-2.54.2+21.10/overlord/configstate/configcore/vitality.go --- snapd-2.53+21.10ubuntu1/overlord/configstate/configcore/vitality.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/configstate/configcore/vitality.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,5 @@ // -*- Mode: Go; indent-tabs-mode: t -*- +//go:build !nomanagers // +build !nomanagers /* diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/devicemgr.go snapd-2.54.2+21.10/overlord/devicestate/devicemgr.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/devicemgr.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/devicemgr.go 2022-01-06 21:25:16.000000000 +0000 @@ -21,9 +21,9 @@ import ( "context" - "encoding/json" "errors" "fmt" + "io/ioutil" "os" "path/filepath" "regexp" @@ -44,6 +44,7 @@ "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/devicestate/internal" "github.com/snapcore/snapd/overlord/hookstate" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/overlord/storecontext" @@ -57,6 +58,7 @@ "github.com/snapcore/snapd/strutil" "github.com/snapcore/snapd/sysconfig" "github.com/snapcore/snapd/systemd" + "github.com/snapcore/snapd/timeutil" "github.com/snapcore/snapd/timings" ) @@ -109,8 +111,10 @@ becomeOperationalBackoff time.Duration registered bool reg chan struct{} + noRegister bool - preseed bool + preseed bool + ntpSyncedOrTimedOut bool } // Manager returns a new device manager. @@ -460,6 +464,11 @@ } } + if m.noRegister { + return nil + } + // noregister marker file is checked below after mostly in-memory checks + if m.changeInFlight("become-operational") { return nil } @@ -489,6 +498,12 @@ } } + // registration is blocked until reboot + if osutil.FileExists(filepath.Join(dirs.SnapRunDir, "noregister")) { + m.noRegister = true + return nil + } + var hasPrepareDeviceHook bool // if there's a gadget specified wait for it if gadget != "" { @@ -1360,6 +1375,56 @@ return m.reg } +type UnregisterOptions struct { + NoRegistrationUntilReboot bool +} + +// Unregister unregisters the device forgetting its serial +// plus the additional behavior described by the UnregisterOptions +func (m *DeviceManager) Unregister(opts *UnregisterOptions) error { + device, err := m.device() + if err != nil { + return err + } + if !release.OnClassic || (device.Brand != "generic" && device.Brand != "canonical") { + return fmt.Errorf("cannot currently unregister device if not classic or model brand is not generic or canonical") + } + + if opts == nil { + opts = &UnregisterOptions{} + } + if opts.NoRegistrationUntilReboot { + if err := os.MkdirAll(dirs.SnapRunDir, 0755); err != nil { + return err + } + if err := ioutil.WriteFile(filepath.Join(dirs.SnapRunDir, "noregister"), nil, 0644); err != nil { + return err + } + } + oldKeyID := device.KeyID + device.Serial = "" + device.KeyID = "" + device.SessionMacaroon = "" + if err := m.setDevice(device); err != nil { + return err + } + // commit forgetting serial and key + m.state.Unlock() + m.state.Lock() + // delete the device key + err = m.withKeypairMgr(func(keypairMgr asserts.KeypairManager) error { + err := keypairMgr.Delete(oldKeyID) + if err != nil { + return fmt.Errorf("cannot delete device key pair: %v", err) + } + return nil + }) + + m.lastBecomeOperationalAttempt = time.Time{} + m.becomeOperationalBackoff = 0 + return err +} + // device returns current device state. func (m *DeviceManager) device() (*auth.DeviceState, error) { return internal.Device(m.state) @@ -1506,7 +1571,7 @@ func (m *DeviceManager) Reboot(systemLabel, mode string) error { rebootCurrent := func() { logger.Noticef("rebooting system") - m.state.RequestRestart(state.RestartSystemNow) + restart.Request(m.state, restart.RestartSystemNow) } // most simple case: just reboot @@ -1530,7 +1595,7 @@ switched := func(systemLabel string, sysAction *SystemAction) { logger.Noticef("rebooting into system %q in %q mode", systemLabel, sysAction.Mode) - m.state.RequestRestart(state.RestartSystemNow) + restart.Request(m.state, restart.RestartSystemNow) } // even if we are already in the right mode we restart here by // passing rebootCurrent as this is what the user requested @@ -1549,7 +1614,7 @@ nop := func() {} switched := func(systemLabel string, sysAction *SystemAction) { logger.Noticef("restarting into system %q for action %q", systemLabel, sysAction.Title) - m.state.RequestRestart(state.RestartSystemNow) + restart.Request(m.state, restart.RestartSystemNow) } // we do nothing (nop) if the mode and system are the same return m.switchToSystemAndMode(systemLabel, action.Mode, nop, switched) @@ -1686,6 +1751,32 @@ return storeContextBackend{m} } +var timeutilIsNTPSynchronized = timeutil.IsNTPSynchronized + +func (m *DeviceManager) ntpSyncedOrWaitedLongerThan(maxWait time.Duration) bool { + if m.ntpSyncedOrTimedOut { + return true + } + if time.Now().After(startTime.Add(maxWait)) { + logger.Noticef("no NTP sync after %v, trying auto-refresh anyway", maxWait) + m.ntpSyncedOrTimedOut = true + return true + } + + var err error + m.ntpSyncedOrTimedOut, err = timeutilIsNTPSynchronized() + if errors.As(err, &timeutil.NoTimedate1Error{}) { + // no timedate1 dbus service, no need to wait for it + m.ntpSyncedOrTimedOut = true + return true + } + if err != nil { + logger.Debugf("cannot check if ntp is syncronized: %v", err) + } + + return m.ntpSyncedOrTimedOut +} + func (m *DeviceManager) hasFDESetupHook() (bool, error) { // state must be locked st := m.state @@ -1747,32 +1838,15 @@ } func (m *DeviceManager) checkFDEFeatures() (et secboot.EncryptionType, err error) { - // TODO: move most of this to kernel/fde.Features // Run fde-setup hook with "op":"features". If the hook // returns any {"features":[...]} reply we consider the // hardware supported. If the hook errors or if it returns // {"error":"hardware-unsupported"} we don't. - req := &fde.SetupRequest{ - Op: "features", - } - output, err := m.runFDESetupHook(req) + features, err := fde.CheckFeatures(m.runFDESetupHook) if err != nil { return et, err } - var res struct { - Features []string `json:"features"` - Error string `json:"error"` - } - if err := json.Unmarshal(output, &res); err != nil { - return et, fmt.Errorf("cannot parse hook output %q: %v", output, err) - } - if res.Features == nil && res.Error == "" { - return et, fmt.Errorf(`cannot use hook: neither "features" nor "error" returned`) - } - if res.Error != "" { - return et, fmt.Errorf("cannot use hook: it returned error: %v", res.Error) - } - if strutil.ListContains(res.Features, "device-setup") { + if strutil.ListContains(features, "device-setup") { et = secboot.EncryptionTypeDeviceSetupHook } else { et = secboot.EncryptionTypeLUKS diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_bootconfig_test.go snapd-2.54.2+21.10/overlord/devicestate/devicestate_bootconfig_test.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_bootconfig_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/devicestate_bootconfig_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -31,6 +31,7 @@ "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" @@ -129,7 +130,7 @@ c.Assert(log, HasLen, 1) c.Check(log[0], Matches, ".* updated boot config assets") // update was applied, thus a restart was requested - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) } else { // update was not applied or failed c.Check(s.restartRequests, HasLen, 0) diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_gadget_test.go snapd-2.54.2+21.10/overlord/devicestate/devicestate_gadget_test.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_gadget_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/devicestate_gadget_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -40,6 +40,7 @@ "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" "github.com/snapcore/snapd/overlord/state" @@ -271,7 +272,7 @@ defer func() { bootloader.Force(nil) }() } - restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error { + restore := devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error { updateCalled = true passedRollbackDir = path st, err := os.Stat(path) @@ -345,11 +346,11 @@ } devicestate.SetBootOkRan(s.mgr, true) - expectedRst := state.RestartSystem + expectedRst := restart.RestartSystem s.state.Lock() s.state.Set("seeded", true) if immediate { - expectedRst = state.RestartSystemNow + expectedRst = restart.RestartSystemNow chg.Set("system-restart-immediate", true) } s.state.Unlock() @@ -366,7 +367,7 @@ c.Check(rollbackDir, Equals, passedRollbackDir) // should have been removed right after update c.Check(osutil.IsDirectory(rollbackDir), Equals, false) - c.Check(s.restartRequests, DeepEquals, []state.RestartType{expectedRst}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{expectedRst}) } func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreSimple(c *C) { @@ -396,7 +397,7 @@ func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNoUpdateNeeded(c *C) { var called bool - restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { + restore := devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { called = true return gadget.ErrNoUpdate }) @@ -423,7 +424,7 @@ c.Skip("this test cannot run as root (permissions are not honored)") } - restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { + restore := devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { return errors.New("unexpected call") }) defer restore() @@ -450,7 +451,7 @@ } func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreUpdateFailed(c *C) { - restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { + restore := devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { return errors.New("gadget exploded") }) defer restore() @@ -474,7 +475,7 @@ } func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreNotDuringFirstboot(c *C) { - restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { + restore := devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { return errors.New("unexpected call") }) defer restore() @@ -518,7 +519,7 @@ } func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreBadGadgetYaml(c *C) { - restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { + restore := devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { return errors.New("unexpected call") }) defer restore() @@ -575,7 +576,7 @@ } func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreParanoidChecks(c *C) { - restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { + restore := devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { return errors.New("unexpected call") }) defer restore() @@ -627,7 +628,7 @@ restore := release.MockOnClassic(true) defer restore() - restore = devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { + restore = devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { return errors.New("unexpected call") }) defer restore() @@ -899,7 +900,7 @@ } func (s *deviceMgrGadgetSuite) TestUpdateGadgetOnCoreHybridFirstboot(c *C) { - restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { + restore := devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { return errors.New("unexpected call") }) defer restore() @@ -1024,7 +1025,7 @@ var updateCalled int var passedRollbackDir string - restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error { + restore := devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error { updateCalled++ passedRollbackDir = path @@ -1062,7 +1063,7 @@ var updateCalled int var passedRollbackDir string - restore := devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error { + restore := devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error { updateCalled++ passedRollbackDir = path @@ -1156,7 +1157,7 @@ } if updated { // update was applied, thus a restart was requested - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystem}) } else { // update was not applied or failed c.Check(s.restartRequests, HasLen, 0) @@ -1488,7 +1489,7 @@ c.Check(log[0], Matches, ".* Updated kernel command line") c.Check(log[1], Matches, ".* Reverted kernel command line change") // update was applied and then undone - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow, state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow, restart.RestartSystemNow}) c.Check(restartCount, Equals, 2) vars, err := s.managedbl.GetBootVars("snapd_extra_cmdline_args") c.Assert(err, IsNil) diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate.go snapd-2.54.2+21.10/overlord/devicestate/devicestate.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/devicestate.go 2022-01-06 21:25:16.000000000 +0000 @@ -27,6 +27,7 @@ "path/filepath" "strconv" "sync" + "time" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/boot" @@ -117,6 +118,14 @@ return false, nil } + // Try to ensure we have an accurate time before doing any + // refreshy stuff. Note that this call will not block. + devMgr := deviceMgr(st) + maxWait := 10 * time.Minute + if !devMgr.ntpSyncedOrWaitedLongerThan(maxWait) { + return false, nil + } + // Either we have a serial or we try anyway if we attempted // for a while to get a serial, this would allow us to at // least upgrade core if that can help. @@ -319,14 +328,14 @@ return naming.NewSnapSet(reqSnaps) } -var errNoDownloadInstallEdge = fmt.Errorf("download and checks edge not found") +var errNoBeforeLocalModificationsEdge = fmt.Errorf("before-local-modifications edge not found") -// extractDownloadInstallEdgesFromTs extracts the first, last download +// extractBeforeLocalModificationsEdgesTs extracts the first, last download // phase and install phase tasks from a TaskSet -func extractDownloadInstallEdgesFromTs(ts *state.TaskSet) (firstDl, lastDl, firstInst, lastInst *state.Task, err error) { - edgeTask := ts.MaybeEdge(snapstate.DownloadAndChecksDoneEdge) +func extractBeforeLocalModificationsEdgesTs(ts *state.TaskSet) (firstDl, lastDl, firstInst, lastInst *state.Task, err error) { + edgeTask := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge) if edgeTask == nil { - return nil, nil, nil, nil, errNoDownloadInstallEdge + return nil, nil, nil, nil, errNoBeforeLocalModificationsEdge } tasks := ts.Tasks() // we know we always start with downloads @@ -401,23 +410,34 @@ newModelSnap *asserts.ModelSnap } -func remodelKernelOrBaseTasks(ctx context.Context, st *state.State, ms modelSnapsForRemodel, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { +func (ms *modelSnapsForRemodel) canHaveUC18PinnedTrack() bool { + return ms.newModelSnap != nil && + (ms.newModelSnap.SnapType == "kernel" || ms.newModelSnap.SnapType == "gadget") +} + +func remodelEssentialSnapTasks(ctx context.Context, st *state.State, ms modelSnapsForRemodel, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { userID := 0 newModelSnapChannel, err := modelSnapChannelFromDefaultOrPinnedTrack(ms.new, ms.newModelSnap) if err != nil { return nil, err } + + addExistingSnapTasks := snapstate.LinkNewBaseOrKernel + if ms.newModelSnap != nil && ms.newModelSnap.SnapType == "gadget" { + addExistingSnapTasks = snapstate.SwitchToNewGadget + } + if ms.currentSnap == ms.newSnap { + // new model uses the same base, kernel or gadget snap changed := false if ms.new.Grade() != asserts.ModelGradeUnset { // UC20 models can specify default channel for all snaps - // including base and kernel - // new model uses the same base or kernel + // including base, kernel and gadget changed, err = installedSnapChannelChanged(st, ms.newSnap, newModelSnapChannel) if err != nil { return nil, err } - } else if ms.newModelSnap != nil && ms.newModelSnap.SnapType == "kernel" { + } else if ms.canHaveUC18PinnedTrack() { // UC18 models could only specify track for the kernel // and gadget snaps changed = ms.currentModelSnap.PinnedTrack != ms.newModelSnap.PinnedTrack @@ -459,55 +479,39 @@ return nil, err } if ts != nil { - if edgeTask := ts.MaybeEdge(snapstate.DownloadAndChecksDoneEdge); edgeTask != nil { - // we have downloads and checks done edge, so - // the update is not a simple + if edgeTask := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge); edgeTask != nil { + // no task is marked as being last + // before local modifications are + // introduced, indicating that the + // update is a simple // switch-snap-channel return ts, nil } else { - // in other cases make sure that the - // kernel or base is linked and available - return snapstate.AddLinkNewBaseOrKernel(st, ts) + if ms.newModelSnap.SnapType == "kernel" || ms.newModelSnap.SnapType == "base" { + // in other cases make sure that + // the kernel or base is linked + // and available, and that + // kernel updates boot assets if + // needed + ts, err = snapstate.AddLinkNewBaseOrKernel(st, ts) + if err != nil { + return nil, err + } + } else if ms.newModelSnap.SnapType == "gadget" { + // gadget snaps may need gadget + // related tasks such as assets + // update or command line update + ts, err = snapstate.AddGadgetAssetsTasks(st, ts) + if err != nil { + return nil, err + } + } + return ts, nil } } } } - return snapstate.LinkNewBaseOrKernel(st, ms.newSnap) -} - -func remodelGadgetTasks(ctx context.Context, st *state.State, ms modelSnapsForRemodel, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - userID := 0 - newGadgetChannel, err := modelSnapChannelFromDefaultOrPinnedTrack(ms.new, ms.newModelSnap) - if err != nil { - return nil, err - } - if ms.currentSnap == ms.newSnap { - // already installed, but may be using a different channel - changed := false - if ms.new.Grade() != asserts.ModelGradeUnset { - // UC20 models can specify default channel for all snaps - // including the gadget - changed, err = installedSnapChannelChanged(st, ms.newSnap, newGadgetChannel) - if err != nil { - return nil, err - } - } else { - // pre UC20 models could only specify a track for the - // gadget - changed = ms.currentModelSnap.PinnedTrack != ms.newModelSnap.PinnedTrack - } - if changed { - return snapstateUpdateWithDeviceContext(st, ms.newSnap, - &snapstate.RevisionOptions{Channel: newGadgetChannel}, - userID, snapstate.Flags{NoReRefresh: true}, deviceCtx, fromChange) - } - return nil, nil - } - - // install the new gadget - return snapstateInstallWithDeviceContext(ctx, st, ms.newSnap, - &snapstate.RevisionOptions{Channel: newGadgetChannel}, - userID, snapstate.Flags{}, deviceCtx, fromChange) + return addExistingSnapTasks(st, ms.newSnap) } func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Model, deviceCtx snapstate.DeviceContext, fromChange string) ([]*state.TaskSet, error) { @@ -522,7 +526,7 @@ newSnap: new.Kernel(), newModelSnap: new.KernelSnap(), } - ts, err := remodelKernelOrBaseTasks(ctx, st, kms, deviceCtx, fromChange) + ts, err := remodelEssentialSnapTasks(ctx, st, kms, deviceCtx, fromChange) if err != nil { return nil, err } @@ -537,7 +541,7 @@ newSnap: new.Base(), newModelSnap: new.BaseSnap(), } - ts, err = remodelKernelOrBaseTasks(ctx, st, bms, deviceCtx, fromChange) + ts, err = remodelEssentialSnapTasks(ctx, st, bms, deviceCtx, fromChange) if err != nil { return nil, err } @@ -552,7 +556,7 @@ newSnap: new.Gadget(), newModelSnap: new.GadgetSnap(), } - ts, err = remodelGadgetTasks(ctx, st, gms, deviceCtx, fromChange) + ts, err = remodelEssentialSnapTasks(ctx, st, gms, deviceCtx, fromChange) if err != nil { return nil, err } @@ -630,7 +634,9 @@ // Terminology // A <- B means B waits for A // "download,verify" are part of the "Download" phase - // "link,start" is part of "Install" phase + // "link,start" is part of "Install" phase which introduces + // system modifications. The last task of the "Download" phase + // is marked with LastBeforeLocalModificationsEdge. // // - all tasks inside ts{Download,Install} already wait for // each other so the chains look something like this: @@ -647,15 +653,16 @@ // verify2 <- download3 (added) // install1 <- install2 (added) // install2 <- install3 (added) - downloadStart, downloadLast, installFirst, installLast, err := extractDownloadInstallEdgesFromTs(ts) + downloadStart, downloadLast, installFirst, installLast, err := extractBeforeLocalModificationsEdgesTs(ts) if err != nil { - if err == errNoDownloadInstallEdge { + if err == errNoBeforeLocalModificationsEdge { // there is no task in the task set marked with - // download edges, which can happen when there - // is a simple channel switch if the snap which - // is part of remodel has the same revision in - // the current channel and one that will be used - // after remodel + // as being last before system modification + // edge, which can happen when there is a simple + // channel switch if the snap which is part of + // remodel has the same revision in the current + // channel and one that will be used after + // remodel continue } return nil, fmt.Errorf("cannot remodel: %v", err) diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_install_mode_test.go snapd-2.54.2+21.10/overlord/devicestate/devicestate_install_mode_test.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_install_mode_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/devicestate_install_mode_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -26,6 +26,7 @@ "io/ioutil" "os" "path/filepath" + "time" . "gopkg.in/check.v1" "gopkg.in/tomb.v2" @@ -43,6 +44,7 @@ "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/hookstate/ctlcmd" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" "github.com/snapcore/snapd/overlord/state" @@ -343,7 +345,7 @@ c.Assert(installRunCalled, Equals, 1) c.Assert(bootMakeBootableCalled, Equals, 1) - c.Assert(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Assert(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) return nil } @@ -421,7 +423,7 @@ c.Assert(waitTasks[0].ID(), Equals, setupRunSystemTask.ID()) // we did request a restart through restartSystemToRunModeTask - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) } func (s *deviceMgrInstallModeSuite) TestInstallWithInstallDeviceHookExpTasks(c *C) { @@ -490,13 +492,13 @@ c.Assert(waitTasks[0].ID(), Equals, installDevice.ID()) // we did request a restart through restartSystemToRunModeTask - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) c.Assert(hooksCalled, HasLen, 1) c.Assert(hooksCalled[0].HookName(), Equals, "install-device") } -func (s *deviceMgrInstallModeSuite) testInstallWithInstallDeviceHookSnapctlReboot(c *C, arg string, rst state.RestartType) { +func (s *deviceMgrInstallModeSuite) testInstallWithInstallDeviceHookSnapctlReboot(c *C, arg string, rst restart.RestartType) { restore := release.MockOnClassic(false) defer restore() @@ -532,15 +534,15 @@ c.Check(installSystem.Err(), IsNil) // we did end up requesting the right shutdown - c.Check(s.restartRequests, DeepEquals, []state.RestartType{rst}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{rst}) } func (s *deviceMgrInstallModeSuite) TestInstallWithInstallDeviceHookSnapctlRebootHalt(c *C) { - s.testInstallWithInstallDeviceHookSnapctlReboot(c, "--halt", state.RestartSystemHaltNow) + s.testInstallWithInstallDeviceHookSnapctlReboot(c, "--halt", restart.RestartSystemHaltNow) } func (s *deviceMgrInstallModeSuite) TestInstallWithInstallDeviceHookSnapctlRebootPoweroff(c *C) { - s.testInstallWithInstallDeviceHookSnapctlReboot(c, "--poweroff", state.RestartSystemPoweroffNow) + s.testInstallWithInstallDeviceHookSnapctlReboot(c, "--poweroff", restart.RestartSystemPoweroffNow) } func (s *deviceMgrInstallModeSuite) TestInstallWithBrokenInstallDeviceHookUnhappy(c *C) { @@ -1558,3 +1560,64 @@ {"snap", "debug", "timings", "--ensure=install-system"}, }) } + +func (s *deviceMgrInstallModeSuite) TestInstallModeWritesTimesyncdClockHappy(c *C) { + now := time.Now() + restore := devicestate.MockTimeNow(func() time.Time { return now }) + defer restore() + + clockTsInSrc := filepath.Join(dirs.GlobalRootDir, "/var/lib/systemd/timesync/clock") + c.Assert(os.MkdirAll(filepath.Dir(clockTsInSrc), 0755), IsNil) + c.Assert(ioutil.WriteFile(clockTsInSrc, nil, 0644), IsNil) + // a month old timestamp file + c.Assert(os.Chtimes(clockTsInSrc, now.AddDate(0, -1, 0), now.AddDate(0, -1, 0)), IsNil) + + s.mockInstallModeChange(c, "dangerous", "") + + s.state.Lock() + defer s.state.Unlock() + + installSystem := s.findInstallSystem() + c.Assert(installSystem, NotNil) + + // installation was successful + c.Check(installSystem.Err(), IsNil) + c.Check(installSystem.Status(), Equals, state.DoneStatus) + + clockTsInDst := filepath.Join(boot.InstallHostWritableDir, "/var/lib/systemd/timesync/clock") + fi, err := os.Stat(clockTsInDst) + c.Assert(err, IsNil) + c.Check(fi.ModTime().Round(time.Second), Equals, now.Round(time.Second)) + c.Check(fi.Size(), Equals, int64(0)) +} + +func (s *deviceMgrInstallModeSuite) TestInstallModeWritesTimesyncdClockErr(c *C) { + now := time.Now() + restore := devicestate.MockTimeNow(func() time.Time { return now }) + defer restore() + + if os.Geteuid() == 0 { + c.Skip("the test cannot be executed by the root user") + } + + clockTsInSrc := filepath.Join(dirs.GlobalRootDir, "/var/lib/systemd/timesync/clock") + c.Assert(os.MkdirAll(filepath.Dir(clockTsInSrc), 0755), IsNil) + c.Assert(ioutil.WriteFile(clockTsInSrc, nil, 0644), IsNil) + + timesyncDirInDst := filepath.Join(boot.InstallHostWritableDir, "/var/lib/systemd/timesync/") + c.Assert(os.MkdirAll(timesyncDirInDst, 0755), IsNil) + c.Assert(os.Chmod(timesyncDirInDst, 0000), IsNil) + defer os.Chmod(timesyncDirInDst, 0755) + + s.mockInstallModeChange(c, "dangerous", "") + + s.state.Lock() + defer s.state.Unlock() + + installSystem := s.findInstallSystem() + c.Assert(installSystem, NotNil) + + // install failed copying the timestamp + c.Check(installSystem.Err(), ErrorMatches, `(?s).*\(cannot seed timesyncd clock: cannot copy clock:.*Permission denied.*`) + c.Check(installSystem.Status(), Equals, state.ErrorStatus) +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_remodel_test.go snapd-2.54.2+21.10/overlord/devicestate/devicestate_remodel_test.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_remodel_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/devicestate_remodel_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -44,6 +44,7 @@ "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" "github.com/snapcore/snapd/overlord/state" @@ -263,7 +264,7 @@ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -282,7 +283,7 @@ tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -358,7 +359,7 @@ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -416,7 +417,7 @@ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -528,7 +529,7 @@ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -545,7 +546,7 @@ tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -693,7 +694,7 @@ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -837,7 +838,7 @@ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -908,7 +909,7 @@ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -1407,7 +1408,7 @@ }) tGadgetUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tGadgetUpdate) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -1415,7 +1416,7 @@ defer restore() gadgetUpdateCalled := false - restore = devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { + restore = devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { gadgetUpdateCalled = true c.Check(policy, NotNil) c.Check(reflect.ValueOf(policy).Pointer(), Equals, reflect.ValueOf(gadget.RemodelUpdatePolicy).Pointer()) @@ -1494,7 +1495,7 @@ c.Check(chg.IsReady(), Equals, true) c.Check(chg.Err(), IsNil) c.Check(gadgetUpdateCalled, Equals, true) - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystem}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystem}) } func (s *deviceMgrRemodelSuite) TestRemodelGadgetAssetsParanoidCheck(c *C) { @@ -1557,7 +1558,7 @@ }) tGadgetUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tGadgetUpdate) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -1565,7 +1566,7 @@ defer restore() gadgetUpdateCalled := false - restore = devicestate.MockGadgetUpdate(func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { + restore = devicestate.MockGadgetUpdate(func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, _ gadget.ContentUpdateObserver) error { return errors.New("unexpected call") }) defer restore() @@ -1603,7 +1604,7 @@ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -1657,7 +1658,7 @@ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -1878,7 +1879,7 @@ tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -2095,7 +2096,12 @@ }) } -func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelBaseSnapsInstalledSnaps(c *C) { +func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelBaseGadgetSnapsInstalledSnaps(c *C) { + // remodel switches to a new set of kernel, base and gadget snaps, but + // those happen to be already installed and tracking the right channels, + // this scenario can happen when the system has gone through many + // remodels and the new gadget, kernel, base snaps were required by one + // of the prior models s.state.Lock() defer s.state.Unlock() s.state.Set("seeded", true) @@ -2172,13 +2178,16 @@ Active: true, TrackingChannel: "20/stable", }) - // new gadget and kernel which are already installed - for _, alreadyInstalledName := range []string{"pc-kernel-new", "core20-new"} { + // new gadget, base and kernel which are already installed + for _, alreadyInstalledName := range []string{"pc-new", "pc-kernel-new", "core20-new"} { snapYaml := "name: pc-kernel-new\nversion: 1\ntype: kernel\n" channel := "20/stable" - if alreadyInstalledName == "core20-new" { + switch alreadyInstalledName { + case "core20-new": snapYaml = "name: core20-new\nversion: 1\ntype: base\n" channel = "latest/stable" + case "pc-new": + snapYaml = "name: pc-new\nversion: 1\ntype: gadget\n" } si := &snap.SideInfo{ RealName: alreadyInstalledName, @@ -2211,8 +2220,8 @@ "default-channel": "20/stable", }, map[string]interface{}{ - "name": "pc", - "id": snaptest.AssertedSnapID("pc"), + "name": "pc-new", + "id": snaptest.AssertedSnapID("pc-new"), "type": "gadget", "default-channel": "20", }, @@ -2223,8 +2232,8 @@ c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") tl := chg.Tasks() - // 2 snaps (2 tasks for each) + recovery system (2 tasks) + set-model - c.Assert(tl, HasLen, 2*2+2+1) + // 2 snaps (2 tasks for each) + assets update from kernel + gadget (3 tasks) + recovery system (2 tasks) + set-model + c.Assert(tl, HasLen, 2*2+1+3+2+1) deviceCtx, err := devicestate.DeviceCtx(s.state, tl[0], nil) c.Assert(err, IsNil) @@ -2238,12 +2247,16 @@ // check the tasks tPrepareKernel := tl[0] - tLinkKernel := tl[1] - tPrepareBase := tl[2] - tLinkBase := tl[3] - tCreateRecovery := tl[4] - tFinalizeRecovery := tl[5] - tSetModel := tl[6] + tUpdateAssetsKernel := tl[1] + tLinkKernel := tl[2] + tPrepareBase := tl[3] + tLinkBase := tl[4] + tPrepareGadget := tl[5] + tUpdateAssets := tl[6] + tUpdateCmdline := tl[7] + tCreateRecovery := tl[8] + tFinalizeRecovery := tl[9] + tSetModel := tl[10] // check the tasks c.Assert(tPrepareKernel.Kind(), Equals, "prepare-snap") @@ -2251,11 +2264,22 @@ c.Assert(tPrepareKernel.WaitTasks(), HasLen, 0) c.Assert(tLinkKernel.Kind(), Equals, "link-snap") c.Assert(tLinkKernel.Summary(), Equals, `Make snap "pc-kernel-new" (222) available to the system during remodel`) + c.Assert(tUpdateAssetsKernel.Kind(), Equals, "update-gadget-assets") + c.Assert(tUpdateAssetsKernel.Summary(), Equals, `Update assets from kernel "pc-kernel-new" (222) for remodel`) c.Assert(tPrepareBase.Kind(), Equals, "prepare-snap") c.Assert(tPrepareBase.Summary(), Equals, `Prepare snap "core20-new" (222) for remodel`) c.Assert(tPrepareBase.WaitTasks(), HasLen, 1) c.Assert(tLinkBase.Kind(), Equals, "link-snap") c.Assert(tLinkBase.Summary(), Equals, `Make snap "core20-new" (222) available to the system during remodel`) + c.Assert(tPrepareGadget.Kind(), Equals, "prepare-snap") + c.Assert(tPrepareGadget.Summary(), Equals, `Prepare snap "pc-new" (222) for remodel`) + c.Assert(tPrepareGadget.WaitTasks(), HasLen, 1) + c.Assert(tUpdateAssets.Kind(), Equals, "update-gadget-assets") + c.Assert(tUpdateAssets.Summary(), Equals, `Update assets from gadget "pc-new" (222) for remodel`) + c.Assert(tUpdateAssets.WaitTasks(), HasLen, 2) + c.Assert(tUpdateCmdline.Kind(), Equals, "update-gadget-cmdline") + c.Assert(tUpdateCmdline.Summary(), Equals, `Update kernel command line from gadget "pc-new" (222) for remodel`) + c.Assert(tUpdateCmdline.WaitTasks(), HasLen, 1) expectedLabel := now.Format("20060102") c.Assert(tCreateRecovery.Kind(), Equals, "create-recovery-system") c.Assert(tCreateRecovery.Summary(), Equals, fmt.Sprintf("Create recovery system with label %q", expectedLabel)) @@ -2266,8 +2290,11 @@ // check the ordering, prepare/link are part of download edge and come first c.Assert(tPrepareKernel.WaitTasks(), HasLen, 0) c.Assert(tLinkKernel.WaitTasks(), DeepEquals, []*state.Task{ + tUpdateAssetsKernel, + }) + c.Assert(tUpdateAssetsKernel.WaitTasks(), DeepEquals, []*state.Task{ tPrepareKernel, - tPrepareBase, + tPrepareGadget, tCreateRecovery, tFinalizeRecovery, }) @@ -2278,21 +2305,32 @@ tPrepareBase, tLinkKernel, }) + c.Assert(tPrepareGadget.WaitTasks(), DeepEquals, []*state.Task{ + tPrepareBase, + }) + c.Assert(tUpdateAssets.WaitTasks(), DeepEquals, []*state.Task{ + tPrepareGadget, + tLinkBase, + }) + c.Assert(tUpdateCmdline.WaitTasks(), DeepEquals, []*state.Task{ + tUpdateAssets, + }) c.Assert(tCreateRecovery.WaitTasks(), DeepEquals, []*state.Task{ // last snap of the download chain (in this case prepare & link // for existing snaps) - tPrepareBase, + tPrepareGadget, }) c.Assert(tFinalizeRecovery.WaitTasks(), DeepEquals, []*state.Task{ // recovery system being created tCreateRecovery, // last snap of the download chain (see above) - tPrepareBase, + tPrepareGadget, }) // setModel waits for everything in the change c.Assert(tSetModel.WaitTasks(), DeepEquals, []*state.Task{ - tPrepareKernel, tLinkKernel, + tPrepareKernel, tUpdateAssetsKernel, tLinkKernel, tPrepareBase, tLinkBase, + tPrepareGadget, tUpdateAssets, tUpdateCmdline, tCreateRecovery, tFinalizeRecovery, }) // verify recovery system setup data on appropriate tasks @@ -2302,18 +2340,21 @@ c.Assert(systemSetupData, DeepEquals, map[string]interface{}{ "label": expectedLabel, "directory": filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", expectedLabel), - "snap-setup-tasks": []interface{}{tPrepareKernel.ID(), tPrepareBase.ID()}, + "snap-setup-tasks": []interface{}{tPrepareKernel.ID(), tPrepareBase.ID(), tPrepareGadget.ID()}, }) } -func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelBaseSnapsInstalledSnapsDifferentChannelThanNew(c *C) { +func (s *deviceMgrRemodelSuite) TestRemodelUC20SwitchKernelBaseGadgetSnapsInstalledSnapsDifferentChannelThanNew(c *C) { + // kernel, base and gadget snaps that are used by the new model are + // already installed, but track a different channel from what is set in + // the new model s.state.Lock() defer s.state.Unlock() s.state.Set("seeded", true) s.state.Set("refresh-privacy-key", "some-privacy-key") restore := devicestate.MockSnapstateUpdateWithDeviceContext(func(st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) { - c.Assert(strutil.ListContains([]string{"core20-new", "pc-kernel-new"}, name), Equals, true, + c.Assert(strutil.ListContains([]string{"core20-new", "pc-kernel-new", "pc-new"}, name), Equals, true, Commentf("unexpected snap %q", name)) c.Check(flags.Required, Equals, false) c.Check(flags.NoReRefresh, Equals, true) @@ -2327,6 +2368,9 @@ if name == "core20-new" { typ = "base" rev = snap.R(223) + } else if name == "pc-new" { + typ = "gadget" + rev = snap.R(224) } tSwitchChannel.Set("snap-setup", &snapstate.SnapSetup{ SideInfo: &snap.SideInfo{ @@ -2408,11 +2452,13 @@ TrackingChannel: "20/stable", }) // new gadget and kernel which are already installed - for _, alreadyInstalledName := range []string{"pc-kernel-new", "core20-new"} { + for _, alreadyInstalledName := range []string{"pc-kernel-new", "core20-new", "pc-new"} { snapYaml := "name: pc-kernel-new\nversion: 1\ntype: kernel\n" channel := "other/other" if alreadyInstalledName == "core20-new" { snapYaml = "name: core20-new\nversion: 1\ntype: base\n" + } else if alreadyInstalledName == "pc-new" { + snapYaml = "name: pc-new\nversion: 1\ntype: gadget\n" } si := &snap.SideInfo{ RealName: alreadyInstalledName, @@ -2446,8 +2492,8 @@ "default-channel": "20", }, map[string]interface{}{ - "name": "pc", - "id": snaptest.AssertedSnapID("pc"), + "name": "pc-new", + "id": snaptest.AssertedSnapID("pc-new"), "type": "gadget", "default-channel": "20", }, @@ -2465,8 +2511,10 @@ c.Assert(chg.Summary(), Equals, "Refresh model assertion from revision 0 to 1") tl := chg.Tasks() - // 2 snaps with (snap switch channel + link snap) + recovery system (2 tasks) + set-model - c.Assert(tl, HasLen, 2*2+2+1) + // 2 snaps with (snap switch channel + link snap) + gadget assets update + // for the kernel snap + gadget snap (switch channel, assets update, cmdline update) + + // recovery system (2 tasks) + set-model + c.Assert(tl, HasLen, 2*2+1+3+2+1) deviceCtx, err := devicestate.DeviceCtx(s.state, tl[0], nil) c.Assert(err, IsNil) @@ -2480,24 +2528,35 @@ // check the tasks tSwitchChannelKernel := tl[0] - tLinkKernel := tl[1] - tSwitchChannelBase := tl[2] - tLinkBase := tl[3] - tCreateRecovery := tl[4] - tFinalizeRecovery := tl[5] - tSetModel := tl[6] + tUpdateAssetsFromKernel := tl[1] + tLinkKernel := tl[2] + tSwitchChannelBase := tl[3] + tLinkBase := tl[4] + tSwitchChannelGadget := tl[5] + tUpdateAssetsFromGadget := tl[6] + tUpdateCmdlineFromGadget := tl[7] + tCreateRecovery := tl[8] + tFinalizeRecovery := tl[9] + tSetModel := tl[10] // check the tasks c.Assert(tSwitchChannelKernel.Kind(), Equals, "switch-snap-channel") c.Assert(tSwitchChannelKernel.Summary(), Equals, `Switch pc-kernel-new channel to 20/stable`) c.Assert(tSwitchChannelKernel.WaitTasks(), HasLen, 0) + c.Assert(tUpdateAssetsFromKernel.Kind(), Equals, "update-gadget-assets") + c.Assert(tUpdateAssetsFromKernel.Summary(), Equals, `Update assets from kernel "pc-kernel-new" (222) for remodel`) c.Assert(tLinkKernel.Kind(), Equals, "link-snap") c.Assert(tLinkKernel.Summary(), Equals, `Make snap "pc-kernel-new" (222) available to the system during remodel`) c.Assert(tSwitchChannelBase.Kind(), Equals, "switch-snap-channel") c.Assert(tSwitchChannelBase.Summary(), Equals, `Switch core20-new channel to latest/stable`) - c.Assert(tSwitchChannelBase.WaitTasks(), HasLen, 0) c.Assert(tLinkBase.Kind(), Equals, "link-snap") c.Assert(tLinkBase.Summary(), Equals, `Make snap "core20-new" (223) available to the system during remodel`) + c.Assert(tSwitchChannelGadget.Kind(), Equals, "switch-snap-channel") + c.Assert(tSwitchChannelGadget.Summary(), Equals, `Switch pc-new channel to 20/stable`) + c.Assert(tUpdateAssetsFromGadget.Kind(), Equals, "update-gadget-assets") + c.Assert(tUpdateAssetsFromGadget.Summary(), Equals, `Update assets from gadget "pc-new" (224) for remodel`) + c.Assert(tUpdateCmdlineFromGadget.Kind(), Equals, "update-gadget-cmdline") + c.Assert(tUpdateCmdlineFromGadget.Summary(), Equals, `Update kernel command line from gadget "pc-new" (224) for remodel`) expectedLabel := now.Format("20060102") c.Assert(tCreateRecovery.Kind(), Equals, "create-recovery-system") c.Assert(tCreateRecovery.Summary(), Equals, fmt.Sprintf("Create recovery system with label %q", expectedLabel)) @@ -2507,21 +2566,35 @@ c.Assert(tSetModel.Summary(), Equals, "Set new model assertion") // check the ordering, prepare/link are part of download edge and come first c.Assert(tSwitchChannelKernel.WaitTasks(), HasLen, 0) - c.Check(tLinkKernel.WaitTasks(), DeepEquals, []*state.Task{ + c.Assert(tSwitchChannelBase.WaitTasks(), DeepEquals, []*state.Task{ tSwitchChannelKernel, }) - c.Assert(tLinkBase.WaitTasks(), DeepEquals, []*state.Task{ + c.Assert(tSwitchChannelGadget.WaitTasks(), DeepEquals, []*state.Task{ tSwitchChannelBase, }) - c.Assert(tCreateRecovery.WaitTasks(), DeepEquals, []*state.Task{}) + c.Assert(tCreateRecovery.WaitTasks(), DeepEquals, []*state.Task{ + tSwitchChannelGadget, + }) c.Assert(tFinalizeRecovery.WaitTasks(), DeepEquals, []*state.Task{ // recovery system being created tCreateRecovery, + tSwitchChannelGadget, + }) + c.Check(tUpdateAssetsFromKernel.WaitTasks(), DeepEquals, []*state.Task{ + tSwitchChannelKernel, tSwitchChannelGadget, + tCreateRecovery, tFinalizeRecovery, + }) + c.Check(tLinkKernel.WaitTasks(), DeepEquals, []*state.Task{ + tUpdateAssetsFromKernel, + }) + c.Assert(tLinkBase.WaitTasks(), DeepEquals, []*state.Task{ + tSwitchChannelBase, tLinkKernel, }) // setModel waits for everything in the change c.Assert(tSetModel.WaitTasks(), DeepEquals, []*state.Task{ - tSwitchChannelKernel, tLinkKernel, + tSwitchChannelKernel, tUpdateAssetsFromKernel, tLinkKernel, tSwitchChannelBase, tLinkBase, + tSwitchChannelGadget, tUpdateAssetsFromGadget, tUpdateCmdlineFromGadget, tCreateRecovery, tFinalizeRecovery, }) // verify recovery system setup data on appropriate tasks @@ -2531,8 +2604,12 @@ c.Assert(systemSetupData, DeepEquals, map[string]interface{}{ "label": expectedLabel, "directory": filepath.Join(boot.InitramfsUbuntuSeedDir, "systems", expectedLabel), - // none of the tasks are downloads so they were not tracked - "snap-setup-tasks": nil, + // tasks carrying snap-setup are tracked + "snap-setup-tasks": []interface{}{ + tSwitchChannelKernel.ID(), + tSwitchChannelBase.ID(), + tSwitchChannelGadget.ID(), + }, }) } @@ -2553,7 +2630,7 @@ tUpdate := s.state.NewTask("fake-update", fmt.Sprintf("Update %s to track %s", name, opts.Channel)) tUpdate.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tUpdate) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -2770,6 +2847,9 @@ } func (s *deviceMgrRemodelSuite) TestRemodelUC20EssentialSnapsTrackingDifferentChannelThanDefaultSameAsNew(c *C) { + // essential snaps from new model are already installed and track + // channels different than declared in the old model, but already the + // same as in the new one s.state.Lock() defer s.state.Unlock() s.state.Set("seeded", true) @@ -2944,6 +3024,8 @@ } func (s *deviceMgrRemodelSuite) TestRemodelUC20EssentialSnapsAlreadyInstalledAndLocal(c *C) { + // remodel when the essential snaps declared in new model are already + // installed, but have a local revision s.state.Lock() defer s.state.Unlock() s.state.Set("seeded", true) @@ -3114,7 +3196,10 @@ }) } -func (s *deviceMgrRemodelSuite) TestRemodelUC20NoDownloadSimpleChannelSwitch(c *C) { +func (s *deviceMgrRemodelSuite) TestRemodelUC20BaseNoDownloadSimpleChannelSwitch(c *C) { + // remodel when a channel declared in new model carries the same + // revision as already installed, so there is no full fledged, but a + // simple channel switch s.state.Lock() defer s.state.Unlock() s.state.Set("seeded", true) @@ -3602,7 +3687,7 @@ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -3881,7 +3966,7 @@ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -3981,7 +4066,7 @@ defer st.Unlock() // not strictly needed, but underlines there's a reboot // happening - st.RequestRestart(state.RestartSystemNow) + restart.Request(st, restart.RestartSystemNow) } if fakeRebootCallsReady { return nil @@ -4016,7 +4101,7 @@ c.Check(chg.IsReady(), Equals, false) c.Assert(chg.Err(), IsNil) // injected by fake restart - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) // 3 calls: promote tried system, old & new model, just the new model c.Check(resealKeyCalls, Equals, 3) // even if errors occur during reseal, set-model is done diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_serial_test.go snapd-2.54.2+21.10/overlord/devicestate/devicestate_serial_test.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_serial_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/devicestate_serial_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2019 Canonical Ltd + * Copyright (C) 2016-2021 Canonical Ltd * * This 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,6 +21,7 @@ import ( "fmt" + "io/ioutil" "net" "net/http" "net/http/httptest" @@ -198,6 +199,9 @@ // check that keypair manager is under device c.Check(osutil.IsDirectory(filepath.Join(dirs.SnapDeviceDir, "private-keys-v1")), Equals, true) + + // cannot unregister + c.Check(s.mgr.Unregister(nil), ErrorMatches, `cannot currently unregister device if not classic or model brand is not generic or canonical`) } func (s *deviceMgrSerialSuite) TestFullDeviceRegistrationHappyWithProxy(c *C) { @@ -2017,3 +2021,184 @@ }) c.Assert(err, IsNil) } + +func (s *deviceMgrSerialSuite) TestFullDeviceUnregisterReregisterClassicGeneric(c *C) { + s.testFullDeviceUnregisterReregisterClassicGeneric(c, nil) +} + +func (s *deviceMgrSerialSuite) TestFullDeviceUnregisterBlockReregisterUntilRebootClassicGeneric(c *C) { + s.testFullDeviceUnregisterReregisterClassicGeneric(c, &devicestate.UnregisterOptions{ + NoRegistrationUntilReboot: true, + }) +} + +func (s *deviceMgrSerialSuite) testFullDeviceUnregisterReregisterClassicGeneric(c *C, opts *devicestate.UnregisterOptions) { + restore := release.MockOnClassic(true) + defer restore() + + r1 := devicestate.MockKeyLength(testKeyLength) + defer r1() + + mockServer := s.mockServer(c, "REQID-1", nil) + defer mockServer.Close() + + r2 := devicestate.MockBaseStoreURL(mockServer.URL) + defer r2() + + // setup state as will be done by first-boot + s.state.Lock() + defer s.state.Unlock() + + // in this case is just marked seeded without snaps + s.state.Set("seeded", true) + + // not started without some installation happening or happened + // have an in-progress installation + inst := s.state.NewChange("install", "...") + task := s.state.NewTask("mount-snap", "...") + inst.AddTask(task) + + // runs the whole device registration process + s.state.Unlock() + s.settle(c) + s.state.Lock() + + becomeOperational := s.findBecomeOperationalChange() + c.Assert(becomeOperational, NotNil) + + c.Check(becomeOperational.Status().Ready(), Equals, true) + c.Check(becomeOperational.Err(), IsNil) + becomeOperational1 := becomeOperational.ID() + + device, err := devicestatetest.Device(s.state) + c.Assert(err, IsNil) + c.Check(device.Brand, Equals, "generic") + c.Check(device.Model, Equals, "generic-classic") + c.Check(device.Serial, Equals, "9999") + + // serial is there + a, err := s.db.Find(asserts.SerialType, map[string]string{ + "brand-id": "generic", + "model": "generic-classic", + "serial": "9999", + }) + c.Assert(err, IsNil) + serial := a.(*asserts.Serial) + + privKey, err := devicestate.KeypairManager(s.mgr).Get(serial.DeviceKey().ID()) + c.Assert(err, IsNil) + c.Check(privKey, NotNil) + c.Check(device.KeyID, Equals, privKey.PublicKey().ID()) + keyID1 := device.KeyID + + // mock having a store session + device.SessionMacaroon = "session-macaroon" + devicestatetest.SetDevice(s.state, device) + + err = s.mgr.Unregister(opts) + c.Assert(err, IsNil) + + device, err = devicestatetest.Device(s.state) + c.Assert(err, IsNil) + c.Check(device.Brand, Equals, "generic") + c.Check(device.Model, Equals, "generic-classic") + // unregistered + c.Check(device.Serial, Equals, "") + // forgot key + c.Check(device.KeyID, Equals, "") + // and session + c.Check(device.SessionMacaroon, Equals, "") + // key was deleted + _, err = devicestate.KeypairManager(s.mgr).Get(keyID1) + c.Check(err, ErrorMatches, "cannot find key pair") + + noRegistrationUntilReboot := opts != nil && opts.NoRegistrationUntilReboot + noregister := filepath.Join(dirs.SnapRunDir, "noregister") + if noRegistrationUntilReboot { + c.Check(noregister, testutil.FilePresent) + c.Assert(os.Remove(noregister), IsNil) + } else { + c.Check(noregister, testutil.FileAbsent) + } + + // runs the whole device registration process again + s.state.Unlock() + s.settle(c) + s.state.Lock() + + becomeOperational = s.findBecomeOperationalChange(becomeOperational1) + c.Assert(becomeOperational, NotNil) + + c.Check(becomeOperational.Status().Ready(), Equals, true) + c.Check(becomeOperational.Err(), IsNil) + + device, err = devicestatetest.Device(s.state) + c.Assert(err, IsNil) + c.Check(device.Brand, Equals, "generic") + c.Check(device.Model, Equals, "generic-classic") + c.Check(device.Serial, Equals, "10000") + + // serial is there + a, err = s.db.Find(asserts.SerialType, map[string]string{ + "brand-id": "generic", + "model": "generic-classic", + "serial": "10000", + }) + c.Assert(err, IsNil) + serial = a.(*asserts.Serial) + + privKey, err = devicestate.KeypairManager(s.mgr).Get(serial.DeviceKey().ID()) + c.Assert(err, IsNil) + c.Check(privKey, NotNil) + c.Check(device.KeyID, Equals, privKey.PublicKey().ID()) + // different from previous key + c.Check(device.KeyID, Not(Equals), keyID1) +} + +func (s *deviceMgrSerialSuite) TestFullDeviceRegistrationBlockedByNoRegister(c *C) { + restore := release.MockOnClassic(true) + defer restore() + + r1 := devicestate.MockKeyLength(testKeyLength) + defer r1() + + mockServer := s.mockServer(c, "REQID-1", nil) + defer mockServer.Close() + + r2 := devicestate.MockBaseStoreURL(mockServer.URL) + defer r2() + + // setup state as will be done by first-boot + s.state.Lock() + defer s.state.Unlock() + + // in this case is just marked seeded without snaps + s.state.Set("seeded", true) + + // not started without some installation happening or happened + // have a in-progress installation + inst := s.state.NewChange("install", "...") + task := s.state.NewTask("mount-snap", "...") + inst.AddTask(task) + + // create /run/snapd/noregister + c.Assert(os.MkdirAll(dirs.SnapRunDir, 0755), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(dirs.SnapRunDir, "noregister"), nil, 0644), IsNil) + + // attempt to run the whole device registration process + s.state.Unlock() + s.settle(c) + s.state.Lock() + + // noregister blocked it + becomeOperational := s.findBecomeOperationalChange() + c.Assert(becomeOperational, IsNil) + + s.state.Unlock() + s.se.Ensure() + s.state.Lock() + + // same, noregister blocked it + becomeOperational = s.findBecomeOperationalChange() + c.Assert(becomeOperational, IsNil) +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_systems_test.go snapd-2.54.2+21.10/overlord/devicestate/devicestate_systems_test.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_systems_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/devicestate_systems_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -40,6 +40,7 @@ "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/seed" @@ -418,7 +419,7 @@ "snapd_recovery_system": "20191119", "snapd_recovery_mode": "install", }) - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) c.Check(s.logbuf.String(), Matches, `.*: restarting into system "20191119" for action "Install"\n`) } @@ -556,7 +557,7 @@ "snapd_recovery_system": label, "snapd_recovery_mode": mode, }) - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) s.restartRequests = nil s.bootloader.BootVars = map[string]string{} @@ -747,7 +748,7 @@ m, err := s.bootloader.GetBootVars("snapd_recovery_mode", "snapd_recovery_system") c.Assert(err, IsNil) // requested restart - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) // but no bootloader changes c.Check(m, DeepEquals, map[string]string{ "snapd_recovery_system": "", @@ -776,7 +777,7 @@ "snapd_recovery_system": "20191119", "snapd_recovery_mode": "install", }) - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) c.Check(s.logbuf.String(), Matches, `.*: rebooting into system "20191119" in "install" mode\n`) } @@ -805,7 +806,7 @@ "snapd_recovery_system": s.mockedSystemSeeds[0].label, "snapd_recovery_mode": mode, }) - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) c.Check(s.logbuf.String(), Matches, fmt.Sprintf(`.*: rebooting into system "20191119" in "%s" mode\n`, mode)) } } @@ -844,7 +845,7 @@ "snapd_recovery_mode": "run", "snapd_recovery_system": s.mockedSystemSeeds[0].label, }) - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) c.Check(s.logbuf.String(), Matches, fmt.Sprintf(`.*: rebooting into system "%s" in "run" mode\n`, s.mockedSystemSeeds[0].label)) } @@ -871,7 +872,7 @@ "snapd_recovery_mode": "", "snapd_recovery_system": "", }) - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) c.Check(s.logbuf.String(), Matches, `.*: rebooting system\n`) } @@ -1370,7 +1371,7 @@ c.Assert(tskCreate.Status(), Equals, state.DoneStatus) c.Assert(tskFinalize.Status(), Equals, state.DoingStatus) // a reboot is expected - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) validateCore20Seed(c, "1234", s.model, s.storeSigning.Trusted) m, err := s.bootloader.GetBootVars("try_recovery_system", "recovery_system_status") @@ -1405,7 +1406,7 @@ testutil.FileEquals, expectedFilesLog.String()) // these things happen on snapd startup - state.MockRestarting(s.state, state.RestartUnset) + restart.MockPending(s.state, restart.RestartUnset) s.state.Set("tried-systems", []string{"1234"}) s.bootloader.SetBootVars(map[string]string{ "try_recovery_system": "", @@ -1545,7 +1546,7 @@ c.Assert(tskCreate.Status(), Equals, state.DoneStatus) c.Assert(tskFinalize.Status(), Equals, state.DoingStatus) // a reboot is expected - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) validateCore20Seed(c, "1234", newModel, s.storeSigning.Trusted, "foo", "bar") m, err := s.bootloader.GetBootVars("try_recovery_system", "recovery_system_status") @@ -1582,7 +1583,7 @@ testutil.FileEquals, expectedFilesLog.String()) // these things happen on snapd startup - state.MockRestarting(s.state, state.RestartUnset) + restart.MockPending(s.state, restart.RestartUnset) s.state.Set("tried-systems", []string{"1234"}) s.bootloader.SetBootVars(map[string]string{ "try_recovery_system": "", @@ -1746,7 +1747,7 @@ c.Assert(tskCreate.Status(), Equals, state.DoneStatus) c.Assert(tskFinalize.Status(), Equals, state.DoingStatus) // a reboot is expected - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) // sanity check asserted snaps location c.Check(filepath.Join(boot.InitramfsUbuntuSeedDir, "systems/1234undo"), testutil.FilePresent) p, err := filepath.Glob(filepath.Join(boot.InitramfsUbuntuSeedDir, "snaps/*")) @@ -1783,7 +1784,7 @@ }) // these things happen on snapd startup - state.MockRestarting(s.state, state.RestartUnset) + restart.MockPending(s.state, restart.RestartUnset) s.state.Set("tried-systems", []string{"1234undo"}) s.bootloader.SetBootVars(map[string]string{ "try_recovery_system": "", @@ -1857,7 +1858,7 @@ c.Assert(tskCreate.Status(), Equals, state.DoneStatus) c.Assert(tskFinalize.Status(), Equals, state.DoingStatus) // a reboot is expected - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) validateCore20Seed(c, "1234", s.model, s.storeSigning.Trusted) m, err := s.bootloader.GetBootVars("try_recovery_system", "recovery_system_status") @@ -1882,7 +1883,7 @@ }) // these things happen on snapd startup - state.MockRestarting(s.state, state.RestartUnset) + restart.MockPending(s.state, restart.RestartUnset) // after reboot the relevant startup code identified that the tried // system failed to operate properly s.state.Set("tried-systems", []string{}) @@ -2040,7 +2041,7 @@ c.Assert(tskCreate.Status(), Equals, state.DoneStatus) c.Assert(tskFinalize.Status(), Equals, state.DoingStatus) // a reboot is expected - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) c.Check(s.bootloader.SetBootVarsCalls, Equals, 2) s.restartRequests = nil @@ -2050,7 +2051,7 @@ // the system unexpectedly reboots before the task is marked as done tskCreate.SetStatus(state.DoStatus) tskFinalize.SetStatus(state.DoStatus) - state.MockRestarting(s.state, state.RestartUnset) + restart.MockPending(s.state, restart.RestartUnset) // we may have rebooted just before the task was marked as done, in // which case tried systems would be populated s.state.Set("tried-systems", []string{"1234undo"}) diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_test.go snapd-2.54.2+21.10/overlord/devicestate/devicestate_test.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/devicestate_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/devicestate_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -51,6 +51,7 @@ "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/ifacestate/ifacerepo" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" "github.com/snapcore/snapd/overlord/state" @@ -63,6 +64,7 @@ "github.com/snapcore/snapd/store/storetest" "github.com/snapcore/snapd/sysconfig" "github.com/snapcore/snapd/testutil" + "github.com/snapcore/snapd/timeutil" "github.com/snapcore/snapd/timings" ) @@ -89,7 +91,7 @@ ancillary []asserts.Assertion - restartRequests []state.RestartType + restartRequests []restart.RestartType restartObserve func() newFakeStore func(storecontext.DeviceBackend) snapstate.StoreService @@ -157,15 +159,15 @@ s.storeSigning = assertstest.NewStoreStack("canonical", nil) s.restartObserve = nil - s.o = overlord.MockWithStateAndRestartHandler(nil, func(req state.RestartType) { + s.o = overlord.Mock() + s.state = s.o.State() + s.state.Lock() + restart.Init(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(req restart.RestartType) { s.restartRequests = append(s.restartRequests, req) if s.restartObserve != nil { s.restartObserve() } - }) - s.state = s.o.State() - s.state.Lock() - s.state.VerifyReboot("boot-id-0") + })) s.state.Unlock() s.se = s.o.StateEngine() @@ -237,6 +239,10 @@ s.AddCleanup(func() { s.ancillary = nil }) s.AddCleanup(func() { s.newFakeStore = nil }) + + s.AddCleanup(devicestate.MockTimeutilIsNTPSynchronized(func() (bool, error) { + return true, nil + })) } func (s *deviceMgrBaseSuite) newStore(devBE storecontext.DeviceBackend) snapstate.StoreService { @@ -1779,3 +1785,90 @@ _, err := mgr.StartOfOperationTime() c.Assert(err, ErrorMatches, `internal error: unexpected call to StartOfOperationTime in preseed mode`) } + +func (s *deviceMgrSuite) TestCanAutoRefreshNTP(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // CanAutoRefresh is ready + s.state.Set("seeded", true) + s.makeModelAssertionInState(c, "canonical", "pc", map[string]interface{}{ + "architecture": "amd64", + "kernel": "pc-kernel", + "gadget": "pc", + }) + devicestatetest.SetDevice(s.state, &auth.DeviceState{ + Brand: "canonical", + Model: "pc", + Serial: "8989", + }) + s.makeSerialAssertionInState(c, "canonical", "pc", "8989") + + // now check that the ntp-sync information is honored + n := 0 + ntpSynced := false + restore := devicestate.MockTimeutilIsNTPSynchronized(func() (bool, error) { + n++ + return ntpSynced, nil + }) + defer restore() + + // not ntp-synced + ok, err := devicestate.CanAutoRefresh(s.state) + c.Assert(err, IsNil) + c.Check(ok, Equals, false) + c.Check(n, Equals, 1) + + // now ntp-synced + ntpSynced = true + ok, err = devicestate.CanAutoRefresh(s.state) + c.Assert(err, IsNil) + c.Check(ok, Equals, true) + c.Check(n, Equals, 2) + + // and the result was cached + ok, err = devicestate.CanAutoRefresh(s.state) + c.Assert(err, IsNil) + c.Check(ok, Equals, true) + c.Check(n, Equals, 2) +} + +func (s *deviceMgrSuite) TestNTPSyncedOrWaitedLongerThan(c *C) { + restore := devicestate.MockTimeutilIsNTPSynchronized(func() (bool, error) { + return false, nil + }) + defer restore() + + // NTP is not synced yet and the (arbitrary selected) wait + // time of 12h since the device manager got started is not + // over yet + syncedOrWaited := devicestate.DeviceManagerNTPSyncedOrWaitedLongerThan(s.mgr, 12*time.Hour) + c.Check(syncedOrWaited, Equals, false) + + // NTP is also not synced here but the wait time of 1 + // Nanosecond since the device manager got started is + // certainly over + syncedOrWaited = devicestate.DeviceManagerNTPSyncedOrWaitedLongerThan(s.mgr, 1*time.Nanosecond) + c.Check(syncedOrWaited, Equals, true) +} + +func (s *deviceMgrSuite) TestNTPSyncedOrWaitedNoTimedate1(c *C) { + n := 0 + restore := devicestate.MockTimeutilIsNTPSynchronized(func() (bool, error) { + n++ + // no timedate1 + return false, timeutil.NoTimedate1Error{Err: fmt.Errorf("boom")} + }) + defer restore() + + // There is no timedate1 dbus service, no point in waiting + syncedOrWaited := devicestate.DeviceManagerNTPSyncedOrWaitedLongerThan(s.mgr, 12*time.Hour) + c.Check(syncedOrWaited, Equals, true) + c.Check(n, Equals, 1) + + // and the result was cached + syncedOrWaited = devicestate.DeviceManagerNTPSyncedOrWaitedLongerThan(s.mgr, 12*time.Hour) + c.Check(syncedOrWaited, Equals, true) + c.Check(n, Equals, 1) + +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/export_test.go snapd-2.54.2+21.10/overlord/devicestate/export_test.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -249,7 +249,7 @@ CreateRecoverySystemTasks = createRecoverySystemTasks ) -func MockGadgetUpdate(mock func(current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error) (restore func()) { +func MockGadgetUpdate(mock func(model gadget.Model, current, update gadget.GadgetData, path string, policy gadget.UpdatePolicyFunc, observer gadget.ContentUpdateObserver) error) (restore func()) { old := gadgetUpdate gadgetUpdate = mock return func() { @@ -344,3 +344,15 @@ func DeviceManagerCheckFDEFeatures(mgr *DeviceManager, st *state.State) (secboot.EncryptionType, error) { return mgr.checkFDEFeatures() } + +func MockTimeutilIsNTPSynchronized(f func() (bool, error)) (restore func()) { + old := timeutilIsNTPSynchronized + timeutilIsNTPSynchronized = f + return func() { + timeutilIsNTPSynchronized = old + } +} + +func DeviceManagerNTPSyncedOrWaitedLongerThan(mgr *DeviceManager, maxWait time.Duration) bool { + return mgr.ntpSyncedOrWaitedLongerThan(maxWait) +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/firstboot20_test.go snapd-2.54.2+21.10/overlord/devicestate/firstboot20_test.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/firstboot20_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/firstboot20_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -39,6 +39,7 @@ "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/ifacestate" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" @@ -309,7 +310,7 @@ // at this point the system is "restarting", pretend the restart has // happened c.Assert(chg.Status(), Equals, state.DoingStatus) - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) st.Unlock() err = s.overlord.Settle(settleTimeout) st.Lock() diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/firstboot_test.go snapd-2.54.2+21.10/overlord/devicestate/firstboot_test.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/firstboot_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/firstboot_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2015-2019 Canonical Ltd + * Copyright (C) 2015-2021 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -49,6 +49,7 @@ "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/ifacestate" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" @@ -1415,7 +1416,7 @@ // at this point the system is "restarting", pretend the restart has // happened c.Assert(chg.Status(), Equals, state.DoingStatus) - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) st.Unlock() err = s.overlord.Settle(settleTimeout) st.Lock() @@ -1758,7 +1759,7 @@ // at this point the system is "restarting", pretend the restart has // happened c.Assert(chg.Status(), Equals, state.DoingStatus) - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) st.Unlock() err = s.overlord.Settle(settleTimeout) st.Lock() @@ -1897,7 +1898,7 @@ // at this point the system is "restarting", pretend the restart has // happened c.Assert(chg.Status(), Equals, state.DoingStatus) - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) st.Unlock() err = s.overlord.Settle(settleTimeout) st.Lock() diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/handlers_gadget.go snapd-2.54.2+21.10/overlord/devicestate/handlers_gadget.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/handlers_gadget.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/handlers_gadget.go 2022-01-06 21:25:16.000000000 +0000 @@ -191,7 +191,7 @@ // modify modeenv inside, which implicitly is guarded by the state lock; // on top of that we do not expect the update to be moving large amounts // of data - err = gadgetUpdate(*currentData, *updateData, snapRollbackDir, updatePolicy, updateObserver) + err = gadgetUpdate(model, *currentData, *updateData, snapRollbackDir, updatePolicy, updateObserver) if err != nil { if err == gadget.ErrNoUpdate { // no update needed diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/handlers.go snapd-2.54.2+21.10/overlord/devicestate/handlers.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/handlers.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/handlers.go 2022-01-06 21:25:16.000000000 +0000 @@ -27,6 +27,7 @@ "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" ) @@ -78,7 +79,7 @@ // do not mark this task done as this makes it racy against taskrunner tear down (the next task // could start). Let this task finish after snapd restart when preseed mode is off. - st.RequestRestart(state.StopDaemon) + restart.Request(st, restart.StopDaemon) } return &state.Retry{Reason: "mark-preseeded will be marked done when snapd is executed in normal mode"} diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/handlers_install.go snapd-2.54.2+21.10/overlord/devicestate/handlers_install.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/handlers_install.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/handlers_install.go 2022-01-06 21:25:16.000000000 +0000 @@ -30,10 +30,12 @@ "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/boot" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/gadget" "github.com/snapcore/snapd/gadget/install" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/randutil" @@ -121,6 +123,31 @@ return nil } +func writeTimesyncdClock(srcRootDir, dstRootDir string) error { + // keep track of the time + const timesyncClockInRoot = "/var/lib/systemd/timesync/clock" + clockSrc := filepath.Join(srcRootDir, timesyncClockInRoot) + clockDst := filepath.Join(dstRootDir, timesyncClockInRoot) + if err := os.MkdirAll(filepath.Dir(clockDst), 0755); err != nil { + return fmt.Errorf("cannot store the clock: %v", err) + } + if !osutil.FileExists(clockSrc) { + logger.Noticef("timesyncd clock timestamp %v does not exist", clockSrc) + return nil + } + // clock file is owned by a specific user/group, thus preserve + // attributes of the source + if err := osutil.CopyFile(clockSrc, clockDst, osutil.CopyFlagPreserveAll); err != nil { + return fmt.Errorf("cannot copy clock: %v", err) + } + // the file is empty however, its modification timestamp is used to set + // up the current time + if err := os.Chtimes(clockDst, timeNow(), timeNow()); err != nil { + return fmt.Errorf("cannot update clock timestamp: %v", err) + } + return nil +} + func writeTimings(st *state.State, rootdir string) error { logPath := filepath.Join(rootdir, "var/log/install-timings.txt.gz") if err := os.MkdirAll(filepath.Dir(logPath), 0755); err != nil { @@ -317,6 +344,12 @@ return fmt.Errorf("cannot store the model: %v", err) } + // preserve systemd-timesyncd clock timestamp, so that RTC-less devices + // can start with a more recent time on the next boot + if err := writeTimesyncdClock(dirs.GlobalRootDir, boot.InstallHostWritableDir); err != nil { + return fmt.Errorf("cannot seed timesyncd clock: %v", err) + } + // configure the run system opts := &sysconfig.Options{TargetRootDir: boot.InstallHostWritableDir, GadgetDir: gadgetDir} // configure cloud init @@ -570,18 +603,18 @@ // request by default a restart as the last action after a // successful install or what install-device requested via // snapctl reboot - rst := state.RestartSystemNow + rst := restart.RestartSystemNow what := "restart" switch rebootOpts.Op { case RebootHaltOp: what = "halt" - rst = state.RestartSystemHaltNow + rst = restart.RestartSystemHaltNow case RebootPoweroffOp: what = "poweroff" - rst = state.RestartSystemPoweroffNow + rst = restart.RestartSystemPoweroffNow } logger.Noticef("request immediate system %s", what) - st.RequestRestart(rst) + restart.Request(st, rst) return nil } diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/handlers_systems.go snapd-2.54.2+21.10/overlord/devicestate/handlers_systems.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/handlers_systems.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/handlers_systems.go 2022-01-06 21:25:16.000000000 +0000 @@ -34,6 +34,7 @@ "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/assertstate" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" @@ -144,8 +145,44 @@ // get all infos infoGetter := func(name string) (info *snap.Info, present bool, err error) { - // snap may be present in the system in which case info comes - // from snapstate + // snaps are either being fetched or present in the system + + if isRemodel { + // in a remodel scenario, the snaps may need to be + // fetched and thus their content can be different from + // what we have in already installed snaps, so we should + // first check the download tasks before consulting + // snapstate + logger.Debugf("requested info for snap %q being installed during remodel", name) + for _, tskID := range setup.SnapSetupTasks { + taskWithSnapSetup := st.Task(tskID) + snapsup, err := snapstate.TaskSnapSetup(taskWithSnapSetup) + if err != nil { + return nil, false, err + } + if snapsup.SnapName() != name { + continue + } + // by the time this task runs, the file has already been + // downloaded and validated + snapFile, err := snapfile.Open(snapsup.MountFile()) + if err != nil { + return nil, false, err + } + info, err = snap.ReadInfoFromSnapFile(snapFile, snapsup.SideInfo) + if err != nil { + return nil, false, err + } + + return info, true, nil + } + } + + // either a remodel scenario, in which case the snap is not + // among the ones being fetched, or just creating a recovery + // system, in which case we use the snaps that are already + // installed + info, err = snapstate.CurrentInfo(st, name) if err == nil { hash, _, err := asserts.SnapFileSHA3_384(info.MountFile()) @@ -158,40 +195,6 @@ if _, ok := err.(*snap.NotInstalledError); !ok { return nil, false, err } - logger.Debugf("requested info for not yet installed snap %q", name) - - if !isRemodel { - // when not in remodel, a recovery system can only be - // created from snaps that are already installed - return nil, false, nil - } - - // in a remodel scenario, the snaps may need to be fetched, and - // thus we can pull the relevant information from the tasks - // carrying snap-setup - - for _, tskID := range setup.SnapSetupTasks { - taskWithSnapSetup := st.Task(tskID) - snapsup, err := snapstate.TaskSnapSetup(taskWithSnapSetup) - if err != nil { - return nil, false, err - } - if snapsup.SnapName() != name { - continue - } - // by the time this task runs, the file has already been - // downloaded and validated - snapFile, err := snapfile.Open(snapsup.MountFile()) - if err != nil { - return nil, false, err - } - info, err = snap.ReadInfoFromSnapFile(snapFile, snapsup.SideInfo) - if err != nil { - return nil, false, err - } - - return info, true, nil - } return nil, false, nil } @@ -272,7 +275,7 @@ t.SetStatus(state.DoneStatus) logger.Noticef("restarting into candidate system %q", label) - m.state.RequestRestart(state.RestartSystemNow) + restart.Request(m.state, restart.RestartSystemNow) return nil } @@ -326,7 +329,7 @@ st.Lock() defer st.Unlock() - if ok, _ := st.Restarting(); ok { + if ok, _ := restart.Pending(st); ok { // don't continue until we are in the restarted snapd t.Logf("Waiting for system reboot...") return &state.Retry{} diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/handlers_test.go snapd-2.54.2+21.10/overlord/devicestate/handlers_test.go --- snapd-2.53+21.10ubuntu1/overlord/devicestate/handlers_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/handlers_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -34,6 +34,7 @@ "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/devicestate/devicestatetest" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/overlord/storecontext" @@ -331,7 +332,7 @@ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name)) tInstall.WaitFor(tValidate) ts := state.NewTaskSet(tDownload, tValidate, tInstall) - ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge) + ts.MarkEdge(tValidate, snapstate.LastBeforeLocalModificationsEdge) return ts, nil }) defer restore() @@ -544,7 +545,7 @@ }) // and snapd stop was requested - c.Check(s.restartRequests, DeepEquals, []state.RestartType{state.StopDaemon}) + c.Check(s.restartRequests, DeepEquals, []restart.RestartType{restart.StopDaemon}) s.cmdUmount.ForgetCalls() diff -Nru snapd-2.53+21.10ubuntu1/overlord/devicestate/sk.yaml snapd-2.54.2+21.10/overlord/devicestate/sk.yaml --- snapd-2.53+21.10ubuntu1/overlord/devicestate/sk.yaml 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/devicestate/sk.yaml 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,38 @@ +f: true +sk: { + "apparmor-features": [ + "caps", + "dbus", + "domain", + "file", + "mount", + "namespaces", + "network", + "network_v8", + "policy", + "ptrace", + "query", + "rlimit", + "signal" + ], + "apparmor-parser-features": [ + "unsafe" + ], + "apparmor-parser-mtime": 1589907589, + "build-id": "366ed66fa63a7e11bcc0661386e12d493f079638", + "cgroup-version": "1", + "nfs-home": false, + "overlay-root": "", + "seccomp-compiler-version": "1a7f0a68eb5ad7c3d91bdd646be926cf2e026f92 2.4.3 9b218ef9a4e508dd8a7f848095cb8875d10a4bf28428ad81fdc3f8dac89108f7 bpf-actlog", + "seccomp-features": [ + "allow", + "errno", + "kill_process", + "kill_thread", + "log", + "trace", + "trap", + "user_notif" + ], + "version": 10 +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/hookstate/ctlcmd/fde_setup.go snapd-2.54.2+21.10/overlord/hookstate/ctlcmd/fde_setup.go --- snapd-2.53+21.10ubuntu1/overlord/hookstate/ctlcmd/fde_setup.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/hookstate/ctlcmd/fde_setup.go 2022-01-06 21:25:16.000000000 +0000 @@ -75,9 +75,9 @@ if err := context.Get("fde-setup-request", &fdeSetup); err != nil { return fmt.Errorf("cannot get fde-setup-op from context: %v", err) } - // Op is either "initial-setup" or "features" + // Op is either "initial-setup", "device-setup" or "features" switch fdeSetup.Op { - case "features", "initial-setup": + case "features", "initial-setup", "device-setup": // fine default: return fmt.Errorf("unknown fde-setup-request op %q", fdeSetup.Op) diff -Nru snapd-2.53+21.10ubuntu1/overlord/hookstate/ctlcmd/fde_setup_test.go snapd-2.54.2+21.10/overlord/hookstate/ctlcmd/fde_setup_test.go --- snapd-2.53+21.10ubuntu1/overlord/hookstate/ctlcmd/fde_setup_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/hookstate/ctlcmd/fde_setup_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -64,7 +64,7 @@ s.st.Lock() defer s.st.Unlock() - mockInstalledSnap(c, s.st, mockFdeSetupKernelYaml) + mockInstalledSnap(c, s.st, mockFdeSetupKernelYaml, "") s.mockTask = s.st.NewTask("test-task", "my test task") hooksup := &hookstate.HookSetup{ Snap: "pc-kernel", @@ -166,3 +166,24 @@ s.mockContext.Unlock() c.Check(fdeSetupResult, DeepEquals, mockStdin) } + +func (s *fdeSetupSuite) TestFdeSetupRequestOpDeviceSetup(c *C) { + mockKey := secboot.EncryptionKey{1, 2, 3, 4} + fdeSetup := &fde.SetupRequest{ + Op: "device-setup", + Key: mockKey[:], + Device: "/dev/sda1", + } + s.mockContext.Lock() + s.mockContext.Set("fde-setup-request", fdeSetup) + s.mockContext.Unlock() + + stdout, stderr, err := ctlcmd.Run(s.mockContext, []string{"fde-setup-request"}, 0) + c.Assert(err, IsNil) + + // the encryption key should be base64 encoded + encodedBase64Key := base64.StdEncoding.EncodeToString(mockKey[:]) + + c.Check(string(stdout), Equals, fmt.Sprintf(`{"op":"device-setup","key":%q,"device":"/dev/sda1"}`+"\n", encodedBase64Key)) + c.Check(string(stderr), Equals, "") +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/hookstate/ctlcmd/is_connected_test.go snapd-2.54.2+21.10/overlord/hookstate/ctlcmd/is_connected_test.go --- snapd-2.53+21.10ubuntu1/overlord/hookstate/ctlcmd/is_connected_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/hookstate/ctlcmd/is_connected_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -134,7 +134,7 @@ exitCode: ctlcmd.ClassicSnapCode, }} -func mockInstalledSnap(c *C, st *state.State, snapYaml string) { +func mockInstalledSnap(c *C, st *state.State, snapYaml, cohortKey string) { info := snaptest.MockSnapCurrent(c, snapYaml, &snap.SideInfo{Revision: snap.R(1)}) snapstate.Set(st, info.InstanceName(), &snapstate.SnapState{ Active: true, @@ -147,6 +147,7 @@ }, Current: info.Revision, TrackingChannel: "stable", + CohortKey: cohortKey, }) } @@ -165,11 +166,11 @@ cc: interface: cups-control audio-record: - interface: audio-record`) + interface: audio-record`, "") mockInstalledSnap(c, s.st, `name: snap2 slots: slot2: - interface: x11`) + interface: x11`, "") mockInstalledSnap(c, s.st, `name: snap3 plugs: plug4: @@ -178,16 +179,16 @@ interface: cups-control slots: slot3: - interface: x11`) + interface: x11`, "") mockInstalledSnap(c, s.st, `name: snap4 slots: slot4: - interface: x11`) + interface: x11`, "") mockInstalledSnap(c, s.st, `name: snap5 confinement: classic plugs: cc: - interface: cups-control`) + interface: cups-control`, "") restore := ctlcmd.MockCgroupSnapNameFromPid(func(pid int) (string, error) { switch { case 1000 < pid && pid < 1100: @@ -269,7 +270,7 @@ mockInstalledSnap(c, s.st, `name: snap1 plugs: plug1: - interface: x11`) + interface: x11`, "") s.st.Set("conns", map[string]interface{}{ "snap1:plug1 snap2:slot2": map[string]interface{}{}, diff -Nru snapd-2.53+21.10ubuntu1/overlord/hookstate/ctlcmd/refresh.go snapd-2.54.2+21.10/overlord/hookstate/ctlcmd/refresh.go --- snapd-2.53+21.10ubuntu1/overlord/hookstate/ctlcmd/refresh.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/hookstate/ctlcmd/refresh.go 2022-01-06 21:25:16.000000000 +0000 @@ -143,15 +143,20 @@ } type updateDetails struct { - Pending string `yaml:"pending,omitempty"` - Channel string `yaml:"channel,omitempty"` - Version string `yaml:"version,omitempty"` - Revision int `yaml:"revision,omitempty"` + Pending string `yaml:"pending,omitempty"` + Channel string `yaml:"channel,omitempty"` + CohortKey string `yaml:"cohort,omitempty"` + Version string `yaml:"version,omitempty"` + Revision int `yaml:"revision,omitempty"` // TODO: epoch Base bool `yaml:"base"` Restart bool `yaml:"restart"` } +type holdDetails struct { + Hold string `yaml:"hold"` +} + // refreshCandidate is a subset of refreshCandidate defined by snapstate and // stored in "refresh-candidates". type refreshCandidate struct { @@ -204,6 +209,14 @@ Pending: pending, } + hasRefreshControl, err := hasSnapRefreshControlInterface(st, context.InstanceName()) + if err != nil { + return nil, err + } + if hasRefreshControl { + up.CohortKey = snapst.CohortKey + } + // try to find revision/version/channel info from refresh-candidates; it // may be missing if the hook is called for snap that is just affected by // refresh but not refreshed itself, in such case this data is not @@ -259,10 +272,19 @@ // no duration specified, use maximum allowed for this gating snap. var holdDuration time.Duration - if err := snapstate.HoldRefresh(st, ctx.InstanceName(), holdDuration, affecting...); err != nil { + remaining, err := snapstate.HoldRefresh(st, ctx.InstanceName(), holdDuration, affecting...) + if err != nil { // TODO: let a snap hold again once for 1h. return err } + var details holdDetails + details.Hold = remaining.String() + + out, err := yaml.Marshal(details) + if err != nil { + return err + } + c.printf("%s", string(out)) return nil } @@ -275,11 +297,11 @@ // running outside of hook if ctx.IsEphemeral() { st := ctx.State() - allow, err := allowRefreshProceedOutsideHook(st, ctx.InstanceName()) + hasRefreshControl, err := hasSnapRefreshControlInterface(st, ctx.InstanceName()) if err != nil { return err } - if !allow { + if !hasRefreshControl { return fmt.Errorf("cannot proceed: requires snap-refresh-control interface") } // we need to check if GateAutoRefreshHook feature is enabled when @@ -305,7 +327,7 @@ return nil } -func allowRefreshProceedOutsideHook(st *state.State, snapName string) (bool, error) { +func hasSnapRefreshControlInterface(st *state.State, snapName string) (bool, error) { conns, err := ifacestate.ConnectionStates(st) if err != nil { return false, fmt.Errorf("internal error: cannot get connections: %s", err) diff -Nru snapd-2.53+21.10ubuntu1/overlord/hookstate/ctlcmd/refresh_test.go snapd-2.54.2+21.10/overlord/hookstate/ctlcmd/refresh_test.go --- snapd-2.53+21.10ubuntu1/overlord/hookstate/ctlcmd/refresh_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/hookstate/ctlcmd/refresh_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -131,15 +131,15 @@ version: 1 hooks: gate-auto-refresh: -`) +`, "") mockInstalledSnap(c, s.st, `name: snap1-base type: base version: 1 -`) +`, "") mockInstalledSnap(c, s.st, `name: kernel type: kernel version: 1 -`) +`, "") s.st.Unlock() for _, test := range refreshFromHookTests { @@ -182,11 +182,11 @@ version: 1 hooks: gate-auto-refresh: -`) +`, "") mockInstalledSnap(c, s.st, `name: snap1-base type: base version: 1 -`) +`, "") candidates := map[string]interface{}{ "snap1-base": mockRefreshCandidate("snap1-base", "edge", "v1", snap.Revision{N: 3}), @@ -197,7 +197,7 @@ stdout, stderr, err := ctlcmd.Run(mockContext, []string{"refresh", "--hold"}, 0) c.Assert(err, IsNil) - c.Check(string(stdout), Equals, "") + c.Check(string(stdout), Equals, "hold: 48h0m0s\n") c.Check(string(stderr), Equals, "") mockContext.Lock() @@ -220,10 +220,11 @@ mockInstalledSnap(c, s.st, `name: foo version: 1 -`) +`, "") // pretend snap foo is held initially - c.Check(snapstate.HoldRefresh(s.st, "snap1", 0, "foo"), IsNil) + _, err = snapstate.HoldRefresh(s.st, "snap1", 0, "foo") + c.Check(err, IsNil) s.st.Unlock() // sanity check @@ -300,7 +301,7 @@ // note: don't mock the plug, it's enough to have it in conns mockInstalledSnap(c, s.st, `name: foo version: 1 -`) +`, "") s.st.Set("conns", map[string]interface{}{ "foo:plug core:slot": map[string]interface{}{"interface": "snap-refresh-control"}, @@ -329,7 +330,7 @@ mockInstalledSnap(c, s.st, `name: foo version: 1 -`) +`, "") setup := &hookstate.HookSetup{Snap: "foo", Revision: snap.R(1)} mockContext, err := hookstate.NewContext(nil, s.st, setup, nil, "") @@ -342,6 +343,64 @@ c.Check(string(stdout), Equals, "pending: none\nchannel: stable\nbase: false\nrestart: false\n") } +func (s *refreshSuite) TestPendingFromSnapWithCohort(c *C) { + s.st.Lock() + defer s.st.Unlock() + + mockInstalledSnap(c, s.st, `name: foo +version: 1 +`, "some-cohort-key") + + setup := &hookstate.HookSetup{Snap: "foo", Revision: snap.R(1)} + mockContext, err := hookstate.NewContext(nil, s.st, setup, nil, "") + c.Check(err, IsNil) + + s.st.Unlock() + defer s.st.Lock() + + stdout, _, err := ctlcmd.Run(mockContext, []string{"refresh", "--pending"}, 0) + c.Assert(err, IsNil) + // cohort is not printed if snap-refresh-control isn't connected + c.Check(string(stdout), Equals, "pending: none\nchannel: stable\nbase: false\nrestart: false\n") + + s.st.Lock() + s.st.Set("conns", map[string]interface{}{ + "foo:plug core:slot": map[string]interface{}{"interface": "snap-refresh-control"}, + }) + s.st.Unlock() + + stdout, _, err = ctlcmd.Run(mockContext, []string{"refresh", "--pending"}, 0) + c.Assert(err, IsNil) + // cohort is printed + c.Check(string(stdout), Equals, "pending: none\nchannel: stable\ncohort: some-cohort-key\nbase: false\nrestart: false\n") +} + +func (s *refreshSuite) TestPendingWithCohort(c *C) { + s.st.Lock() + defer s.st.Unlock() + + mockInstalledSnap(c, s.st, `name: foo +version: 1 +`, "some-cohort-key") + + task := s.st.NewTask("test-task", "my test task") + setup := &hookstate.HookSetup{Snap: "foo", Revision: snap.R(1), Hook: "gate-auto-refresh"} + mockContext, err := hookstate.NewContext(task, s.st, setup, s.mockHandler, "") + c.Check(err, IsNil) + + s.st.Set("conns", map[string]interface{}{ + "foo:plug core:slot": map[string]interface{}{"interface": "snap-refresh-control"}, + }) + + s.st.Unlock() + defer s.st.Lock() + + stdout, _, err := ctlcmd.Run(mockContext, []string{"refresh", "--pending"}, 0) + c.Assert(err, IsNil) + // cohort is printed + c.Check(string(stdout), Equals, "pending: none\nchannel: stable\ncohort: some-cohort-key\nbase: false\nrestart: false\n") +} + func (s *refreshSuite) TestRefreshProceedFromSnapError(c *C) { restore := ctlcmd.MockAutoRefreshForGatingSnap(func(st *state.State, gatingSnap string) error { c.Check(gatingSnap, Equals, "foo") @@ -354,7 +413,7 @@ // note: don't mock the plug, it's enough to have it in conns mockInstalledSnap(c, s.st, `name: foo version: 1 -`) +`, "") s.st.Set("conns", map[string]interface{}{ "foo:plug core:slot": map[string]interface{}{"interface": "snap-refresh-control"}, }) @@ -388,7 +447,7 @@ // note: don't mock the plug, it's enough to have it in conns mockInstalledSnap(c, s.st, `name: foo version: 1 -`) +`, "") s.st.Set("conns", map[string]interface{}{ "foo:plug core:slot": map[string]interface{}{ "interface": "snap-refresh-control", diff -Nru snapd-2.53+21.10ubuntu1/overlord/hookstate/ctlcmd/services_test.go snapd-2.54.2+21.10/overlord/hookstate/ctlcmd/services_test.go --- snapd-2.53+21.10ubuntu1/overlord/hookstate/ctlcmd/services_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/hookstate/ctlcmd/services_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -599,6 +599,7 @@ case "show": c.Check(args[2], Matches, `snap\.test-snap\.\w+-service\.service`) return []byte(fmt.Sprintf(`Id=%s +Names=%[1]s Type=simple ActiveState=active UnitFileState=enabled @@ -630,6 +631,7 @@ c.Assert(args[0], Equals, "show") c.Check(args[2], Equals, "snap.test-snap.test-service.service") return []byte(`Id=snap.test-snap.test-service.service +Names=snap.test-snap.test-service.service Type=simple ActiveState=active UnitFileState=enabled diff -Nru snapd-2.53+21.10ubuntu1/overlord/hookstate/hookmgr.go snapd-2.54.2+21.10/overlord/hookstate/hookmgr.go --- snapd-2.53+21.10ubuntu1/overlord/hookstate/hookmgr.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/hookstate/hookmgr.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2017 Canonical Ltd + * Copyright (C) 2016-2021 Canonical Ltd * * This 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,6 +37,7 @@ "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/configstate/settings" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" @@ -314,6 +315,20 @@ return m.runHook(context, snapst, hooksup, tomb) } +// runHookGuardForRestarting helps avoiding running a hook if we are +// restarting by returning state.Retry in such case. +func (m *HookManager) runHookGuardForRestarting(context *Context) error { + context.Lock() + defer context.Unlock() + if ok, _ := restart.Pending(m.state); ok { + return &state.Retry{} + } + + // keep count of running hooks + atomic.AddInt32(&m.runningHooks, 1) + return nil +} + func (m *HookManager) runHook(context *Context, snapst *snapstate.SnapState, hooksup *HookSetup, tomb *tomb.Tomb) error { mustHijack := m.hijacked(hooksup.Hook, hooksup.Snap) != nil hookExists := false @@ -336,13 +351,9 @@ if hookExists || mustHijack { // we will run something, not a noop - if ok, _ := m.state.Restarting(); ok { - // don't start running a hook if we are restarting - return &state.Retry{} + if err := m.runHookGuardForRestarting(context); err != nil { + return err } - - // keep count of running hooks - atomic.AddInt32(&m.runningHooks, 1) defer atomic.AddInt32(&m.runningHooks, -1) } else if !hooksup.Always { // a noop with no 'always' flag: bail diff -Nru snapd-2.53+21.10ubuntu1/overlord/hookstate/hooks.go snapd-2.54.2+21.10/overlord/hookstate/hooks.go --- snapd-2.53+21.10ubuntu1/overlord/hookstate/hooks.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/hookstate/hooks.go 2022-01-06 21:25:16.000000000 +0000 @@ -241,7 +241,7 @@ // no duration specified, use maximum allowed for this gating snap. var holdDuration time.Duration - if err := snapstate.HoldRefresh(st, snapName, holdDuration, affecting...); err != nil { + if _, err := snapstate.HoldRefresh(st, snapName, holdDuration, affecting...); err != nil { // log the original hook error as we either ignore it or error out from // this handler, in both cases hookErr won't be logged by hook manager. h.context.Errorf("error: %v (while handling previous hook error: %v)", err, hookErr) diff -Nru snapd-2.53+21.10ubuntu1/overlord/hookstate/hookstate_test.go snapd-2.54.2+21.10/overlord/hookstate/hookstate_test.go --- snapd-2.53+21.10ubuntu1/overlord/hookstate/hookstate_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/hookstate/hookstate_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -37,6 +37,7 @@ "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/hookstate/hooktest" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" @@ -74,6 +75,9 @@ dirs.SetRootDir(c.MkDir()) s.o = overlord.Mock() s.state = s.o.State() + s.state.Lock() + restart.Init(s.state, "boot-id-0", nil) + s.state.Unlock() manager, err := hookstate.Manager(s.state, s.o.TaskRunner()) c.Assert(err, IsNil) s.manager = manager @@ -269,7 +273,9 @@ func (s *hookManagerSuite) TestHookTaskEnsureRestarting(c *C) { // we do no start new hooks runs if we are restarting - s.state.RequestRestart(state.RestartDaemon) + s.state.Lock() + restart.MockPending(s.state, restart.RestartDaemon) + s.state.Unlock() s.se.Ensure() s.se.Wait() diff -Nru snapd-2.53+21.10ubuntu1/overlord/hookstate/hooks_test.go snapd-2.54.2+21.10/overlord/hookstate/hooks_test.go --- snapd-2.53+21.10ubuntu1/overlord/hookstate/hooks_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/hookstate/hooks_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -242,7 +242,8 @@ defer st.Unlock() // pretend that snap-a is initially held by itself. - c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a"), IsNil) + _, err := snapstate.HoldRefresh(st, "snap-a", 0, "snap-a") + c.Assert(err, IsNil) // sanity checkIsHeld(c, st, "snap-a", "snap-a") @@ -292,7 +293,8 @@ defer st.Unlock() // pretend that snap-b is initially held by snap-a. - c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b"), IsNil) + _, err := snapstate.HoldRefresh(st, "snap-a", 0, "snap-b") + c.Assert(err, IsNil) // sanity checkIsHeld(c, st, "snap-b", "snap-a") diff -Nru snapd-2.53+21.10ubuntu1/overlord/managers_test.go snapd-2.54.2+21.10/overlord/managers_test.go --- snapd-2.53+21.10ubuntu1/overlord/managers_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/managers_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -67,6 +67,7 @@ "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/hookstate/ctlcmd" "github.com/snapcore/snapd/overlord/ifacestate" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/servicestate" "github.com/snapcore/snapd/overlord/servicestate/servicestatetest" "github.com/snapcore/snapd/overlord/snapshotstate" @@ -180,7 +181,7 @@ }) s.automaticSnapshots = nil - r := snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string) (*client.Snapshot, error) { + r := snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string, _ *dirs.SnapDirOptions) (*client.Snapshot, error) { s.automaticSnapshots = append(s.automaticSnapshots, automaticSnapshotCall{InstanceName: si.InstanceName(), SnapConfig: cfg, Usernames: usernames}) return nil, nil }) @@ -1952,10 +1953,10 @@ st.Lock() c.Assert(err, IsNil) - // final steps will are post poned until we are in the restarted snapd - ok, rst := st.Restarting() + // final steps will are postponed until we are in the restarted snapd + ok, rst := restart.Pending(st) c.Assert(ok, Equals, true) - c.Assert(rst, Equals, state.RestartSystem) + c.Assert(rst, Equals, restart.RestartSystem) t := findKind(chg, "auto-connect") c.Assert(t, NotNil) @@ -1969,10 +1970,9 @@ "snap_mode": boot.TryStatus, }) - // simulate successful restart happened - state.MockRestarting(st, state.RestartUnset) - bloader.BootVars["snap_mode"] = boot.DefaultStatus - bloader.SetBootBase("core_x1.snap") + // simulate successful restart happened, technically "core" is of type + // "os", but for the purpose of the mock it is handled like a base + s.mockSuccessfulReboot(c, bloader, []snap.Type{snap.TypeBase}) st.Unlock() err = s.o.Settle(settleTimeout) @@ -1980,6 +1980,99 @@ c.Assert(err, IsNil) c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err())) + + c.Assert(bloader.BootVars, DeepEquals, map[string]string{ + "snap_core": "core_x1.snap", + "snap_try_core": "", + "snap_try_kernel": "", + "snap_mode": "", + }) +} + +func (s *mgrsSuite) TestInstallCoreSnapUpdatesBootloaderEnvAndFailWithRollback(c *C) { + bloader := boottest.MockUC16Bootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + defer bootloader.Force(nil) + bloader.SetBootBase("core_99.snap") + + restore := release.MockOnClassic(false) + defer restore() + + model := s.brands.Model("my-brand", "my-model", modelDefaults) + + const packageOS = ` +name: core +version: 16.04-1 +type: os +` + snapPath := makeTestSnap(c, packageOS) + + st := s.o.State() + st.Lock() + defer st.Unlock() + + // setup model assertion + assertstatetest.AddMany(st, s.brands.AccountsAndKeys("my-brand")...) + devicestatetest.SetDevice(st, &auth.DeviceState{ + Brand: "my-brand", + Model: "my-model", + Serial: "serialserialserial", + }) + err := assertstate.Add(st, model) + c.Assert(err, IsNil) + + ts, _, err := snapstate.InstallPath(st, &snap.SideInfo{RealName: "core"}, snapPath, "", "", snapstate.Flags{}) + c.Assert(err, IsNil) + chg := st.NewChange("install-snap", "...") + chg.AddAll(ts) + + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + + // final steps will be postponed until we are in the restarted snapd + ok, rst := restart.Pending(st) + c.Assert(ok, Equals, true) + c.Assert(rst, Equals, restart.RestartSystem) + + t := findKind(chg, "auto-connect") + c.Assert(t, NotNil) + c.Assert(t.Status(), Equals, state.DoingStatus, Commentf("install-snap change failed with: %v", chg.Err())) + + // this is already set + c.Assert(bloader.BootVars, DeepEquals, map[string]string{ + "snap_core": "core_99.snap", + "snap_try_core": "core_x1.snap", + "snap_try_kernel": "", + "snap_mode": boot.TryStatus, + }) + + // simulate a reboot in which bootloader updates the env + s.mockRollbackAcrossReboot(c, bloader, []snap.Type{snap.TypeBase}) + + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) + + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + + c.Assert(chg.Status(), Equals, state.ErrorStatus, Commentf("install-snap change did not fail")) + tLink := findKind(chg, "link-snap") + c.Assert(tLink, NotNil) + c.Assert(tLink.Status(), Equals, state.UndoneStatus) + + c.Assert(bloader.BootVars, DeepEquals, map[string]string{ + "snap_core": "core_99.snap", + "snap_try_core": "", + "snap_try_kernel": "", + "snap_mode": "", + }) } type rebootEnv interface { @@ -1989,25 +2082,27 @@ func (s *baseMgrsSuite) mockSuccessfulReboot(c *C, be rebootEnv, which []snap.Type) { st := s.o.State() - restarting, restartType := st.Restarting() + restarting, restartType := restart.Pending(st) c.Assert(restarting, Equals, true, Commentf("mockSuccessfulReboot called when there was no pending restart")) - c.Assert(restartType, Equals, state.RestartSystem, Commentf("mockSuccessfulReboot called but restartType is not SystemRestart but %v", restartType)) - state.MockRestarting(st, state.RestartUnset) - err := be.SetTryingDuringReboot(which) - c.Assert(err, IsNil) + c.Assert(restartType, Equals, restart.RestartSystem, Commentf("mockSuccessfulReboot called but restartType is not SystemRestart but %v", restartType)) + restart.MockPending(st, restart.RestartUnset) + if len(which) > 0 { + err := be.SetTryingDuringReboot(which) + c.Assert(err, IsNil) + } s.o.DeviceManager().ResetToPostBootState() st.Unlock() defer st.Lock() - err = s.o.DeviceManager().Ensure() + err := s.o.DeviceManager().Ensure() c.Assert(err, IsNil) } func (s *baseMgrsSuite) mockRollbackAcrossReboot(c *C, be rebootEnv, which []snap.Type) { st := s.o.State() - restarting, restartType := st.Restarting() + restarting, restartType := restart.Pending(st) c.Assert(restarting, Equals, true, Commentf("mockRollbackAcrossReboot called when there was no pending restart")) - c.Assert(restartType, Equals, state.RestartSystem, Commentf("mockRollbackAcrossReboot called but restartType is not SystemRestart but %v", restartType)) - state.MockRestarting(st, state.RestartUnset) + c.Assert(restartType, Equals, restart.RestartSystem, Commentf("mockRollbackAcrossReboot called but restartType is not SystemRestart but %v", restartType)) + restart.MockPending(st, restart.RestartUnset) err := be.SetRollbackAcrossReboot(which) c.Assert(err, IsNil) s.o.DeviceManager().ResetToPostBootState() @@ -2087,7 +2182,7 @@ st.Lock() c.Assert(err, IsNil) // we are in restarting state and the change is not done yet - restarting, _ := st.Restarting() + restarting, _ := restart.Pending(st) c.Check(restarting, Equals, true) c.Check(chg.Status(), Equals, state.DoingStatus) @@ -2107,6 +2202,14 @@ c.Assert(err, IsNil) c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("install-snap change failed with: %v", chg.Err())) + + c.Assert(bloader.BootVars, DeepEquals, map[string]string{ + "snap_core": "core18_2.snap", + "snap_try_core": "", + "snap_kernel": "pc-kernel_x1.snap", + "snap_try_kernel": "", + "snap_mode": "", + }) } func (s *mgrsSuite) TestInstallKernelSnapUndoUpdatesBootloaderEnv(c *C) { @@ -2193,7 +2296,7 @@ }) // we are in restarting state and the change is not done yet - restarting, _ := st.Restarting() + restarting, _ := restart.Pending(st) c.Check(restarting, Equals, true) c.Check(chg.Status(), Equals, state.DoingStatus) // pretend we restarted @@ -2214,8 +2317,25 @@ "snap_kernel": "pc-kernel_x1.snap", "snap_mode": boot.TryStatus, }) - restarting, _ = st.Restarting() + restarting, _ = restart.Pending(st) c.Check(restarting, Equals, true) + + // pretend we restarted back to the old kernel + s.mockSuccessfulReboot(c, bloader, []snap.Type{snap.TypeKernel}) + + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + + // and we undo the bootvars and trigger a reboot + c.Check(bloader.BootVars, DeepEquals, map[string]string{ + "snap_core": "core18_2.snap", + "snap_try_core": "", + "snap_try_kernel": "", + "snap_kernel": "pc-kernel_123.snap", + "snap_mode": "", + }) } func (s *mgrsSuite) TestInstallKernelSnap20UpdatesBootloaderEnv(c *C) { @@ -2323,7 +2443,7 @@ }) // we are in restarting state and the change is not done yet - restarting, _ := st.Restarting() + restarting, _ := restart.Pending(st) c.Check(restarting, Equals, true) c.Check(chg.Status(), Equals, state.DoingStatus) @@ -2517,7 +2637,7 @@ c.Assert(currentTryKernel.Filename(), Equals, kernelSnapInfo.Filename()) // we are in restarting state and the change is not done yet - restarting, _ := st.Restarting() + restarting, _ := restart.Pending(st) c.Check(restarting, Equals, true) c.Check(chg.Status(), Equals, state.DoingStatus) // pretend we restarted @@ -2531,7 +2651,7 @@ c.Assert(chg.Status(), Equals, state.ErrorStatus) // we should have triggered a reboot to undo the boot changes - restarting, _ = st.Restarting() + restarting, _ = restart.Pending(st) c.Check(restarting, Equals, true) // we need to reboot with a "new" try kernel, so kernel_status was set again @@ -3781,7 +3901,7 @@ c.Assert(err, IsNil) // simulate successful restart happened - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) tts[2].Tasks()[0].SetStatus(state.DefaultStatus) st.Unlock() @@ -4291,6 +4411,15 @@ return i } +func validateGadgetSwitchTasks(c *C, tasks []*state.Task, label, rev string) int { + var i int + c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Update assets from gadget %q (%s) for remodel`, label, rev)) + i++ + c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Update kernel command line from gadget %q (%s) for remodel`, label, rev)) + i++ + return i +} + // byReadyTime sorts a list of tasks by their "ready" time type byReadyTime []*state.Task @@ -4627,7 +4756,7 @@ // simulate successful restart happened and that the bootvars // got updated - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) bloader.SetBootVars(map[string]string{ "snap_mode": boot.DefaultStatus, "snap_core": "core20_2.snap", @@ -4786,9 +4915,9 @@ c.Assert(chg.Status(), Equals, state.ErrorStatus) // and we are in restarting state - restarting, restartType := st.Restarting() + restarting, restartType := restart.Pending(st) c.Check(restarting, Equals, true) - c.Check(restartType, Equals, state.RestartSystem) + c.Check(restartType, Equals, restart.RestartSystem) // and the undo gave us our old kernel back c.Assert(bloader.BootVars, DeepEquals, map[string]string{ @@ -4923,7 +5052,7 @@ c.Assert(chg.Status(), Equals, state.ErrorStatus) // and we are *not* in restarting state - restarting, _ := st.Restarting() + restarting, _ := restart.Pending(st) c.Check(restarting, Equals, false) // bootvars unchanged c.Assert(bloader.BootVars, DeepEquals, map[string]string{ @@ -5422,9 +5551,9 @@ c.Assert(chg.Status(), Equals, state.ErrorStatus) // and we are in restarting state - restarting, restartType := st.Restarting() + restarting, restartType := restart.Pending(st) c.Check(restarting, Equals, true) - c.Check(restartType, Equals, state.RestartSystem) + c.Check(restartType, Equals, restart.RestartSystem) // and the undo gave us our old kernel back c.Assert(ms.bloader.BootVars, DeepEquals, map[string]string{ @@ -5478,7 +5607,7 @@ c.Assert(chg.Status(), Equals, state.ErrorStatus) // and we are *not* in restarting state - restarting, _ := st.Restarting() + restarting, _ := restart.Pending(st) c.Check(restarting, Equals, false) // and the undo gave us our old kernel back @@ -5676,14 +5805,18 @@ } type mockUpdater struct { - onUpdate error + updateCalls int + onUpdate error } func (m *mockUpdater) Backup() error { return nil } func (m *mockUpdater) Rollback() error { return nil } -func (m *mockUpdater) Update() error { return m.onUpdate } +func (m *mockUpdater) Update() error { + m.updateCalls++ + return m.onUpdate +} func (s *mgrsSuite) TestRemodelSwitchToDifferentGadget(c *C) { bloader := bootloadertest.Mock("mock", c.MkDir()) @@ -5798,11 +5931,11 @@ c.Check(updaterForStructureCalls, Equals, 1) // gadget update requests a restart - restarting, _ := st.Restarting() + restarting, _ := restart.Pending(st) c.Check(restarting, Equals, true) // simulate successful restart happened - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) st.Unlock() err = s.o.Settle(settleTimeout) @@ -6182,24 +6315,77 @@ base: core20 ` +const pcGadget22SnapYaml = ` +version: 1.0 +name: pc +type: gadget +base: core22 +` + +const oldPcGadgetSnapYaml = ` +version: 1.0 +name: old-pc +type: gadget +base: core20 +` + const pcKernelSnapYaml = ` version: 1.0 name: pc-kernel type: kernel ` +const pcKernel22SnapYaml = ` +version: 1.0 +name: pc-kernel +type: kernel +base: core22 +` + const core20SnapYaml = ` version: 1.0 name: core20 type: base ` +const core22SnapYaml = ` +version: 1.0 +name: core22 +type: base +` + const snapdSnapYaml = ` version: 1.0 name: snapd type: snapd ` +const oldPcGadgetYamlForRemodel = ` +volumes: + pc: + schema: gpt + bootloader: grub + structure: + - name: ubuntu-seed + filesystem: vfat + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + role: system-seed + size: 100M + content: + - source: grubx64.efi + target: grubx64.efi + - name: ubuntu-boot + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + role: system-boot + filesystem: ext4 + size: 100M + - name: ubuntu-data + role: system-data + type: EF,C12A7328-F81F-11D2-BA4B-00A0C93EC93B + filesystem: ext4 + size: 500M +` + const grubBootConfig = "# Snapd-Boot-Config-Edition: 1\n" var ( @@ -6211,32 +6397,48 @@ {"bootx64.efi", "content"}, {"grubx64.efi", "content"}, } + oldPcGadgetFiles = append(pcGadgetFiles, [][]string{ + {"meta/gadget.yaml", oldPcGadgetYamlForRemodel}, + // SHA3-384: 7e5c973da86f7398deffd45b9225175da1dd6ae8fcffa1a20219b32bab9f4846da10e823736cd818ceada74d35337c98 + {"grubx64.efi", "old-gadget-content"}, + {"cmdline.extra", "foo bar baz"}, + }...) pcKernelFiles = [][]string{ {"kernel.efi", "kernel-efi"}, } + pcKernel22Files = [][]string{ + {"kernel.efi", "kernel-efi"}, + } snapYamlsForRemodel = map[string]string{ + "old-pc": oldPcGadgetSnapYaml, "pc": pcGadgetSnapYaml, "pc-kernel": pcKernelSnapYaml, "core20": core20SnapYaml, + "core22": core22SnapYaml, "snapd": snapdSnapYaml, "baz": "version: 1.0\nname: baz\nbase: core20", } snapFilesForRemodel = map[string][][]string{ - "pc": pcGadgetFiles, + "old-pc": oldPcGadgetFiles, + "pc": pcGadgetFiles, // use a different fileset, such that the pc snap with this // content will have a different digest than the regular pc snap "pc-rev-33": append(pcGadgetFiles, []string{ - "this-is-new", "", + "this-is-new", "new-in-pc-rev-33", }), "pc-kernel": pcKernelFiles, // similar reasoning as for the pc snap "pc-kernel-rev-33": append(pcKernelFiles, []string{ - "this-is-new", "", + "this-is-new", "new-in-pc-kernel-rev-33", }), // and again "core20-rev-33": { - {"this-is-new", ""}, + {"this-is-new", "new-in-core20-rev-33"}, }, + "pc-kernel-track-22": pcKernel22Files, + "pc-track-22": append(pcGadgetFiles, []string{ + "cmdline.extra", "uc22", + }), } // headers of a regular UC20 model assertion @@ -6520,9 +6722,9 @@ c.Check(chg.Status(), Equals, state.DoingStatus, Commentf("remodel change failed: %v", chg.Err())) c.Check(devicestate.RemodelingChange(st), NotNil) - restarting, kind := st.Restarting() + restarting, kind := restart.Pending(st) c.Check(restarting, Equals, true) - c.Assert(kind, Equals, state.RestartSystemNow) + c.Assert(kind, Equals, restart.RestartSystemNow) now := time.Now() expectedLabel := now.Format("20060102") @@ -6548,7 +6750,7 @@ c.Assert(err, Equals, state.ErrNoState) // simulate successful reboot to recovery and back - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) // this would be done by snap-bootstrap in initramfs err = bl.SetBootVars(map[string]string{ "try_recovery_system": expectedLabel, @@ -6893,9 +7095,9 @@ // first comes a reboot to the new recovery system c.Check(chg.Status(), Equals, state.DoingStatus, Commentf("remodel change failed: %v", chg.Err())) c.Check(devicestate.RemodelingChange(st), NotNil) - restarting, kind := st.Restarting() + restarting, kind := restart.Pending(st) c.Check(restarting, Equals, true) - c.Assert(kind, Equals, state.RestartSystemNow) + c.Assert(kind, Equals, restart.RestartSystemNow) m, err := boot.ReadModeenv("") c.Assert(err, IsNil) c.Check(m.CurrentRecoverySystems, DeepEquals, []string{"1234", expectedLabel}) @@ -6906,8 +7108,13 @@ "try_recovery_system": expectedLabel, "recovery_system_status": "try", }) + const usesSnapd = true + sd := seedtest.ValidateSeed(c, boot.InitramfsUbuntuSeedDir, expectedLabel, usesSnapd, s.storeSigning.Trusted) + // rev-33 ships a new file + verifyModelEssentialSnapHasContent(c, sd, "pc-kernel", "this-is-new", "new-in-pc-kernel-rev-33") + // simulate successful reboot to recovery and back - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) // this would be done by snap-bootstrap in initramfs err = bl.SetBootVars(map[string]string{ "try_recovery_system": expectedLabel, @@ -6926,12 +7133,12 @@ st.Lock() c.Assert(err, IsNil) // we're installing a new kernel, so another reboot - restarting, kind = st.Restarting() + restarting, kind = restart.Pending(st) c.Check(restarting, Equals, true) - c.Assert(kind, Equals, state.RestartSystem) + c.Assert(kind, Equals, restart.RestartSystem) c.Assert(chg.Status(), Equals, state.DoingStatus, Commentf("remodel change failed: %v", chg.Err())) // and we've rebooted - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) // kernel has booted vars, err = rbl.GetBootVars("kernel_status") c.Assert(err, IsNil) @@ -7026,9 +7233,9 @@ // first comes a reboot to the new recovery system c.Check(chg.Status(), Equals, state.DoingStatus, Commentf("remodel change failed: %v", chg.Err())) c.Check(devicestate.RemodelingChange(st), NotNil) - restarting, kind := st.Restarting() + restarting, kind := restart.Pending(st) c.Check(restarting, Equals, true) - c.Assert(kind, Equals, state.RestartSystemNow) + c.Assert(kind, Equals, restart.RestartSystemNow) m, err := boot.ReadModeenv("") c.Assert(err, IsNil) c.Check(m.CurrentRecoverySystems, DeepEquals, []string{"1234", expectedLabel}) @@ -7039,8 +7246,13 @@ "try_recovery_system": expectedLabel, "recovery_system_status": "try", }) + const usesSnapd = true + sd := seedtest.ValidateSeed(c, boot.InitramfsUbuntuSeedDir, expectedLabel, usesSnapd, s.storeSigning.Trusted) + // rev-33 ships a new file + verifyModelEssentialSnapHasContent(c, sd, "pc", "this-is-new", "new-in-pc-rev-33") + // simulate successful reboot to recovery and back - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) // this would be done by snap-bootstrap in initramfs err = bl.SetBootVars(map[string]string{ "try_recovery_system": expectedLabel, @@ -7081,6 +7293,21 @@ validateRefreshTasks(c, tasks[i:], "pc", "33", isGadget) } +func verifyModelEssentialSnapHasContent(c *C, sd seed.Seed, name string, file, content string) { + for _, ms := range sd.EssentialSnaps() { + c.Logf("mode snap %q %v", ms.SnapName(), ms.Path) + if ms.SnapName() == name { + sf, err := snapfile.Open(ms.Path) + c.Assert(err, IsNil) + d, err := sf.ReadFile(file) + c.Assert(err, IsNil) + c.Assert(string(d), Equals, content) + return + } + } + c.Errorf("expected file %q not found seed snap of name %q", file, name) +} + func (s *mgrsSuite) TestRemodelUC20DifferentBaseChannel(c *C) { s.testRemodelUC20WithRecoverySystemSimpleSetUp(c) // use a different set of files, such that the snap digest must also be different @@ -7129,9 +7356,9 @@ // first comes a reboot to the new recovery system c.Check(chg.Status(), Equals, state.DoingStatus, Commentf("remodel change failed: %v", chg.Err())) c.Check(devicestate.RemodelingChange(st), NotNil) - restarting, kind := st.Restarting() + restarting, kind := restart.Pending(st) c.Check(restarting, Equals, true) - c.Assert(kind, Equals, state.RestartSystemNow) + c.Assert(kind, Equals, restart.RestartSystemNow) m, err := boot.ReadModeenv("") c.Assert(err, IsNil) c.Check(m.CurrentRecoverySystems, DeepEquals, []string{"1234", expectedLabel}) @@ -7142,8 +7369,13 @@ "try_recovery_system": expectedLabel, "recovery_system_status": "try", }) + const usesSnapd = true + sd := seedtest.ValidateSeed(c, boot.InitramfsUbuntuSeedDir, expectedLabel, usesSnapd, s.storeSigning.Trusted) + // rev-33 ships a new file + verifyModelEssentialSnapHasContent(c, sd, "core20", "this-is-new", "new-in-core20-rev-33") + // simulate successful reboot to recovery and back - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) // this would be done by snap-bootstrap in initramfs err = bl.SetBootVars(map[string]string{ "try_recovery_system": expectedLabel, @@ -7162,9 +7394,9 @@ st.Lock() c.Assert(err, IsNil) // we are switching the core, so more reboots are expected - restarting, kind = st.Restarting() + restarting, kind = restart.Pending(st) c.Check(restarting, Equals, true) - c.Assert(kind, Equals, state.RestartSystem) + c.Assert(kind, Equals, restart.RestartSystem) c.Assert(chg.Status(), Equals, state.DoingStatus, Commentf("remodel change failed: %v", chg.Err())) // restarting to a new base m, err = boot.ReadModeenv("") @@ -7172,7 +7404,7 @@ c.Assert(m.TryBase, Equals, "core20_33.snap") c.Assert(m.BaseStatus, Equals, "try") // we've rebooted - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) // and pretend we boot the base m.BaseStatus = "trying" c.Assert(m.Write(), IsNil) @@ -7208,97 +7440,676 @@ validateRefreshTasks(c, tasks[i:], "core20", "33", noConfigure) } -func (s *mgrsSuite) TestCheckRefreshFailureWithConcurrentRemoveOfConnectedSnap(c *C) { - hookMgr := s.o.HookManager() - c.Assert(hookMgr, NotNil) - - // force configure hook failure for some-snap. - hookMgr.RegisterHijack("configure", "some-snap", func(ctx *hookstate.Context) error { - return fmt.Errorf("failing configure hook") +func (s *mgrsSuite) TestRemodelUC20BackToPreviousGadget(c *C) { + s.testRemodelUC20WithRecoverySystemSimpleSetUp(c) + c.Assert(os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "proc"), 0755), IsNil) + restore := osutil.MockProcCmdline(filepath.Join(dirs.GlobalRootDir, "proc/cmdline")) + defer restore() + newModel := s.brands.Model("can0nical", "my-model", uc20ModelDefaults, map[string]interface{}{ + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": fakeSnapID("pc-kernel"), + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "old-pc", + "id": fakeSnapID("old-pc"), + "type": "gadget", + "default-channel": "20/edge", + }, + }, + "revision": "1", }) - - snapPath, _ := s.makeStoreTestSnap(c, someSnapYaml, "40") - s.serveSnap(snapPath, "40") - snapPath, _ = s.makeStoreTestSnap(c, otherSnapYaml, "50") - s.serveSnap(snapPath, "50") - - mockServer := s.mockStore(c) - defer mockServer.Close() + bl, err := bootloader.Find(boot.InitramfsUbuntuSeedDir, &bootloader.Options{Role: bootloader.RoleRecovery}) + c.Assert(err, IsNil) st := s.o.State() st.Lock() defer st.Unlock() - st.Set("conns", map[string]interface{}{ - "other-snap:media-hub some-snap:media-hub": map[string]interface{}{"interface": "media-hub", "auto": false}, - }) + a11, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-name": "old-pc", + "snap-id": fakeSnapID("old-pc"), + "publisher-id": "can0nical", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + c.Assert(assertstate.Add(st, a11), IsNil) + c.Assert(s.storeSigning.Add(a11), IsNil) - si := &snap.SideInfo{RealName: "some-snap", SnapID: fakeSnapID("some-snap"), Revision: snap.R(1)} - snapInfo := snaptest.MockSnap(c, someSnapYaml, si) + s.makeInstalledSnapInStateForRemodel(c, "old-pc", snap.R(1), "20/edge") - oi := &snap.SideInfo{RealName: "other-snap", SnapID: fakeSnapID("other-snap"), Revision: snap.R(1)} - otherInfo := snaptest.MockSnap(c, otherSnapYaml, oi) + now := time.Now() + expectedLabel := now.Format("20060102") - snapstate.Set(st, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{si}, - Current: snap.R(1), - SnapType: "app", - }) - snapstate.Set(st, "other-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{oi}, - Current: snap.R(1), - SnapType: "app", + updater := &mockUpdater{} + restore = gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, rootDir, rollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + // use a mock updater pretends an update was applied + return updater, nil }) + defer restore() - // add snaps to the repo and connect them - repo := s.o.InterfaceManager().Repository() - c.Assert(repo.AddSnap(snapInfo), IsNil) - c.Assert(repo.AddSnap(otherInfo), IsNil) - _, err := repo.Connect(&interfaces.ConnRef{ - PlugRef: interfaces.PlugRef{Snap: "other-snap", Name: "media-hub"}, - SlotRef: interfaces.SlotRef{Snap: "some-snap", Name: "media-hub"}, - }, nil, nil, nil, nil, nil) - c.Assert(err, IsNil) - - // refresh all - c.Assert(assertstate.RefreshSnapDeclarations(st, 0, nil), IsNil) - - ts, err := snapstate.Update(st, "some-snap", nil, 0, snapstate.Flags{}) + chg, err := devicestate.Remodel(st, newModel) c.Assert(err, IsNil) - chg := st.NewChange("refresh", "...") - chg.AddAll(ts) + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil, Commentf(s.logbuf.String())) + // gadget update has not been applied yet + c.Check(updater.updateCalls, Equals, 0) + + // first comes a reboot to the new recovery system + c.Check(chg.Status(), Equals, state.DoingStatus, Commentf("remodel change failed: %v", chg.Err())) + c.Check(devicestate.RemodelingChange(st), NotNil) + restarting, kind := restart.Pending(st) + c.Check(restarting, Equals, true) + c.Assert(kind, Equals, restart.RestartSystemNow) + m, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(m.CurrentRecoverySystems, DeepEquals, []string{"1234", expectedLabel}) + c.Check(m.GoodRecoverySystems, DeepEquals, []string{"1234"}) + vars, err := bl.GetBootVars("try_recovery_system", "recovery_system_status") + c.Assert(err, IsNil) + c.Assert(vars, DeepEquals, map[string]string{ + "try_recovery_system": expectedLabel, + "recovery_system_status": "try", + }) + // simulate successful reboot to recovery and back + restart.MockPending(st, restart.RestartUnset) + // this would be done by snap-bootstrap in initramfs + err = bl.SetBootVars(map[string]string{ + "try_recovery_system": expectedLabel, + "recovery_system_status": "tried", + }) + c.Assert(err, IsNil) + // reset, so that after-reboot handling of tried system is executed + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) + + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + // update has been called for all 3 structures because of the remodel + // policy (there is no content bump, so there would be no updates + // otherwise) + c.Check(updater.updateCalls, Equals, 3) + // a reboot was requested, as mock updated were applied + restarting, kind = restart.Pending(st) + c.Check(restarting, Equals, true) + c.Assert(kind, Equals, restart.RestartSystem) + + m, err = boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 foo bar baz", + }) + + // pretend we have the right command line + c.Assert(ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "proc/cmdline"), + []byte("snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 foo bar baz"), 0444), + IsNil) + + // run post boot code again + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) + + // verify command lines again + m, err = boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 foo bar baz", + }) + + c.Check(chg.Status(), Equals, state.DoneStatus, Commentf("remodel change failed: %v", chg.Err())) + + var snapst snapstate.SnapState + err = snapstate.Get(st, "old-pc", &snapst) + c.Assert(err, IsNil) + // and the gadget tracking channel is the same as in the model + c.Check(snapst.TrackingChannel, Equals, "20/edge") + + // ensure sorting is correct + tasks := chg.Tasks() + sort.Sort(byReadyTime(tasks)) + + var i int + + // prepare first + c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Prepare snap "old-pc" (1) for remodel`)) + i++ + // then recovery system + i += validateRecoverySystemTasks(c, tasks[i:], expectedLabel) + // then gadget switch with update of assets and kernel command line + i += validateGadgetSwitchTasks(c, tasks[i:], "old-pc", "1") + // finally new model assertion + c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Set new model assertion`)) + i++ + c.Check(i, Equals, len(tasks)) +} + +func (s *mgrsSuite) TestRemodelUC20ExistingGadgetSnapDifferentChannel(c *C) { + // a remodel where the target model uses a gadget that is already + // present (possibly due to being used by one of the previous models) + // but tracks a different channel than what the new model ordains + s.testRemodelUC20WithRecoverySystemSimpleSetUp(c) + c.Assert(os.MkdirAll(filepath.Join(dirs.GlobalRootDir, "proc"), 0755), IsNil) + restore := osutil.MockProcCmdline(filepath.Join(dirs.GlobalRootDir, "proc/cmdline")) + defer restore() + newModel := s.brands.Model("can0nical", "my-model", uc20ModelDefaults, map[string]interface{}{ + "snaps": []interface{}{ + map[string]interface{}{ + "name": "pc-kernel", + "id": fakeSnapID("pc-kernel"), + "type": "kernel", + "default-channel": "20", + }, + map[string]interface{}{ + "name": "old-pc", + "id": fakeSnapID("old-pc"), + "type": "gadget", + "default-channel": "20/edge", + }, + }, + "revision": "1", + }) + bl, err := bootloader.Find(boot.InitramfsUbuntuSeedDir, &bootloader.Options{Role: bootloader.RoleRecovery}) + c.Assert(err, IsNil) + + st := s.o.State() + st.Lock() + defer st.Unlock() + + a11, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-name": "old-pc", + "snap-id": fakeSnapID("old-pc"), + "publisher-id": "can0nical", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + c.Assert(assertstate.Add(st, a11), IsNil) + c.Assert(s.storeSigning.Add(a11), IsNil) + + snapInfo := s.makeInstalledSnapInStateForRemodel(c, "old-pc", snap.R(1), "20/beta") + // there already is a snap revision assertion for this snap, just serve + // it in the mock store + s.serveSnap(snapInfo.MountFile(), "1") + + now := time.Now() + expectedLabel := now.Format("20060102") + + updater := &mockUpdater{} + restore = gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, rootDir, rollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + // use a mock updater pretends an update was applied + return updater, nil + }) + defer restore() + + chg, err := devicestate.Remodel(st, newModel) + c.Assert(err, IsNil) + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil, Commentf(s.logbuf.String())) + // gadget update has not been applied yet + c.Check(updater.updateCalls, Equals, 0) + + // first comes a reboot to the new recovery system + c.Check(chg.Status(), Equals, state.DoingStatus, Commentf("remodel change failed: %v", chg.Err())) + c.Check(devicestate.RemodelingChange(st), NotNil) + restarting, kind := restart.Pending(st) + c.Check(restarting, Equals, true) + c.Assert(kind, Equals, restart.RestartSystemNow) + m, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(m.CurrentRecoverySystems, DeepEquals, []string{"1234", expectedLabel}) + c.Check(m.GoodRecoverySystems, DeepEquals, []string{"1234"}) + vars, err := bl.GetBootVars("try_recovery_system", "recovery_system_status") + c.Assert(err, IsNil) + c.Assert(vars, DeepEquals, map[string]string{ + "try_recovery_system": expectedLabel, + "recovery_system_status": "try", + }) + // simulate successful reboot to recovery and back + restart.MockPending(st, restart.RestartUnset) + // this would be done by snap-bootstrap in initramfs + err = bl.SetBootVars(map[string]string{ + "try_recovery_system": expectedLabel, + "recovery_system_status": "tried", + }) + c.Assert(err, IsNil) + // reset, so that after-reboot handling of tried system is executed + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) + + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + // update has been called for all 3 structures because of the remodel + // policy (there is no content bump, so there would be no updates + // otherwise) + c.Check(updater.updateCalls, Equals, 3) + // a reboot was requested, as mock updated were applied + restarting, kind = restart.Pending(st) + c.Check(restarting, Equals, true) + c.Assert(kind, Equals, restart.RestartSystem) + + m, err = boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 foo bar baz", + }) + + // pretend we have the right command line + c.Assert(ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "proc/cmdline"), + []byte("snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 foo bar baz"), 0444), + IsNil) + + // run post boot code again + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) + + // verify command lines again + m, err = boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 foo bar baz", + }) + + c.Check(chg.Status(), Equals, state.DoneStatus, Commentf("remodel change failed: %v", chg.Err())) + + var snapst snapstate.SnapState + err = snapstate.Get(st, "old-pc", &snapst) + c.Assert(err, IsNil) + // and the gadget tracking channel is the same as in the model + c.Check(snapst.TrackingChannel, Equals, "20/edge") + + // ensure sorting is correct + tasks := chg.Tasks() + sort.Sort(byReadyTime(tasks)) + + var i int + + // prepare first + c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Switch snap "old-pc" from channel "20/beta" to "20/edge"`)) + i++ + // then recovery system + i += validateRecoverySystemTasks(c, tasks[i:], expectedLabel) + // then gadget switch with update of assets and kernel command line + i += validateGadgetSwitchTasks(c, tasks[i:], "old-pc", "1") + // finally new model assertion + c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Set new model assertion`)) + i++ + c.Check(i, Equals, len(tasks)) +} + +func (s *mgrsSuite) TestCheckRefreshFailureWithConcurrentRemoveOfConnectedSnap(c *C) { + hookMgr := s.o.HookManager() + c.Assert(hookMgr, NotNil) + + // force configure hook failure for some-snap. + hookMgr.RegisterHijack("configure", "some-snap", func(ctx *hookstate.Context) error { + return fmt.Errorf("failing configure hook") + }) + + snapPath, _ := s.makeStoreTestSnap(c, someSnapYaml, "40") + s.serveSnap(snapPath, "40") + snapPath, _ = s.makeStoreTestSnap(c, otherSnapYaml, "50") + s.serveSnap(snapPath, "50") + + mockServer := s.mockStore(c) + defer mockServer.Close() + + st := s.o.State() + st.Lock() + defer st.Unlock() + + st.Set("conns", map[string]interface{}{ + "other-snap:media-hub some-snap:media-hub": map[string]interface{}{"interface": "media-hub", "auto": false}, + }) + + si := &snap.SideInfo{RealName: "some-snap", SnapID: fakeSnapID("some-snap"), Revision: snap.R(1)} + snapInfo := snaptest.MockSnap(c, someSnapYaml, si) + + oi := &snap.SideInfo{RealName: "other-snap", SnapID: fakeSnapID("other-snap"), Revision: snap.R(1)} + otherInfo := snaptest.MockSnap(c, otherSnapYaml, oi) + + snapstate.Set(st, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: snap.R(1), + SnapType: "app", + }) + snapstate.Set(st, "other-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{oi}, + Current: snap.R(1), + SnapType: "app", + }) + + // add snaps to the repo and connect them + repo := s.o.InterfaceManager().Repository() + c.Assert(repo.AddSnap(snapInfo), IsNil) + c.Assert(repo.AddSnap(otherInfo), IsNil) + _, err := repo.Connect(&interfaces.ConnRef{ + PlugRef: interfaces.PlugRef{Snap: "other-snap", Name: "media-hub"}, + SlotRef: interfaces.SlotRef{Snap: "some-snap", Name: "media-hub"}, + }, nil, nil, nil, nil, nil) + c.Assert(err, IsNil) + + // refresh all + c.Assert(assertstate.RefreshSnapDeclarations(st, 0, nil), IsNil) + + ts, err := snapstate.Update(st, "some-snap", nil, 0, snapstate.Flags{}) + c.Assert(err, IsNil) + chg := st.NewChange("refresh", "...") + chg.AddAll(ts) // remove other-snap ts2, err := snapstate.Remove(st, "other-snap", snap.R(0), &snapstate.RemoveFlags{Purge: true}) c.Assert(err, IsNil) - chg2 := st.NewChange("remove-snap", "...") - chg2.AddAll(ts2) + chg2 := st.NewChange("remove-snap", "...") + chg2.AddAll(ts2) + + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + + c.Check(err, IsNil) + + // the refresh change has failed due to configure hook error + c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks:\n.*failing configure hook.*`) + c.Check(chg.Status(), Equals, state.ErrorStatus) + + // download-snap is one of the first tasks in the refresh change, check that it was undone + var downloadSnapStatus state.Status + for _, t := range chg.Tasks() { + if t.Kind() == "download-snap" { + downloadSnapStatus = t.Status() + break + } + } + c.Check(downloadSnapStatus, Equals, state.UndoneStatus) + + // the remove change succeeded + c.Check(chg2.Err(), IsNil) + c.Check(chg2.Status(), Equals, state.DoneStatus) +} + +func dumpTasks(c *C, when string, tasks []*state.Task) { + c.Logf("--- tasks dump %s", when) + for _, tsk := range tasks { + c.Logf(" -- %4s %10s %15s %s", tsk.ID(), tsk.Status(), tsk.Kind(), tsk.Summary()) + } +} + +func (s *mgrsSuite) TestRemodelUC20ToUC22(c *C) { + s.testRemodelUC20WithRecoverySystemSimpleSetUp(c) + restore := osutil.MockProcCmdline(filepath.Join(dirs.GlobalRootDir, "proc/cmdline")) + defer restore() + + st := s.o.State() + st.Lock() + defer st.Unlock() + + // make core22 a thing + a11, err := s.storeSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{ + "series": "16", + "snap-name": "core22", + "snap-id": fakeSnapID("core22"), + "publisher-id": "can0nical", + "timestamp": time.Now().Format(time.RFC3339), + }, nil, "") + c.Assert(err, IsNil) + c.Assert(assertstate.Add(st, a11), IsNil) + c.Assert(s.storeSigning.Add(a11), IsNil) + + snapPath, _ := s.makeStoreTestSnapWithFiles(c, snapYamlsForRemodel["core22"], "1", nil) + s.serveSnap(snapPath, "1") + snapPath, _ = s.makeStoreTestSnapWithFiles(c, pcKernel22SnapYaml, "33", snapFilesForRemodel["pc-kernel-track-22"]) + s.serveSnap(snapPath, "33") + snapPath, _ = s.makeStoreTestSnapWithFiles(c, pcGadget22SnapYaml, "34", snapFilesForRemodel["pc-track-22"]) + s.serveSnap(snapPath, "34") + + newModel := s.brands.Model("can0nical", "my-model", uc20ModelDefaults, map[string]interface{}{ + // replace the base + "base": "core22", + "snaps": []interface{}{ + // kernel and gadget snaps with new tracks + map[string]interface{}{ + "name": "pc-kernel", + "id": fakeSnapID("pc-kernel"), + "type": "kernel", + "default-channel": "22", + }, + map[string]interface{}{ + "name": "pc", + "id": fakeSnapID("pc"), + "type": "gadget", + "default-channel": "22", + }, + }, + "revision": "1", + }) + bl, err := bootloader.Find(boot.InitramfsUbuntuSeedDir, &bootloader.Options{Role: bootloader.RoleRecovery}) + c.Assert(err, IsNil) + + // remodel updates a gadget, setup a mock updater that pretends an + // update was applied + updater := &mockUpdater{} + restore = gadget.MockUpdaterForStructure(func(ps *gadget.LaidOutStructure, rootDir, rollbackDir string, observer gadget.ContentUpdateObserver) (gadget.Updater, error) { + // use a mock updater pretends an update was applied + return updater, nil + }) + defer restore() + + now := time.Now() + expectedLabel := now.Format("20060102") + + chg, err := devicestate.Remodel(st, newModel) + c.Assert(err, IsNil) + dumpTasks(c, "at the beginning", chg.Tasks()) + + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil, Commentf(s.logbuf.String())) + // gadget update has been not been applied yet + c.Check(updater.updateCalls, Equals, 0) + + dumpTasks(c, "after recovery system", chg.Tasks()) + + // first comes a reboot to the new recovery system + c.Check(chg.Status(), Equals, state.DoingStatus, Commentf("remodel change failed: %v", chg.Err())) + c.Check(devicestate.RemodelingChange(st), NotNil) + restarting, kind := restart.Pending(st) + c.Check(restarting, Equals, true) + c.Assert(kind, Equals, restart.RestartSystemNow) + m, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(m.CurrentRecoverySystems, DeepEquals, []string{"1234", expectedLabel}) + c.Check(m.GoodRecoverySystems, DeepEquals, []string{"1234"}) + vars, err := bl.GetBootVars("try_recovery_system", "recovery_system_status") + c.Assert(err, IsNil) + c.Assert(vars, DeepEquals, map[string]string{ + "try_recovery_system": expectedLabel, + "recovery_system_status": "try", + }) + // simulate successful reboot to recovery and back + restart.MockPending(st, restart.RestartUnset) + // this would be done by snap-bootstrap in initramfs + err = bl.SetBootVars(map[string]string{ + "try_recovery_system": expectedLabel, + "recovery_system_status": "tried", + }) + c.Assert(err, IsNil) + // reset, so that after-reboot handling of tried system is executed + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) + // next we'll observe kernel getting installed st.Unlock() err = s.o.Settle(settleTimeout) st.Lock() + c.Assert(err, IsNil) - c.Check(err, IsNil) + dumpTasks(c, "after kernel install", chg.Tasks()) + // gadget update has been not been applied yet + c.Check(updater.updateCalls, Equals, 0) - // the refresh change has failed due to configure hook error - c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks:\n.*failing configure hook.*`) - c.Check(chg.Status(), Equals, state.ErrorStatus) + restarting, kind = restart.Pending(st) + c.Check(restarting, Equals, true) + c.Assert(kind, Equals, restart.RestartSystem) + c.Assert(chg.Status(), Equals, state.DoingStatus, Commentf("remodel change failed: %v", chg.Err())) + // and we've rebooted + restart.MockPending(st, restart.RestartUnset) + // pretend the kernel has booted + rbl, err := bootloader.Find(dirs.GlobalRootDir, &bootloader.Options{Role: bootloader.RoleRunMode}) + c.Assert(err, IsNil) + vars, err = rbl.GetBootVars("kernel_status") + c.Assert(err, IsNil) + c.Assert(vars, DeepEquals, map[string]string{ + "kernel_status": "try", + }) + err = rbl.SetBootVars(map[string]string{ + "kernel_status": "trying", + }) + c.Assert(err, IsNil) - // download-snap is one of the first tasks in the refresh change, check that it was undone - var downloadSnapStatus state.Status - for _, t := range chg.Tasks() { - if t.Kind() == "download-snap" { - downloadSnapStatus = t.Status() - break - } - } - c.Check(downloadSnapStatus, Equals, state.UndoneStatus) + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) - // the remove change succeeded - c.Check(chg2.Err(), IsNil) - c.Check(chg2.Status(), Equals, state.DoneStatus) + // next the base + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + + dumpTasks(c, "after base install", chg.Tasks()) + // gadget update has been not been applied yet + c.Check(updater.updateCalls, Equals, 0) + + // restarting to a new base + m, err = boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m.TryBase, Equals, "core22_1.snap") + c.Assert(m.BaseStatus, Equals, "try") + // we've rebooted + restart.MockPending(st, restart.RestartUnset) + // and pretend we boot the base + m.BaseStatus = "trying" + c.Assert(m.Write(), IsNil) + + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) + + // next the gadget which updates the command line + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + + // gadget update has been applied + c.Check(updater.updateCalls, Equals, 3) + + dumpTasks(c, "after gadget install", chg.Tasks()) + + // the gadget has updated the kernel command line + restarting, kind = restart.Pending(st) + c.Check(restarting, Equals, true) + c.Assert(kind, Equals, restart.RestartSystem) + c.Assert(chg.Status(), Equals, state.DoingStatus, Commentf("remodel change failed: %v", chg.Err())) + m, err = boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1", + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 uc22", + }) + // we've rebooted + restart.MockPending(st, restart.RestartUnset) + // pretend we have the right command line + c.Assert(ioutil.WriteFile(filepath.Join(dirs.GlobalRootDir, "proc/cmdline"), + []byte("snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 uc22"), 0444), + IsNil) + + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) + + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("remodel change failed: %v", chg.Err())) + + dumpTasks(c, "after set-model", chg.Tasks()) + + var snapst snapstate.SnapState + err = snapstate.Get(st, "core22", &snapst) + c.Assert(err, IsNil) + + // ensure sorting is correct + tasks := chg.Tasks() + sort.Sort(byReadyTime(tasks)) + + var i int + // first all downloads/checks in sequential order + i += validateDownloadCheckTasks(c, tasks[i:], "pc-kernel", "33", "22/stable") + i += validateDownloadCheckTasks(c, tasks[i:], "core22", "1", "latest/stable") + i += validateDownloadCheckTasks(c, tasks[i:], "pc", "34", "22/stable") + // then create recovery + i += validateRecoverySystemTasks(c, tasks[i:], expectedLabel) + // then all refreshes and install in sequential order (no configure hooks for bases though) + i += validateRefreshTasks(c, tasks[i:], "pc-kernel", "33", isKernel) + i += validateInstallTasks(c, tasks[i:], "core22", "1", noConfigure) + i += validateRefreshTasks(c, tasks[i:], "pc", "34", isGadget) + // finally new model assertion + c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Set new model assertion`)) + i++ + c.Check(i, Equals, len(tasks)) + + m, err = boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ + "snapd_recovery_mode=run console=ttyS0 console=tty1 panic=-1 uc22", + }) + c.Check(m.CurrentRecoverySystems, DeepEquals, []string{ + "1234", expectedLabel, + }) + c.Check(m.GoodRecoverySystems, DeepEquals, []string{ + "1234", expectedLabel, + }) + c.Check(m.Base, Equals, "core22_1.snap") } func (s *mgrsSuite) TestInstallKernelSnapRollbackUpdatesBootloaderEnv(c *C) { @@ -7382,7 +8193,7 @@ }) // we are in restarting state and the change is not done yet - restarting, _ := st.Restarting() + restarting, _ := restart.Pending(st) c.Check(restarting, Equals, true) c.Check(chg.Status(), Equals, state.DoingStatus) s.mockRollbackAcrossReboot(c, bloader, []snap.Type{snap.TypeKernel}) @@ -7608,16 +8419,16 @@ // check the snapd task state c.Check(chg.Status(), Equals, state.DoingStatus) - restarting, kind := st.Restarting() + restarting, kind := restart.Pending(st) c.Check(restarting, Equals, true) - c.Assert(kind, Equals, state.RestartDaemon) + c.Assert(kind, Equals, restart.RestartDaemon) // now we do want the ensure loop to run though r = servicestate.MockEnsuredSnapServices(s.o.ServiceManager(), false) defer r() // mock a restart of snapd to progress with the change - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) // let the change run its course st.Unlock() @@ -7630,7 +8441,7 @@ // we don't restart since the unit file was just rewritten, no services were // killed - restarting, _ = st.Restarting() + restarting, _ = restart.Pending(st) c.Check(restarting, Equals, false) // the unit file was rewritten to use Wants= now @@ -7847,16 +8658,16 @@ // check the snapd task state c.Check(chg.Status(), Equals, state.DoingStatus) - restarting, kind := st.Restarting() + restarting, kind := restart.Pending(st) c.Check(restarting, Equals, true) - c.Assert(kind, Equals, state.RestartDaemon) + c.Assert(kind, Equals, restart.RestartDaemon) // now we do want the ensure loop to run though r = servicestate.MockEnsuredSnapServices(s.o.ServiceManager(), false) defer r() // mock a restart of snapd to progress with the change - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) // let the change try to run its course st.Unlock() @@ -7869,9 +8680,9 @@ // we do end up restarting now, since we tried to restart the service but // failed and so to be safe as possible we reboot the system immediately - restarting, kind = st.Restarting() + restarting, kind = restart.Pending(st) c.Check(restarting, Equals, true) - c.Assert(kind, Equals, state.RestartSystemNow) + c.Assert(kind, Equals, restart.RestartSystemNow) // the unit file was rewritten to use Wants= now rewrittenUnitFile := fmt.Sprintf(unitTempl, @@ -7888,7 +8699,7 @@ // of this test it's enough to ensure that 1) a restart is requested and 2) // the manager ensure loop doesn't fail after we restart since the unit // files don't need to be rewritten - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) // we want the service ensure loop to run again to show it doesn't break // anything @@ -8026,12 +8837,12 @@ // boot config is updated after link-snap, so first comes the // daemon restart c.Check(chg.Status(), Equals, state.DoingStatus) - restarting, kind := st.Restarting() + restarting, kind := restart.Pending(st) c.Check(restarting, Equals, true) - c.Assert(kind, Equals, state.RestartDaemon) + c.Assert(kind, Equals, restart.RestartDaemon) // simulate successful daemon restart happened - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) // let the change run its course st.Unlock() @@ -8040,12 +8851,12 @@ c.Assert(err, IsNil) c.Check(chg.Status(), Equals, state.DoneStatus, Commentf("change failed: %v", chg.Err())) - restarting, kind = st.Restarting() + restarting, kind = restart.Pending(st) if updated { // boot config updated, thus a system restart was // requested c.Check(restarting, Equals, true) - c.Assert(kind, Equals, state.RestartSystem) + c.Assert(kind, Equals, restart.RestartSystem) } else { c.Check(restarting, Equals, false) } @@ -8174,11 +8985,11 @@ c.Assert(err, IsNil) c.Check(chg.Status(), Equals, state.DoingStatus) - restarting, _ := st.Restarting() + restarting, _ := restart.Pending(st) c.Check(restarting, Equals, true) // simulate successful restart happened - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) if si.RealName == "core" { // pretend we switched to a new core bl.SetBootVars(map[string]string{ @@ -8360,12 +9171,12 @@ if update { // when updated, a system restart will be requested c.Check(chg.Status(), Equals, state.DoingStatus, Commentf("change failed: %v", chg.Err())) - restarting, kind := st.Restarting() + restarting, kind := restart.Pending(st) c.Check(restarting, Equals, true) - c.Assert(kind, Equals, state.RestartSystem) + c.Assert(kind, Equals, restart.RestartSystem) // simulate successful system restart happened - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) m, err := boot.ReadModeenv("") c.Assert(err, IsNil) @@ -8514,45 +9325,279 @@ }) } -func (s *mgrsSuite) TestGadgetKernelCommandLineTransitionExtraToFull(c *C) { - mabloader := bootloadertest.Mock("mock", c.MkDir()).WithTrustedAssets() - mabloader.StaticCommandLine = "mock static" - bootloader.Force(mabloader) - defer bootloader.Force(nil) +func (s *mgrsSuite) TestGadgetKernelCommandLineTransitionExtraToFull(c *C) { + mabloader := bootloadertest.Mock("mock", c.MkDir()).WithTrustedAssets() + mabloader.StaticCommandLine = "mock static" + bootloader.Force(mabloader) + defer bootloader.Force(nil) + + err := mabloader.SetBootVars(map[string]string{ + "snapd_extra_cmdline_args": "extra args", + "snapd_full_cmdline_args": "", + }) + c.Assert(err, IsNil) + + // add new gadget snap kernel command line drop-in file + sf := snaptest.MakeTestSnapWithFiles(c, pcGadget, [][]string{ + {"meta/gadget.yaml", pcGadgetYaml}, + {"cmdline.full", "full args"}, + }) + + const currentCmdline = "snapd_recovery_mode=run mock static extra args" + const update = true + currentFiles := [][]string{ + {"meta/gadget.yaml", pcGadgetYaml}, + {"cmdline.extra", "extra args"}, + } + const cmdlineAfterReboot = "snapd_recovery_mode=run full args" + s.testGadgetKernelCommandLine(c, sf, &snap.SideInfo{RealName: "pc"}, mabloader, + currentFiles, currentCmdline, cmdlineAfterReboot, update) + + m, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ + "snapd_recovery_mode=run full args", + }) + vars, err := mabloader.GetBootVars("snapd_extra_cmdline_args", "snapd_full_cmdline_args") + c.Assert(err, IsNil) + c.Assert(vars, DeepEquals, map[string]string{ + "snapd_extra_cmdline_args": "", + "snapd_full_cmdline_args": "full args", + }) +} + +func (s *mgrsSuite) testUpdateKernelBaseSingleRebootSetup(c *C) (*boottest.RunBootenv20, *state.Change) { + bloader := boottest.MockUC20RunBootenv(bootloadertest.Mock("mock", c.MkDir())) + bootloader.Force(bloader) + s.AddCleanup(func() { bootloader.Force(nil) }) + + // a revision which is assumed to be installed + kernel, err := snap.ParsePlaceInfoFromSnapFileName("pc-kernel_1.snap") + c.Assert(err, IsNil) + restore := bloader.SetEnabledKernel(kernel) + s.AddCleanup(restore) + + restore = release.MockOnClassic(false) + s.AddCleanup(restore) + + mockServer := s.mockStore(c) + s.AddCleanup(func() { mockServer.Close() }) + + st := s.o.State() + st.Lock() + defer st.Unlock() + + model := s.brands.Model("can0nical", "my-model", uc20ModelDefaults) + // setup model assertion + devicestatetest.SetDevice(st, &auth.DeviceState{ + Brand: "can0nical", + Model: "my-model", + Serial: "serialserialserial", + }) + err = assertstate.Add(st, model) + c.Assert(err, IsNil) + + // mock the modeenv file + m := &boot.Modeenv{ + Mode: "run", + Base: "core20_1.snap", + CurrentKernels: []string{"pc-kernel_1.snap"}, + CurrentRecoverySystems: []string{"1234"}, + GoodRecoverySystems: []string{"1234"}, + + Model: model.Model(), + BrandID: model.BrandID(), + Grade: string(model.Grade()), + ModelSignKeyID: model.SignKeyID(), + } + err = m.WriteTo("") + c.Assert(err, IsNil) + c.Assert(s.o.DeviceManager().ReloadModeenv(), IsNil) + + pcKernelYaml := "name: pc-kernel\nversion: 1.0\ntype: kernel" + baseYaml := "name: core20\nversion: 1.0\ntype: base" + siKernel := &snap.SideInfo{RealName: "pc-kernel", SnapID: fakeSnapID("pc-kernel"), Revision: snap.R(1)} + snaptest.MockSnap(c, pcKernelYaml, siKernel) + siBase := &snap.SideInfo{RealName: "core20", SnapID: fakeSnapID("core20"), Revision: snap.R(1)} + snaptest.MockSnap(c, baseYaml, siBase) + + // test setup adds core, get rid of it + snapstate.Set(st, "core", nil) + snapstate.Set(st, "pc-kernel", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{siKernel}, + Current: snap.R(1), + SnapType: "kernel", + }) + snapstate.Set(st, "core20", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{siBase}, + Current: snap.R(1), + SnapType: "base", + }) + + p, _ := s.makeStoreTestSnap(c, pcKernelYaml, "2") + s.serveSnap(p, "2") + p, _ = s.makeStoreTestSnap(c, baseYaml, "2") + s.serveSnap(p, "2") + + affected, tss, err := snapstate.UpdateMany(context.Background(), st, []string{"pc-kernel", "core20"}, 0, nil) + c.Assert(err, IsNil) + c.Assert(affected, DeepEquals, []string{"core20", "pc-kernel"}) + chg := st.NewChange("update-many", "...") + for _, ts := range tss { + // skip the taskset of UpdateMany that does the + // check-rerefresh, see tsWithoutReRefresh for details + if ts.Tasks()[0].Kind() == "check-rerefresh" { + c.Logf("skipping rerefresh") + continue + } + chg.AddAll(ts) + } + return bloader, chg +} + +func (s *mgrsSuite) TestUpdateKernelBaseSingleRebootHappy(c *C) { + bloader, chg := s.testUpdateKernelBaseSingleRebootSetup(c) + st := s.o.State() + st.Lock() + defer st.Unlock() + + st.Unlock() + err := s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil, Commentf(s.logbuf.String())) + + // final steps will are postponed until we are in the restarted snapd + ok, rst := restart.Pending(st) + c.Assert(ok, Equals, true) + c.Assert(rst, Equals, restart.RestartSystem) + + // auto connects aren't done yet + autoConnects := 0 + for _, tsk := range chg.Tasks() { + if tsk.Kind() == "auto-connect" { + expectedStatus := state.DoStatus + snapsup, err := snapstate.TaskSnapSetup(tsk) + c.Assert(err, IsNil) + if snapsup.InstanceName() == "core20" { + expectedStatus = state.DoingStatus + } + c.Assert(tsk.Status(), Equals, expectedStatus, + Commentf("%q has status other than %s", tsk.Summary(), expectedStatus)) + autoConnects++ + } + } + // one for kernel, one for base + c.Check(autoConnects, Equals, 2) + + // try snaps are set + currentTryKernel, err := bloader.TryKernel() + c.Assert(err, IsNil) + c.Assert(currentTryKernel.Filename(), Equals, "pc-kernel_2.snap") + m, err := boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Check(m.BaseStatus, Equals, boot.TryStatus) + c.Check(m.TryBase, Equals, "core20_2.snap") + + // simulate successful restart happened + restart.MockPending(st, restart.RestartUnset) + err = bloader.SetTryingDuringReboot([]snap.Type{snap.TypeKernel}) + c.Assert(err, IsNil) + m.BaseStatus = boot.TryingStatus + c.Assert(m.Write(), IsNil) + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) + + // go on + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + + c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("change failed with: %v", chg.Err())) +} + +func (s *mgrsSuite) TestUpdateKernelBaseSingleRebootKernelUndo(c *C) { + bloader, chg := s.testUpdateKernelBaseSingleRebootSetup(c) + st := s.o.State() + st.Lock() + defer st.Unlock() - err := mabloader.SetBootVars(map[string]string{ - "snapd_extra_cmdline_args": "extra args", - "snapd_full_cmdline_args": "", - }) - c.Assert(err, IsNil) + st.Unlock() + err := s.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil, Commentf(s.logbuf.String())) - // add new gadget snap kernel command line drop-in file - sf := snaptest.MakeTestSnapWithFiles(c, pcGadget, [][]string{ - {"meta/gadget.yaml", pcGadgetYaml}, - {"cmdline.full", "full args"}, - }) + // final steps will are postponed until we are in the restarted snapd + ok, rst := restart.Pending(st) + c.Assert(ok, Equals, true) + c.Assert(rst, Equals, restart.RestartSystem) - const currentCmdline = "snapd_recovery_mode=run mock static extra args" - const update = true - currentFiles := [][]string{ - {"meta/gadget.yaml", pcGadgetYaml}, - {"cmdline.extra", "extra args"}, + // auto connects aren't done yet + autoConnects := 0 + for _, tsk := range chg.Tasks() { + if tsk.Kind() == "auto-connect" { + expectedStatus := state.DoStatus + snapsup, err := snapstate.TaskSnapSetup(tsk) + c.Assert(err, IsNil) + if snapsup.InstanceName() == "core20" { + expectedStatus = state.DoingStatus + } + c.Assert(tsk.Status(), Equals, expectedStatus, + Commentf("%q has status other than %s", tsk.Summary(), expectedStatus)) + autoConnects++ + } } - const cmdlineAfterReboot = "snapd_recovery_mode=run full args" - s.testGadgetKernelCommandLine(c, sf, &snap.SideInfo{RealName: "pc"}, mabloader, - currentFiles, currentCmdline, cmdlineAfterReboot, update) + // one for kernel, one for base + c.Check(autoConnects, Equals, 2) + // try snaps are set + currentTryKernel, err := bloader.TryKernel() + c.Assert(err, IsNil) + c.Assert(currentTryKernel.Filename(), Equals, "pc-kernel_2.snap") m, err := boot.ReadModeenv("") c.Assert(err, IsNil) - c.Assert([]string(m.CurrentKernelCommandLines), DeepEquals, []string{ - "snapd_recovery_mode=run full args", - }) - vars, err := mabloader.GetBootVars("snapd_extra_cmdline_args", "snapd_full_cmdline_args") + c.Check(m.BaseStatus, Equals, boot.TryStatus) + c.Check(m.TryBase, Equals, "core20_2.snap") + + // simulate successful restart happened + restart.MockPending(st, restart.RestartUnset) + // pretend the kernel panics during boot, kernel status gets reset to "" + err = bloader.SetRollbackAcrossReboot([]snap.Type{snap.TypeKernel}) c.Assert(err, IsNil) - c.Assert(vars, DeepEquals, map[string]string{ - "snapd_extra_cmdline_args": "", - "snapd_full_cmdline_args": "full args", - }) + s.o.DeviceManager().ResetToPostBootState() + + // go on + st.Unlock() + err = s.o.Settle(settleTimeout) + st.Lock() + // devicemgr's ensure boot ok tries to launch a revert + c.Check(err, ErrorMatches, `.*snap "pc-kernel" has "update-many" change in progress.*snap "core20" has "update-many" change in progress.*`) + + c.Assert(chg.Status(), Equals, state.ErrorStatus, Commentf("change failed with: %v", chg.Err())) + c.Assert(chg.Err(), ErrorMatches, `(?ms).*cannot finish core20 installation, there was a rollback across reboot\)`) + // there is no try kernel, bootloader references only the old one + _, err = bloader.TryKernel() + c.Assert(err, Equals, bootloader.ErrNoTryKernelRef) + kpi, err := bloader.Kernel() + c.Assert(err, IsNil) + c.Assert(kpi.Filename(), Equals, "pc-kernel_1.snap") + m, err = boot.ReadModeenv("") + c.Assert(err, IsNil) + c.Assert(m.BaseStatus, Equals, "") + c.Assert(m.TryBase, Equals, "") + c.Assert(m.Base, Equals, "core20_1.snap") + + for _, tsk := range chg.Tasks() { + if tsk.Kind() == "link-snap" { + c.Assert(tsk.Status(), Equals, state.UndoneStatus, + Commentf("%q has status other than undone", tsk.Summary())) + } + } } type gadgetUpdatesSuite struct { @@ -8713,7 +9758,7 @@ c.Assert(t, NotNil) c.Assert(t.Status(), Equals, state.DoingStatus, Commentf("install-snap change failed with: %v", chg.Err())) // simulate successful restart happened - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) // settle again st.Unlock() @@ -8899,7 +9944,7 @@ c.Assert(t, NotNil) c.Assert(t.Status(), Equals, state.DoingStatus, Commentf("install-snap change failed with: %v", chg.Err())) // simulate successful restart happened after gadget update - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) // settle again st.Unlock() @@ -8973,7 +10018,7 @@ {"boot-assets/bcm2710-rpi-2-b.dtb", "bcm2710-rpi-2-b.dtb rev1"}, {"boot-assets/bcm2710-rpi-3-b.dtb", "bcm2710-rpi-3-b.dtb rev1"}, }) - // we have old style boot asssets in the bootloader dir + // we have old style boot assets in the bootloader dir snaptest.PopulateDir(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName), [][]string{ {"start.elf", "start.elf rev1"}, {"bcm2710-rpi-2-b.dtb", "bcm2710-rpi-2-b.dtb rev1"}, @@ -9040,6 +10085,319 @@ c.Assert(chg.Err(), IsNil) } +func (ms *gadgetUpdatesSuite) mockSnapUpgradeWithFilesWithRev(c *C, snapYaml string, rev string, files [][]string) { + snapPath, _ := ms.makeStoreTestSnapWithFiles(c, snapYaml, rev, files) + ms.serveSnap(snapPath, rev) +} + +func (ms *gadgetUpdatesSuite) TestOldGadgetOldKernelRefreshToKernelRefWithGadgetAssetsCyclicDependency(c *C) { + kernelYaml := ` +assets: + pidtbs: + update: true + content: + - dtbs/broadcom/ + - dtbs/overlays/` + + structureName := "ubuntu-seed" + oldGadgetYaml := fmt.Sprintf(` +volumes: + volume-id: + schema: mbr + bootloader: u-boot + structure: + - name: %s + filesystem: vfat + type: 0C + size: 1200M + content: + - source: boot-assets/ + target: /`, structureName) + // the gadget specifically bumps the edition to trigger a cirular dependency + finalDesiredGadgetYaml := fmt.Sprintf(` +volumes: + volume-id: + schema: mbr + bootloader: u-boot + structure: + - name: %s + filesystem: vfat + type: 0C + size: 1200M + update: + edition: 1 + content: + - source: boot-assets/ + target: / + - source: $kernel:pidtbs/dtbs/broadcom/ + target: / + - source: $kernel:pidtbs/dtbs/overlays/ + target: /overlays`, structureName) + ms.makeMockedDev(c, structureName) + + intermediaryGadgetYaml := fmt.Sprintf(` +volumes: + volume-id: + schema: mbr + bootloader: u-boot + structure: + - name: %s + filesystem: vfat + type: 0C + size: 1200M + content: + - source: boot-assets/ + target: / + - source: $kernel:pidtbs/dtbs/broadcom/ + target: / + - source: $kernel:pidtbs/dtbs/overlays/ + target: /overlays`, structureName) + + st := ms.o.State() + st.Lock() + defer st.Unlock() + + // we have an installed old style pi gadget + gadgetSnapYaml := "name: pi\nversion: 1.0\ntype: gadget" + ms.mockInstalledSnapWithFiles(c, gadgetSnapYaml, [][]string{ + {"meta/gadget.yaml", oldGadgetYaml}, + {"boot-assets/start.elf", "start.elf rev1"}, + {"boot-assets/bcm2710-rpi-2-b.dtb", "bcm2710-rpi-2-b.dtb rev1"}, + {"boot-assets/bcm2710-rpi-3-b.dtb", "bcm2710-rpi-3-b.dtb rev1"}, + }) + // we have old style boot assets in the bootloader dir + snaptest.PopulateDir(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName), [][]string{ + {"start.elf", "start.elf rev0"}, + {"bcm2710-rpi-2-b.dtb", "bcm2710-rpi-2-b.dtb rev0"}, + {"bcm2710-rpi-3-b.dtb", "bcm2710-rpi-3-b.dtb rev0"}, + }) + + // we have an installed old-style kernel snap + kernelSnapYaml := "name: pi-kernel\nversion: 1.0\ntype: kernel" + ms.mockInstalledSnapWithFiles(c, kernelSnapYaml, nil) + + // add new kernel snap with kernel-refs to fake store + ms.mockSnapUpgradeWithFilesWithRev(c, kernelSnapYaml, "2", [][]string{ + {"meta/kernel.yaml", kernelYaml}, + {"dtbs/broadcom/bcm2710-rpi-2-b.dtb", "bcm2710-rpi-2-b.dtb rev2-from-kernel"}, + {"dtbs/broadcom/bcm2710-rpi-3-b.dtb", "bcm2710-rpi-3-b.dtb rev2-from-kernel"}, + {"dtbs/overlays/uart0.dtbo", "uart0.dtbo rev2-from-kernel"}, + }) + + // add new gadget snap with kernel-refs to fake store + ms.mockSnapUpgradeWithFilesWithRev(c, gadgetSnapYaml, "2", [][]string{ + {"meta/gadget.yaml", finalDesiredGadgetYaml}, + {"boot-assets/start.elf", "start.elf rev1"}, + // notice: no dtbs anymore in the gadget + }) + + // we can neither update the gadget nor the kernel because of circular + // dependency, as the new gadget refers to assets from the kernel, but + // those are not present in the old (installed) kernel, and the new + // kernel provides assets that are not consumed by the old (installed) + // gadget + + affected, tasksets, err := snapstate.UpdateMany(context.TODO(), st, []string{"pi"}, 0, &snapstate.Flags{}) + c.Assert(err, IsNil) + sort.Strings(affected) + c.Check(affected, DeepEquals, []string{"pi"}) + + addTaskSetsToChange := func(chg *state.Change, tss []*state.TaskSet) { + for _, ts := range tasksets { + // skip the taskset of UpdateMany that does the + // check-rerefresh, see tsWithoutReRefresh for details + if ts.Tasks()[0].Kind() == "check-rerefresh" { + continue + } + chg.AddAll(ts) + } + } + chg := st.NewChange("upgrade-snaps", "...") + addTaskSetsToChange(chg, tasksets) + + st.Unlock() + err = ms.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + c.Assert(chg.Err(), ErrorMatches, `(?s).*\(cannot resolve content for structure #0 \("ubuntu-seed"\) at index 1: cannot find "pidtbs" in kernel info .*\)`) + + restarting, _ := restart.Pending(st) + c.Assert(restarting, Equals, false, Commentf("unexpected restart")) + + // let's try updating the kernel; + affected, tasksets, err = snapstate.UpdateMany(context.TODO(), st, []string{"pi-kernel"}, 0, &snapstate.Flags{}) + c.Assert(err, IsNil) + sort.Strings(affected) + c.Check(affected, DeepEquals, []string{"pi-kernel"}) + + chg = st.NewChange("upgrade-snaps", "...") + addTaskSetsToChange(chg, tasksets) + + st.Unlock() + err = ms.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + c.Assert(chg.Err(), ErrorMatches, `(?s).*\(gadget does not consume any of the kernel assets needing synced update "pidtbs"\)`) + + // undo unlink-current-snap triggers a reboot + ms.mockSuccessfulReboot(c, ms.bloader, nil) + // let the change run until it is done + st.Unlock() + err = ms.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + // we already now it is considered as failed + c.Assert(chg.Status(), Equals, state.ErrorStatus) + + // but we can actually perform the full upgrade set if we first refresh + // to an intermediate gadget revision which does not declare an update, + // but does now reference the kernel assets, to force going through this + // revision, declare it uses a transitional epoch 1* (which can read + // epoch 0, the default) + gadgetSnapYamlIntermediate := ` +name: pi +version: 1.0 +type: gadget +epoch: 1* +` + // while the final gadget will have an epoch 1 + gadgetSnapYamlFinal := ` +name: pi +version: 1.0 +type: gadget +epoch: 1 +` + // make both revisions available in the fake store + ms.mockSnapUpgradeWithFilesWithRev(c, gadgetSnapYamlIntermediate, "3", [][]string{ + {"meta/gadget.yaml", intermediaryGadgetYaml}, + {"boot-assets/start.elf", "start.elf rev1"}, + // the intermediary gadget snap has these files but it doesn't really + // mattter since update does not set an edition, so no update is + // attempted using these files + {"bcm2710-rpi-2-b.dtb", "bcm2710-rpi-2-b.dtb rev1"}, + {"bcm2710-rpi-3-b.dtb", "bcm2710-rpi-3-b.dtb rev1"}, + }) + ms.mockSnapUpgradeWithFilesWithRev(c, gadgetSnapYamlFinal, "4", [][]string{ + {"meta/gadget.yaml", finalDesiredGadgetYaml}, + {"boot-assets/start.elf", "start.elf rev1"}, + }) + + affected, tasksets, err = snapstate.UpdateMany(context.TODO(), st, []string{"pi"}, 0, &snapstate.Flags{}) + c.Assert(err, IsNil) + sort.Strings(affected) + c.Check(affected, DeepEquals, []string{"pi"}) + + chg = st.NewChange("upgrade-snaps", "...") + addTaskSetsToChange(chg, tasksets) + + st.Unlock() + err = ms.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + c.Assert(chg.Err(), IsNil) + + // file content is still unchanged because there is no edition bump + c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName, "bcm2710-rpi-2-b.dtb"), + testutil.FileContains, "bcm2710-rpi-2-b.dtb rev0") + c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName, "bcm2710-rpi-3-b.dtb"), + testutil.FileContains, "bcm2710-rpi-3-b.dtb rev0") + c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName, "overlays/uart0.dtbo"), + testutil.FileAbsent) + // gadget content is not updated because there is no edition update + c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName, "start.elf"), + testutil.FileContains, "start.elf rev0") + + // thus there is no reboot either + restarting, _ = restart.Pending(st) + c.Assert(restarting, Equals, false, Commentf("unexpected restart")) + + // and now we can perform a refresh of the kernel + affected, tasksets, err = snapstate.UpdateMany(context.TODO(), st, []string{"pi-kernel"}, 0, &snapstate.Flags{}) + c.Assert(err, IsNil) + sort.Strings(affected) + c.Check(affected, DeepEquals, []string{"pi-kernel"}) + + chg = st.NewChange("upgrade-snaps", "...") + addTaskSetsToChange(chg, tasksets) + + st.Unlock() + err = ms.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + c.Assert(chg.Err(), IsNil) + + // At this point the gadget and kernel are updated and the kernel + // required a restart. Check that *before* this restart the DTB + // files from the kernel are in place. + c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName, "bcm2710-rpi-2-b.dtb"), + testutil.FileContains, "bcm2710-rpi-2-b.dtb rev2-from-kernel") + c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName, "bcm2710-rpi-3-b.dtb"), + testutil.FileContains, "bcm2710-rpi-3-b.dtb rev2-from-kernel") + c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName, "overlays/uart0.dtbo"), + testutil.FileContains, "uart0.dtbo rev2-from-kernel") + // gadget content is not updated because there is no edition update + c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName, "start.elf"), + testutil.FileContains, "start.elf rev0") + + // pretend we restarted, both a kernel and boot assets update + ms.mockSuccessfulReboot(c, ms.bloader, []snap.Type{snap.TypeKernel}) + st.Unlock() + err = ms.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + c.Assert(chg.Err(), IsNil) + c.Assert(chg.Status(), Equals, state.DoneStatus) + + var ss snapstate.SnapState + c.Assert(snapstate.Get(st, "pi", &ss), IsNil) + // the transitional revision + c.Assert(ss.Current, Equals, snap.R(3)) + + // also check that the gadget asset updates for the second refresh of + // the gadget snap get applied since that is important for some use + // cases and is probably why folks got into the circular dependency in + // the first place, for this we add another revision of the gadget snap + + affected, tasksets, err = snapstate.UpdateMany(context.TODO(), st, []string{"pi"}, 0, &snapstate.Flags{}) + c.Assert(err, IsNil) + sort.Strings(affected) + c.Check(affected, DeepEquals, []string{"pi"}) + + chg = st.NewChange("upgrade-snaps", "...") + addTaskSetsToChange(chg, tasksets) + + st.Unlock() + err = ms.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + c.Assert(chg.Err(), IsNil) + + // gadget assets that come from the kernel are unchanged + c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName, "bcm2710-rpi-2-b.dtb"), + testutil.FileContains, "bcm2710-rpi-2-b.dtb rev2-from-kernel") + c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName, "bcm2710-rpi-3-b.dtb"), + testutil.FileContains, "bcm2710-rpi-3-b.dtb rev2-from-kernel") + c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName, "overlays/uart0.dtbo"), + testutil.FileContains, "uart0.dtbo rev2-from-kernel") + // but an assets that comes directly from the gadget was updated + c.Check(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName, "start.elf"), + testutil.FileContains, "start.elf rev1") + + // pretend we restarted for the gadget refresh + ms.mockSuccessfulReboot(c, ms.bloader, nil) + // and let the change run until it is done + st.Unlock() + err = ms.o.Settle(settleTimeout) + st.Lock() + c.Assert(err, IsNil) + c.Assert(chg.Err(), IsNil) + c.Assert(chg.Status(), Equals, state.DoneStatus) + + c.Assert(snapstate.Get(st, "pi", &ss), IsNil) + // the final revision + c.Assert(ss.Current, Equals, snap.R(4)) +} + func snapTaskStatusForChange(chg *state.Change) map[string]state.Status { taskStates := make(map[string]state.Status) for _, t := range chg.Tasks() { @@ -9106,7 +10464,7 @@ {"boot-assets/bcm2710-rpi-2-b.dtb", "bcm2710-rpi-2-b.dtb rev1"}, {"boot-assets/bcm2710-rpi-3-b.dtb", "bcm2710-rpi-3-b.dtb rev1"}, }) - // we have old style boot asssets in the bootloader dir + // we have old style boot assets in the bootloader dir snaptest.PopulateDir(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName), [][]string{ {"start.elf", "start.elf rev1"}, {"bcm2710-rpi-2-b.dtb", "bcm2710-rpi-2-b.dtb rev1"}, @@ -9241,7 +10599,7 @@ {"boot-assets/bcm2710-rpi-2-b.dtb", "bcm2710-rpi-2-b.dtb rev1"}, {"boot-assets/bcm2710-rpi-3-b.dtb", "bcm2710-rpi-3-b.dtb rev1"}, }) - // we have old style boot asssets in the bootloader dir + // we have old style boot assets in the bootloader dir snaptest.PopulateDir(filepath.Join(dirs.GlobalRootDir, "/run/mnt/", structureName), [][]string{ {"start.elf", "start.elf rev1"}, {"bcm2710-rpi-2-b.dtb", "bcm2710-rpi-2-b.dtb rev1"}, diff -Nru snapd-2.53+21.10ubuntu1/overlord/overlord.go snapd-2.54.2+21.10/overlord/overlord.go --- snapd-2.53+21.10ubuntu1/overlord/overlord.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/overlord.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2017 Canonical Ltd + * Copyright (C) 2016-2021 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -44,6 +44,7 @@ "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/ifacestate" "github.com/snapcore/snapd/overlord/patch" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/servicestate" "github.com/snapcore/snapd/overlord/snapshotstate" "github.com/snapcore/snapd/overlord/snapstate" @@ -86,8 +87,6 @@ startOfOperationTime time.Time - // restarts - restartBehavior RestartBehavior // managers inited bool startedUp bool @@ -104,35 +103,21 @@ proxyConf func(req *http.Request) (*url.URL, error) } -// RestartBehavior controls how to hanndle and carry forward restart requests -// via the state. -type RestartBehavior interface { - HandleRestart(t state.RestartType) - // RebootAsExpected is called early when either a reboot was - // requested by snapd and happened or no reboot was expected at all. - RebootAsExpected(st *state.State) error - // RebootDidNotHappen is called early instead when a reboot was - // requested by snad but did not happen. - RebootDidNotHappen(st *state.State) error -} - var storeNew = store.New // New creates a new Overlord with all its state managers. -// It can be provided with an optional RestartBehavior. -func New(restartBehavior RestartBehavior) (*Overlord, error) { +// It can be provided with an optional restart.Handler. +func New(restartHandler restart.Handler) (*Overlord, error) { o := &Overlord{ - loopTomb: new(tomb.Tomb), - inited: true, - restartBehavior: restartBehavior, + loopTomb: new(tomb.Tomb), + inited: true, } backend := &overlordStateBackend{ - path: dirs.SnapStateFile, - ensureBefore: o.ensureBefore, - requestRestart: o.requestRestart, + path: dirs.SnapStateFile, + ensureBefore: o.ensureBefore, } - s, err := loadState(backend, restartBehavior) + s, err := loadState(backend, restartHandler) if err != nil { return nil, err } @@ -224,7 +209,7 @@ o.stateEng.AddManager(mgr) } -func loadState(backend state.Backend, restartBehavior RestartBehavior) (*state.State, error) { +func loadState(backend state.Backend, restartHandler restart.Handler) (*state.State, error) { curBootID, err := osutil.BootID() if err != nil { return nil, fmt.Errorf("fatal: cannot find current boot id: %v", err) @@ -240,9 +225,7 @@ return nil, fmt.Errorf("fatal: directory %q must be present", stateDir) } s := state.New(backend) - s.Lock() - s.VerifyReboot(curBootID) - s.Unlock() + initRestart(s, curBootID, restartHandler) patch.Init(s) return s, nil } @@ -264,7 +247,7 @@ perfTimings.Save(s) s.Unlock() - err = verifyReboot(s, curBootID, restartBehavior) + err = initRestart(s, curBootID, restartHandler) if err != nil { return nil, err } @@ -277,24 +260,10 @@ return s, nil } -func verifyReboot(s *state.State, curBootID string, restartBehavior RestartBehavior) error { +func initRestart(s *state.State, curBootID string, restartHandler restart.Handler) error { s.Lock() defer s.Unlock() - err := s.VerifyReboot(curBootID) - if err != nil && err != state.ErrExpectedReboot { - return err - } - expectedRebootDidNotHappen := err == state.ErrExpectedReboot - if restartBehavior != nil { - if expectedRebootDidNotHappen { - return restartBehavior.RebootDidNotHappen(s) - } - return restartBehavior.RebootAsExpected(s) - } - if expectedRebootDidNotHappen { - logger.Noticef("expected system restart but it did not happen") - } - return nil + return restart.Init(s, curBootID, restartHandler) } func (o *Overlord) newStoreWithContext(storeCtx store.DeviceAndAuthContext) snapstate.StoreService { @@ -404,14 +373,6 @@ } } -func (o *Overlord) requestRestart(t state.RestartType) { - if o.restartBehavior == nil { - logger.Noticef("restart requested but no behavior set") - } else { - o.restartBehavior.HandleRestart(t) - } -} - var preseedExitWithError = func(err error) { fmt.Fprintf(os.Stderr, "cannot preseed: %v\n", err) os.Exit(1) @@ -634,18 +595,16 @@ // Mock creates an Overlord without any managers and with a backend // not using disk. Managers can be added with AddManager. For testing. func Mock() *Overlord { - return MockWithStateAndRestartHandler(nil, nil) + return MockWithState(nil) } -// MockWithStateAndRestartHandler creates an Overlord with the given state +// MockWithState creates an Overlord with the given state // unless it is nil in which case it uses a state backend not using -// disk. It will use the given handler on restart requests. Managers -// can be added with AddManager. For testing. -func MockWithStateAndRestartHandler(s *state.State, handleRestart func(state.RestartType)) *Overlord { +// disk. Managers can be added with AddManager. For testing. +func MockWithState(s *state.State) *Overlord { o := &Overlord{ - loopTomb: new(tomb.Tomb), - inited: false, - restartBehavior: mockRestartBehavior(handleRestart), + loopTomb: new(tomb.Tomb), + inited: false, } if s == nil { s = state.New(mockBackend{o: o}) @@ -665,23 +624,6 @@ o.addManager(mgr) } -type mockRestartBehavior func(state.RestartType) - -func (rb mockRestartBehavior) HandleRestart(t state.RestartType) { - if rb == nil { - return - } - rb(t) -} - -func (rb mockRestartBehavior) RebootAsExpected(*state.State) error { - panic("internal error: overlord.Mock should not invoke RebootAsExpected") -} - -func (rb mockRestartBehavior) RebootDidNotHappen(*state.State) error { - panic("internal error: overlord.Mock should not invoke RebootDidNotHappen") -} - type mockBackend struct { o *Overlord } @@ -700,7 +642,3 @@ mb.o.ensureBefore(d) } - -func (mb mockBackend) RequestRestart(t state.RestartType) { - mb.o.requestRestart(t) -} diff -Nru snapd-2.53+21.10ubuntu1/overlord/overlord_test.go snapd-2.54.2+21.10/overlord/overlord_test.go --- snapd-2.53+21.10ubuntu1/overlord/overlord_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/overlord_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -42,6 +42,7 @@ "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/ifacestate" "github.com/snapcore/snapd/overlord/patch" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" @@ -1133,38 +1134,46 @@ o, err := overlord.New(nil) c.Assert(err, IsNil) - o.State().RequestRestart(state.RestartDaemon) + st := o.State() + st.Lock() + defer st.Unlock() + + restart.Request(st, restart.RestartDaemon) } -type testRestartBehavior struct { - restartRequested state.RestartType +type testRestartHandler struct { + restartRequested restart.RestartType rebootState string rebootVerifiedErr error } -func (rb *testRestartBehavior) HandleRestart(t state.RestartType) { +func (rb *testRestartHandler) HandleRestart(t restart.RestartType) { rb.restartRequested = t } -func (rb *testRestartBehavior) RebootAsExpected(_ *state.State) error { +func (rb *testRestartHandler) RebootAsExpected(_ *state.State) error { rb.rebootState = "as-expected" return rb.rebootVerifiedErr } -func (rb *testRestartBehavior) RebootDidNotHappen(_ *state.State) error { +func (rb *testRestartHandler) RebootDidNotHappen(_ *state.State) error { rb.rebootState = "did-not-happen" return rb.rebootVerifiedErr } func (ovs *overlordSuite) TestRequestRestartHandler(c *C) { - rb := &testRestartBehavior{} + rb := &testRestartHandler{} o, err := overlord.New(rb) c.Assert(err, IsNil) - o.State().RequestRestart(state.RestartDaemon) + st := o.State() + st.Lock() + defer st.Unlock() + + restart.Request(st, restart.RestartDaemon) - c.Check(rb.restartRequested, Equals, state.RestartDaemon) + c.Check(rb.restartRequested, Equals, restart.RestartDaemon) } func (ovs *overlordSuite) TestVerifyRebootNoPendingReboot(c *C) { @@ -1172,7 +1181,7 @@ err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600) c.Assert(err, IsNil) - rb := &testRestartBehavior{} + rb := &testRestartHandler{} _, err = overlord.New(rb) c.Assert(err, IsNil) @@ -1185,7 +1194,7 @@ err := ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600) c.Assert(err, IsNil) - rb := &testRestartBehavior{} + rb := &testRestartHandler{} _, err = overlord.New(rb) c.Assert(err, IsNil) @@ -1199,7 +1208,7 @@ c.Assert(err, IsNil) e := errors.New("boom") - rb := &testRestartBehavior{rebootVerifiedErr: e} + rb := &testRestartHandler{rebootVerifiedErr: e} _, err = overlord.New(rb) c.Assert(err, Equals, e) @@ -1215,7 +1224,7 @@ err = ioutil.WriteFile(dirs.SnapStateFile, fakeState, 0600) c.Assert(err, IsNil) - rb := &testRestartBehavior{} + rb := &testRestartHandler{} _, err = overlord.New(rb) c.Assert(err, IsNil) @@ -1232,7 +1241,7 @@ c.Assert(err, IsNil) e := errors.New("boom") - rb := &testRestartBehavior{rebootVerifiedErr: e} + rb := &testRestartHandler{rebootVerifiedErr: e} _, err = overlord.New(rb) c.Assert(err, Equals, e) diff -Nru snapd-2.53+21.10ubuntu1/overlord/restart/restart.go snapd-2.54.2+21.10/overlord/restart/restart.go --- snapd-2.53+21.10ubuntu1/overlord/restart/restart.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/restart/restart.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,161 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +// Package restart implements requesting restarts from any part of the code that has access to state. +package restart + +import ( + "github.com/snapcore/snapd/overlord/state" +) + +type RestartType int + +const ( + RestartUnset RestartType = iota + RestartDaemon + RestartSystem + // RestartSystemNow is like RestartSystem but action is immediate + RestartSystemNow + // RestartSocket will restart the daemon so that it goes into + // socket activation mode. + RestartSocket + // Stop just stops the daemon (used with image pre-seeding) + StopDaemon + // RestartSystemHaltNow will shutdown --halt the system asap + RestartSystemHaltNow + // RestartSystemPoweroffNow will shutdown --poweroff the system asap + RestartSystemPoweroffNow +) + +// Handler can handle restart requests and whether expected reboots happen. +type Handler interface { + HandleRestart(t RestartType) + // RebootAsExpected is called early when either a reboot was + // requested by snapd and happened or no reboot was expected at all. + RebootAsExpected(st *state.State) error + // RebootDidNotHappen is called early instead when a reboot was + // requested by snapd but did not happen. + RebootDidNotHappen(st *state.State) error +} + +// Init initializes the support for restarts requests. +// It takes the current boot id to track and verify reboots and a +// Handler that handles the actual requests and reacts to reboot +// happening. +// It must be called with the state lock held. +func Init(st *state.State, curBootID string, h Handler) error { + rs := &restartState{ + h: h, + bootID: curBootID, + } + var fromBootID string + err := st.Get("system-restart-from-boot-id", &fromBootID) + if err != nil && err != state.ErrNoState { + return err + } + st.Cache(restartStateKey{}, rs) + if fromBootID == "" { + return rs.rebootAsExpected(st) + } + if fromBootID == curBootID { + return rs.rebootDidNotHappen(st) + } + // we rebooted alright + ClearReboot(st) + return rs.rebootAsExpected(st) +} + +// ClearReboot clears state information about tracking requested reboots. +func ClearReboot(st *state.State) { + st.Set("system-restart-from-boot-id", nil) +} + +type restartStateKey struct{} + +type restartState struct { + restarting RestartType + h Handler + bootID string +} + +func (rs *restartState) handleRestart(t RestartType) { + if rs.h != nil { + rs.h.HandleRestart(t) + } +} + +func (rs *restartState) rebootAsExpected(st *state.State) error { + if rs.h != nil { + return rs.h.RebootAsExpected(st) + } + return nil +} + +func (rs *restartState) rebootDidNotHappen(st *state.State) error { + if rs.h != nil { + return rs.h.RebootDidNotHappen(st) + } + return nil +} + +// Request asks for a restart of the managing process. +// The state needs to be locked to request a restart. +func Request(st *state.State, t RestartType) { + cached := st.Cached(restartStateKey{}) + if cached == nil { + panic("internal error: cannot request a restart before restart.Init") + } + rs := cached.(*restartState) + switch t { + case RestartSystem, RestartSystemNow, RestartSystemHaltNow, RestartSystemPoweroffNow: + st.Set("system-restart-from-boot-id", rs.bootID) + } + rs.restarting = t + rs.handleRestart(t) +} + +// Pending returns whether a restart was requested with Request and of which type. +func Pending(st *state.State) (bool, RestartType) { + cached := st.Cached(restartStateKey{}) + if cached == nil { + return false, RestartUnset + } + rs := cached.(*restartState) + return rs.restarting != RestartUnset, rs.restarting +} + +func MockPending(st *state.State, restarting RestartType) RestartType { + cached := st.Cached(restartStateKey{}) + if cached == nil { + panic("internal error: cannot mock a restart request before restart.Init") + } + rs := cached.(*restartState) + old := rs.restarting + rs.restarting = restarting + return old +} + +func ReplaceBootID(st *state.State, bootID string) { + cached := st.Cached(restartStateKey{}) + if cached == nil { + panic("internal error: cannot mock a restart request before restart.Init") + } + rs := cached.(*restartState) + rs.bootID = bootID +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/restart/restart_test.go snapd-2.54.2+21.10/overlord/restart/restart_test.go --- snapd-2.53+21.10ubuntu1/overlord/restart/restart_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/restart/restart_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,143 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * 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 + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package restart_test + +import ( + "testing" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/overlord/restart" + "github.com/snapcore/snapd/overlord/state" +) + +func TestRestart(t *testing.T) { TestingT(t) } + +type restartSuite struct{} + +var _ = Suite(&restartSuite{}) + +type testHandler struct { + restartRequested bool + rebootAsExpected bool + rebootDidNotHappen bool +} + +func (h *testHandler) HandleRestart(t restart.RestartType) { + h.restartRequested = true +} + +func (h *testHandler) RebootAsExpected(*state.State) error { + h.rebootAsExpected = true + return nil +} + +func (h *testHandler) RebootDidNotHappen(*state.State) error { + h.rebootDidNotHappen = true + return nil +} + +func (s *restartSuite) TestRequestRestartDaemon(c *C) { + st := state.New(nil) + + st.Lock() + defer st.Unlock() + + // uninitialized + ok, t := restart.Pending(st) + c.Check(ok, Equals, false) + c.Check(t, Equals, restart.RestartUnset) + + h := &testHandler{} + + err := restart.Init(st, "boot-id-1", h) + c.Assert(err, IsNil) + c.Check(h.rebootAsExpected, Equals, true) + + ok, t = restart.Pending(st) + c.Check(ok, Equals, false) + c.Check(t, Equals, restart.RestartUnset) + + restart.Request(st, restart.RestartDaemon) + + c.Check(h.restartRequested, Equals, true) + + ok, t = restart.Pending(st) + c.Check(ok, Equals, true) + c.Check(t, Equals, restart.RestartDaemon) +} + +func (s *restartSuite) TestRequestRestartDaemonNoHandler(c *C) { + st := state.New(nil) + + st.Lock() + defer st.Unlock() + + err := restart.Init(st, "boot-id-1", nil) + c.Assert(err, IsNil) + + restart.Request(st, restart.RestartDaemon) + + ok, t := restart.Pending(st) + c.Check(ok, Equals, true) + c.Check(t, Equals, restart.RestartDaemon) +} + +func (s *restartSuite) TestRequestRestartSystemAndVerifyReboot(c *C) { + st := state.New(nil) + st.Lock() + defer st.Unlock() + + h := &testHandler{} + err := restart.Init(st, "boot-id-1", h) + c.Assert(err, IsNil) + c.Check(h.rebootAsExpected, Equals, true) + + ok, t := restart.Pending(st) + c.Check(ok, Equals, false) + c.Check(t, Equals, restart.RestartUnset) + + restart.Request(st, restart.RestartSystem) + + c.Check(h.restartRequested, Equals, true) + + ok, t = restart.Pending(st) + c.Check(ok, Equals, true) + c.Check(t, Equals, restart.RestartSystem) + + var fromBootID string + c.Check(st.Get("system-restart-from-boot-id", &fromBootID), IsNil) + c.Check(fromBootID, Equals, "boot-id-1") + + h1 := &testHandler{} + err = restart.Init(st, "boot-id-1", h1) + c.Assert(err, IsNil) + c.Check(h1.rebootAsExpected, Equals, false) + c.Check(h1.rebootDidNotHappen, Equals, true) + fromBootID = "" + c.Check(st.Get("system-restart-from-boot-id", &fromBootID), IsNil) + c.Check(fromBootID, Equals, "boot-id-1") + + h2 := &testHandler{} + err = restart.Init(st, "boot-id-2", h2) + c.Assert(err, IsNil) + c.Check(h2.rebootAsExpected, Equals, true) + c.Check(st.Get("system-restart-from-boot-id", &fromBootID), Equals, state.ErrNoState) +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/servicestate/quota_control_test.go snapd-2.54.2+21.10/overlord/servicestate/quota_control_test.go --- snapd-2.53+21.10ubuntu1/overlord/servicestate/quota_control_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/servicestate/quota_control_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -153,8 +153,8 @@ svc := "snap." + name + ".svc1.service" return []expectedSystemctl{ { - expArgs: []string{"show", "--property=Id,ActiveState,UnitFileState,Type", svc}, - output: fmt.Sprintf("Id=%s\nActiveState=active\nUnitFileState=enabled\nType=simple\n", svc), + expArgs: []string{"show", "--property=Id,ActiveState,UnitFileState,Type,Names", svc}, + output: fmt.Sprintf("Id=%s\nNames=%[1]s\nActiveState=active\nUnitFileState=enabled\nType=simple\n", svc), }, {expArgs: []string{"stop", svc}}, { diff -Nru snapd-2.53+21.10ubuntu1/overlord/servicestate/servicemgr.go snapd-2.54.2+21.10/overlord/servicestate/servicemgr.go --- snapd-2.53+21.10ubuntu1/overlord/servicestate/servicemgr.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/servicestate/servicemgr.go 2022-01-06 21:25:16.000000000 +0000 @@ -30,6 +30,7 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/progress" @@ -179,7 +180,7 @@ // we need to immediately reboot in the hopes that this restores // services to a functioning state - m.state.RequestRestart(state.RestartSystemNow) + restart.Request(m.state, restart.RestartSystemNow) return fmt.Errorf("error trying to restart killed services, immediately rebooting: %v", err) } diff -Nru snapd-2.53+21.10ubuntu1/overlord/servicestate/servicemgr_test.go snapd-2.54.2+21.10/overlord/servicestate/servicemgr_test.go --- snapd-2.53+21.10ubuntu1/overlord/servicestate/servicemgr_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/servicestate/servicemgr_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -34,6 +34,7 @@ "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/overlord" "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/servicestate" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" @@ -54,7 +55,7 @@ se *overlord.StateEngine state *state.State - restartRequests []state.RestartType + restartRequests []restart.RestartType restartObserve func() uc18Model *asserts.Model @@ -73,16 +74,15 @@ s.restartRequests = nil s.restartObserve = nil - s.o = overlord.MockWithStateAndRestartHandler(nil, func(req state.RestartType) { + s.o = overlord.Mock() + s.state = s.o.State() + s.state.Lock() + restart.Init(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(req restart.RestartType) { s.restartRequests = append(s.restartRequests, req) if s.restartObserve != nil { s.restartObserve() } - }) - - s.state = s.o.State() - s.state.Lock() - s.state.VerifyReboot("boot-id-0") + })) s.state.Unlock() s.se = s.o.StateEngine() @@ -1309,7 +1309,7 @@ c.Assert(svcFile, testutil.FileEquals, content) // we requested a restart - c.Assert(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Assert(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) } func (s *ensureSnapServiceSuite) TestEnsureSnapServicesWritesServicesFilesAndTriesRestartButFailsButThenFallsbackToReboot(c *C) { @@ -1380,5 +1380,5 @@ c.Assert(svcFile, testutil.FileEquals, content) // we requested a restart - c.Assert(s.restartRequests, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Assert(s.restartRequests, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) } diff -Nru snapd-2.53+21.10ubuntu1/overlord/servicestate/servicestate.go snapd-2.54.2+21.10/overlord/servicestate/servicestate.go --- snapd-2.53+21.10ubuntu1/overlord/servicestate/servicestate.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/servicestate/servicestate.go 2022-01-06 21:25:16.000000000 +0000 @@ -313,7 +313,7 @@ return fmt.Errorf("cannot get status of services of app %q: expected %d results, got %d", appInfo.Name, len(serviceNames), len(sts)) } for _, st := range sts { - switch filepath.Ext(st.UnitName) { + switch filepath.Ext(st.Name) { case ".service": appInfo.Enabled = st.Enabled appInfo.Active = st.Active @@ -326,7 +326,7 @@ }) case ".socket": appInfo.Activators = append(appInfo.Activators, client.AppActivator{ - Name: sockSvcFileToName[st.UnitName], + Name: sockSvcFileToName[st.Name], Enabled: st.Enabled, Active: st.Active, Type: "socket", diff -Nru snapd-2.53+21.10ubuntu1/overlord/servicestate/servicestate_test.go snapd-2.54.2+21.10/overlord/servicestate/servicestate_test.go --- snapd-2.53+21.10ubuntu1/overlord/servicestate/servicestate_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/servicestate/servicestate_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -72,13 +72,17 @@ activeState = "inactive" unitState = "disabled" } - if strings.HasSuffix(unit, ".timer") || strings.HasSuffix(unit, ".socket") { + if strings.HasSuffix(unit, ".timer") || strings.HasSuffix(unit, ".socket") || strings.HasSuffix(unit, ".target") { + // Units using the baseProperties query return []byte(fmt.Sprintf(`Id=%s +Names=%[1]s ActiveState=%s UnitFileState=%s `, args[2], activeState, unitState)), nil } else { + // Units using the extendedProperties query return []byte(fmt.Sprintf(`Id=%s +Names=%[1]s Type=simple ActiveState=%s UnitFileState=%s diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapshotstate/backend/backend.go snapd-2.54.2+21.10/overlord/snapshotstate/backend/backend.go --- snapd-2.53+21.10ubuntu1/overlord/snapshotstate/backend/backend.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapshotstate/backend/backend.go 2022-01-06 21:25:16.000000000 +0000 @@ -258,7 +258,7 @@ } // EstimateSnapshotSize calculates estimated size of the snapshot. -func EstimateSnapshotSize(si *snap.Info, usernames []string) (uint64, error) { +func EstimateSnapshotSize(si *snap.Info, usernames []string, opts *dirs.SnapDirOptions) (uint64, error) { var total uint64 calculateSize := func(path string, finfo os.FileInfo, err error) error { if finfo.Mode().IsRegular() { @@ -284,15 +284,15 @@ } } - users, err := usersForUsernames(usernames) + users, err := usersForUsernames(usernames, opts) if err != nil { return 0, err } for _, usr := range users { - if err := visitDir(si.UserDataDir(usr.HomeDir)); err != nil { + if err := visitDir(si.UserDataDir(usr.HomeDir, opts)); err != nil { return 0, err } - if err := visitDir(si.UserCommonDataDir(usr.HomeDir)); err != nil { + if err := visitDir(si.UserCommonDataDir(usr.HomeDir, opts)); err != nil { return 0, err } } @@ -302,7 +302,7 @@ } // Save a snapshot -func Save(ctx context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string) (*client.Snapshot, error) { +func Save(ctx context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string, opts *dirs.SnapDirOptions) (*client.Snapshot, error) { if err := os.MkdirAll(dirs.SnapshotsDir, 0700); err != nil { return nil, err } @@ -334,13 +334,13 @@ return nil, err } - users, err := usersForUsernames(usernames) + users, err := usersForUsernames(usernames, opts) if err != nil { return nil, err } for _, usr := range users { - if err := addDirToZip(ctx, snapshot, w, usr.Username, userArchiveName(usr), si.UserDataDir(usr.HomeDir)); err != nil { + if err := addDirToZip(ctx, snapshot, w, usr.Username, userArchiveName(usr), si.UserDataDir(usr.HomeDir, opts)); err != nil { return nil, err } } diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapshotstate/backend/backend_test.go snapd-2.54.2+21.10/overlord/snapshotstate/backend/backend_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapshotstate/backend/backend_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapshotstate/backend/backend_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -87,11 +87,11 @@ name: "bar", content: "common system canary\n", }, { - dir: si.UserDataDir(homeDir), + dir: si.UserDataDir(homeDir, nil), name: "ufoo", content: "versioned user canary\n", }, { - dir: si.UserCommonDataDir(homeDir), + dir: si.UserCommonDataDir(homeDir, nil), name: "ubar", content: "common user canary\n", }, @@ -449,7 +449,7 @@ info := &snap.Info{SideInfo: snap.SideInfo{RealName: "hello-snap", Revision: snap.R(42), SnapID: "hello-id"}, Version: "v1.33", Epoch: epoch} cfg := map[string]interface{}{"some-setting": false} - shw, err := backend.Save(context.TODO(), 12, info, cfg, []string{"snapuser"}) + shw, err := backend.Save(context.TODO(), 12, info, cfg, []string{"snapuser"}, nil) c.Assert(err, check.IsNil) c.Check(shw.SetID, check.Equals, uint64(12)) @@ -637,7 +637,7 @@ cfg := map[string]interface{}{"some-setting": false} shID := uint64(12) - shw, err := backend.Save(context.TODO(), shID, info, cfg, []string{"snapuser"}) + shw, err := backend.Save(context.TODO(), shID, info, cfg, []string{"snapuser"}, nil) c.Assert(err, check.IsNil) c.Check(shw.SetID, check.Equals, shID) c.Check(shw.Snap, check.Equals, info.InstanceName()) @@ -691,7 +691,7 @@ c.Check(diff().Run(), check.NotNil, comm) // restore leaves things like they were (again and again) - rs, err := shr.Restore(context.TODO(), snap.R(0), nil, logger.Debugf) + rs, err := shr.Restore(context.TODO(), snap.R(0), nil, logger.Debugf, nil) c.Assert(err, check.IsNil, comm) rs.Cleanup() c.Check(diff().Run(), check.IsNil, comm) @@ -711,7 +711,7 @@ info := &snap.Info{SideInfo: snap.SideInfo{RealName: "hello-snap", Revision: snap.R(42), SnapID: "hello-id"}, Version: "v1.33", Epoch: epoch} cfg := map[string]interface{}{"some-setting": false} - shw, err := backend.Save(context.TODO(), 12, info, cfg, []string{"snapuser"}) + shw, err := backend.Save(context.TODO(), 12, info, cfg, []string{"snapuser"}, nil) c.Assert(err, check.IsNil) c.Check(shw.SetID, check.Equals, uint64(12)) @@ -735,7 +735,7 @@ info := &snap.Info{SideInfo: snap.SideInfo{RealName: "hello-snap", Revision: snap.R(42), SnapID: "hello-id"}, Version: "v1.33", Epoch: epoch} shID := uint64(12) - shw, err := backend.Save(context.TODO(), shID, info, nil, []string{"snapuser"}) + shw, err := backend.Save(context.TODO(), shID, info, nil, []string{"snapuser"}, nil) c.Assert(err, check.IsNil) c.Check(shw.Revision, check.Equals, info.Revision) @@ -769,7 +769,7 @@ c.Check(diff().Run(), check.NotNil) // restore leaves things like they were, but in the new dir - rs, err := shr.Restore(context.TODO(), snap.R("17"), nil, logger.Debugf) + rs, err := shr.Restore(context.TODO(), snap.R("17"), nil, logger.Debugf, nil) c.Assert(err, check.IsNil) rs.Cleanup() c.Check(diff().Run(), check.IsNil) @@ -1000,7 +1000,7 @@ info := &snap.Info{SideInfo: snap.SideInfo{RealName: "hello-snap", Revision: snap.R(42), SnapID: "hello-id"}, Version: "v1.33", Epoch: epoch} shID := uint64(12) - shw, err := backend.Save(ctx, shID, info, nil, []string{"snapuser"}) + shw, err := backend.Save(ctx, shID, info, nil, []string{"snapuser"}, nil) c.Assert(err, check.IsNil) export, err := backend.NewSnapshotExport(ctx, shw.SetID) @@ -1030,7 +1030,7 @@ cfg := map[string]interface{}{"some-setting": false} shID := uint64(12) - shw, err := backend.Save(ctx, shID, info, cfg, []string{"snapuser"}) + shw, err := backend.Save(ctx, shID, info, cfg, []string{"snapuser"}, nil) c.Assert(err, check.IsNil) c.Check(shw.SetID, check.Equals, shID) @@ -1067,7 +1067,20 @@ } func (s *snapshotSuite) TestEstimateSnapshotSize(c *check.C) { - restore := backend.MockUsersForUsernames(func(usernames []string) ([]*user.User, error) { + + for _, t := range []struct { + snapDir string + opts *dirs.SnapDirOptions + }{ + {dirs.UserHomeSnapDir, nil}, + {dirs.UserHomeSnapDir, &dirs.SnapDirOptions{HiddenSnapDataDir: false}}, + {dirs.HiddenSnapDataHomeDir, &dirs.SnapDirOptions{HiddenSnapDataDir: true}}} { + s.testEstimateSnapshotSize(c, t.snapDir, t.opts) + } +} + +func (s *snapshotSuite) testEstimateSnapshotSize(c *check.C, snapDataDir string, opts *dirs.SnapDirOptions) { + restore := backend.MockUsersForUsernames(func(usernames []string, _ *dirs.SnapDirOptions) ([]*user.User, error) { return []*user.User{{HomeDir: filepath.Join(s.root, "home/user1")}}, nil }) defer restore() @@ -1085,8 +1098,8 @@ "/var/snap/foo/7", "/var/snap/foo/common", "/var/snap/foo/common/a", - "/home/user1/snap/foo/7/somedata", - "/home/user1/snap/foo/common", + filepath.Join("/home/user1", snapDataDir, "foo/7/somedata"), + filepath.Join("/home/user1", snapDataDir, "foo/common"), } var data []byte var expected int @@ -1094,16 +1107,16 @@ data = append(data, 0) expected += len(data) c.Assert(os.MkdirAll(filepath.Join(s.root, d), 0755), check.IsNil) - c.Assert(ioutil.WriteFile(filepath.Join(s.root, d, "somfile"), data, 0644), check.IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(s.root, d, "somefile"), data, 0644), check.IsNil) } - sz, err := backend.EstimateSnapshotSize(info, nil) + sz, err := backend.EstimateSnapshotSize(info, nil, opts) c.Assert(err, check.IsNil) c.Check(sz, check.Equals, uint64(expected)) } func (s *snapshotSuite) TestEstimateSnapshotSizeEmpty(c *check.C) { - restore := backend.MockUsersForUsernames(func(usernames []string) ([]*user.User, error) { + restore := backend.MockUsersForUsernames(func(usernames []string, _ *dirs.SnapDirOptions) ([]*user.User, error) { return []*user.User{{HomeDir: filepath.Join(s.root, "home/user1")}}, nil }) defer restore() @@ -1125,14 +1138,14 @@ c.Assert(os.MkdirAll(filepath.Join(s.root, d), 0755), check.IsNil) } - sz, err := backend.EstimateSnapshotSize(info, nil) + sz, err := backend.EstimateSnapshotSize(info, nil, nil) c.Assert(err, check.IsNil) c.Check(sz, check.Equals, uint64(0)) } func (s *snapshotSuite) TestEstimateSnapshotPassesUsernames(c *check.C) { var gotUsernames []string - restore := backend.MockUsersForUsernames(func(usernames []string) ([]*user.User, error) { + restore := backend.MockUsersForUsernames(func(usernames []string, _ *dirs.SnapDirOptions) ([]*user.User, error) { gotUsernames = usernames return nil, nil }) @@ -1145,13 +1158,13 @@ }, } - _, err := backend.EstimateSnapshotSize(info, []string{"user1", "user2"}) + _, err := backend.EstimateSnapshotSize(info, []string{"user1", "user2"}, nil) c.Assert(err, check.IsNil) c.Check(gotUsernames, check.DeepEquals, []string{"user1", "user2"}) } func (s *snapshotSuite) TestEstimateSnapshotSizeNotDataDirs(c *check.C) { - restore := backend.MockUsersForUsernames(func(usernames []string) ([]*user.User, error) { + restore := backend.MockUsersForUsernames(func(usernames []string, _ *dirs.SnapDirOptions) ([]*user.User, error) { return []*user.User{{HomeDir: filepath.Join(s.root, "home/user1")}}, nil }) defer restore() @@ -1161,10 +1174,11 @@ SideInfo: snap.SideInfo{Revision: snap.R(7)}, } - sz, err := backend.EstimateSnapshotSize(info, nil) + sz, err := backend.EstimateSnapshotSize(info, nil, nil) c.Assert(err, check.IsNil) c.Check(sz, check.Equals, uint64(0)) } + func (s *snapshotSuite) TestExportTwice(c *check.C) { // use mocking done in snapshotSuite.SetUpTest info := &snap.Info{ @@ -1177,7 +1191,7 @@ } // create a snapshot shID := uint64(12) - _, err := backend.Save(context.TODO(), shID, info, nil, []string{"snapuser"}) + _, err := backend.Save(context.TODO(), shID, info, nil, []string{"snapuser"}, nil) c.Check(err, check.IsNil) // content.json + num_files + export.json + footer @@ -1509,7 +1523,7 @@ Version: "v1.33", } shID := uint64(12) - shw, err := backend.Save(ctx, shID, info, nil, []string{"snapuser"}) + shw, err := backend.Save(ctx, shID, info, nil, []string{"snapuser"}, nil) c.Check(err, check.IsNil) // now export it @@ -1531,7 +1545,7 @@ }, Version: "v1.33", } - shw, err = backend.Save(ctx, shID, info, nil, []string{"snapuser"}) + shw, err = backend.Save(ctx, shID, info, nil, []string{"snapuser"}, nil) c.Check(err, check.IsNil) export3, err := backend.NewSnapshotExport(ctx, shw.SetID) diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapshotstate/backend/export_test.go snapd-2.54.2+21.10/overlord/snapshotstate/backend/export_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapshotstate/backend/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapshotstate/backend/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -25,6 +25,7 @@ "time" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil/sys" "github.com/snapcore/snapd/snap" ) @@ -47,14 +48,6 @@ } } -func MockUserLookupId(newLookupId func(string) (*user.User, error)) func() { - oldLookupId := userLookupId - userLookupId = newLookupId - return func() { - userLookupId = oldLookupId - } -} - func MockOsOpen(newOsOpen func(string) (*os.File, error)) func() { oldOsOpen := osOpen osOpen = newOsOpen @@ -103,7 +96,7 @@ } } -func MockUsersForUsernames(f func(usernames []string) ([]*user.User, error)) (restore func()) { +func MockUsersForUsernames(f func(usernames []string, opts *dirs.SnapDirOptions) ([]*user.User, error)) (restore func()) { old := usersForUsernames usersForUsernames = f return func() { diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapshotstate/backend/helpers.go snapd-2.54.2+21.10/overlord/snapshotstate/backend/helpers.go --- snapd-2.53+21.10ubuntu1/overlord/snapshotstate/backend/helpers.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapshotstate/backend/helpers.go 2022-01-06 21:25:16.000000000 +0000 @@ -27,14 +27,13 @@ "os/exec" "os/user" "path/filepath" - "strconv" "strings" - "syscall" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil/sys" + "github.com/snapcore/snapd/snap" ) func zipMember(f *os.File, member string) (r io.ReadCloser, sz int64, err error) { @@ -94,9 +93,9 @@ userLookupId = user.LookupId ) -func usersForUsernamesImpl(usernames []string) ([]*user.User, error) { +func usersForUsernamesImpl(usernames []string, opts *dirs.SnapDirOptions) ([]*user.User, error) { if len(usernames) == 0 { - return allUsers() + return snap.AllUsers(opts) } users := make([]*user.User, 0, len(usernames)) for _, username := range usernames { @@ -132,59 +131,6 @@ return users, nil } -func allUsers() ([]*user.User, error) { - ds, err := filepath.Glob(dirs.SnapDataHomeGlob) - if err != nil { - // can't happen? - return nil, err - } - - users := make([]*user.User, 1, len(ds)+1) - root, err := user.LookupId("0") - if err != nil { - return nil, err - } - users[0] = root - seen := make(map[uint32]bool, len(ds)+1) - seen[0] = true - var st syscall.Stat_t - for _, d := range ds { - err := syscall.Stat(d, &st) - if err != nil { - continue - } - if seen[st.Uid] { - continue - } - seen[st.Uid] = true - usr, err := userLookupId(strconv.FormatUint(uint64(st.Uid), 10)) - if err != nil { - // Treat all non-nil errors as user.Unknown{User,Group}Error's, as - // currently Go's handling of returned errno from get{pw,gr}nam_r - // in the cgo implementation of user.Lookup is lacking, and thus - // user.Unknown{User,Group}Error is returned only when errno is 0 - // and the list of users/groups is empty, but as per the man page - // for get{pw,gr}nam_r, there are many other errno's that typical - // systems could return to indicate that the user/group wasn't - // found, however unfortunately the POSIX standard does not actually - // dictate what errno should be used to indicate "user/group not - // found", and so even if Go is more robust, it may not ever be - // fully robust. See from the man page: - // - // > It [POSIX.1-2001] does not call "not found" an error, hence - // > does not specify what value errno might have in this situation. - // > But that makes it impossible to recognize errors. - // - // See upstream Go issue: https://github.com/golang/go/issues/40334 - continue - } else { - users = append(users, usr) - } - } - - return users, nil -} - var ( sysGeteuid = sys.Geteuid execLookPath = exec.LookPath diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapshotstate/backend/reader.go snapd-2.54.2+21.10/overlord/snapshotstate/backend/reader.go --- snapd-2.53+21.10ubuntu1/overlord/snapshotstate/backend/reader.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapshotstate/backend/reader.go 2022-01-06 21:25:16.000000000 +0000 @@ -34,6 +34,7 @@ "syscall" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/jsonutil" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" @@ -196,7 +197,7 @@ // If successful this will replace the existing data (for the given revision, // or the one in the snapshot) with that contained in the snapshot. It keeps // track of the old data in the task so it can be undone (or cleaned up). -func (r *Reader) Restore(ctx context.Context, current snap.Revision, usernames []string, logf Logf) (rs *RestoreState, e error) { +func (r *Reader) Restore(ctx context.Context, current snap.Revision, usernames []string, logf Logf, opts *dirs.SnapDirOptions) (rs *RestoreState, e error) { rs = &RestoreState{} defer func() { if e != nil { @@ -247,7 +248,7 @@ continue } - dest = si.UserDataDir(usr.HomeDir) + dest = si.UserDataDir(usr.HomeDir, opts) fi, err := os.Stat(usr.HomeDir) if err != nil { if osutil.IsDirNotExist(err) { diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapshotstate/export_test.go snapd-2.54.2+21.10/overlord/snapshotstate/export_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapshotstate/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapshotstate/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -26,6 +26,7 @@ "time" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/overlord/snapshotstate/backend" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" @@ -122,7 +123,7 @@ } } -func MockBackendRestore(f func(*backend.Reader, context.Context, snap.Revision, []string, backend.Logf) (*backend.RestoreState, error)) (restore func()) { +func MockBackendRestore(f func(*backend.Reader, context.Context, snap.Revision, []string, backend.Logf, *dirs.SnapDirOptions) (*backend.RestoreState, error)) (restore func()) { old := backendRestore backendRestore = f return func() { @@ -170,7 +171,7 @@ } } -func MockBackendEstimateSnapshotSize(f func(*snap.Info, []string) (uint64, error)) (restore func()) { +func MockBackendEstimateSnapshotSize(f func(*snap.Info, []string, *dirs.SnapDirOptions) (uint64, error)) (restore func()) { old := backendEstimateSnapshotSize backendEstimateSnapshotSize = f return func() { diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapshotstate/snapshotmgr.go snapd-2.54.2+21.10/overlord/snapshotstate/snapshotmgr.go --- snapd-2.53+21.10ubuntu1/overlord/snapshotstate/snapshotmgr.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapshotstate/snapshotmgr.go 2022-01-06 21:25:16.000000000 +0000 @@ -29,6 +29,7 @@ "gopkg.in/tomb.v2" "github.com/snapcore/snapd/client" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/snapshotstate/backend" @@ -220,9 +221,17 @@ if err != nil { return err } - _, err = backendSave(tomb.Context(nil), snapshot.SetID, cur, cfg, snapshot.Users) + st := task.State() + + st.Lock() + opts, err := snapstate.GetSnapDirOptions(st) + st.Unlock() + if err != nil { + return err + } + + _, err = backendSave(tomb.Context(nil), snapshot.SetID, cur, cfg, snapshot.Users, opts) if err != nil { - st := task.State() st.Lock() defer st.Unlock() removeSnapshotState(st, snapshot.SetID) @@ -299,7 +308,14 @@ task.Logf(format, args...) } - restoreState, err := backendRestore(reader, tomb.Context(nil), snapshot.Current, snapshot.Users, logf) + st.Lock() + opts, err := snapstate.GetSnapDirOptions(st) + st.Unlock() + if err != nil { + return err + } + + restoreState, err := backendRestore(reader, tomb.Context(nil), snapshot.Current, snapshot.Users, logf, opts) if err != nil { return err } @@ -432,7 +448,7 @@ snapstate.EstimateSnapshotSize = EstimateSnapshotSize } -func MockBackendSave(f func(context.Context, uint64, *snap.Info, map[string]interface{}, []string) (*client.Snapshot, error)) (restore func()) { +func MockBackendSave(f func(context.Context, uint64, *snap.Info, map[string]interface{}, []string, *dirs.SnapDirOptions) (*client.Snapshot, error)) (restore func()) { old := backendSave backendSave = f return func() { diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapshotstate/snapshotmgr_test.go snapd-2.54.2+21.10/overlord/snapshotstate/snapshotmgr_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapshotstate/snapshotmgr_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapshotstate/snapshotmgr_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -37,6 +37,7 @@ "github.com/snapcore/snapd/overlord" "github.com/snapcore/snapd/overlord/snapshotstate" "github.com/snapcore/snapd/overlord/snapshotstate/backend" + "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" "github.com/snapcore/snapd/testutil" @@ -278,7 +279,7 @@ buf := json.RawMessage(`{"hello": "there"}`) return &buf, nil })() - defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string) (*client.Snapshot, error) { + defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string, _ *dirs.SnapDirOptions) (*client.Snapshot, error) { c.Check(id, check.Equals, uint64(42)) c.Check(si, check.DeepEquals, &snapInfo) c.Check(cfg, check.DeepEquals, map[string]interface{}{"hello": "there"}) @@ -299,12 +300,50 @@ c.Assert(err, check.IsNil) } +func (snapshotSuite) TestDoSaveGetsSnapDirOpts(c *check.C) { + restore := snapstate.MockGetSnapDirOptions(func(*state.State) (*dirs.SnapDirOptions, error) { + return &dirs.SnapDirOptions{HiddenSnapDataDir: true}, nil + }) + defer restore() + + snapInfo := snap.Info{ + SideInfo: snap.SideInfo{ + RealName: "a-snap", + Revision: snap.R(-1), + }, + Version: "1.33", + } + defer snapshotstate.MockSnapstateCurrentInfo(func(_ *state.State, snapname string) (*snap.Info, error) { + c.Check(snapname, check.Equals, "a-snap") + return &snapInfo, nil + })() + + var checkOpts bool + defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string, opts *dirs.SnapDirOptions) (*client.Snapshot, error) { + c.Check(opts.HiddenSnapDataDir, check.Equals, true) + checkOpts = true + return nil, nil + })() + + st := state.New(nil) + st.Lock() + task := st.NewTask("save-snapshot", "...") + task.Set("snapshot-setup", map[string]interface{}{ + "snap": "a-snap", + }) + st.Unlock() + + err := snapshotstate.DoSave(task, &tomb.Tomb{}) + c.Assert(err, check.IsNil) + c.Check(checkOpts, check.Equals, true) +} + func (snapshotSuite) TestDoSaveFailsWithNoSnap(c *check.C) { defer snapshotstate.MockSnapstateCurrentInfo(func(*state.State, string) (*snap.Info, error) { return nil, errors.New("bzzt") })() defer snapshotstate.MockConfigGetSnapConfig(func(*state.State, string) (*json.RawMessage, error) { return nil, nil })() - defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string) (*client.Snapshot, error) { + defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string, options *dirs.SnapDirOptions) (*client.Snapshot, error) { return nil, nil })() @@ -331,7 +370,7 @@ } defer snapshotstate.MockSnapstateCurrentInfo(func(*state.State, string) (*snap.Info, error) { return &snapInfo, nil })() defer snapshotstate.MockConfigGetSnapConfig(func(*state.State, string) (*json.RawMessage, error) { return nil, nil })() - defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string) (*client.Snapshot, error) { + defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string, options *dirs.SnapDirOptions) (*client.Snapshot, error) { return nil, nil })() @@ -355,7 +394,7 @@ } defer snapshotstate.MockSnapstateCurrentInfo(func(*state.State, string) (*snap.Info, error) { return &snapInfo, nil })() defer snapshotstate.MockConfigGetSnapConfig(func(*state.State, string) (*json.RawMessage, error) { return nil, nil })() - defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string) (*client.Snapshot, error) { + defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string, options *dirs.SnapDirOptions) (*client.Snapshot, error) { return nil, errors.New("bzzt") })() @@ -384,7 +423,7 @@ defer snapshotstate.MockConfigGetSnapConfig(func(*state.State, string) (*json.RawMessage, error) { return nil, errors.New("bzzt") })() - defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string) (*client.Snapshot, error) { + defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string, options *dirs.SnapDirOptions) (*client.Snapshot, error) { return nil, nil })() @@ -415,7 +454,7 @@ buf := json.RawMessage(`"hello-there"`) return &buf, nil })() - defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string) (*client.Snapshot, error) { + defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string, options *dirs.SnapDirOptions) (*client.Snapshot, error) { return nil, nil })() @@ -448,7 +487,7 @@ defer snapshotstate.MockConfigGetSnapConfig(func(_ *state.State, snapname string) (*json.RawMessage, error) { return nil, nil })() - defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string) (*client.Snapshot, error) { + defer snapshotstate.MockBackendSave(func(_ context.Context, id uint64, si *snap.Info, cfg map[string]interface{}, usernames []string, options *dirs.SnapDirOptions) (*client.Snapshot, error) { var expirations map[uint64]interface{} st.Lock() defer st.Unlock() @@ -518,7 +557,7 @@ rs.calls = append(rs.calls, "open") return &backend.Reader{}, nil }), - snapshotstate.MockBackendRestore(func(*backend.Reader, context.Context, snap.Revision, []string, backend.Logf) (*backend.RestoreState, error) { + snapshotstate.MockBackendRestore(func(*backend.Reader, context.Context, snap.Revision, []string, backend.Logf, *dirs.SnapDirOptions) (*backend.RestoreState, error) { rs.calls = append(rs.calls, "restore") return &backend.RestoreState{}, nil }), @@ -557,7 +596,7 @@ Snapshot: client.Snapshot{Conf: map[string]interface{}{"hello": "there"}}, }, nil })() - defer snapshotstate.MockBackendRestore(func(_ *backend.Reader, _ context.Context, _ snap.Revision, users []string, _ backend.Logf) (*backend.RestoreState, error) { + defer snapshotstate.MockBackendRestore(func(_ *backend.Reader, _ context.Context, _ snap.Revision, users []string, _ backend.Logf, options *dirs.SnapDirOptions) (*backend.RestoreState, error) { rs.calls = append(rs.calls, "restore") c.Check(users, check.DeepEquals, []string{"a-user", "b-user"}) return &backend.RestoreState{}, nil @@ -599,7 +638,7 @@ Snapshot: client.Snapshot{Snap: "a-snap", Conf: nil}, }, nil })() - defer snapshotstate.MockBackendRestore(func(_ *backend.Reader, _ context.Context, _ snap.Revision, users []string, _ backend.Logf) (*backend.RestoreState, error) { + defer snapshotstate.MockBackendRestore(func(_ *backend.Reader, _ context.Context, _ snap.Revision, users []string, _ backend.Logf, options *dirs.SnapDirOptions) (*backend.RestoreState, error) { rs.calls = append(rs.calls, "restore") c.Check(users, check.DeepEquals, []string{"a-user", "b-user"}) return &backend.RestoreState{}, nil @@ -682,7 +721,7 @@ } func (rs *readerSuite) TestDoRestoreFailsOnRestoreError(c *check.C) { - defer snapshotstate.MockBackendRestore(func(*backend.Reader, context.Context, snap.Revision, []string, backend.Logf) (*backend.RestoreState, error) { + defer snapshotstate.MockBackendRestore(func(*backend.Reader, context.Context, snap.Revision, []string, backend.Logf, *dirs.SnapDirOptions) (*backend.RestoreState, error) { rs.calls = append(rs.calls, "restore") return nil, errors.New("bzzt") })() diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapshotstate/snapshotstate.go snapd-2.54.2+21.10/overlord/snapshotstate/snapshotstate.go --- snapd-2.53+21.10ubuntu1/overlord/snapshotstate/snapshotstate.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapshotstate/snapshotstate.go 2022-01-06 21:25:16.000000000 +0000 @@ -109,7 +109,13 @@ if err != nil { return 0, err } - sz, err := backendEstimateSnapshotSize(cur, users) + + opts, err := snapstate.GetSnapDirOptions(st) + if err != nil { + return 0, err + } + + sz, err := backendEstimateSnapshotSize(cur, users, opts) if err != nil { return 0, err } diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapshotstate/snapshotstate_test.go snapd-2.54.2+21.10/overlord/snapshotstate/snapshotstate_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapshotstate/snapshotstate_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapshotstate/snapshotstate_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1150,6 +1150,21 @@ } func (snapshotSuite) TestRestoreIntegration(c *check.C) { + testRestoreIntegration(c, dirs.UserHomeSnapDir, nil) +} + +func (snapshotSuite) TestRestoreIntegrationHiddenSnapDir(c *check.C) { + opts := &dirs.SnapDirOptions{HiddenSnapDataDir: true} + + restore := snapstate.MockGetSnapDirOptions(func(*state.State) (*dirs.SnapDirOptions, error) { + return opts, nil + }) + defer restore() + + testRestoreIntegration(c, dirs.HiddenSnapDataHomeDir, opts) +} + +func testRestoreIntegration(c *check.C, snapDataDir string, opts *dirs.SnapDirOptions) { if os.Geteuid() == 0 { c.Skip("this test cannot run as root (runuser will fail)") } @@ -1194,16 +1209,16 @@ snapInfo := snaptest.MockSnap(c, fmt.Sprintf("{name: %s, version: v1}", name), sideInfo) for _, home := range []string{homedirA, homedirB} { - c.Assert(os.MkdirAll(filepath.Join(home, "snap", name, fmt.Sprint(i+1), "canary-"+name), 0755), check.IsNil) - c.Assert(os.MkdirAll(filepath.Join(home, "snap", name, "common", "common-"+name), 0755), check.IsNil) + c.Assert(os.MkdirAll(filepath.Join(home, snapDataDir, name, fmt.Sprint(i+1), "canary-"+name), 0755), check.IsNil) + c.Assert(os.MkdirAll(filepath.Join(home, snapDataDir, name, "common", "common-"+name), 0755), check.IsNil) } - _, err := backend.Save(context.TODO(), 42, snapInfo, nil, []string{"a-user", "b-user"}) + _, err := backend.Save(context.TODO(), 42, snapInfo, nil, []string{"a-user", "b-user"}, opts) c.Assert(err, check.IsNil) } // move the old away - c.Assert(os.Rename(filepath.Join(homedirA, "snap"), filepath.Join(homedirA, "snap.old")), check.IsNil) + c.Assert(os.Rename(filepath.Join(homedirA, snapDataDir), filepath.Join(homedirA, "snap.old")), check.IsNil) // remove b-user's home c.Assert(os.RemoveAll(homedirB), check.IsNil) @@ -1227,8 +1242,8 @@ } // check it was all brought back \o/ - out, err := exec.Command("diff", "-rN", filepath.Join(homedirA, "snap"), filepath.Join("snap.old")).CombinedOutput() - c.Assert(err, check.IsNil) + out, err := exec.Command("diff", "-rN", filepath.Join(homedirA, snapDataDir), filepath.Join("snap.old")).CombinedOutput() + c.Check(err, check.IsNil) c.Check(string(out), check.Equals, "") } @@ -1275,7 +1290,7 @@ c.Assert(os.MkdirAll(filepath.Join(homedir, "snap", name, fmt.Sprint(i+1), "canary-"+name), 0755), check.IsNil) c.Assert(os.MkdirAll(filepath.Join(homedir, "snap", name, "common", "common-"+name), 0755), check.IsNil) - _, err := backend.Save(context.TODO(), 42, snapInfo, nil, []string{"a-user"}) + _, err := backend.Save(context.TODO(), 42, snapInfo, nil, []string{"a-user"}, nil) c.Assert(err, check.IsNil) } @@ -1847,7 +1862,7 @@ Current: sideInfo.Revision, }) - defer snapshotstate.MockBackendEstimateSnapshotSize(func(info *snap.Info, users []string) (uint64, error) { + defer snapshotstate.MockBackendEstimateSnapshotSize(func(*snap.Info, []string, *dirs.SnapDirOptions) (uint64, error) { return 123, nil })() @@ -1868,7 +1883,7 @@ Current: sideInfo.Revision, }) - defer snapshotstate.MockBackendEstimateSnapshotSize(func(info *snap.Info, users []string) (uint64, error) { + defer snapshotstate.MockBackendEstimateSnapshotSize(func(*snap.Info, []string, *dirs.SnapDirOptions) (uint64, error) { return 100, nil })() @@ -1896,7 +1911,7 @@ Current: sideInfo.Revision, }) - defer snapshotstate.MockBackendEstimateSnapshotSize(func(info *snap.Info, users []string) (uint64, error) { + defer snapshotstate.MockBackendEstimateSnapshotSize(func(*snap.Info, []string, *dirs.SnapDirOptions) (uint64, error) { return 0, fmt.Errorf("an error") })() @@ -1917,7 +1932,7 @@ }) var gotUsers []string - defer snapshotstate.MockBackendEstimateSnapshotSize(func(info *snap.Info, users []string) (uint64, error) { + defer snapshotstate.MockBackendEstimateSnapshotSize(func(info *snap.Info, users []string, opts *dirs.SnapDirOptions) (uint64, error) { gotUsers = users return 0, nil })() diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/aliasesv2_test.go snapd-2.54.2+21.10/overlord/snapstate/aliasesv2_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/aliasesv2_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/aliasesv2_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -576,10 +576,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) expected := fakeOps{ @@ -635,10 +633,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) expected := fakeOps{ @@ -755,10 +751,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) expected := fakeOps{ @@ -958,10 +952,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) expected := fakeOps{ @@ -1030,10 +1022,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) expected := fakeOps{ @@ -1127,10 +1117,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) expected := fakeOps{ @@ -1179,10 +1167,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) expected := fakeOps{ @@ -1228,10 +1214,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) expected := fakeOps{ @@ -1396,10 +1380,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) expected := fakeOps{ @@ -1460,10 +1442,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("%v", chg.Err())) expected := fakeOps{ diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/autorefresh_gating.go snapd-2.54.2+21.10/overlord/snapstate/autorefresh_gating.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/autorefresh_gating.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/autorefresh_gating.go 2022-01-06 21:25:16.000000000 +0000 @@ -144,16 +144,21 @@ // HoldRefresh marks affectingSnaps as held for refresh for up to holdTime. // HoldTime of zero denotes maximum allowed hold time. -// Holding may fail for only some snaps in which case HoldError is returned and -// it contains the details of failed ones. -func HoldRefresh(st *state.State, gatingSnap string, holdDuration time.Duration, affectingSnaps ...string) error { +// Holding fails if not all snaps can be held, in that case HoldError is returned +// and it contains the details of snaps that prevented holding. On success the +// function returns the remaining hold time. The remaining hold time is the +// minimum of the remaining hold time for all affecting snaps. +func HoldRefresh(st *state.State, gatingSnap string, holdDuration time.Duration, affectingSnaps ...string) (time.Duration, error) { gating, err := refreshGating(st) if err != nil { - return err + return 0, err } herr := &HoldError{ SnapsInError: make(map[string]HoldDurationError), } + + var durationMin time.Duration + now := timeNow() for _, heldSnap := range affectingSnaps { hold, ok := gating[heldSnap][gatingSnap] @@ -165,7 +170,7 @@ lastRefreshTime, err := lastRefreshed(st, heldSnap) if err != nil { - return err + return 0, err } mp := maxPostponement - maxPostponementBuffer @@ -213,6 +218,11 @@ gating[heldSnap] = make(map[string]*holdState) } gating[heldSnap][gatingSnap] = hold + + // note, left is guaranteed to be > 0 at this point + if durationMin == 0 || left < durationMin { + durationMin = left + } } if len(herr.SnapsInError) > 0 { @@ -228,9 +238,9 @@ } st.Set("snaps-hold", gating) if len(herr.SnapsInError) > 0 { - return herr + return 0, herr } - return nil + return durationMin, nil } // ProceedWithRefresh unblocks all snaps held by gatingSnap for refresh. This diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/autorefresh_gating_test.go snapd-2.54.2+21.10/overlord/snapstate/autorefresh_gating_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/autorefresh_gating_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/autorefresh_gating_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -353,12 +353,16 @@ mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-a", "snap-b", "snap-c", "snap-d", "snap-e", "snap-f") - c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil) + _, err := snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c") + c.Assert(err, IsNil) // this could be merged with the above HoldRefresh call, but it's fine if // done separately too. - c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-e"), IsNil) - c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-e"), IsNil) - c.Assert(snapstate.HoldRefresh(st, "snap-f", 0, "snap-f"), IsNil) + _, err = snapstate.HoldRefresh(st, "snap-a", 0, "snap-e") + c.Assert(err, IsNil) + _, err = snapstate.HoldRefresh(st, "snap-d", 0, "snap-e") + c.Assert(err, IsNil) + _, err = snapstate.HoldRefresh(st, "snap-f", 0, "snap-f") + c.Assert(err, IsNil) var gating map[string]map[string]*snapstate.HoldState c.Assert(st.Get("snaps-hold", &gating), IsNil) @@ -381,6 +385,65 @@ }) } +func (s *autorefreshGatingSuite) TestHoldRefreshReturnsMinimumHoldTime(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + now := "2021-05-10T10:00:00Z" + restore := snapstate.MockTimeNow(func() time.Time { + t, err := time.Parse(time.RFC3339, now) + c.Assert(err, IsNil) + return t + }) + defer restore() + + mockInstalledSnap(c, st, snapAyaml, false) + mockInstalledSnap(c, st, snapByaml, false) + mockInstalledSnap(c, st, snapCyaml, false) + mockInstalledSnap(c, st, snapDyaml, false) + mockInstalledSnap(c, st, snapEyaml, false) + mockInstalledSnap(c, st, snapFyaml, false) + + mockLastRefreshed(c, st, "2021-05-09T10:00:00Z", "snap-a", "snap-b", "snap-c", "snap-d", "snap-e", "snap-f") + + // only holding self: max postponement - buffer time returned + rem, err := snapstate.HoldRefresh(st, "snap-a", 0, "snap-a") + c.Assert(err, IsNil) + c.Check(rem.String(), Equals, "2136h0m0s") + + // holding self and some other snaps, max hold time of holding other snaps returned. + rem, err = snapstate.HoldRefresh(st, "snap-a", 0, "snap-a", "snap-b", "snap-c", "snap-e") + c.Assert(err, IsNil) + c.Check(rem.String(), Equals, "48h0m0s") + + // advance time + now = "2021-05-11T12:00:00Z" + rem, err = snapstate.HoldRefresh(st, "snap-a", 0, "snap-a", "snap-b", "snap-c", "snap-e") + c.Assert(err, IsNil) + // it's now less due to previous hold + c.Check(rem.String(), Equals, "22h0m0s") + + var gating map[string]map[string]*snapstate.HoldState + c.Assert(st.Get("snaps-hold", &gating), IsNil) + c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ + "snap-b": { + // holding of other snaps for maxOtherHoldDuration (48h) + "snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), + }, + "snap-c": { + "snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), + }, + "snap-e": { + "snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-05-12T10:00:00Z"), + }, + "snap-a": { + // holding self set for maxPostponement minus 1 day due to last refresh. + "snap-a": snapstate.MockHoldState("2021-05-10T10:00:00Z", "2021-08-07T10:00:00Z"), + }, + }) +} + func (s *autorefreshGatingSuite) TestHoldRefreshHelperMultipleTimes(c *C) { st := s.state st.Lock() @@ -402,7 +465,9 @@ // hold it for just a bit (10h) initially hold := time.Hour * 10 - c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) + rem, err := snapstate.HoldRefresh(st, "snap-b", hold, "snap-a") + c.Assert(err, IsNil) + c.Check(rem.String(), Equals, "48h0m0s") var gating map[string]map[string]*snapstate.HoldState c.Assert(st.Get("snaps-hold", &gating), IsNil) c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ @@ -413,7 +478,9 @@ // holding for a shorter time is fine too hold = time.Hour * 5 - c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) + rem, err = snapstate.HoldRefresh(st, "snap-b", hold, "snap-a") + c.Assert(err, IsNil) + c.Check(rem.String(), Equals, "48h0m0s") c.Assert(st.Get("snaps-hold", &gating), IsNil) c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ "snap-a": { @@ -428,7 +495,9 @@ // default hold time requested hold = 0 - c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) + rem, err = snapstate.HoldRefresh(st, "snap-b", hold, "snap-a") + c.Assert(err, IsNil) + c.Check(rem.String(), Equals, "26h0m0s") c.Assert(st.Get("snaps-hold", &gating), IsNil) c.Check(gating, DeepEquals, map[string]map[string]*snapstate.HoldState{ "snap-a": { @@ -459,7 +528,9 @@ // request default hold time var hold time.Duration - c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) + rem, err := snapstate.HoldRefresh(st, "snap-b", hold, "snap-a") + c.Assert(err, IsNil) + c.Check(rem.String(), Equals, "24h0m0s") var gating map[string]map[string]*snapstate.HoldState c.Assert(st.Get("snaps-hold", &gating), IsNil) @@ -485,11 +556,13 @@ hold := time.Hour * 24 * 3 // holding self for 3 days - c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-a"), IsNil) + _, err := snapstate.HoldRefresh(st, "snap-a", hold, "snap-a") + c.Assert(err, IsNil) // snap-b holds snap-a for 1 day hold = time.Hour * 24 - c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-a"), IsNil) + _, err = snapstate.HoldRefresh(st, "snap-b", hold, "snap-a") + c.Assert(err, IsNil) var gating map[string]map[string]*snapstate.HoldState c.Assert(st.Get("snaps-hold", &gating), IsNil) @@ -521,11 +594,12 @@ // holding itself hold := time.Hour * 24 * 96 - c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-a"), ErrorMatches, `cannot hold some snaps:\n - requested holding duration for snap "snap-a" of 2304h0m0s by snap "snap-a" exceeds maximum holding time`) + _, err := snapstate.HoldRefresh(st, "snap-a", hold, "snap-a") + c.Assert(err, ErrorMatches, `cannot hold some snaps:\n - requested holding duration for snap "snap-a" of 2304h0m0s by snap "snap-a" exceeds maximum holding time`) // holding other snap hold = time.Hour * 49 - err := snapstate.HoldRefresh(st, "snap-a", hold, "snap-b") + _, err = snapstate.HoldRefresh(st, "snap-a", hold, "snap-b") c.Check(err, ErrorMatches, `cannot hold some snaps:\n - requested holding duration for snap "snap-b" of 49h0m0s by snap "snap-a" exceeds maximum holding time`) herr, ok := err.(*snapstate.HoldError) c.Assert(ok, Equals, true) @@ -538,17 +612,21 @@ // hold for maximum allowed for other snaps hold = time.Hour * 48 - c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-b"), IsNil) + _, err = snapstate.HoldRefresh(st, "snap-a", hold, "snap-b") + c.Assert(err, IsNil) // 2 days passed since it was first held now = "2021-05-12T10:00:00Z" hold = time.Minute * 2 - c.Assert(snapstate.HoldRefresh(st, "snap-a", hold, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-a" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`) + _, err = snapstate.HoldRefresh(st, "snap-a", hold, "snap-b") + c.Assert(err, ErrorMatches, `cannot hold some snaps:\n - snap "snap-a" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`) // refreshed long time ago (> maxPostponement) mockLastRefreshed(c, st, "2021-01-01T10:00:00Z", "snap-b") hold = time.Hour * 2 - c.Assert(snapstate.HoldRefresh(st, "snap-b", hold, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`) - c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "snap-b"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`) + _, err = snapstate.HoldRefresh(st, "snap-b", hold, "snap-b") + c.Assert(err, ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`) + _, err = snapstate.HoldRefresh(st, "snap-b", 0, "snap-b") + c.Assert(err, ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "snap-b" anymore, maximum refresh postponement exceeded`) } func (s *autorefreshGatingSuite) TestHoldAndProceedWithRefreshHelper(c *C) { @@ -575,10 +653,13 @@ c.Assert(err, IsNil) c.Check(held, IsNil) - c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil) - c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-c"), IsNil) + _, err = snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c") + c.Assert(err, IsNil) + _, err = snapstate.HoldRefresh(st, "snap-d", 0, "snap-c") + c.Assert(err, IsNil) // holding self - c.Assert(snapstate.HoldRefresh(st, "snap-d", time.Hour*24*4, "snap-d"), IsNil) + _, err = snapstate.HoldRefresh(st, "snap-d", time.Hour*24*4, "snap-d") + c.Assert(err, IsNil) held, err = snapstate.HeldSnaps(st) c.Assert(err, IsNil) @@ -622,16 +703,20 @@ defer restore() // snap-b, base-snap-b get refreshed and affect snap-b (gating snap) - c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "snap-b", "base-snap-b"), IsNil) + _, err := snapstate.HoldRefresh(st, "snap-b", 0, "snap-b", "base-snap-b") + c.Assert(err, IsNil) // unrealted snap-d gets refreshed and holds itself - c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d"), IsNil) + _, err = snapstate.HoldRefresh(st, "snap-d", 0, "snap-d") + c.Assert(err, IsNil) // advance time by 49h now = "2021-05-03T11:00:00Z" // snap-b, base-snap-b and snap-c get refreshed and snap-a (gating snap) wants to hold them - c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "snap-b", "base-snap-b", "snap-c"), ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "base-snap-b" anymore, maximum refresh postponement exceeded`) + _, err = snapstate.HoldRefresh(st, "snap-b", 0, "snap-b", "base-snap-b", "snap-c") + c.Assert(err, ErrorMatches, `cannot hold some snaps:\n - snap "snap-b" cannot hold snap "base-snap-b" anymore, maximum refresh postponement exceeded`) // snap-bb (gating snap) wants to hold base-snap-b as well and succeeds since it didn't exceed its holding time yet - c.Assert(snapstate.HoldRefresh(st, "snap-bb", 0, "base-snap-b"), IsNil) + _, err = snapstate.HoldRefresh(st, "snap-bb", 0, "base-snap-b") + c.Assert(err, IsNil) held, err := snapstate.HeldSnaps(st) c.Assert(err, IsNil) @@ -660,8 +745,10 @@ mockInstalledSnap(c, st, snapCyaml, false) mockInstalledSnap(c, st, snapDyaml, false) - c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil) - c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil) + _, err := snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c") + c.Assert(err, IsNil) + _, err = snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c") + c.Assert(err, IsNil) // sanity held, err := snapstate.HeldSnaps(st) c.Assert(err, IsNil) @@ -731,8 +818,10 @@ mockInstalledSnap(c, st, snapCyaml, false) mockInstalledSnap(c, st, snapDyaml, false) - c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c"), IsNil) - c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c"), IsNil) + _, err := snapstate.HoldRefresh(st, "snap-a", 0, "snap-b", "snap-c") + c.Assert(err, IsNil) + _, err = snapstate.HoldRefresh(st, "snap-d", 0, "snap-d", "snap-c") + c.Assert(err, IsNil) c.Assert(snapstate.ResetGatingForRefreshed(st, "snap-b", "snap-c"), IsNil) var gating map[string]map[string]*snapstate.HoldState @@ -760,9 +849,11 @@ mockInstalledSnap(c, st, snapDyaml, false) // snap-a is holding itself and 3 other snaps - c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a", "snap-b", "snap-c", "snap-d"), IsNil) + _, err := snapstate.HoldRefresh(st, "snap-a", 0, "snap-a", "snap-b", "snap-c", "snap-d") + c.Assert(err, IsNil) // in addition, snap-c is held by snap-d. - c.Assert(snapstate.HoldRefresh(st, "snap-d", 0, "snap-c"), IsNil) + _, err = snapstate.HoldRefresh(st, "snap-d", 0, "snap-c") + c.Assert(err, IsNil) // sanity check held, err := snapstate.HeldSnaps(st) @@ -1283,7 +1374,8 @@ defer restore() // pretend some snaps are held - c.Assert(snapstate.HoldRefresh(st, "gating-snap", 0, "snap-a", "snap-d"), IsNil) + _, err := snapstate.HoldRefresh(st, "gating-snap", 0, "snap-a", "snap-d") + c.Assert(err, IsNil) // sanity check heldSnaps, err := snapstate.HeldSnaps(st) c.Assert(err, IsNil) @@ -1612,6 +1704,11 @@ st.Lock() defer st.Unlock() + // enable gate-auto-refresh-hook feature + tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.gate-auto-refresh-hook", true) + tr.Commit() + s.o.TaskRunner().AddHandler("run-hook", func(t *state.Task, tomb *tomb.Tomb) error { var hsup hookstate.HookSetup t.State().Lock() @@ -1676,10 +1773,8 @@ chg.AddAll(ts) } - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(chg.Status(), Equals, state.DoneStatus) c.Check(chg.Err(), IsNil) @@ -1799,7 +1894,8 @@ chg := s.testAutoRefreshPhase2(c, nil, func(snapName string) { if snapName == "snap-b" { // pretend than snap-b calls snapctl --hold to hold refresh of base-snap-b - c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil) + _, err := snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b") + c.Assert(err, IsNil) } }, expected) @@ -1852,8 +1948,10 @@ s.testAutoRefreshPhase2(c, func() { // pretend that snap-a and base-snap-b are initially held - c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil) - c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil) + _, err := snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a") + c.Assert(err, IsNil) + _, err = snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b") + c.Assert(err, IsNil) }, func(snapName string) { if snapName == "snap-a" { // pretend than snap-a calls snapctl --proceed @@ -1883,10 +1981,12 @@ switch snapName { case "snap-b": // pretend that snap-b calls snapctl --hold to hold refresh of base-snap-b - c.Assert(snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b"), IsNil) + _, err := snapstate.HoldRefresh(s.state, "snap-b", 0, "base-snap-b") + c.Assert(err, IsNil) case "snap-a": // pretend that snap-a calls snapctl --hold to hold itself - c.Assert(snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a"), IsNil) + _, err := snapstate.HoldRefresh(s.state, "snap-a", 0, "snap-a") + c.Assert(err, IsNil) default: c.Fatalf("unexpected snap %q", snapName) } @@ -1964,10 +2064,8 @@ chg.AddAll(ts) } - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(installSizeCalled, Equals, true) if fail { @@ -2043,10 +2141,8 @@ conflictChange.AddTask(conflictTask) conflictTask.WaitFor(tss[0].Tasks()[0]) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus) c.Check(chg.Err(), IsNil) @@ -2147,6 +2243,11 @@ st.Lock() defer st.Unlock() + // enable gate-auto-refresh-hook feature + tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.gate-auto-refresh-hook", true) + tr.Commit() + restore := snapstate.MockSnapsToRefresh(func(gatingTask *state.Task) ([]*snapstate.RefreshCandidate, error) { c.Assert(gatingTask.Kind(), Equals, "conditional-auto-refresh") var candidates map[string]*snapstate.RefreshCandidate @@ -2206,10 +2307,8 @@ chg.AddAll(ts) } - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus) c.Check(chg.Err(), IsNil) @@ -2309,8 +2408,10 @@ defer restore() // pretend some snaps are held - c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil) - c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a"), IsNil) + _, err := snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b") + c.Assert(err, IsNil) + _, err = snapstate.HoldRefresh(st, "snap-a", 0, "snap-a") + c.Assert(err, IsNil) lastRefreshTime := time.Now().Add(-99 * time.Hour) st.Set("last-refresh", lastRefreshTime) @@ -2419,7 +2520,8 @@ defer restore() // pretend snap-b holds base-snap-b. - c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil) + _, err := snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b") + c.Assert(err, IsNil) // pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed) c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil) @@ -2519,8 +2621,10 @@ defer restore() // pretend some snaps are held - c.Assert(snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b"), IsNil) - c.Assert(snapstate.HoldRefresh(st, "snap-a", 0, "snap-a"), IsNil) + _, err := snapstate.HoldRefresh(st, "snap-b", 0, "base-snap-b") + c.Assert(err, IsNil) + _, err = snapstate.HoldRefresh(st, "snap-a", 0, "snap-a") + c.Assert(err, IsNil) // pretend that snap-b triggers auto-refresh (by calling snapctl refresh --proceed) c.Assert(snapstate.AutoRefreshForGatingSnap(st, "snap-b"), IsNil) @@ -2580,13 +2684,7 @@ restore := snapstatetest.MockDeviceModel(DefaultModel()) defer restore() - refreshedDate, err := time.Parse(time.RFC3339, "2021-01-01T10:00:00Z") - c.Assert(err, IsNil) - restoreRevDate := snapstate.MockRevisionDate(func(sn *snap.Info) time.Time { - return refreshedDate - }) - defer restoreRevDate() - + refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1) requiredRevision = "1" names, _, err := snapstate.AutoRefreshPhase1(context.TODO(), st, "") c.Assert(err, IsNil) @@ -2607,19 +2705,19 @@ InstanceName: "snap-c", SnapID: "snap-c-id", Revision: snap.R(1), - Epoch: snap.E("0"), + Epoch: snap.E("1*"), RefreshedDate: refreshedDate, }, { InstanceName: "some-other-snap", SnapID: "some-other-snap-id", Revision: snap.R(1), - Epoch: snap.E("0"), + Epoch: snap.E("1*"), RefreshedDate: refreshedDate, }, { InstanceName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1), - Epoch: snap.E("0"), + Epoch: snap.E("1*"), RefreshedDate: refreshedDate, }}, }, { diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/backend/copydata.go snapd-2.54.2+21.10/overlord/snapstate/backend/copydata.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/backend/copydata.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/backend/copydata.go 2022-01-06 21:25:16.000000000 +0000 @@ -20,17 +20,24 @@ package backend import ( + "errors" + "fmt" + "io/ioutil" "os" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/progress" "github.com/snapcore/snapd/snap" ) +var allUsers = snap.AllUsers + // CopySnapData makes a copy of oldSnap data for newSnap in its data directories. -func (b Backend) CopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter) error { +func (b Backend) CopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter, opts *dirs.SnapDirOptions) error { // deal with the old data or - // otherwise just create a empty data dir + // otherwise just create an empty data dir // Make sure the base data directory exists for instance snaps if newSnap.InstanceKey != "" { @@ -53,16 +60,16 @@ return nil } - return copySnapData(oldSnap, newSnap) + return copySnapData(oldSnap, newSnap, opts) } // UndoCopySnapData removes the copy that may have been done for newInfo snap of oldInfo snap data and also the data directories that may have been created for newInfo snap. -func (b Backend) UndoCopySnapData(newInfo *snap.Info, oldInfo *snap.Info, meter progress.Meter) error { +func (b Backend) UndoCopySnapData(newInfo, oldInfo *snap.Info, _ progress.Meter, opts *dirs.SnapDirOptions) error { if oldInfo != nil && oldInfo.Revision == newInfo.Revision { // nothing to do return nil } - err1 := b.RemoveSnapData(newInfo) + err1 := b.RemoveSnapData(newInfo, opts) if err1 != nil { logger.Noticef("Cannot remove data directories for %q: %v", newInfo.InstanceName(), err1) } @@ -70,12 +77,12 @@ var err2 error if oldInfo == nil { // first install, remove created common data dir - err2 = b.RemoveSnapCommonData(newInfo) + err2 = b.RemoveSnapCommonData(newInfo, opts) if err2 != nil { logger.Noticef("Cannot remove common data directories for %q: %v", newInfo.InstanceName(), err2) } } else { - err2 = b.untrashData(newInfo) + err2 = b.untrashData(newInfo, opts) if err2 != nil { logger.Noticef("Cannot restore original data for %q while undoing: %v", newInfo.InstanceName(), err2) } @@ -86,15 +93,136 @@ // ClearTrashedData removes the trash. It returns no errors on the assumption that it is called very late in the game. func (b Backend) ClearTrashedData(oldSnap *snap.Info) { - dirs, err := snapDataDirs(oldSnap) + dataDirs, err := snapDataDirs(oldSnap, nil) if err != nil { logger.Noticef("Cannot remove previous data for %q: %v", oldSnap.InstanceName(), err) return } - for _, d := range dirs { + opts := &dirs.SnapDirOptions{HiddenSnapDataDir: true} + hiddenDirs, err := snapDataDirs(oldSnap, opts) + if err != nil { + logger.Noticef("Cannot remove previous data for %q: %v", oldSnap.InstanceName(), err) + return + } + + // this will have duplicates but the second remove will just be ignored + dataDirs = append(dataDirs, hiddenDirs...) + for _, d := range dataDirs { if err := clearTrash(d); err != nil { logger.Noticef("Cannot remove %s: %v", d, err) } } } + +func (b Backend) HideSnapData(snapName string) error { + postMigrationOpts := &dirs.SnapDirOptions{HiddenSnapDataDir: true} + + users, err := allUsers(nil) + if err != nil { + return err + } + + for _, usr := range users { + uid, gid, err := osutil.UidGid(usr) + if err != nil { + return err + } + + // nothing to migrate + oldSnapDir := snap.UserSnapDir(usr.HomeDir, snapName, nil) + if _, err := os.Stat(oldSnapDir); errors.Is(err, os.ErrNotExist) { + continue + } else if err != nil { + return fmt.Errorf("cannot stat snap dir %q: %w", oldSnapDir, err) + } + + // create the new hidden snap dir + hiddenSnapDir := snap.SnapDir(usr.HomeDir, postMigrationOpts) + if err := osutil.MkdirAllChown(hiddenSnapDir, 0700, uid, gid); err != nil { + return fmt.Errorf("cannot create snap dir %q: %w", hiddenSnapDir, err) + } + + // move the snap's dir + newSnapDir := snap.UserSnapDir(usr.HomeDir, snapName, postMigrationOpts) + if err := osutil.AtomicRename(oldSnapDir, newSnapDir); err != nil { + return fmt.Errorf("cannot move %q to %q: %w", oldSnapDir, newSnapDir, err) + } + + // remove ~/snap if it's empty + if err := removeIfEmpty(snap.SnapDir(usr.HomeDir, nil)); err != nil { + return fmt.Errorf("failed to remove old snap dir: %w", err) + } + } + + return nil +} + +func (b Backend) UndoHideSnapData(snapName string) error { + postMigrationOpts := &dirs.SnapDirOptions{HiddenSnapDataDir: true} + + users, err := allUsers(postMigrationOpts) + if err != nil { + return err + } + + var firstErr error + handle := func(err error) { + // keep going, restore previous state as much as possible + if firstErr == nil { + firstErr = err + } else { + logger.Noticef(err.Error()) + } + } + + for _, usr := range users { + uid, gid, err := osutil.UidGid(usr) + if err != nil { + handle(err) + continue + } + + // skip it if wasn't migrated + hiddenSnapDir := snap.UserSnapDir(usr.HomeDir, snapName, postMigrationOpts) + if _, err := os.Stat(hiddenSnapDir); err != nil { + if !errors.Is(err, os.ErrNotExist) { + handle(fmt.Errorf("cannot read files in %q: %w", hiddenSnapDir, err)) + } + continue + } + + // ensure parent dirs exist + exposedDir := snap.SnapDir(usr.HomeDir, nil) + if err := osutil.MkdirAllChown(exposedDir, 0700, uid, gid); err != nil { + handle(fmt.Errorf("cannot create snap dir %q: %w", exposedDir, err)) + continue + } + + exposedSnapDir := snap.UserSnapDir(usr.HomeDir, snapName, nil) + if err := osutil.AtomicRename(hiddenSnapDir, exposedSnapDir); err != nil { + handle(fmt.Errorf("cannot move %q to %q: %w", hiddenSnapDir, exposedSnapDir, err)) + } + + // remove ~/.snap/data dir if empty + hiddenDir := snap.SnapDir(usr.HomeDir, postMigrationOpts) + if err := removeIfEmpty(hiddenDir); err != nil { + handle(fmt.Errorf("cannot remove dir %q: %w", hiddenDir, err)) + } + } + + return firstErr +} + +var removeIfEmpty = func(dir string) error { + files, err := ioutil.ReadDir(dir) + if err != nil { + return err + } + + if len(files) > 0 { + return nil + } + + return os.Remove(dir) +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/backend/copydata_test.go snapd-2.54.2+21.10/overlord/snapstate/backend/copydata_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/backend/copydata_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/backend/copydata_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -20,9 +20,11 @@ package backend_test import ( + "errors" "fmt" "io/ioutil" "os" + "os/user" "path/filepath" "regexp" "strconv" @@ -30,6 +32,7 @@ . "gopkg.in/check.v1" "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/snapstate/backend" "github.com/snapcore/snapd/progress" @@ -64,7 +67,22 @@ ) func (s *copydataSuite) TestCopyData(c *C) { - homedir := filepath.Join(s.tempdir, "home", "user1", "snap") + for _, t := range []struct { + snapDir string + opts *dirs.SnapDirOptions + }{ + {snapDir: dirs.UserHomeSnapDir, opts: nil}, + {snapDir: dirs.UserHomeSnapDir, opts: &dirs.SnapDirOptions{}}, + {snapDir: dirs.HiddenSnapDataHomeDir, opts: &dirs.SnapDirOptions{HiddenSnapDataDir: true}}} { + s.testCopyData(c, t.snapDir, t.opts) + c.Assert(os.RemoveAll(s.tempdir), IsNil) + s.tempdir = c.MkDir() + dirs.SetRootDir(s.tempdir) + } +} + +func (s *copydataSuite) testCopyData(c *C, snapDir string, opts *dirs.SnapDirOptions) { + homedir := filepath.Join(s.tempdir, "home", "user1", snapDir) homeData := filepath.Join(homedir, "hello/10") err := os.MkdirAll(homeData, 0755) c.Assert(err, IsNil) @@ -76,7 +94,7 @@ v1 := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) // just creates data dirs in this case - err = s.be.CopySnapData(v1, nil, progress.Null) + err = s.be.CopySnapData(v1, nil, progress.Null, opts) c.Assert(err, IsNil) canaryDataFile := filepath.Join(v1.DataDir(), "canary.txt") @@ -91,7 +109,7 @@ c.Assert(err, IsNil) v2 := snaptest.MockSnap(c, helloYaml2, &snap.SideInfo{Revision: snap.R(20)}) - err = s.be.CopySnapData(v2, v1, progress.Null) + err = s.be.CopySnapData(v2, v1, progress.Null, opts) c.Assert(err, IsNil) newCanaryDataFile := filepath.Join(dirs.SnapDataDir, "hello/20", "canary.txt") @@ -114,11 +132,11 @@ defer func() { dirs.SnapDataHomeGlob = oldSnapDataHomeGlob }() v1 := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) - c.Assert(s.be.CopySnapData(v1, nil, progress.Null), IsNil) + c.Assert(s.be.CopySnapData(v1, nil, progress.Null, nil), IsNil) c.Assert(os.Chmod(v1.DataDir(), 0), IsNil) v2 := snaptest.MockSnap(c, helloYaml2, &snap.SideInfo{Revision: snap.R(20)}) - err := s.be.CopySnapData(v2, v1, progress.Null) + err := s.be.CopySnapData(v2, v1, progress.Null, nil) c.Check(err, ErrorMatches, "cannot copy .*") } @@ -131,7 +149,7 @@ dirs.SnapDataHomeGlob = filepath.Join(s.tempdir, "no-such-home", "*", "snap") v1 := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) - err := s.be.CopySnapData(v1, nil, progress.Null) + err := s.be.CopySnapData(v1, nil, progress.Null, nil) c.Assert(err, IsNil) canaryDataFile := filepath.Join(v1.DataDir(), "canary.txt") @@ -142,7 +160,7 @@ c.Assert(err, IsNil) v2 := snaptest.MockSnap(c, helloYaml2, &snap.SideInfo{Revision: snap.R(20)}) - err = s.be.CopySnapData(v2, v1, progress.Null) + err = s.be.CopySnapData(v2, v1, progress.Null, nil) c.Assert(err, IsNil) _, err = os.Stat(filepath.Join(v2.DataDir(), "canary.txt")) @@ -176,7 +194,11 @@ } func (s copydataSuite) populateHomeData(c *C, user string, revision snap.Revision) (homedir string) { - homedir = filepath.Join(s.tempdir, "home", user, "snap") + return s.populateHomeDataWithSnapDir(c, user, dirs.UserHomeSnapDir, revision) +} + +func (s copydataSuite) populateHomeDataWithSnapDir(c *C, user string, snapDir string, revision snap.Revision) (homedir string) { + homedir = filepath.Join(s.tempdir, "home", user, snapDir) homeData := filepath.Join(homedir, "hello", revision.String()) err := os.MkdirAll(homeData, 0755) c.Assert(err, IsNil) @@ -187,15 +209,31 @@ } func (s *copydataSuite) TestCopyDataDoUndo(c *C) { + for _, t := range []struct { + snapDir string + opts *dirs.SnapDirOptions + }{ + {snapDir: dirs.UserHomeSnapDir}, + {snapDir: dirs.UserHomeSnapDir, opts: &dirs.SnapDirOptions{}}, + {snapDir: dirs.HiddenSnapDataHomeDir, opts: &dirs.SnapDirOptions{HiddenSnapDataDir: true}}, + } { + s.testCopyDataUndo(c, t.snapDir, t.opts) + c.Assert(os.RemoveAll(s.tempdir), IsNil) + s.tempdir = c.MkDir() + dirs.SetRootDir(s.tempdir) + } +} + +func (s *copydataSuite) testCopyDataUndo(c *C, snapDir string, opts *dirs.SnapDirOptions) { v1 := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) s.populateData(c, snap.R(10)) - homedir := s.populateHomeData(c, "user1", snap.R(10)) + homedir := s.populateHomeDataWithSnapDir(c, "user1", snapDir, snap.R(10)) // pretend we install a new version v2 := snaptest.MockSnap(c, helloYaml2, &snap.SideInfo{Revision: snap.R(20)}) // copy data - err := s.be.CopySnapData(v2, v1, progress.Null) + err := s.be.CopySnapData(v2, v1, progress.Null, opts) c.Assert(err, IsNil) v2data := filepath.Join(dirs.SnapDataDir, "hello/20") l, err := filepath.Glob(filepath.Join(v2data, "*")) @@ -206,7 +244,7 @@ c.Assert(err, IsNil) c.Assert(l, HasLen, 1) - err = s.be.UndoCopySnapData(v2, v1, progress.Null) + err = s.be.UndoCopySnapData(v2, v1, progress.Null, opts) c.Assert(err, IsNil) // now removed @@ -229,14 +267,14 @@ v2 := snaptest.MockSnap(c, helloYaml2, &snap.SideInfo{Revision: snap.R(20)}) // copy data - err := s.be.CopySnapData(v2, v1, progress.Null) + err := s.be.CopySnapData(v2, v1, progress.Null, nil) c.Assert(err, IsNil) v2data := filepath.Join(dirs.SnapDataDir, "hello/20") l, err := filepath.Glob(filepath.Join(v2data, "*")) c.Assert(err, IsNil) c.Assert(l, HasLen, 1) - err = s.be.UndoCopySnapData(v2, v1, progress.Null) + err = s.be.UndoCopySnapData(v2, v1, progress.Null, nil) c.Assert(err, IsNil) // now removed @@ -248,14 +286,14 @@ v1 := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) // first install - err := s.be.CopySnapData(v1, nil, progress.Null) + err := s.be.CopySnapData(v1, nil, progress.Null, nil) c.Assert(err, IsNil) _, err = os.Stat(v1.DataDir()) c.Assert(err, IsNil) _, err = os.Stat(v1.CommonDataDir()) c.Assert(err, IsNil) - err = s.be.UndoCopySnapData(v1, nil, progress.Null) + err = s.be.UndoCopySnapData(v1, nil, progress.Null, nil) c.Assert(err, IsNil) _, err = os.Stat(v1.DataDir()) c.Check(os.IsNotExist(err), Equals, true) @@ -264,6 +302,12 @@ } func (s *copydataSuite) TestCopyDataDoABA(c *C) { + for _, opts := range []*dirs.SnapDirOptions{nil, {}, {HiddenSnapDataDir: true}} { + s.testCopyDataDoABA(c, opts) + } +} + +func (s *copydataSuite) testCopyDataDoABA(c *C, opts *dirs.SnapDirOptions) { v1 := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) s.populateData(c, snap.R(10)) c.Check(s.populatedData("10"), Equals, "10\n") @@ -275,7 +319,7 @@ c.Check(s.populatedData("20"), Equals, "20\n") // and now we pretend to refresh back to v1 (r10) - c.Check(s.be.CopySnapData(v1, v2, progress.Null), IsNil) + c.Check(s.be.CopySnapData(v1, v2, progress.Null, opts), IsNil) // so 10 now has 20's data c.Check(s.populatedData("10"), Equals, "20\n") @@ -300,14 +344,14 @@ c.Check(s.populatedData("20"), Equals, "20\n") // and now we pretend to refresh back to v1 (r10) - c.Check(s.be.CopySnapData(v1, v2, progress.Null), IsNil) + c.Check(s.be.CopySnapData(v1, v2, progress.Null, nil), IsNil) // so v1 (r10) now has v2 (r20)'s data and we have trash c.Check(s.populatedData("10"), Equals, "20\n") c.Check(s.populatedData("10.old"), Equals, "10\n") // but oh no! we have to undo it! - c.Check(s.be.UndoCopySnapData(v1, v2, progress.Null), IsNil) + c.Check(s.be.UndoCopySnapData(v1, v2, progress.Null, nil), IsNil) // so now v1 (r10) has v1 (r10)'s data and v2 (r20) has v2 (r20)'s data and we have no trash c.Check(s.populatedData("10"), Equals, "10\n") @@ -327,10 +371,10 @@ v2 := snaptest.MockSnap(c, helloYaml2, &snap.SideInfo{Revision: snap.R(20)}) // copy data - err := s.be.CopySnapData(v2, v1, progress.Null) + err := s.be.CopySnapData(v2, v1, progress.Null, nil) c.Assert(err, IsNil) - err = s.be.CopySnapData(v2, v1, progress.Null) + err = s.be.CopySnapData(v2, v1, progress.Null, nil) c.Assert(err, IsNil) v2data := filepath.Join(dirs.SnapDataDir, "hello/20") @@ -354,15 +398,15 @@ v2 := snaptest.MockSnap(c, helloYaml2, &snap.SideInfo{Revision: snap.R(20)}) // copy data - err := s.be.CopySnapData(v2, v1, progress.Null) + err := s.be.CopySnapData(v2, v1, progress.Null, nil) c.Assert(err, IsNil) v2data := filepath.Join(dirs.SnapDataDir, "hello/20") - err = s.be.UndoCopySnapData(v2, v1, progress.Null) + err = s.be.UndoCopySnapData(v2, v1, progress.Null, nil) c.Assert(err, IsNil) - err = s.be.UndoCopySnapData(v2, v1, progress.Null) + err = s.be.UndoCopySnapData(v2, v1, progress.Null, nil) c.Assert(err, IsNil) // now removed @@ -377,10 +421,10 @@ v1 := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) // first install - err := s.be.CopySnapData(v1, nil, progress.Null) + err := s.be.CopySnapData(v1, nil, progress.Null, nil) c.Assert(err, IsNil) - err = s.be.CopySnapData(v1, nil, progress.Null) + err = s.be.CopySnapData(v1, nil, progress.Null, nil) c.Assert(err, IsNil) _, err = os.Stat(v1.DataDir()) @@ -388,7 +432,7 @@ _, err = os.Stat(v1.CommonDataDir()) c.Assert(err, IsNil) - err = s.be.UndoCopySnapData(v1, nil, progress.Null) + err = s.be.UndoCopySnapData(v1, nil, progress.Null, nil) c.Assert(err, IsNil) _, err = os.Stat(v1.DataDir()) c.Check(os.IsNotExist(err), Equals, true) @@ -400,17 +444,17 @@ v1 := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) // first install - err := s.be.CopySnapData(v1, nil, progress.Null) + err := s.be.CopySnapData(v1, nil, progress.Null, nil) c.Assert(err, IsNil) _, err = os.Stat(v1.DataDir()) c.Assert(err, IsNil) _, err = os.Stat(v1.CommonDataDir()) c.Assert(err, IsNil) - err = s.be.UndoCopySnapData(v1, nil, progress.Null) + err = s.be.UndoCopySnapData(v1, nil, progress.Null, nil) c.Assert(err, IsNil) - err = s.be.UndoCopySnapData(v1, nil, progress.Null) + err = s.be.UndoCopySnapData(v1, nil, progress.Null, nil) c.Assert(err, IsNil) _, err = os.Stat(v1.DataDir()) @@ -433,7 +477,7 @@ } // copy data will fail - err := s.be.CopySnapData(v2, v1, progress.Null) + err := s.be.CopySnapData(v2, v1, progress.Null, nil) c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot copy %s to %s: .*: "cp: boom" \(3\)`, q(v1.DataDir()), q(v2.DataDir()))) } @@ -456,7 +500,7 @@ c.Assert(os.Chmod(filepath.Join(homedir2, "hello", "10", "canary.home"), 0), IsNil) // try to copy data - err := s.be.CopySnapData(v2, v1, progress.Null) + err := s.be.CopySnapData(v2, v1, progress.Null, nil) c.Assert(err, NotNil) // the copy data failed, so check it cleaned up after itself (but not too much!) @@ -487,7 +531,7 @@ } // copy data works - err := s.be.CopySnapData(v1, v1, progress.Null) + err := s.be.CopySnapData(v1, v1, progress.Null, nil) c.Assert(err, IsNil) // the data is still there :-) @@ -523,7 +567,7 @@ } // undo copy data works - err := s.be.UndoCopySnapData(v1, v1, progress.Null) + err := s.be.UndoCopySnapData(v1, v1, progress.Null, nil) c.Assert(err, IsNil) // the data is still there :-) @@ -535,5 +579,270 @@ } { c.Check(osutil.FileExists(fn), Equals, true, Commentf(fn)) } +} + +func (s *copydataSuite) TestHideSnapData(c *C) { + info := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) + + // mock user home + homedir := filepath.Join(s.tempdir, "home", "user") + usr, err := user.Current() + c.Assert(err, IsNil) + usr.HomeDir = homedir + + restore := backend.MockAllUsers(func(*dirs.SnapDirOptions) ([]*user.User, error) { + return []*user.User{usr}, nil + }) + defer restore() + + // writes a file canary.home file to the rev dir of the "hello" snap + s.populateHomeData(c, "user", snap.R(10)) + + // write file in common + err = os.MkdirAll(info.UserCommonDataDir(homedir, nil), 0770) + c.Assert(err, IsNil) + + commonFilePath := filepath.Join(info.UserCommonDataDir(homedir, nil), "file.txt") + err = ioutil.WriteFile(commonFilePath, []byte("some content"), 0640) + c.Assert(err, IsNil) + + // make 'current' symlink + revDir := snap.UserDataDir(homedir, "hello", snap.R(10), nil) + // path must be relative, otherwise move would make it dangling + err = os.Symlink(filepath.Base(revDir), filepath.Join(revDir, "..", "current")) + c.Assert(err, IsNil) + + err = s.be.HideSnapData("hello") + c.Assert(err, IsNil) + + // check versioned file was moved + opts := &dirs.SnapDirOptions{HiddenSnapDataDir: true} + revFile := filepath.Join(info.UserDataDir(homedir, opts), "canary.home") + data, err := ioutil.ReadFile(revFile) + c.Assert(err, IsNil) + c.Assert(data, DeepEquals, []byte("10\n")) + + // check common file was moved + commonFile := filepath.Join(info.UserCommonDataDir(homedir, opts), "file.txt") + data, err = ioutil.ReadFile(commonFile) + c.Assert(err, IsNil) + c.Assert(data, DeepEquals, []byte("some content")) + + // check 'current' symlink has correct attributes and target + link := filepath.Join(homedir, dirs.HiddenSnapDataHomeDir, "hello", "current") + linkInfo, err := os.Lstat(link) + c.Assert(err, IsNil) + c.Assert(linkInfo.Mode()&os.ModeSymlink, Equals, os.ModeSymlink) + + target, err := os.Readlink(link) + c.Assert(err, IsNil) + c.Assert(target, Equals, "10") + + // check old '~/snap' folder was removed + _, err = os.Stat(snap.SnapDir(homedir, nil)) + c.Assert(errors.Is(err, os.ErrNotExist), Equals, true) +} + +func (s *copydataSuite) TestHideSnapDataSkipNoData(c *C) { + info := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) + + // mock user home + homedir := filepath.Join(s.tempdir, "home", "user") + usr, err := user.Current() + c.Assert(err, IsNil) + usr.HomeDir = homedir + + // create user without snap dir (to be skipped) + usrNoSnapDir := &user.User{ + HomeDir: filepath.Join(s.tempdir, "home", "other-user"), + Name: "other-user", + Uid: "1001", + Gid: "1001", + } + restore := backend.MockAllUsers(func(_ *dirs.SnapDirOptions) ([]*user.User, error) { + return []*user.User{usr, usrNoSnapDir}, nil + }) + defer restore() + + s.populateHomeData(c, "user", snap.R(10)) + + // make 'current' symlink + revDir := info.UserDataDir(homedir, nil) + linkPath := filepath.Join(revDir, "..", "current") + err = os.Symlink(revDir, linkPath) + c.Assert(err, IsNil) + + // empty user dir is skipped + err = s.be.HideSnapData("hello") + c.Assert(err, IsNil) + + // only the user with snap data was migrated + newSnapDir := filepath.Join(homedir, dirs.HiddenSnapDataHomeDir) + matches, err := filepath.Glob(dirs.HiddenSnapDataHomeGlob) + c.Assert(err, IsNil) + c.Assert(matches, HasLen, 1) + c.Assert(matches[0], Equals, newSnapDir) +} + +func (s *copydataSuite) TestUndoHideSnapData(c *C) { + info := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) + + // mock user home dir + homedir := filepath.Join(s.tempdir, "home", "user") + usr, err := user.Current() + c.Assert(err, IsNil) + usr.HomeDir = homedir + + restore := backend.MockAllUsers(func(_ *dirs.SnapDirOptions) ([]*user.User, error) { + return []*user.User{usr}, nil + }) + defer restore() + + // write file in revisioned dir + opts := &dirs.SnapDirOptions{HiddenSnapDataDir: true} + err = os.MkdirAll(info.UserDataDir(homedir, opts), 0770) + c.Assert(err, IsNil) + + hiddenRevFile := filepath.Join(info.UserDataDir(homedir, opts), "file.txt") + err = ioutil.WriteFile(hiddenRevFile, []byte("some content"), 0640) + c.Assert(err, IsNil) + + // write file in common + err = os.MkdirAll(info.UserCommonDataDir(homedir, opts), 0770) + c.Assert(err, IsNil) + + hiddenCommonFile := filepath.Join(info.UserCommonDataDir(homedir, opts), "file.txt") + err = ioutil.WriteFile(hiddenCommonFile, []byte("other content"), 0640) + c.Assert(err, IsNil) + + // make 'current' symlink + revDir := info.UserDataDir(homedir, opts) + // path must be relative otherwise the move would make it dangling + err = os.Symlink(filepath.Base(revDir), filepath.Join(revDir, "..", "current")) + c.Assert(err, IsNil) + + // undo migration + err = s.be.UndoHideSnapData("hello") + c.Assert(err, IsNil) + + // check versioned file was restored + revFile := filepath.Join(info.UserDataDir(homedir, nil), "file.txt") + data, err := ioutil.ReadFile(revFile) + c.Assert(err, IsNil) + c.Assert(data, DeepEquals, []byte("some content")) + + // check common file was restored + commonFile := filepath.Join(info.UserCommonDataDir(homedir, nil), "file.txt") + data, err = ioutil.ReadFile(commonFile) + c.Assert(err, IsNil) + c.Assert(data, DeepEquals, []byte("other content")) + + // check symlink points to revisioned dir + exposedDir := filepath.Join(homedir, dirs.UserHomeSnapDir) + target, err := os.Readlink(filepath.Join(exposedDir, "hello", "current")) + c.Assert(err, IsNil) + c.Assert(target, Equals, "10") + + // ~/.snap/data was removed + _, err = os.Stat(snap.SnapDir(homedir, opts)) + c.Assert(errors.Is(err, os.ErrNotExist), Equals, true) +} + +func (s *copydataSuite) TestCleanupAfterCopyAndMigration(c *C) { + homedir := filepath.Join(s.tempdir, "home", "user") + usr, err := user.Current() + c.Assert(err, IsNil) + usr.HomeDir = homedir + + restore := backend.MockAllUsers(func(_ *dirs.SnapDirOptions) ([]*user.User, error) { + return []*user.User{usr}, nil + }) + defer restore() + + // add trashed data in exposed dir + s.populateHomeData(c, "user", snap.R(10)) + v1 := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) + exposedTrash := filepath.Join(homedir, "snap", "hello", "10.old") + c.Assert(os.MkdirAll(exposedTrash, 0770), IsNil) + + // add trashed data in hidden dir + s.populateHomeDataWithSnapDir(c, "user", dirs.HiddenSnapDataHomeDir, snap.R(10)) + hiddenTrash := filepath.Join(homedir, ".snap", "data", "hello", "10.old") + c.Assert(os.MkdirAll(exposedTrash, 0770), IsNil) + + s.be.ClearTrashedData(v1) + + // clear should remove both + exists, _, err := osutil.DirExists(exposedTrash) + c.Assert(err, IsNil) + c.Assert(exists, Equals, false) + + exists, _, err = osutil.DirExists(hiddenTrash) + c.Assert(err, IsNil) + c.Assert(exists, Equals, false) +} + +func (s *copydataSuite) TestRemoveIfEmpty(c *C) { + file := filepath.Join(s.tempdir, "random") + c.Assert(ioutil.WriteFile(file, []byte("stuff"), 0664), IsNil) + + // dir contains a file, shouldn't do anything + c.Assert(backend.RemoveIfEmpty(s.tempdir), IsNil) + files, err := ioutil.ReadDir(s.tempdir) + c.Assert(err, IsNil) + c.Check(files, HasLen, 1) + c.Check(filepath.Join(s.tempdir, files[0].Name()), testutil.FileEquals, "stuff") + + c.Assert(os.Remove(file), IsNil) + + // dir is empty, should be removed + c.Assert(backend.RemoveIfEmpty(s.tempdir), IsNil) + c.Assert(osutil.FileExists(file), Equals, false) +} + +func (s *copydataSuite) TestUndoHideKeepGoingPreserveFirstErr(c *C) { + firstTime := true + restore := backend.MockRemoveIfEmpty(func(dir string) error { + var err error + if firstTime { + err = errors.New("first error") + firstTime = false + } else { + err = errors.New("other error") + } + + return err + }) + defer restore() + + info := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) + + // mock two users so that the undo is done twice + var usrs []*user.User + for _, usrName := range []string{"usr1", "usr2"} { + homedir := filepath.Join(s.tempdir, "home", usrName) + usr, err := user.Current() + c.Assert(err, IsNil) + usr.HomeDir = homedir + + opts := &dirs.SnapDirOptions{HiddenSnapDataDir: true} + err = os.MkdirAll(info.UserDataDir(homedir, opts), 0770) + c.Assert(err, IsNil) + + usrs = append(usrs, usr) + } + restUsers := backend.MockAllUsers(func(_ *dirs.SnapDirOptions) ([]*user.User, error) { + return usrs, nil + }) + defer restUsers() + + buf, restLogger := logger.MockLogger() + defer restLogger() + + err := s.be.UndoHideSnapData("hello") + // the first error is returned + c.Assert(err, ErrorMatches, `cannot remove dir ".*": first error`) + // the undo keeps going and logs the next error + c.Assert(buf, Matches, `.*cannot remove dir ".*": other error\n`) } diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/backend/export_test.go snapd-2.54.2+21.10/overlord/snapstate/backend/export_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/backend/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/backend/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -21,11 +21,15 @@ import ( "os/exec" + "os/user" + + "github.com/snapcore/snapd/dirs" ) var ( AddMountUnit = addMountUnit RemoveMountUnit = removeMountUnit + RemoveIfEmpty = removeIfEmpty ) func MockUpdateFontconfigCaches(f func() error) (restore func()) { @@ -43,3 +47,20 @@ commandFromSystemSnap = old } } + +func MockAllUsers(f func(options *dirs.SnapDirOptions) ([]*user.User, error)) func() { + old := allUsers + allUsers = f + return func() { + allUsers = old + } + +} + +func MockRemoveIfEmpty(f func(dir string) error) func() { + old := removeIfEmpty + removeIfEmpty = f + return func() { + removeIfEmpty = old + } +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/backend/snapdata.go snapd-2.54.2+21.10/overlord/snapstate/backend/snapdata.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/backend/snapdata.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/backend/snapdata.go 2022-01-06 21:25:16.000000000 +0000 @@ -33,8 +33,8 @@ ) // RemoveSnapData removes the data for the given version of the given snap. -func (b Backend) RemoveSnapData(snap *snap.Info) error { - dirs, err := snapDataDirs(snap) +func (b Backend) RemoveSnapData(snap *snap.Info, opts *dirs.SnapDirOptions) error { + dirs, err := snapDataDirs(snap, opts) if err != nil { return err } @@ -43,8 +43,8 @@ } // RemoveSnapCommonData removes the data common between versions of the given snap. -func (b Backend) RemoveSnapCommonData(snap *snap.Info) error { - dirs, err := snapCommonDataDirs(snap) +func (b Backend) RemoveSnapCommonData(snap *snap.Info, opts *dirs.SnapDirOptions) error { + dirs, err := snapCommonDataDirs(snap, opts) if err != nil { return err } @@ -72,8 +72,8 @@ return nil } -func (b Backend) untrashData(snap *snap.Info) error { - dirs, err := snapDataDirs(snap) +func (b Backend) untrashData(snap *snap.Info, opts *dirs.SnapDirOptions) error { + dirs, err := snapDataDirs(snap, opts) if err != nil { return err } @@ -98,14 +98,14 @@ } // snapDataDirs returns the list of data directories for the given snap version -func snapDataDirs(snap *snap.Info) ([]string, error) { +func snapDataDirs(snap *snap.Info, opts *dirs.SnapDirOptions) ([]string, error) { // collect the directories, homes first - found, err := filepath.Glob(snap.DataHomeDir()) + found, err := filepath.Glob(snap.DataHomeDir(opts)) if err != nil { return nil, err } // then the /root user (including GlobalRootDir for tests) - found = append(found, snap.UserDataDir(filepath.Join(dirs.GlobalRootDir, "/root/"))) + found = append(found, snap.UserDataDir(filepath.Join(dirs.GlobalRootDir, "/root/"), opts)) // then system data found = append(found, snap.DataDir()) @@ -113,9 +113,9 @@ } // snapCommonDataDirs returns the list of data directories common between versions of the given snap -func snapCommonDataDirs(snap *snap.Info) ([]string, error) { +func snapCommonDataDirs(snap *snap.Info, opts *dirs.SnapDirOptions) ([]string, error) { // collect the directories, homes first - found, err := filepath.Glob(snap.CommonDataHomeDir()) + found, err := filepath.Glob(snap.CommonDataHomeDir(opts)) if err != nil { return nil, err } @@ -135,8 +135,8 @@ // Copy all data for oldSnap to newSnap // (but never overwrite) -func copySnapData(oldSnap, newSnap *snap.Info) (err error) { - oldDataDirs, err := snapDataDirs(oldSnap) +func copySnapData(oldSnap, newSnap *snap.Info, opts *dirs.SnapDirOptions) (err error) { + oldDataDirs, err := snapDataDirs(oldSnap, opts) if err != nil { return err } diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/backend/snapdata_test.go snapd-2.54.2+21.10/overlord/snapstate/backend/snapdata_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/backend/snapdata_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/backend/snapdata_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -58,7 +58,7 @@ c.Assert(err, IsNil) info := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) - err = s.be.RemoveSnapData(info) + err = s.be.RemoveSnapData(info, nil) c.Assert(err, IsNil) c.Assert(osutil.FileExists(homeData), Equals, false) @@ -78,7 +78,7 @@ info := snaptest.MockSnap(c, helloYaml1, &snap.SideInfo{Revision: snap.R(10)}) - err = s.be.RemoveSnapCommonData(info) + err = s.be.RemoveSnapCommonData(info, nil) c.Assert(err, IsNil) c.Assert(osutil.FileExists(homeCommonData), Equals, false) c.Assert(osutil.FileExists(filepath.Dir(homeCommonData)), Equals, true) diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/backend.go snapd-2.54.2+21.10/overlord/snapstate/backend.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/backend.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/backend.go 2022-01-06 21:25:16.000000000 +0000 @@ -27,6 +27,7 @@ "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/client" "github.com/snapcore/snapd/cmd/snaplock/runinhibit" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/snapstate/backend" @@ -71,7 +72,7 @@ type managerBackend interface { // install related SetupSnap(snapFilePath, instanceName string, si *snap.SideInfo, dev boot.Device, opts *backend.SetupSnapOptions, meter progress.Meter) (snap.Type, *backend.InstallRecord, error) - CopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter) error + CopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter, opts *dirs.SnapDirOptions) error LinkSnap(info *snap.Info, dev boot.Device, linkCtx backend.LinkContext, tm timings.Measurer) (rebootRequired bool, err error) StartServices(svcs []*snap.AppInfo, disabledSvcs []string, meter progress.Meter, tm timings.Measurer) error StopServices(svcs []*snap.AppInfo, reason snap.ServiceStopReason, meter progress.Meter, tm timings.Measurer) error @@ -80,7 +81,7 @@ // the undoers for install UndoSetupSnap(s snap.PlaceInfo, typ snap.Type, installRecord *backend.InstallRecord, dev boot.Device, meter progress.Meter) error - UndoCopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter) error + UndoCopySnapData(newSnap, oldSnap *snap.Info, meter progress.Meter, opts *dirs.SnapDirOptions) error // cleanup ClearTrashedData(oldSnap *snap.Info) @@ -88,8 +89,8 @@ UnlinkSnap(info *snap.Info, linkCtx backend.LinkContext, meter progress.Meter) error RemoveSnapFiles(s snap.PlaceInfo, typ snap.Type, installRecord *backend.InstallRecord, dev boot.Device, meter progress.Meter) error RemoveSnapDir(s snap.PlaceInfo, hasOtherInstances bool) error - RemoveSnapData(info *snap.Info) error - RemoveSnapCommonData(info *snap.Info) error + RemoveSnapData(info *snap.Info, opts *dirs.SnapDirOptions) error + RemoveSnapCommonData(info *snap.Info, opts *dirs.SnapDirOptions) error RemoveSnapDataDir(info *snap.Info, hasOtherInstances bool) error RemoveSnapMountUnits(s snap.PlaceInfo, meter progress.Meter) error DiscardSnapNamespace(snapName string) error @@ -107,4 +108,8 @@ RunInhibitSnapForUnlink(info *snap.Info, hint runinhibit.Hint, decision func() error) (*osutil.FileLock, error) // (not a backend method because doInstall cannot access the backend) // WithSnapLock(info *snap.Info, action func() error) error + + // ~/.snap/data migration related + HideSnapData(snapName string) error + UndoHideSnapData(snapName string) error } diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/backend_test.go snapd-2.54.2+21.10/overlord/snapstate/backend_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/backend_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/backend_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -34,6 +34,7 @@ "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/cmd/snaplock/runinhibit" + "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/snapstate" @@ -79,6 +80,8 @@ inhibitHint runinhibit.Hint requireSnapdTooling bool + + dirOpts *dirs.SnapDirOptions } type fakeOps []fakeOp @@ -278,6 +281,8 @@ Symlink: "$SNAP/usr", }, } + case "channel-for-base/stable": + info.Base = "some-base" case "channel-for-user-daemon": info.Apps = map[string]*snap.AppInfo{ "user-daemon": { @@ -375,6 +380,9 @@ case "kernel-id": name = "kernel" typ = snap.TypeKernel + case "brand-kernel-id": + name = "brand-kernel" + typ = snap.TypeKernel case "brand-gadget-id": name = "brand-gadget" typ = snap.TypeGadget @@ -712,6 +720,7 @@ linkSnapWaitTrigger string linkSnapFailTrigger string linkSnapMaybeReboot bool + linkSnapRebootFor map[string]bool copySnapDataFailTrigger string emptyContainer snap.Container @@ -910,27 +919,29 @@ }) } -func (f *fakeSnappyBackend) CopySnapData(newInfo, oldInfo *snap.Info, p progress.Meter) error { +func (f *fakeSnappyBackend) CopySnapData(newInfo, oldInfo *snap.Info, p progress.Meter, opts *dirs.SnapDirOptions) error { p.Notify("copy-data") - old := "" + op := &fakeOp{ + op: "copy-data", + path: newInfo.MountDir(), + old: "", + } + if oldInfo != nil { - old = oldInfo.MountDir() + op.old = oldInfo.MountDir() + } + + if opts != nil && opts.HiddenSnapDataDir { + op.dirOpts = opts } if newInfo.MountDir() == f.copySnapDataFailTrigger { - f.appendOp(&fakeOp{ - op: "copy-data.failed", - path: newInfo.MountDir(), - old: old, - }) + op.op = "copy-data.failed" + f.appendOp(op) return errors.New("fail") } - f.appendOp(&fakeOp{ - op: "copy-data", - path: newInfo.MountDir(), - old: old, - }) + f.appendOp(op) return f.maybeErrForLastOp() } @@ -963,7 +974,8 @@ reboot := false if f.linkSnapMaybeReboot { - reboot = info.InstanceName() == dev.Base() + reboot = info.InstanceName() == dev.Base() || + (f.linkSnapRebootFor != nil && f.linkSnapRebootFor[info.InstanceName()]) } return reboot, nil @@ -1052,7 +1064,7 @@ return f.maybeErrForLastOp() } -func (f *fakeSnappyBackend) UndoCopySnapData(newInfo *snap.Info, oldInfo *snap.Info, p progress.Meter) error { +func (f *fakeSnappyBackend) UndoCopySnapData(newInfo *snap.Info, oldInfo *snap.Info, p progress.Meter, opts *dirs.SnapDirOptions) error { p.Notify("undo-copy-data") old := "" if oldInfo != nil { @@ -1087,7 +1099,7 @@ return f.maybeErrForLastOp() } -func (f *fakeSnappyBackend) RemoveSnapData(info *snap.Info) error { +func (f *fakeSnappyBackend) RemoveSnapData(info *snap.Info, opts *dirs.SnapDirOptions) error { f.appendOp(&fakeOp{ op: "remove-snap-data", path: info.MountDir(), @@ -1095,7 +1107,7 @@ return f.maybeErrForLastOp() } -func (f *fakeSnappyBackend) RemoveSnapCommonData(info *snap.Info) error { +func (f *fakeSnappyBackend) RemoveSnapCommonData(info *snap.Info, opts *dirs.SnapDirOptions) error { f.appendOp(&fakeOp{ op: "remove-snap-common-data", path: info.MountDir(), @@ -1169,12 +1181,13 @@ }) } -func (f *fakeSnappyBackend) ForeignTask(kind string, status state.Status, snapsup *snapstate.SnapSetup) { +func (f *fakeSnappyBackend) ForeignTask(kind string, status state.Status, snapsup *snapstate.SnapSetup) error { f.appendOp(&fakeOp{ op: kind + ":" + status.String(), name: snapsup.InstanceName(), revno: snapsup.Revision(), }) + return f.maybeErrForLastOp() } type byAlias []*backend.Alias @@ -1226,6 +1239,16 @@ return osutil.NewFileLock(filepath.Join(f.lockDir, info.InstanceName()+".lock")) } +func (f *fakeSnappyBackend) HideSnapData(snapName string) error { + f.appendOp(&fakeOp{op: "hide-snap-data", name: snapName}) + return f.maybeErrForLastOp() +} + +func (f *fakeSnappyBackend) UndoHideSnapData(snapName string) error { + f.appendOp(&fakeOp{op: "undo-hide-snap-data", name: snapName}) + return f.maybeErrForLastOp() +} + func (f *fakeSnappyBackend) appendOp(op *fakeOp) { f.mu.Lock() defer f.mu.Unlock() diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/booted.go snapd-2.54.2+21.10/overlord/snapstate/booted.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/booted.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/booted.go 2022-01-06 21:25:16.000000000 +0000 @@ -35,7 +35,7 @@ // fallback logic will revert to "os=v1" but on the filesystem snappy // still has the "active" version set to "v2" which is // misleading. This code will check what kernel/os booted and set -// those versions active.To do this it creates a Change and kicks +// those versions active. To do this it creates a Change and kicks // start it directly. func UpdateBootRevisions(st *state.State) error { const errorPrefix = "cannot update revisions after boot changes: " diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/booted_test.go snapd-2.54.2+21.10/overlord/snapstate/booted_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/booted_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/booted_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -37,6 +37,7 @@ "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/overlord" "github.com/snapcore/snapd/overlord/ifacestate/ifacerepo" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/servicestate" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" @@ -81,6 +82,9 @@ bs.fakeBackend = &fakeSnappyBackend{} bs.o = overlord.Mock() bs.state = bs.o.State() + bs.state.Lock() + restart.Init(bs.state, "boot-id-0", nil) + bs.state.Unlock() bs.snapmgr, err = snapstate.Manager(bs.state, bs.o.TaskRunner()) c.Assert(err, IsNil) @@ -314,13 +318,13 @@ snapsup := &snapstate.SnapSetup{SideInfo: si, Type: snap.TypeOS} // core snap, restarting ... wait - state.MockRestarting(st, state.RestartSystem) + restart.MockPending(st, restart.RestartSystem) snaptest.MockSnap(c, "name: core\ntype: os\nversion: 1", si) err = snapstate.FinishRestart(task, snapsup) c.Check(err, FitsTypeOf, &state.Retry{}) // core snap, restarted, waiting for current core revision - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) bs.bootloader.BootVars["snap_mode"] = boot.TryingStatus err = snapstate.FinishRestart(task, snapsup) c.Check(err, DeepEquals, &state.Retry{After: 5 * time.Second}) @@ -366,12 +370,12 @@ snapsup := &snapstate.SnapSetup{SideInfo: si, Type: snap.TypeBase} snaptest.MockSnap(c, "name: core18\ntype: base\nversion: 1", si) // core snap, restarting ... wait - state.MockRestarting(st, state.RestartSystem) + restart.MockPending(st, restart.RestartSystem) err = snapstate.FinishRestart(task, snapsup) c.Check(err, FitsTypeOf, &state.Retry{}) // core snap, restarted, waiting for current core revision - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) bs.bootloader.BootVars["snap_mode"] = boot.TryingStatus err = snapstate.FinishRestart(task, snapsup) c.Check(err, DeepEquals, &state.Retry{After: 5 * time.Second}) @@ -418,12 +422,12 @@ snapsup := &snapstate.SnapSetup{SideInfo: si, Type: snap.TypeKernel} snaptest.MockSnap(c, "name: kernel\ntype: kernel\nversion: 1", si) // kernel snap, restarting ... wait - state.MockRestarting(st, state.RestartSystem) + restart.MockPending(st, restart.RestartSystem) err = snapstate.FinishRestart(task, snapsup) c.Check(err, FitsTypeOf, &state.Retry{}) // kernel snap, restarted, waiting for current core revision - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) bs.bootloader.BootVars["snap_mode"] = boot.TryingStatus err = snapstate.FinishRestart(task, snapsup) c.Check(err, DeepEquals, &state.Retry{After: 5 * time.Second}) diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/check_snap.go snapd-2.54.2+21.10/overlord/snapstate/check_snap.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/check_snap.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/check_snap.go 2022-01-06 21:25:16.000000000 +0000 @@ -447,7 +447,7 @@ } // check that the snap installed in the system (via snapst) can be -// upgraded to info (i.e. that info's epoch can read sanpst's epoch) +// upgraded to info (i.e. that info's epoch can read snapst's epoch) func earlyEpochCheck(info *snap.Info, snapst *SnapState) error { if snapst == nil { // no snapst, no problem diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/dbus_test.go snapd-2.54.2+21.10/overlord/snapstate/dbus_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/dbus_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/dbus_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -288,9 +288,7 @@ chg.AddAll(ts) } - s.state.Unlock() s.settle(c) - s.state.Lock() // The order of installation is indeterminant, but one will fail c.Check(chg.Err(), ErrorMatches, `cannot perform the following tasks:\n- Make snap "(some|other)-snap" \(11\) available to the system \(snap "(some|other)-snap" requesting to activate on system bus name "org.example.Foo" conflicts with snap "(some|other)-snap" use\)`) diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/export_test.go snapd-2.54.2+21.10/overlord/snapstate/export_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -212,6 +212,8 @@ var ( RefreshedSnaps = refreshedSnaps ReRefreshFilter = reRefreshFilter + + MaybeRestoreValidationSetsAndRevertSnaps = maybeRestoreValidationSetsAndRevertSnaps ) type UpdateFilter = updateFilter @@ -366,3 +368,27 @@ snapsToRefresh = old } } + +func MockAddCurrentTrackingToValidationSetsStack(f func(st *state.State) error) (restore func()) { + old := AddCurrentTrackingToValidationSetsStack + AddCurrentTrackingToValidationSetsStack = f + return func() { + AddCurrentTrackingToValidationSetsStack = old + } +} + +func MockRestoreValidationSetsTracking(f func(*state.State) error) (restore func()) { + old := RestoreValidationSetsTracking + RestoreValidationSetsTracking = f + return func() { + RestoreValidationSetsTracking = old + } +} + +func MockMaybeRestoreValidationSetsAndRevertSnaps(f func(st *state.State, refreshedSnaps []string) ([]*state.TaskSet, error)) (restore func()) { + old := maybeRestoreValidationSetsAndRevertSnaps + maybeRestoreValidationSetsAndRevertSnaps = f + return func() { + maybeRestoreValidationSetsAndRevertSnaps = old + } +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/flags.go snapd-2.54.2+21.10/overlord/snapstate/flags.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/flags.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/flags.go 2022-01-06 21:25:16.000000000 +0000 @@ -35,6 +35,8 @@ // Revert flags the SnapSetup as coming from a revert Revert bool `json:"revert,omitempty"` + // If reverting, set this status for the reverted revision. + RevertStatus RevertStatus `json:"revert-status,omitempty"` // RemoveSnapPath is used via InstallPath to flag that the file passed in is // temporary and should be removed diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_copy_test.go snapd-2.54.2+21.10/overlord/snapstate/handlers_copy_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_copy_test.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/handlers_copy_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,83 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package snapstate_test + +import ( + "errors" + + . "gopkg.in/check.v1" + + "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/snap" +) + +type copySnapDataSuite struct { + baseHandlerSuite +} + +var _ = Suite(©SnapDataSuite{}) + +func (s *copySnapDataSuite) SetUpTest(c *C) { + s.baseHandlerSuite.SetUpTest(c) +} + +func (s *copySnapDataSuite) TestDoCopySnapDataFailedRead(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // With a snap "pkg" at revision 42 + si := &snap.SideInfo{RealName: "pkg", Revision: snap.R(42)} + snapstate.Set(s.state, "pkg", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + Active: true, + }) + + // With an app belonging to the snap that is apparently running. + snapstate.MockSnapReadInfo(func(name string, si *snap.SideInfo) (*snap.Info, error) { + return nil, errors.New("some error") + }) + + // We can unlink the current revision of that snap, by setting IgnoreRunning flag. + task := s.state.NewTask("copy-snap-data", "test") + task.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: &snap.SideInfo{ + RealName: "pkg", + Revision: snap.R(42), + }, + }) + chg := s.state.NewChange("dummy", "...") + chg.AddTask(task) + + // Run the task we created + s.state.Unlock() + s.se.Ensure() + s.se.Wait() + s.state.Lock() + + // And observe the results. + var snapst snapstate.SnapState + err := snapstate.Get(s.state, "pkg", &snapst) + c.Assert(err, IsNil) + c.Check(task.Status(), Equals, state.ErrorStatus) + c.Check(chg.Status(), Equals, state.ErrorStatus) + c.Check(chg.Err(), ErrorMatches, `(?s).*\(some error\)`) +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_discard_test.go snapd-2.54.2+21.10/overlord/snapstate/handlers_discard_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_discard_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/handlers_discard_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -38,7 +38,7 @@ var _ = Suite(&discardSnapSuite{}) func (s *discardSnapSuite) SetUpTest(c *C) { - s.setup(c, nil) + s.baseHandlerSuite.SetUpTest(c) s.state.Lock() defer s.state.Unlock() diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_download_test.go snapd-2.54.2+21.10/overlord/snapstate/handlers_download_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_download_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/handlers_download_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -43,7 +43,7 @@ var _ = Suite(&downloadSnapSuite{}) func (s *downloadSnapSuite) SetUpTest(c *C) { - s.setup(c, nil) + s.baseHandlerSuite.SetUpTest(c) s.fakeStore = &fakeStore{ state: s.state, diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers.go snapd-2.54.2+21.10/overlord/snapstate/handlers.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/handlers.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2018 Canonical Ltd + * Copyright (C) 2016-2021 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -34,6 +34,7 @@ "gopkg.in/tomb.v2" + "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/boot" "github.com/snapcore/snapd/cmd/snaplock/runinhibit" "github.com/snapcore/snapd/dirs" @@ -45,6 +46,7 @@ "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/configstate/settings" "github.com/snapcore/snapd/overlord/ifacestate/ifacerepo" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate/backend" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/progress" @@ -198,7 +200,7 @@ return channel } -func findLinkSnapTask(st *state.State, snapName string) (*state.Task, error) { +func findLinkSnapTaskForSnap(st *state.State, snapName string) (*state.Task, error) { for _, chg := range st.Changes() { if chg.Status().Ready() { continue @@ -267,7 +269,7 @@ } // if a previous version of snapd persisted Prereq only, fill the contentAttrs. - // There will be not content attrs, so it will not update an outdated default provider + // There will be no content attrs, so it will not update an outdated default provider if len(snapsup.PrereqContentAttrs) == 0 && len(snapsup.Prereq) != 0 { snapsup.PrereqContentAttrs = make(map[string][]string, len(snapsup.Prereq)) @@ -305,7 +307,7 @@ } // in progress? - if linkTask, err := findLinkSnapTask(st, snapName); err != nil { + if linkTask, err := findLinkSnapTaskForSnap(st, snapName); err != nil { return nil, err } else if linkTask != nil { return nil, onInFlight @@ -386,7 +388,7 @@ // Checks for conflicting tasks. Returns true if the operation should be skipped. The error // can be a state.Retry if the operation should be retried later. func shouldSkipToAvoidConflict(task *state.Task, snapName string) (bool, error) { - otherTask, err := findLinkSnapTask(task.State(), snapName) + otherTask, err := findLinkSnapTaskForSnap(task.State(), snapName) if err != nil { return false, err } @@ -875,6 +877,7 @@ for i := 0; i < 10; i++ { _, readInfoErr = readInfo(snapsup.InstanceName(), snapsup.SideInfo, errorOnBroken) if readInfoErr == nil { + logger.Debugf("snap %q (%v) available at %q", snapsup.InstanceName(), snapsup.Revision(), snapsup.placeInfo().MountDir()) break } if _, ok := readInfoErr.(*snap.NotFoundError); !ok { @@ -1103,7 +1106,7 @@ return err } - newInfo, err := readInfo(snapsup.InstanceName(), snapsup.SideInfo, 0) + newInfo, err := readInfo(snapsup.InstanceName(), snapsup.SideInfo, errorOnBroken) if err != nil { return err } @@ -1113,8 +1116,15 @@ return err } + st.Lock() + opts, err := GetSnapDirOptions(st) + st.Unlock() + if err != nil { + return err + } + pb := NewTaskProgressAdapterUnlocked(t) - if copyDataErr := m.backend.CopySnapData(newInfo, oldInfo, pb); copyDataErr != nil { + if copyDataErr := m.backend.CopySnapData(newInfo, oldInfo, pb, opts); copyDataErr != nil { if oldInfo != nil { // there is another revision of the snap, cannot remove // shared data directory @@ -1159,8 +1169,15 @@ return err } + st.Lock() + opts, err := GetSnapDirOptions(st) + st.Unlock() + if err != nil { + return err + } + pb := NewTaskProgressAdapterUnlocked(t) - if err := m.backend.UndoCopySnapData(newInfo, oldInfo, pb); err != nil { + if err := m.backend.UndoCopySnapData(newInfo, oldInfo, pb, opts); err != nil { return err } @@ -1332,9 +1349,14 @@ oldCandidateIndex := snapst.LastIndex(cand.Revision) + var oldRevsBeforeCand []snap.Revision if oldCandidateIndex < 0 { snapst.Sequence = append(snapst.Sequence, cand) } else if !snapsup.Revert { + // save the revs before the candidate, so undoLink can account for discarded revs when putting it back + for _, si := range snapst.Sequence[:oldCandidateIndex] { + oldRevsBeforeCand = append(oldRevsBeforeCand, si.Revision) + } // remove the old candidate from the sequence, add it at the end copy(snapst.Sequence[oldCandidateIndex:len(snapst.Sequence)-1], snapst.Sequence[oldCandidateIndex+1:]) snapst.Sequence[len(snapst.Sequence)-1] = cand @@ -1417,7 +1439,7 @@ if !deviceCtx.Classic() && deviceCtx.Model().Base() != "" { linkCtx.RequireMountedSnapdSnap = true } - reboot, err := m.backend.LinkSnap(newInfo, deviceCtx, linkCtx, perfTimings) + needsReboot, err := m.backend.LinkSnap(newInfo, deviceCtx, linkCtx, perfTimings) // defer a cleanup helper which will unlink the snap if anything fails after // this point defer func() { @@ -1479,6 +1501,21 @@ t.Set("old-refresh-inhibited-time", oldRefreshInhibitedTime) t.Set("old-cohort-key", oldCohortKey) t.Set("old-last-refresh-time", oldLastRefreshTime) + t.Set("old-revs-before-cand", oldRevsBeforeCand) + if snapsup.Revert { + t.Set("old-revert-status", snapst.RevertStatus) + switch snapsup.RevertStatus { + case NotBlocked: + if snapst.RevertStatus == nil { + snapst.RevertStatus = make(map[int]RevertStatus) + } + snapst.RevertStatus[oldCurrent.N] = NotBlocked + default: + delete(snapst.RevertStatus, oldCurrent.N) + } + } else { + delete(snapst.RevertStatus, cand.Revision.N) + } // Record the fact that the snap was refreshed successfully. snapst.RefreshInhibitedTime = nil @@ -1548,7 +1585,24 @@ // if we just installed a core snap, request a restart // so that we switch executing its snapd. - m.maybeRestart(t, newInfo, reboot) + var canReboot bool + if needsReboot { + var cannotReboot bool + // system reboot is required, but can this task request that? + if err := t.Get("cannot-reboot", &cannotReboot); err != nil && err != state.ErrNoState { + return err + } + if !cannotReboot { + // either the task was created before that variable was + // introduced or the task can request a reboot + canReboot = true + } else { + t.Logf("reboot postponed to later tasks") + } + } + if !needsReboot || canReboot { + m.maybeRestart(t, newInfo, needsReboot) + } return nil } @@ -1585,7 +1639,7 @@ } t.Logf(restartReason) - st.RequestRestart(state.RestartDaemon) + restart.Request(st, restart.RestartDaemon) } func daemonRestartReason(st *state.State, typ snap.Type) string { @@ -1744,6 +1798,10 @@ if err := t.Get("old-cohort-key", &oldCohortKey); err != nil && err != state.ErrNoState { return err } + var oldRevsBeforeCand []snap.Revision + if err := t.Get("old-revs-before-cand", &oldRevsBeforeCand); err != nil && err != state.ErrNoState { + return err + } if len(snapst.Sequence) == 1 { // XXX: shouldn't these two just log and carry on? this is an undo handler... @@ -1774,6 +1832,10 @@ if oldCandidateIndex < 0 { snapst.Sequence = append(snapst.Sequence[:currentIndex], snapst.Sequence[currentIndex+1:]...) } else if !isRevert { + // account for revisions discarded before the install failed + discarded := countMissingRevs(oldRevsBeforeCand, snapst.Sequence) + oldCandidateIndex -= discarded + oldCand := snapst.Sequence[currentIndex] copy(snapst.Sequence[oldCandidateIndex+1:], snapst.Sequence[oldCandidateIndex:]) snapst.Sequence[oldCandidateIndex] = oldCand @@ -1790,6 +1852,16 @@ snapst.LastRefreshTime = oldLastRefreshTime snapst.CohortKey = oldCohortKey + if isRevert { + var oldRevertStatus map[int]RevertStatus + err := t.Get("old-revert-status", &oldRevertStatus) + if err != nil && err != state.ErrNoState { + return err + } + // may be nil if not set (e.g. created by old snapd) + snapst.RevertStatus = oldRevertStatus + } + newInfo, err := readInfo(snapsup.InstanceName(), snapsup.SideInfo, 0) if err != nil { return err @@ -1871,11 +1943,25 @@ // core snap -> next core snap if release.OnClassic && newInfo.Type() == snap.TypeOS && oldCurrent.Unset() { t.Logf("Requested daemon restart (undo classic initial core install)") - st.RequestRestart(state.RestartDaemon) + restart.Request(st, restart.RestartDaemon) } return nil } +// countMissingRevs counts how many of the revisions aren't present in the sequence of sideInfos +func countMissingRevs(revisions []snap.Revision, sideInfos []*snap.SideInfo) int { + var found int + for _, rev := range revisions { + for _, si := range sideInfos { + if si.Revision == rev { + found++ + } + } + } + + return len(revisions) - found +} + type doSwitchFlags struct { switchCurrentChannel bool } @@ -2345,13 +2431,21 @@ return err } - if err = m.backend.RemoveSnapData(info); err != nil { + st.Lock() + opts, err := GetSnapDirOptions(st) + st.Unlock() + + if err != nil { + return err + } + + if err = m.backend.RemoveSnapData(info, opts); err != nil { return err } if len(snapst.Sequence) == 1 { // Only remove data common between versions if this is the last version - if err = m.backend.RemoveSnapCommonData(info); err != nil { + if err = m.backend.RemoveSnapCommonData(info, opts); err != nil { return err } @@ -2390,6 +2484,9 @@ return fmt.Errorf("internal error: cannot discard snap %q: still active", snapsup.InstanceName()) } + // drop any potential revert status for this revision + delete(snapst.RevertStatus, snapsup.Revision().N) + if len(snapst.Sequence) == 1 { snapst.Sequence = nil snapst.Current = snap.Revision{} @@ -3048,13 +3145,14 @@ } // refreshedSnaps returns the instance names of the snaps successfully refreshed -// in the last batch of refreshes before the given (re-refresh) task. +// in the last batch of refreshes before the given (re-refresh) task; failed is +// true if any of the snaps failed to refresh. // // It does this by advancing through the given task's change's tasks, keeping // track of the instance names from the first SnapSetup in every lane, stopping // when finding the given task, and resetting things when finding a different // re-refresh task (that indicates the end of a batch that isn't the given one). -func refreshedSnaps(reTask *state.Task) []string { +func refreshedSnaps(reTask *state.Task) (snapNames []string, failed bool) { // NOTE nothing requires reTask to be a check-rerefresh task, nor even to be in // a refresh-ish change, but it doesn't make much sense to call this otherwise. tid := reTask.ID() @@ -3096,15 +3194,16 @@ laneSnaps[lane] = snapsup.InstanceName() } - snapNames := make([]string, 0, len(laneSnaps)) + snapNames = make([]string, 0, len(laneSnaps)) for _, name := range laneSnaps { if name == "" { // the lane was unsuccessful + failed = true continue } snapNames = append(snapNames, name) } - return snapNames + return snapNames, failed } // reRefreshSetup holds the necessary details to re-refresh snaps that need it @@ -3140,14 +3239,50 @@ if !changeReadyUpToTask(t) { return &state.Retry{After: reRefreshRetryTimeout, Reason: "pending refreshes"} } - snaps := refreshedSnaps(t) + + snaps, failed := refreshedSnaps(t) + if len(snaps) > 0 { + if err := pruneRefreshCandidates(st, snaps...); err != nil { + return err + } + } + + // if any snap failed to refresh, reconsider validation set tracking + if failed { + tasksets, err := maybeRestoreValidationSetsAndRevertSnaps(st, snaps) + if err != nil { + return err + } + if len(tasksets) > 0 { + chg := t.Change() + for _, taskset := range tasksets { + chg.AddAll(taskset) + } + st.EnsureBefore(0) + t.SetStatus(state.DoneStatus) + return nil + } + // else - validation sets tracking got restored or wasn't affected, carry on + } + if len(snaps) == 0 { // nothing to do (maybe everything failed) return nil } - if err := pruneRefreshCandidates(st, snaps...); err != nil { - return err + // update validation sets stack: there are two possibilities + // - if maybeRestoreValidationSetsAndRevertSnaps restored previous tracking + // or refresh succeeded and it hasn't changed then this is a noop + // (AddCurrentTrackingToValidationSetsStack ignores tracking if identical + // to the topmost stack entry); + // - if maybeRestoreValidationSetsAndRevertSnaps kept new tracking + // because its constraints were met even after partial failure or + // refresh succeeded and tracking got updated, then + // this creates a new copy of validation-sets tracking data. + if AddCurrentTrackingToValidationSetsStack != nil { + if err := AddCurrentTrackingToValidationSetsStack(st); err != nil { + return err + } } var re reRefreshSetup @@ -3216,6 +3351,86 @@ return nil } +// maybeRestoreValidationSetsAndRevertSnaps restores validation-sets to their +// previous state using validation sets stack if there are any enforced +// validation sets and - if necessary - creates tasksets to revert some or all +// of the refreshed snaps to their previous revisions to satisfy the restored +// validation sets tracking. +var maybeRestoreValidationSetsAndRevertSnaps = func(st *state.State, refreshedSnaps []string) ([]*state.TaskSet, error) { + enforcedSets, err := EnforcedValidationSets(st) + if err != nil { + return nil, err + } + if enforcedSets == nil { + // no enforced validation sets, nothing to do + return nil, nil + } + + installedSnaps, ignoreValidation, err := InstalledSnaps(st) + if err != nil { + return nil, err + } + if err := enforcedSets.CheckInstalledSnaps(installedSnaps, ignoreValidation); err == nil { + // validation sets are still correct, nothing to do + return nil, nil + } + + // restore previous validation sets tracking state + if err := RestoreValidationSetsTracking(st); err != nil { + return nil, fmt.Errorf("cannot restore validation sets: %v", err) + } + + // no snaps were refreshed, after restoring validation sets tracking + // there is nothing else to do + if len(refreshedSnaps) == 0 { + return nil, nil + } + + // check installed snaps again against restored validation-sets. + // this may fail which is fine, but it tells us which snaps are + // at invalid revisions and need reverting. + // note: we need to fetch enforced sets again because of RestoreValidationSetsTracking. + enforcedSets, err = EnforcedValidationSets(st) + if err != nil { + return nil, err + } + if enforcedSets == nil { + return nil, fmt.Errorf("internal error: no enforced validation sets after restoring from the stack") + } + err = enforcedSets.CheckInstalledSnaps(installedSnaps, ignoreValidation) + if err == nil { + // all fine after restoring validation sets: this can happen if previous + // validation sets only required a snap (regardless of its revision), then + // after update they require a specific snap revision, so after restoring + // we are back with the good state. + return nil, nil + } + verr, ok := err.(*snapasserts.ValidationSetsValidationError) + if !ok { + return nil, err + } + if len(verr.WrongRevisionSnaps) == 0 { + // if we hit ValidationSetsValidationError but it's not about wrong revisions, + // then something is really broken (we shouldn't have invalid or missing required + // snaps at this point). + return nil, fmt.Errorf("internal error: unexpected validation error of installed snaps after unsuccesfull refresh: %v", verr) + } + // revert some or all snaps + var tss []*state.TaskSet + for _, snapName := range refreshedSnaps { + if verr.WrongRevisionSnaps[snapName] != nil { + // XXX: should we be extra paranoid and use RevertToRevision with + // the specific revision from verr.WrongRevisionSnaps? + ts, err := Revert(st, snapName, Flags{RevertStatus: NotBlocked}) + if err != nil { + return nil, err + } + tss = append(tss, ts) + } + } + return tss, nil +} + // InjectTasks makes all the halt tasks of the mainTask wait for extraTasks; // extraTasks join the same lane and change as the mainTask. func InjectTasks(mainTask *state.Task, extraTasks *state.TaskSet) { @@ -3251,3 +3466,26 @@ InjectTasks(mainTask, state.NewTaskSet(autoConnect)) mainTask.Logf("added auto-connect task") } + +// GetSnapDirOptions returns *dirs.SnapDirOptions configured according to the +// enabled experimental features. The state must be locked by the caller. +var GetSnapDirOptions = getSnapDirOptions + +func getSnapDirOptions(state *state.State) (*dirs.SnapDirOptions, error) { + tr := config.NewTransaction(state) + + hiddenDir, err := features.Flag(tr, features.HiddenSnapDataHomeDir) + if err != nil { + return nil, fmt.Errorf("cannot read feature flag %q: %w", features.HiddenSnapDataHomeDir, err) + } + + return &dirs.SnapDirOptions{HiddenSnapDataDir: hiddenDir}, nil +} + +func MockGetSnapDirOptions(f func(*state.State) (*dirs.SnapDirOptions, error)) (restore func()) { + old := getSnapDirOptions + GetSnapDirOptions = f + return func() { + GetSnapDirOptions = old + } +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_link_test.go snapd-2.54.2+21.10/overlord/snapstate/handlers_link_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_link_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/handlers_link_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016 Canonical Ltd + * Copyright (C) 2016-2021 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -36,6 +36,7 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/configstate/config" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/servicestate" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" @@ -49,29 +50,19 @@ type linkSnapSuite struct { baseHandlerSuite - stateBackend *witnessRestartReqStateBackend + restartRequested []restart.RestartType } var _ = Suite(&linkSnapSuite{}) -type witnessRestartReqStateBackend struct { - restartRequested []state.RestartType -} - -func (b *witnessRestartReqStateBackend) Checkpoint([]byte) error { - return nil -} - -func (b *witnessRestartReqStateBackend) RequestRestart(t state.RestartType) { - b.restartRequested = append(b.restartRequested, t) -} - -func (b *witnessRestartReqStateBackend) EnsureBefore(time.Duration) {} - func (s *linkSnapSuite) SetUpTest(c *C) { - s.stateBackend = &witnessRestartReqStateBackend{} + s.baseHandlerSuite.SetUpTest(c) - s.setup(c, s.stateBackend) + s.state.Lock() + restart.Init(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) { + s.restartRequested = append(s.restartRequested, t) + })) + s.state.Unlock() s.AddCleanup(snapstatetest.MockDeviceModel(DefaultModel())) @@ -79,6 +70,7 @@ snapstate.SnapServiceOptions = servicestate.SnapServiceOptions s.AddCleanup(func() { snapstate.SnapServiceOptions = oldSnapServiceOptions + s.restartRequested = nil }) } @@ -141,7 +133,7 @@ c.Check(snapst.UserID, Equals, 2) c.Check(snapst.CohortKey, Equals, "") c.Check(t.Status(), Equals, state.DoneStatus) - c.Check(s.stateBackend.restartRequested, HasLen, 0) + c.Check(s.restartRequested, HasLen, 0) // we end with the auxiliary store info c.Check(snapstate.AuxStoreInfoFilename("foo-id"), testutil.FilePresent) @@ -192,7 +184,7 @@ c.Check(snapst.UserID, Equals, 2) c.Check(snapst.CohortKey, Equals, "wobbling") c.Check(t.Status(), Equals, state.DoneStatus) - c.Check(s.stateBackend.restartRequested, HasLen, 0) + c.Check(s.restartRequested, HasLen, 0) // we end with the auxiliary store info c.Check(snapstate.AuxStoreInfoFilename("foo-id"), testutil.FilePresent) @@ -759,7 +751,7 @@ c.Check(typ, Equals, snap.TypeOS) c.Check(t.Status(), Equals, state.DoneStatus) - c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartDaemon}) + c.Check(s.restartRequested, DeepEquals, []restart.RestartType{restart.RestartDaemon}) c.Check(t.Log(), HasLen, 1) c.Check(t.Log()[0], Matches, `.*INFO Requested daemon restart\.`) } @@ -801,7 +793,7 @@ c.Check(typ, Equals, snap.TypeSnapd) c.Check(t.Status(), Equals, state.DoneStatus) - c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartDaemon}) + c.Check(s.restartRequested, DeepEquals, []restart.RestartType{restart.RestartDaemon}) c.Check(t.Log(), HasLen, 1) c.Check(t.Log()[0], Matches, `.*INFO Requested daemon restart \(snapd snap\)\.`) } @@ -818,10 +810,6 @@ s.state.Lock() defer s.state.Unlock() - // we need to init the boot-id - err := s.state.VerifyReboot("some-boot-id") - c.Assert(err, IsNil) - si := &snap.SideInfo{ RealName: "core18", SnapID: "core18-id", @@ -839,7 +827,7 @@ s.state.Lock() c.Check(t.Status(), Equals, state.DoneStatus) - c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartSystem}) + c.Check(s.restartRequested, DeepEquals, []restart.RestartType{restart.RestartSystem}) c.Assert(t.Log(), HasLen, 1) c.Check(t.Log()[0], Matches, `.*INFO Requested system restart.*`) } @@ -857,8 +845,8 @@ defer s.state.Unlock() // we need to init the boot-id - err := s.state.VerifyReboot("some-boot-id") - c.Assert(err, IsNil) + //err := s.state.VerifyReboot("some-boot-id") + //c.Assert(err, IsNil) si := &snap.SideInfo{ RealName: "core18", @@ -879,7 +867,7 @@ s.state.Lock() c.Check(t.Status(), Equals, state.DoneStatus) - c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartSystemNow}) + c.Check(s.restartRequested, DeepEquals, []restart.RestartType{restart.RestartSystemNow}) c.Assert(t.Log(), HasLen, 1) c.Check(t.Log()[0], Matches, `.*INFO Requested system restart.*`) } @@ -918,7 +906,7 @@ c.Check(typ, Equals, snap.TypeSnapd) c.Check(t.Status(), Equals, state.DoneStatus) - c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartDaemon}) + c.Check(s.restartRequested, DeepEquals, []restart.RestartType{restart.RestartDaemon}) c.Check(t.Log(), HasLen, 1) } @@ -965,7 +953,7 @@ c.Check(typ, Equals, snap.TypeOS) c.Check(t.Status(), Equals, state.DoneStatus) - c.Check(s.stateBackend.restartRequested, IsNil) + c.Check(s.restartRequested, IsNil) c.Check(t.Log(), HasLen, 0) } @@ -1253,7 +1241,7 @@ c.Check(snapst.Current, Equals, snap.R(1)) c.Check(t.Status(), Equals, state.UndoneStatus) - c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartDaemon}) + c.Check(s.restartRequested, DeepEquals, []restart.RestartType{restart.RestartDaemon}) c.Check(lp.instanceNames, DeepEquals, []string{"core", "core"}) } @@ -1268,9 +1256,6 @@ s.state.Lock() defer s.state.Unlock() - // we need to init the boot-id - err := s.state.VerifyReboot("some-boot-id") - c.Assert(err, IsNil) si1 := &snap.SideInfo{ RealName: "core18", @@ -1305,14 +1290,14 @@ s.state.Lock() var snapst snapstate.SnapState - err = snapstate.Get(s.state, "core18", &snapst) + err := snapstate.Get(s.state, "core18", &snapst) c.Assert(err, IsNil) c.Check(snapst.Active, Equals, true) c.Check(snapst.Sequence, HasLen, 1) c.Check(snapst.Current, Equals, snap.R(1)) c.Check(t.Status(), Equals, state.UndoneStatus) - c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartSystem}) + c.Check(s.restartRequested, DeepEquals, []restart.RestartType{restart.RestartSystem}) } func (s *linkSnapSuite) TestDoUndoLinkSnapCoreClassic(c *C) { @@ -1353,7 +1338,7 @@ c.Assert(err, Equals, state.ErrNoState) c.Check(t.Status(), Equals, state.UndoneStatus) - c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartDaemon, state.RestartDaemon}) + c.Check(s.restartRequested, DeepEquals, []restart.RestartType{restart.RestartDaemon, restart.RestartDaemon}) } @@ -1452,7 +1437,7 @@ defer s.state.Unlock() c.Check(t.Status(), Equals, state.ErrorStatus) - c.Check(s.stateBackend.restartRequested, HasLen, 0) + c.Check(s.restartRequested, HasLen, 0) // we end without the auxiliary store info c.Check(snapstate.AuxStoreInfoFilename("foo-id"), testutil.FileAbsent) @@ -1749,7 +1734,7 @@ c.Check(s.fakeBackend.ops, DeepEquals, expected) // 2 restarts, one from link snap, another one from undo - c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartDaemon, state.RestartDaemon}) + c.Check(s.restartRequested, DeepEquals, []restart.RestartType{restart.RestartDaemon, restart.RestartDaemon}) c.Check(t.Log(), HasLen, 3) c.Check(t.Log()[0], Matches, `.*INFO Requested daemon restart \(snapd snap\)\.`) c.Check(t.Log()[2], Matches, `.*INFO Requested daemon restart \(snapd snap\)\.`) @@ -1822,7 +1807,7 @@ // 1 restart from link snap, the other restart happens // in undoUnlinkCurrentSnap (not tested here) - c.Check(s.stateBackend.restartRequested, DeepEquals, []state.RestartType{state.RestartDaemon}) + c.Check(s.restartRequested, DeepEquals, []restart.RestartType{restart.RestartDaemon}) c.Assert(t.Log(), HasLen, 1) c.Check(t.Log()[0], Matches, `.*INFO Requested daemon restart \(snapd snap\)\.`) } @@ -1930,7 +1915,7 @@ err := s.snapmgr.MaybeUndoRemodelBootChanges(t) c.Assert(err, IsNil) - c.Check(s.stateBackend.restartRequested, HasLen, 0) + c.Check(s.restartRequested, HasLen, 0) } func (s *linkSnapSuite) TestMaybeUndoRemodelBootChangesSameKernel(c *C) { @@ -1949,7 +1934,7 @@ err := s.snapmgr.MaybeUndoRemodelBootChanges(t) c.Assert(err, IsNil) - c.Check(s.stateBackend.restartRequested, HasLen, 0) + c.Check(s.restartRequested, HasLen, 0) } func (s *linkSnapSuite) TestMaybeUndoRemodelBootChangesNeedsUndo(c *C) { @@ -1965,10 +1950,6 @@ restore = snapstate.MockSnapReadInfo(snap.ReadInfo) defer restore() - // we need to init the boot-id - err := s.state.VerifyReboot("some-boot-id") - c.Assert(err, IsNil) - // we pretend we do a remodel from kernel -> new-kernel s.setMockKernelRemodelCtx(c, "kernel", "new-kernel") @@ -2005,7 +1986,7 @@ }) // now we simulate that the new kernel is getting undone - err = s.snapmgr.MaybeUndoRemodelBootChanges(t) + err := s.snapmgr.MaybeUndoRemodelBootChanges(t) c.Assert(err, IsNil) // that will schedule a boot into the previous kernel @@ -2014,8 +1995,8 @@ "snap_kernel": "new-kernel_1.snap", "snap_try_kernel": "kernel_1.snap", }) - c.Check(s.stateBackend.restartRequested, HasLen, 1) - c.Check(s.stateBackend.restartRequested[0], Equals, state.RestartSystem) + c.Check(s.restartRequested, HasLen, 1) + c.Check(s.restartRequested[0], Equals, restart.RestartSystem) } func (s *linkSnapSuite) testDoLinkSnapWithToolingDependency(c *C, classicOrBase string) { diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_mount_test.go snapd-2.54.2+21.10/overlord/snapstate/handlers_mount_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_mount_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/handlers_mount_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -40,7 +40,7 @@ var _ = Suite(&mountSnapSuite{}) func (s *mountSnapSuite) SetUpTest(c *C) { - s.setup(c, nil) + s.baseHandlerSuite.SetUpTest(c) s.AddCleanup(snapstatetest.MockDeviceModel(DefaultModel())) } diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_prepare_test.go snapd-2.54.2+21.10/overlord/snapstate/handlers_prepare_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_prepare_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/handlers_prepare_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -44,13 +44,13 @@ fakeBackend *fakeSnappyBackend } -func (s *baseHandlerSuite) setup(c *C, b state.Backend) { +func (s *baseHandlerSuite) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) dirs.SetRootDir(c.MkDir()) s.fakeBackend = &fakeSnappyBackend{} - s.state = state.New(b) + s.state = state.New(nil) s.runner = state.NewTaskRunner(s.state) var err error @@ -83,10 +83,6 @@ s.AddCleanup(restoreSecurityProfilesDiscardLate) } -func (s *baseHandlerSuite) SetUpTest(c *C) { - s.setup(c, nil) -} - type prepareSnapSuite struct { baseHandlerSuite } diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_prereq_test.go snapd-2.54.2+21.10/overlord/snapstate/handlers_prereq_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_prereq_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/handlers_prereq_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -48,7 +48,7 @@ var _ = Suite(&prereqSuite{}) func (s *prereqSuite) SetUpTest(c *C) { - s.setup(c, nil) + s.baseHandlerSuite.SetUpTest(c) s.fakeStore = &fakeStore{ state: s.state, diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_rerefresh_test.go snapd-2.54.2+21.10/overlord/snapstate/handlers_rerefresh_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_rerefresh_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/handlers_rerefresh_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -28,9 +28,12 @@ . "gopkg.in/check.v1" + "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/asserts/snapasserts" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" + "github.com/snapcore/snapd/snap/snaptest" . "github.com/snapcore/snapd/testutil" ) @@ -204,7 +207,7 @@ // wrapper around snapstate.RefreshedSnaps for easier testing func refreshedSnaps(task *state.Task) string { - snaps := snapstate.RefreshedSnaps(task) + snaps, _ := snapstate.RefreshedSnaps(task) sort.Strings(snaps) return strings.Join(snaps, ",") } @@ -369,3 +372,228 @@ c.Check(snapstate.ReRefreshFilter(&snap.Info{Epoch: snap.E("0")}, snapst), Equals, false) c.Check(snapstate.ReRefreshFilter(&snap.Info{Epoch: snap.Epoch{}}, snapst), Equals, false) } + +func (s *refreshSuite) TestMaybeRestoreValidationSetsAndRevertSnaps(c *C) { + restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { + return nil, nil + }) + defer restore() + + st := s.state + st.Lock() + defer st.Unlock() + + refreshedSnaps := []string{"foo", "bar"} + // nothing to do with no enforced validation sets + ts, err := snapstate.MaybeRestoreValidationSetsAndRevertSnaps(st, refreshedSnaps) + c.Assert(err, IsNil) + c.Check(ts, IsNil) +} + +func (s *validationSetsSuite) TestMaybeRestoreValidationSetsAndRevertSnapsOneRevert(c *C) { + var enforcedValidationSetsCalled int + restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { + enforcedValidationSetsCalled++ + + vs := snapasserts.NewValidationSets() + var snap1, snap2, snap3 map[string]interface{} + snap3 = map[string]interface{}{ + "id": "abcKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap3", + "presence": "required", + } + + switch enforcedValidationSetsCalled { + case 1: + // refreshed validation sets + snap1 = map[string]interface{}{ + "id": "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap1", + "presence": "required", + "revision": "3", + } + // require snap2 at revision 5 (if snap refresh succeeded, but it didn't, so + // current revision of the snap is wrong) + snap2 = map[string]interface{}{ + "id": "bgtKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap2", + "presence": "required", + "revision": "5", + } + case 2: + // validation sets restored from history + snap1 = map[string]interface{}{ + "id": "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap1", + "presence": "required", + "revision": "1", + } + snap2 = map[string]interface{}{ + "id": "bgtKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap2", + "presence": "required", + "revision": "2", + } + default: + c.Fatalf("unexpected call to EnforcedValidatioSets") + } + vsa1 := s.mockValidationSetAssert(c, "bar", "2", snap1, snap2, snap3) + vs.Add(vsa1.(*asserts.ValidationSet)) + return vs, nil + }) + defer restore() + + var restoreValidationSetsTrackingCalled int + restoreRestoreValidationSetsTracking := snapstate.MockRestoreValidationSetsTracking(func(*state.State) error { + restoreValidationSetsTrackingCalled++ + return nil + }) + defer restoreRestoreValidationSetsTracking() + + st := s.state + st.Lock() + defer st.Unlock() + + // snaps installed after partial refresh + si1 := &snap.SideInfo{RealName: "some-snap1", SnapID: "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(3)} + si11 := &snap.SideInfo{RealName: "some-snap1", SnapID: "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(1)} + snapstate.Set(s.state, "some-snap1", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si11, si1}, + Current: snap.R(3), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-snap1`, si1) + + // some-snap2 failed to refresh and remains at revision 2 + si2 := &snap.SideInfo{RealName: "some-snap2", SnapID: "bgtKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(2)} + snapstate.Set(s.state, "some-snap2", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si2}, + Current: snap.R(2), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-snap2`, si2) + + si3 := &snap.SideInfo{RealName: "some-snap3", SnapID: "abcKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(3)} + snapstate.Set(s.state, "some-snap3", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si3}, + Current: snap.R(3), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-snap3`, si3) + + // some-snap2 failed to refresh + refreshedSnaps := []string{"some-snap1", "some-snap3"} + ts, err := snapstate.MaybeRestoreValidationSetsAndRevertSnaps(st, refreshedSnaps) + c.Assert(err, IsNil) + + // we expect revert of snap1 + c.Assert(ts, HasLen, 1) + revertTasks := ts[0].Tasks() + c.Assert(taskKinds(revertTasks), DeepEquals, []string{ + "prerequisites", + "prepare-snap", + "stop-snap-services", + "remove-aliases", + "unlink-current-snap", + "setup-profiles", + "link-snap", + "auto-connect", + "set-auto-aliases", + "setup-aliases", + "start-snap-services", + "run-hook[configure]", + "run-hook[check-health]", + }) + + snapsup, err := snapstate.TaskSnapSetup(revertTasks[0]) + c.Assert(err, IsNil) + c.Check(snapsup.Flags, Equals, snapstate.Flags{Revert: true, RevertStatus: snapstate.NotBlocked}) + c.Check(snapsup.InstanceName(), Equals, "some-snap1") + c.Check(snapsup.Revision(), Equals, snap.R(1)) + + c.Check(restoreValidationSetsTrackingCalled, Equals, 1) + c.Check(enforcedValidationSetsCalled, Equals, 2) +} + +func (s *validationSetsSuite) TestMaybeRestoreValidationSetsAndRevertJustValidationSetsRestore(c *C) { + var enforcedValidationSetsCalled int + restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { + enforcedValidationSetsCalled++ + + vs := snapasserts.NewValidationSets() + var snap1, snap2 map[string]interface{} + snap2 = map[string]interface{}{ + "id": "abcKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap2", + "presence": "required", + } + + switch enforcedValidationSetsCalled { + case 1: + // refreshed validation sets + // snap1 revision 3 is now required (but snap wasn't refreshed) + snap1 = map[string]interface{}{ + "id": "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap1", + "presence": "required", + "revision": "3", + } + case 2: + // validation sets restored from history + snap1 = map[string]interface{}{ + "id": "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap1", + "presence": "required", + "revision": "1", + } + default: + c.Fatalf("unexpected call to EnforcedValidatioSets") + } + vsa1 := s.mockValidationSetAssert(c, "bar", "2", snap1, snap2) + vs.Add(vsa1.(*asserts.ValidationSet)) + return vs, nil + }) + defer restore() + + var restoreValidationSetsTrackingCalled int + restoreRestoreValidationSetsTracking := snapstate.MockRestoreValidationSetsTracking(func(*state.State) error { + restoreValidationSetsTrackingCalled++ + return nil + }) + defer restoreRestoreValidationSetsTracking() + + st := s.state + st.Lock() + defer st.Unlock() + + // snaps in the system after partial refresh + si1 := &snap.SideInfo{RealName: "some-snap1", SnapID: "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(1)} + snapstate.Set(s.state, "some-snap1", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si1}, + Current: snap.R(1), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-snap1`, si1) + + si3 := &snap.SideInfo{RealName: "some-snap2", SnapID: "abcKhntON3vR7kwEbVPsILm7bUViPDzx", Revision: snap.R(3)} + snapstate.Set(s.state, "some-snap2", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si3}, + Current: snap.R(3), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-snap2`, si3) + + refreshedSnaps := []string{"some-snap2"} + ts, err := snapstate.MaybeRestoreValidationSetsAndRevertSnaps(st, refreshedSnaps) + c.Assert(err, IsNil) + + // we expect no snap reverts + c.Assert(ts, HasLen, 0) + c.Check(restoreValidationSetsTrackingCalled, Equals, 1) + c.Check(enforcedValidationSetsCalled, Equals, 2) +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_test.go snapd-2.54.2+21.10/overlord/snapstate/handlers_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/handlers_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/handlers_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -24,6 +24,9 @@ . "gopkg.in/check.v1" + "github.com/snapcore/snapd/dirs" + "github.com/snapcore/snapd/features" + "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" "github.com/snapcore/snapd/overlord/state" @@ -33,14 +36,12 @@ type handlersSuite struct { baseHandlerSuite - - stateBackend *witnessRestartReqStateBackend } var _ = Suite(&handlersSuite{}) func (s *handlersSuite) SetUpTest(c *C) { - s.setup(c, s.stateBackend) + s.baseHandlerSuite.SetUpTest(c) s.AddCleanup(snapstatetest.MockDeviceModel(DefaultModel())) } @@ -283,3 +284,22 @@ c.Assert(logs, HasLen, 1) c.Check(logs[0], testutil.Contains, "ERROR something failed") } + +func (s *handlersSuite) TestGetSnapDirOptions(c *C) { + s.state.Lock() + defer s.state.Unlock() + + tr := config.NewTransaction(s.state) + + // set feature flag + confKey := fmt.Sprintf("experimental.%s", features.HiddenSnapDataHomeDir) + err := tr.Set("core", confKey, "true") + c.Assert(err, IsNil) + + tr.Commit() + + // check options reflect flag + opts, err := snapstate.GetSnapDirOptions(s.state) + c.Assert(err, IsNil) + c.Check(opts, DeepEquals, &dirs.SnapDirOptions{HiddenSnapDataDir: true}) +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/refreshhints.go snapd-2.54.2+21.10/overlord/snapstate/refreshhints.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/refreshhints.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/refreshhints.go 2022-01-06 21:25:16.000000000 +0000 @@ -23,8 +23,10 @@ "fmt" "time" + "github.com/snapcore/snapd/features" "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" "github.com/snapcore/snapd/snap" @@ -195,9 +197,27 @@ // pruneRefreshCandidates removes the given snaps from refresh-candidates map // in the state. func pruneRefreshCandidates(st *state.State, snaps ...string) error { + tr := config.NewTransaction(st) + gateAutoRefreshHook, err := features.Flag(tr, features.GateAutoRefreshHook) + if err != nil && !config.IsNoOption(err) { + return err + } + // Remove refresh-candidates from state if gate-auto-refresh-hook feature is + // not enabled. This acts as a workaround for the case where a snapd from + // edge was used and created refresh-candidates in the old format (an array) + // with the feature enabled, but the feature was then disabled so the new + // map format will never make it into the state. + // When the feature is enabled then auto-refresh code will re-initialize + // refresh-candidates in the correct format expected here. + // See https://forum.snapcraft.io/t/cannot-r-emove-snap-json-cannot-unmarshal-array-into-go-value-of-type-map-string-snapstate-refreshcandidate/27276 + if !gateAutoRefreshHook { + st.Set("refresh-candidates", nil) + return nil + } + var candidates map[string]*refreshCandidate - err := st.Get("refresh-candidates", &candidates) + err = st.Get("refresh-candidates", &candidates) if err != nil { if err == state.ErrNoState { return nil diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/refreshhints_test.go snapd-2.54.2+21.10/overlord/snapstate/refreshhints_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/refreshhints_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/refreshhints_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -30,6 +30,7 @@ "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/builtin" "github.com/snapcore/snapd/overlord/auth" + "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/ifacestate/ifacerepo" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" @@ -334,6 +335,11 @@ st.Lock() defer st.Unlock() + // enable gate-auto-refresh-hook feature + tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.gate-auto-refresh-hook", true) + tr.Commit() + // check that calling PruneRefreshCandidates when there is nothing to do is fine. c.Assert(snapstate.PruneRefreshCandidates(st, "some-snap"), IsNil) @@ -390,6 +396,24 @@ c.Check(ok, Equals, true) } +func (s *refreshHintsTestSuite) TestPruneRefreshCandidatesIncorrectFormat(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + // bad format - an array + candidates := []*snapstate.RefreshCandidate{{ + SnapSetup: snapstate.SnapSetup{Type: "app", SideInfo: &snap.SideInfo{RealName: "snap-a", Revision: snap.R(1)}}, + }} + st.Set("refresh-candidates", candidates) + + // it doesn't fail as long as experimental.gate-auto-refresh-hook is not enabled + c.Assert(snapstate.PruneRefreshCandidates(st, "snap-a"), IsNil) + var data interface{} + // and refresh-candidates has been removed from the state + c.Check(st.Get("refresh-candidates", data), Equals, state.ErrNoState) +} + func (s *refreshHintsTestSuite) TestRefreshHintsNotApplicableWrongArch(c *C) { s.state.Lock() snapstate.Set(s.state, "other-snap", &snapstate.SnapState{ diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/snapmgr.go snapd-2.54.2+21.10/overlord/snapstate/snapmgr.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/snapmgr.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/snapmgr.go 2022-01-06 21:25:16.000000000 +0000 @@ -131,11 +131,25 @@ return snap.MountFile(snapsup.InstanceName(), snapsup.Revision()) } +// RevertStatus is a status of a snap revert; anything other than DefaultStatus +// denotes a reverted snap revision that needs special handling in terms of +// refresh blocking. +type RevertStatus int + +const ( + DefaultStatus RevertStatus = iota + NotBlocked +) + // SnapState holds the state for a snap installed in the system. type SnapState struct { SnapType string `json:"type"` // Use Type and SetType Sequence []*snap.SideInfo `json:"sequence"` - Active bool `json:"active,omitempty"` + + // RevertStatus maps revisions to RevertStatus for revisions that + // need special handling in Block(). + RevertStatus map[int]RevertStatus `json:"revert-status,omitempty"` + Active bool `json:"active,omitempty"` // LastActiveDisabledServices is a list of services that were disabled in // this snap when it was last active - i.e. when it was disabled, before @@ -264,16 +278,23 @@ } // Block returns revisions that should be blocked on refreshes, -// computed from Sequence[currentRevisionIndex+1:]. +// computed from Sequence[currentRevisionIndex+1:] and considering +// special casing resulting from snapst.RevertStatus map. func (snapst *SnapState) Block() []snap.Revision { - // return revisions from Sequence[currentIndex:] + // return revisions from Sequence[currentIndex:], potentially excluding + // some of them based on RevertStatus. currentIndex := snapst.LastIndex(snapst.Current) if currentIndex < 0 || currentIndex+1 == len(snapst.Sequence) { return nil } - out := make([]snap.Revision, len(snapst.Sequence)-currentIndex-1) - for i, si := range snapst.Sequence[currentIndex+1:] { - out[i] = si.Revision + out := []snap.Revision{} + for _, si := range snapst.Sequence[currentIndex+1:] { + if status, ok := snapst.RevertStatus[si.Revision.N]; ok { + if status == NotBlocked { + continue + } + } + out = append(out, si.Revision) } return out } diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstate.go snapd-2.54.2+21.10/overlord/snapstate/snapstate.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstate.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/snapstate.go 2022-01-06 21:25:16.000000000 +0000 @@ -43,6 +43,7 @@ "github.com/snapcore/snapd/overlord/auth" "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/ifacestate/ifacerepo" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate/backend" "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/release" @@ -65,10 +66,14 @@ ) const ( - DownloadAndChecksDoneEdge = state.TaskSetEdge("download-and-checks-done") - BeginEdge = state.TaskSetEdge("begin") - BeforeHooksEdge = state.TaskSetEdge("before-hooks") - HooksEdge = state.TaskSetEdge("hooks") + BeginEdge = state.TaskSetEdge("begin") + BeforeHooksEdge = state.TaskSetEdge("before-hooks") + HooksEdge = state.TaskSetEdge("hooks") + BeforeMaybeRebootEdge = state.TaskSetEdge("before-maybe-reboot") + MaybeRebootEdge = state.TaskSetEdge("maybe-reboot") + MaybeRebootWaitEdge = state.TaskSetEdge("maybe-reboot-wait") + AfterMaybeRebootWaitEdge = state.TaskSetEdge("after-maybe-reboot-wait") + LastBeforeLocalModificationsEdge = state.TaskSetEdge("last-before-local-modifications") ) var ErrNothingToDo = errors.New("nothing to do") @@ -160,6 +165,46 @@ return &snapsup, snapst, nil } +// pathInfo holds information about a path install +type pathInfo struct { + *snap.Info + path string + sideInfo *snap.SideInfo +} + +func (i pathInfo) DownloadSize() int64 { + return i.Size +} + +// SnapBase returns the base snap of the snap. +func (i pathInfo) SnapBase() string { + return i.Base +} + +func (i pathInfo) Prereq(st *state.State) []string { + return getKeys(defaultProviderContentAttrs(st, i.Info)) +} + +func (i pathInfo) SnapSetupForUpdate(st *state.State, params updateParamsFunc, _ int, _ *Flags) (*SnapSetup, *SnapState, error) { + update := i.Info + + _, flags, snapst := params(update) + + providerContentAttrs := defaultProviderContentAttrs(st, update) + snapsup := SnapSetup{ + Base: i.Base, + Prereq: getKeys(providerContentAttrs), + PrereqContentAttrs: providerContentAttrs, + SideInfo: i.sideInfo, + SnapPath: i.path, + Flags: flags.ForSnapSetup(), + Type: i.Type(), + PlugsOnly: len(i.Slots) == 0, + InstanceKey: i.InstanceKey, + } + return &snapsup, snapst, nil +} + // soundness check var _ readyUpdateInfo = installSnapInfo{} @@ -450,7 +495,11 @@ retain = 3 } } - retain-- // we're adding one + + // if we're not using an already present revision, account for the one being added + if snapst.LastIndex(targetRevision) == -1 { + retain-- // we're adding one + } seq := snapst.Sequence currentIndex := snapst.LastIndex(snapst.Current) @@ -510,18 +559,27 @@ installSet.WaitAll(ts) installSet.MarkEdge(prereq, BeginEdge) installSet.MarkEdge(setupAliases, BeforeHooksEdge) + installSet.MarkEdge(setupSecurity, BeforeMaybeRebootEdge) + installSet.MarkEdge(linkSnap, MaybeRebootEdge) + installSet.MarkEdge(autoConnect, MaybeRebootWaitEdge) + installSet.MarkEdge(setAutoAliases, AfterMaybeRebootWaitEdge) if installHook != nil { installSet.MarkEdge(installHook, HooksEdge) } - ts.AddAllWithEdges(installSet) + // if snap is being installed from the store, then the last task before + // any system modifications are done is check validate-snap, otherwise + // it's the prepare-snap if checkAsserts != nil { - ts.MarkEdge(checkAsserts, DownloadAndChecksDoneEdge) + installSet.MarkEdge(checkAsserts, LastBeforeLocalModificationsEdge) + } else { + installSet.MarkEdge(prepare, LastBeforeLocalModificationsEdge) } - if flags&skipConfigure != 0 { return installSet, nil } + ts.AddAllWithEdges(installSet) + // we do not support configuration for bases or the "snapd" snap yet if snapsup.Type != snap.TypeBase && snapsup.Type != snap.TypeSnapd { confFlags := 0 @@ -594,7 +652,7 @@ // For snapd snap updates this will also rerun wrappers generation to fully // catch up with any change. func FinishRestart(task *state.Task, snapsup *SnapSetup) (err error) { - if ok, _ := task.State().Restarting(); ok { + if ok, _ := restart.Pending(task.State()); ok { // don't continue until we are in the restarted snapd task.Logf("Waiting for automatic snapd restart...") return &state.Retry{} @@ -696,11 +754,11 @@ // something else will error chg.Get("system-restart-immediate", &immediate) } - rst := state.RestartSystem + rst := restart.RestartSystem if immediate { - rst = state.RestartSystemNow + rst = restart.RestartSystemNow } - task.State().RequestRestart(rst) + restart.Request(task.State(), rst) } func contentAttr(attrer interfaces.Attrer) string { @@ -879,7 +937,7 @@ } } - channel, err = resolveChannel(st, instanceName, snapst.TrackingChannel, channel, deviceCtx) + channel, err = resolveChannel(instanceName, snapst.TrackingChannel, channel, deviceCtx) if err != nil { return nil, nil, err } @@ -950,7 +1008,9 @@ // Install returns a set of tasks for installing a snap. // Note that the state must be locked by the caller. // -// The returned TaskSet will contain a DownloadAndChecksDoneEdge. +// The returned TaskSet will contain a LastBeforeLocalModificationsEdge +// identifying the last task before the first task that introduces system +// modifications. func Install(ctx context.Context, st *state.State, name string, opts *RevisionOptions, userID int, flags Flags) (*state.TaskSet, error) { return InstallWithDeviceContext(ctx, st, name, opts, userID, flags, nil, "") } @@ -959,7 +1019,9 @@ // It will query for the snap with the given deviceCtx. // Note that the state must be locked by the caller. // -// The returned TaskSet will contain a DownloadAndChecksDoneEdge. +// The returned TaskSet will contain a LastBeforeLocalModificationsEdge +// identifying the last task before the first task that introduces system +// modifications. func InstallWithDeviceContext(ctx context.Context, st *state.State, name string, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext, fromChange string) (*state.TaskSet, error) { if opts == nil { opts = &RevisionOptions{} @@ -1005,31 +1067,9 @@ return nil, err } - tr := config.NewTransaction(st) - checkDiskSpaceInstall, err := features.Flag(tr, features.CheckDiskSpaceInstall) - if err != nil && !config.IsNoOption(err) { + if err := checkDiskSpace(st, "install", []minimalInstallInfo{installSnapInfo{info}}, userID); err != nil { return nil, err } - if checkDiskSpaceInstall { - // check if there is enough disk space for requested snap and its - // prerequisites. - totalSize, err := installSize(st, []minimalInstallInfo{installSnapInfo{info}}, userID) - if err != nil { - return nil, err - } - requiredSpace := safetyMarginDiskSpace(totalSize) - path := dirs.SnapdStateDir(dirs.GlobalRootDir) - if err := osutilCheckFreeSpace(path, requiredSpace); err != nil { - if _, ok := err.(*osutil.NotEnoughDiskSpaceError); ok { - return nil, &InsufficientSpaceError{ - Path: path, - Snaps: []string{info.InstanceName()}, - ChangeKind: "install", - } - } - return nil, err - } - } providerContentAttrs := defaultProviderContentAttrs(st, info) snapsup := &SnapSetup{ @@ -1058,6 +1098,74 @@ return doInstall(st, &snapst, snapsup, 0, fromChange, nil) } +func InstallPathMany(ctx context.Context, st *state.State, sideInfos []*snap.SideInfo, paths []string, userID int, flags *Flags) ([]*state.TaskSet, error) { + if flags == nil { + flags = &Flags{} + } + + deviceCtx, err := DevicePastSeeding(st, nil) + if err != nil { + return nil, err + } + + var updates []minimalInstallInfo + var names []string + stateByInstanceName := make(map[string]*SnapState, len(paths)) + flagsByInstanceName := make(map[string]Flags, len(paths)) + + for i, path := range paths { + si := sideInfos[i] + name := si.RealName + + info, container, err := backend.OpenSnapFile(path, si) + if err != nil { + return nil, err + } + + if err := validateContainer(container, info, logger.Noticef); err != nil { + return nil, err + } + if err := snap.ValidateInstanceName(name); err != nil { + return nil, fmt.Errorf("invalid instance name: %v", err) + } + + var snapst SnapState + if err = Get(st, name, &snapst); err != nil && err != state.ErrNoState { + return nil, err + } + + flags, err := earlyChecks(st, &snapst, info, *flags) + if err != nil { + return nil, err + } + + if !(flags.JailMode || flags.DevMode) { + flags.Classic = flags.Classic || snapst.Flags.Classic + } + + updates = append(updates, pathInfo{Info: info, path: path, sideInfo: si}) + names = append(names, name) + stateByInstanceName[name] = &snapst + flagsByInstanceName[name] = flags + } + + if err := checkDiskSpace(st, "install", updates, userID); err != nil { + return nil, err + } + + params := func(update *snap.Info) (*RevisionOptions, Flags, *SnapState) { + name := update.InstanceName() + return nil, flagsByInstanceName[name], stateByInstanceName[name] + } + + _, tasksets, err := doUpdate(ctx, st, names, updates, params, userID, flags, deviceCtx, "") + if err != nil { + return nil, err + } + + return tasksets, nil +} + // InstallMany installs everything from the given list of names. // Note that the state must be locked by the caller. func InstallMany(st *state.State, names []string, userID int) ([]string, []*state.TaskSet, error) { @@ -1067,6 +1175,8 @@ return nil, nil, err } + names = strutil.Deduplicate(names) + toInstall := make([]string, 0, len(names)) for _, name := range names { var snapst SnapState @@ -1095,35 +1205,13 @@ return nil, nil, err } - tr := config.NewTransaction(st) - checkDiskSpaceInstall, err := features.Flag(tr, features.CheckDiskSpaceInstall) - if err != nil && !config.IsNoOption(err) { - return nil, nil, err + snapInfos := make([]minimalInstallInfo, len(installs)) + for i, sar := range installs { + snapInfos[i] = installSnapInfo{sar.Info} } - if checkDiskSpaceInstall { - // check if there is enough disk space for requested snaps and their - // prerequisites. - snapInfos := make([]minimalInstallInfo, len(installs)) - for i, sar := range installs { - snapInfos[i] = installSnapInfo{sar.Info} - } - totalSize, err := installSize(st, snapInfos, userID) - if err != nil { - return nil, nil, err - } - requiredSpace := safetyMarginDiskSpace(totalSize) - path := dirs.SnapdStateDir(dirs.GlobalRootDir) - if err := osutilCheckFreeSpace(path, requiredSpace); err != nil { - if _, ok := err.(*osutil.NotEnoughDiskSpaceError); ok { - return nil, nil, &InsufficientSpaceError{ - Path: path, - Snaps: toInstall, - ChangeKind: "install", - } - } - return nil, nil, err - } + if err = checkDiskSpace(st, "install", snapInfos, userID); err != nil { + return nil, nil, err } tasksets := make([]*state.TaskSet, 0, len(installs)) @@ -1193,6 +1281,76 @@ // consider. type updateFilter func(*snap.Info, *SnapState) bool +func rearrangeBaseKernelForSingleReboot(kernelTs, bootBaseTs *state.TaskSet) error { + haveBase, haveKernel := bootBaseTs != nil, kernelTs != nil + if !haveBase && !haveKernel { + // neither base nor kernel update + return nil + } + if haveBase != haveKernel { + // have one but not the other + return nil + } + + // both kernel and boot base are being updated, reorder link-snap and + // auto-connect tasks from both snaps such that we end up with the + // following ordering: + // + // tasks of base and kernel -> + // link-snap(base) -> + // link-snap(kernel)(r) -> + // auto-connect(base) -> + // auto-connect(kernel) -> + // remaining tasks of base and kernel + // + // where (r) denotes the task that can effectively request a reboot + + beforeLinkSnapKernel := kernelTs.MaybeEdge(BeforeMaybeRebootEdge) + linkSnapKernel := kernelTs.MaybeEdge(MaybeRebootEdge) + autoConnectKernel := kernelTs.MaybeEdge(MaybeRebootWaitEdge) + + if linkSnapKernel == nil || autoConnectKernel == nil || beforeLinkSnapKernel == nil { + return fmt.Errorf("internal error: cannot identify link-snap or auto-connect or the preceding task for the kernel snap") + } + kernelLanes := linkSnapKernel.Lanes() + + linkSnapBase := bootBaseTs.MaybeEdge(MaybeRebootEdge) + autoConnectBase := bootBaseTs.MaybeEdge(MaybeRebootWaitEdge) + afterAutoConnectBase := bootBaseTs.MaybeEdge(AfterMaybeRebootWaitEdge) + if linkSnapBase == nil || autoConnectBase == nil || afterAutoConnectBase == nil { + return fmt.Errorf("internal error: cannot identify link-snap or auto-connect or the following task for the base snap") + } + baseLanes := linkSnapBase.Lanes() + + for _, lane := range kernelLanes { + linkSnapBase.JoinLane(lane) + autoConnectBase.JoinLane(lane) + } + for _, lane := range baseLanes { + linkSnapKernel.JoinLane(lane) + autoConnectKernel.JoinLane(lane) + } + // make link-snap base wait for the last task directly preceding + // link-snap of the kernel + linkSnapBase.WaitFor(beforeLinkSnapKernel) + // order: link-snap-base -> link-snap-kernel + linkSnapKernel.WaitFor(linkSnapBase) + // order: link-snap-kernel -> auto-connect-base + autoConnectBase.WaitFor(linkSnapKernel) + // order: auto-connect-base -> auto-connect-kernel + autoConnectKernel.WaitFor(autoConnectBase) + // make the first task after auto-connect base wait for auto-connect + // kernel, this task already waits for auto-connect of base + afterAutoConnectBase.WaitFor(autoConnectKernel) + + // cannot-reboot indicates that a task cannot invoke a reboot + linkSnapBase.Set("cannot-reboot", true) + + // first auto connect will wait for reboot, but the restart pending flag + // will be cleared for the second one that runs + return nil +} + func updateManyFiltered(ctx context.Context, st *state.State, names []string, userID int, filter updateFilter, flags *Flags, fromChange string) ([]string, []*state.TaskSet, error) { if flags == nil { flags = &Flags{} @@ -1208,6 +1366,8 @@ return nil, nil, err } + names = strutil.Deduplicate(names) + refreshOpts := &store.RefreshOptions{IsAutoRefresh: flags.IsAutoRefresh} updates, stateByInstanceName, ignoreValidation, err := refreshCandidates(ctx, st, names, user, refreshOpts) if err != nil { @@ -1252,35 +1412,9 @@ toUpdate[i] = installSnapInfo{up} } - tr := config.NewTransaction(st) - checkDiskSpaceRefresh, err := features.Flag(tr, features.CheckDiskSpaceRefresh) - if err != nil && !config.IsNoOption(err) { + if err = checkDiskSpace(st, "refresh", toUpdate, userID); err != nil { return nil, nil, err } - if checkDiskSpaceRefresh { - // check if there is enough disk space for requested snap and its - // prerequisites. - totalSize, err := installSize(st, toUpdate, userID) - if err != nil { - return nil, nil, err - } - requiredSpace := safetyMarginDiskSpace(totalSize) - path := dirs.SnapdStateDir(dirs.GlobalRootDir) - if err := osutilCheckFreeSpace(path, requiredSpace); err != nil { - snaps := make([]string, len(updates)) - for i, up := range updates { - snaps[i] = up.InstanceName() - } - if _, ok := err.(*osutil.NotEnoughDiskSpaceError); ok { - return nil, nil, &InsufficientSpaceError{ - Path: path, - Snaps: snaps, - ChangeKind: "refresh", - } - } - return nil, nil, err - } - } updated, tasksets, err := doUpdate(ctx, st, names, toUpdate, params, userID, flags, deviceCtx, fromChange) if err != nil { @@ -1335,7 +1469,7 @@ reportUpdated[snapName] = true } - // first snapd, core, bases, then rest + // first snapd, core, kernel, bases, then rest sort.Stable(byType(updates)) prereqs := make(map[string]*state.TaskSet) waitPrereq := func(ts *state.TaskSet, prereqName string) { @@ -1344,7 +1478,7 @@ ts.WaitAll(preTs) } } - var kernelTs, gadgetTs *state.TaskSet + var kernelTs, gadgetTs, bootBaseTs *state.TaskSet // updates is sorted by kind so this will process first core // and bases and then other snaps @@ -1381,18 +1515,38 @@ // prereqs were processed already, wait for // them as necessary for the other kind of // snaps + + // because "core" has type "os" it is ordered before all + // other updates, right after snapd, that introduces a + // dependency on "core" tasks for all other snaps, and + // thus preventing us from reordering them to perform a + // single reboot when both kernel and base (core in this + // case) are updated waitPrereq(ts, defaultCoreSnapName) waitPrereq(ts, "snapd") if update.SnapBase() != "" { + // the kernel snap is ordered before the base, + // so its task will not have an implicit + // dependency on base update waitPrereq(ts, update.SnapBase()) } } - // keep track of kernel/gadget udpates + // keep track of kernel/gadget/base updates switch update.Type() { case snap.TypeKernel: kernelTs = ts case snap.TypeGadget: gadgetTs = ts + case snap.TypeBase: + if update.InstanceName() == deviceCtx.Model().Base() { + // only the boot base is relevant for reboots + bootBaseTs = ts + } + case snap.TypeOS: + // nothing to do, we cannot set up a single reboot with + // "core" due to all tasks of other snaps impolitely + // waiting for all tasks of "core", what makes us end up + // with a task wait loop } scheduleUpdate(update.InstanceName(), ts) @@ -1408,6 +1562,14 @@ kernelTs.WaitAll(gadgetTs) } + if deviceCtx.Model().Base() != "" { + // reordering of kernel and base tasks is supported only on + // UC18+ devices + if err := rearrangeBaseKernelForSingleReboot(kernelTs, bootBaseTs); err != nil { + return nil, nil, err + } + } + if len(newAutoAliases) != 0 { addAutoAliasesTs, err := applyAutoAliasesDelta(st, newAutoAliases, "refresh", refreshAll, fromChange, scheduleUpdate) if err != nil { @@ -1561,7 +1723,7 @@ // resolveChannel returns the effective channel to use, based on the requested // channel and constrains set by device model, or an error if switching to // requested channel is forbidden. -func resolveChannel(st *state.State, snapName, oldChannel, newChannel string, deviceCtx DeviceContext) (effectiveChannel string, err error) { +func resolveChannel(snapName, oldChannel, newChannel string, deviceCtx DeviceContext) (effectiveChannel string, err error) { if newChannel == "" { return "", nil } @@ -1685,7 +1847,7 @@ return nil, err } - opts.Channel, err = resolveChannel(st, name, snapst.TrackingChannel, opts.Channel, deviceCtx) + opts.Channel, err = resolveChannel(name, snapst.TrackingChannel, opts.Channel, deviceCtx) if err != nil { return nil, err } @@ -1726,7 +1888,10 @@ // Update initiates a change updating a snap. // Note that the state must be locked by the caller. // -// The returned TaskSet will contain a DownloadAndChecksDoneEdge. +// The returned TaskSet can contain a LastBeforeLocalModificationsEdge +// identifying the last task before the first task that introduces system +// modifications. If no such edge is set, then none of the tasks introduce +// system modifications. func Update(st *state.State, name string, opts *RevisionOptions, userID int, flags Flags) (*state.TaskSet, error) { return UpdateWithDeviceContext(st, name, opts, userID, flags, nil, "") } @@ -1735,7 +1900,10 @@ // It will query for the snap with the given deviceCtx. // Note that the state must be locked by the caller. // -// The returned TaskSet will contain a DownloadAndChecksDoneEdge. +// The returned TaskSet can contain a LastBeforeLocalModificationsEdge +// identifying the last task before the first task that introduces system +// modifications. If no such edge is set, then none of the tasks introduce +// system modifications. func UpdateWithDeviceContext(st *state.State, name string, opts *RevisionOptions, userID int, flags Flags, deviceCtx DeviceContext, fromChange string) (*state.TaskSet, error) { if opts == nil { opts = &RevisionOptions{} @@ -1761,7 +1929,7 @@ return nil, err } - opts.Channel, err = resolveChannel(st, name, snapst.TrackingChannel, opts.Channel, deviceCtx) + opts.Channel, err = resolveChannel(name, snapst.TrackingChannel, opts.Channel, deviceCtx) if err != nil { return nil, err } @@ -1800,35 +1968,9 @@ toUpdate[i] = installSnapInfo{up} } - tr := config.NewTransaction(st) - checkDiskSpaceRefresh, err := features.Flag(tr, features.CheckDiskSpaceRefresh) - if err != nil && !config.IsNoOption(err) { + if err = checkDiskSpace(st, "refresh", toUpdate, userID); err != nil { return nil, err } - if checkDiskSpaceRefresh { - // check if there is enough disk space for requested snap and its - // prerequisites. - totalSize, err := installSize(st, toUpdate, userID) - if err != nil { - return nil, err - } - requiredSpace := safetyMarginDiskSpace(totalSize) - path := dirs.SnapdStateDir(dirs.GlobalRootDir) - if err := osutilCheckFreeSpace(path, requiredSpace); err != nil { - snaps := make([]string, len(updates)) - for i, up := range updates { - snaps[i] = up.InstanceName() - } - if _, ok := err.(*osutil.NotEnoughDiskSpaceError); ok { - return nil, &InsufficientSpaceError{ - Path: path, - Snaps: snaps, - ChangeKind: "refresh", - } - } - return nil, err - } - } params := func(update *snap.Info) (*RevisionOptions, Flags, *SnapState) { return opts, flags, &snapst @@ -1931,7 +2073,7 @@ } if sideInfo == nil { // refresh from given revision from store - return updateToRevisionInfo(st, snapst, opts.Revision, userID, deviceCtx) + return updateToRevisionInfo(st, snapst, opts.Revision, userID, flags, deviceCtx) } // refresh-to-local, this assumes the snap revision is mounted @@ -1942,6 +2084,10 @@ // into the Autorefresh function. var AutoRefreshAssertions func(st *state.State, userID int) error +var AddCurrentTrackingToValidationSetsStack func(st *state.State) error + +var RestoreValidationSetsTracking func(st *state.State) error + // AutoRefresh is the wrapper that will do a refresh of all the installed // snaps on the system. In addition to that it will also refresh important // assertions. @@ -2135,35 +2281,9 @@ toUpdate[i] = up } - tr := config.NewTransaction(st) - checkDiskSpaceRefresh, err := features.Flag(tr, features.CheckDiskSpaceRefresh) - if err != nil && !config.IsNoOption(err) { + if err := checkDiskSpace(st, "refresh", toUpdate, 0); err != nil { return nil, err } - if checkDiskSpaceRefresh { - // check if there is enough disk space for requested snaps and their - // prerequisites. - totalSize, err := installSize(st, toUpdate, 0) - if err != nil { - return nil, err - } - requiredSpace := safetyMarginDiskSpace(totalSize) - path := dirs.SnapdStateDir(dirs.GlobalRootDir) - if err := osutilCheckFreeSpace(path, requiredSpace); err != nil { - snaps := make([]string, len(updates)) - for i, up := range updates { - snaps[i] = up.InstanceName() - } - if _, ok := err.(*osutil.NotEnoughDiskSpaceError); ok { - return nil, &InsufficientSpaceError{ - Path: path, - Snaps: snaps, - ChangeKind: "refresh", - } - } - return nil, err - } - } updated, tasksets, err := doUpdate(ctx, st, nil, toUpdate, nil, userID, flags, deviceCtx, fromChange) if err != nil { @@ -2174,7 +2294,56 @@ return tasksets, nil } -// LinkNewBaseOrKernel will create prepare/link-snap tasks for a remodel +// checkDiskSpace checks if there is enough space for the requested snaps and their prerequisites +func checkDiskSpace(st *state.State, changeKind string, infos []minimalInstallInfo, userID int) error { + var featFlag features.SnapdFeature + + switch changeKind { + case "install": + featFlag = features.CheckDiskSpaceInstall + case "refresh": + featFlag = features.CheckDiskSpaceRefresh + default: + return fmt.Errorf("cannot check disk space for invalid change kind %q", changeKind) + } + + tr := config.NewTransaction(st) + enabled, err := features.Flag(tr, featFlag) + if err != nil && !config.IsNoOption(err) { + return err + } + + if !enabled { + return nil + } + + totalSize, err := installSize(st, infos, userID) + if err != nil { + return err + } + + requiredSpace := safetyMarginDiskSpace(totalSize) + path := dirs.SnapdStateDir(dirs.GlobalRootDir) + if err := osutilCheckFreeSpace(path, requiredSpace); err != nil { + snaps := make([]string, len(infos)) + for i, up := range infos { + snaps[i] = up.InstanceName() + } + if _, ok := err.(*osutil.NotEnoughDiskSpaceError); ok { + return &InsufficientSpaceError{ + Path: path, + Snaps: snaps, + ChangeKind: changeKind, + } + } + return err + } + + return nil +} + +// LinkNewBaseOrKernel creates a new task set with prepare/link-snap, and +// additionally update-gadget-assets for the kernel snap, tasks for a remodel. func LinkNewBaseOrKernel(st *state.State, name string) (*state.TaskSet, error) { var snapst SnapState err := Get(st, name, &snapst) @@ -2199,7 +2368,7 @@ // good default: // bad - return nil, fmt.Errorf("cannot link type %v", info.Type()) + return nil, fmt.Errorf("internal error: cannot link type %v", info.Type()) } snapsup := &SnapSetup{ @@ -2212,40 +2381,157 @@ prepareSnap := st.NewTask("prepare-snap", fmt.Sprintf(i18n.G("Prepare snap %q (%s) for remodel"), snapsup.InstanceName(), snapst.Current)) prepareSnap.Set("snap-setup", &snapsup) - + prev := prepareSnap + ts := state.NewTaskSet(prepareSnap) + // preserve the same order as during the update + if info.Type() == snap.TypeKernel { + // kernel snaps can carry boot assets + gadgetUpdate := st.NewTask("update-gadget-assets", fmt.Sprintf(i18n.G("Update assets from %s %q (%s) for remodel"), snapsup.Type, snapsup.InstanceName(), snapst.Current)) + gadgetUpdate.Set("snap-setup-task", prepareSnap.ID()) + gadgetUpdate.WaitFor(prev) + ts.AddTask(gadgetUpdate) + prev = gadgetUpdate + } linkSnap := st.NewTask("link-snap", fmt.Sprintf(i18n.G("Make snap %q (%s) available to the system during remodel"), snapsup.InstanceName(), snapst.Current)) linkSnap.Set("snap-setup-task", prepareSnap.ID()) - linkSnap.WaitFor(prepareSnap) - - // we need this for remodel - ts := state.NewTaskSet(prepareSnap, linkSnap) - ts.MarkEdge(prepareSnap, DownloadAndChecksDoneEdge) + linkSnap.WaitFor(prev) + ts.AddTask(linkSnap) + // prepare-snap is the last task that carries no system modifications + ts.MarkEdge(prepareSnap, LastBeforeLocalModificationsEdge) return ts, nil } -func AddLinkNewBaseOrKernel(st *state.State, ts *state.TaskSet) (*state.TaskSet, error) { - var snapSetupTaskID string +func findSnapSetupTask(tasks []*state.Task) (*state.Task, *SnapSetup, error) { var snapsup SnapSetup - allTasks := ts.Tasks() - for _, tsk := range allTasks { + for _, tsk := range tasks { if tsk.Has("snap-setup") { - snapSetupTaskID = tsk.ID() if err := tsk.Get("snap-setup", &snapsup); err != nil { - return nil, err + return nil, nil, err } - break + return tsk, &snapsup, nil } } - if snapSetupTaskID == "" { + return nil, nil, nil +} + +// AddLinkNewBaseOrKernel creates the same tasks as LinkNewBaseOrKernel but adds +// them to the provided task set. +func AddLinkNewBaseOrKernel(st *state.State, ts *state.TaskSet) (*state.TaskSet, error) { + allTasks := ts.Tasks() + snapSetupTask, snapsup, err := findSnapSetupTask(allTasks) + if err != nil { + return nil, err + } + if snapSetupTask == nil { return nil, fmt.Errorf("internal error: cannot identify task with snap-setup") } - + // the first task added here waits for the last task in the existing set + prev := allTasks[len(allTasks)-1] + // preserve the same order as during the update + if snapsup.Type == snap.TypeKernel { + // kernel snaps can carry boot assets + gadgetUpdate := st.NewTask("update-gadget-assets", fmt.Sprintf(i18n.G("Update assets from %s %q (%s) for remodel"), snapsup.Type, snapsup.InstanceName(), snapsup.Revision())) + gadgetUpdate.Set("snap-setup-task", snapSetupTask.ID()) + // wait for the last task in existing set + gadgetUpdate.WaitFor(prev) + ts.AddTask(gadgetUpdate) + prev = gadgetUpdate + } linkSnap := st.NewTask("link-snap", fmt.Sprintf(i18n.G("Make snap %q (%s) available to the system during remodel"), snapsup.InstanceName(), snapsup.SideInfo.Revision)) - linkSnap.Set("snap-setup-task", snapSetupTaskID) - // wait for the last task in existing set - linkSnap.WaitFor(allTasks[len(allTasks)-1]) + linkSnap.Set("snap-setup-task", snapSetupTask.ID()) + linkSnap.WaitFor(prev) ts.AddTask(linkSnap) + // make sure that remodel can identify which tasks introduce actual + // changes to the system and order them correctly + if edgeTask := ts.MaybeEdge(LastBeforeLocalModificationsEdge); edgeTask == nil { + // no task in the task set is marked as last before system + // modifications are introduced, so we need to mark the last + // task in the set, as tasks introduced here modify system state + ts.MarkEdge(allTasks[len(allTasks)-1], LastBeforeLocalModificationsEdge) + } + return ts, nil +} + +// LinkNewBaseOrKernel creates a new task set with +// prepare/update-gadget-assets/update-gadget-cmdline tasks for the gadget snap, +// for remodel. +func SwitchToNewGadget(st *state.State, name string) (*state.TaskSet, error) { + var snapst SnapState + err := Get(st, name, &snapst) + if err == state.ErrNoState { + return nil, &snap.NotInstalledError{Snap: name} + } + if err != nil { + return nil, err + } + + if err := CheckChangeConflict(st, name, nil); err != nil { + return nil, err + } + + info, err := snapst.CurrentInfo() + if err != nil { + return nil, err + } + + if info.Type() != snap.TypeGadget { + return nil, fmt.Errorf("internal error: cannot link type %v", info.Type()) + } + + snapsup := &SnapSetup{ + SideInfo: snapst.CurrentSideInfo(), + Flags: snapst.Flags.ForSnapSetup(), + Type: info.Type(), + PlugsOnly: len(info.Slots) == 0, + InstanceKey: snapst.InstanceKey, + } + + prepareSnap := st.NewTask("prepare-snap", fmt.Sprintf(i18n.G("Prepare snap %q (%s) for remodel"), snapsup.InstanceName(), snapst.Current)) + prepareSnap.Set("snap-setup", &snapsup) + + gadgetUpdate := st.NewTask("update-gadget-assets", fmt.Sprintf(i18n.G("Update assets from %s %q (%s) for remodel"), snapsup.Type, snapsup.InstanceName(), snapst.Current)) + gadgetUpdate.WaitFor(prepareSnap) + gadgetUpdate.Set("snap-setup-task", prepareSnap.ID()) + gadgetCmdline := st.NewTask("update-gadget-cmdline", fmt.Sprintf(i18n.G("Update kernel command line from %s %q (%s) for remodel"), snapsup.Type, snapsup.InstanceName(), snapst.Current)) + gadgetCmdline.WaitFor(gadgetUpdate) + gadgetCmdline.Set("snap-setup-task", prepareSnap.ID()) + + ts := state.NewTaskSet(prepareSnap, gadgetUpdate, gadgetCmdline) + // prepare-snap is the last task that carries no system modifications + ts.MarkEdge(prepareSnap, LastBeforeLocalModificationsEdge) + return ts, nil +} + +// AddGadgetAssetsTasks creates the same tasks as SwitchToNewGadget but adds +// them to the provided task set. +func AddGadgetAssetsTasks(st *state.State, ts *state.TaskSet) (*state.TaskSet, error) { + allTasks := ts.Tasks() + snapSetupTask, snapsup, err := findSnapSetupTask(allTasks) + if err != nil { + return nil, err + } + if snapSetupTask == nil { + return nil, fmt.Errorf("internal error: cannot identify task with snap-setup") + } + gadgetUpdate := st.NewTask("update-gadget-assets", fmt.Sprintf(i18n.G("Update assets from %s %q (%s) for remodel"), snapsup.Type, snapsup.InstanceName(), snapsup.Revision())) + gadgetUpdate.Set("snap-setup-task", snapSetupTask.ID()) + // wait for the last task in existing set + gadgetUpdate.WaitFor(allTasks[len(allTasks)-1]) + ts.AddTask(gadgetUpdate) + // gadget snaps can carry kernel command line fragments + gadgetCmdline := st.NewTask("update-gadget-cmdline", fmt.Sprintf(i18n.G("Update kernel command line from %s %q (%s) for remodel"), snapsup.Type, snapsup.InstanceName(), snapsup.Revision())) + gadgetCmdline.Set("snap-setup-task", snapSetupTask.ID()) + gadgetCmdline.WaitFor(gadgetUpdate) + ts.AddTask(gadgetCmdline) + // make sure that remodel can identify which tasks introduce actual + // changes to the system and order them correctly + if edgeTask := ts.MaybeEdge(LastBeforeLocalModificationsEdge); edgeTask == nil { + // no task in the task set is marked as last before system + // modifications are introduced, so we need to mark the last + // task in the set, as tasks introduced here modify system state + ts.MarkEdge(allTasks[len(allTasks)-1], LastBeforeLocalModificationsEdge) + } return ts, nil } @@ -2643,6 +2929,8 @@ // RemoveMany removes everything from the given list of names. // Note that the state must be locked by the caller. func RemoveMany(st *state.State, names []string) ([]string, []*state.TaskSet, error) { + names = strutil.Deduplicate(names) + if err := validateSnapNames(names); err != nil { return nil, nil, err } @@ -2941,23 +3229,24 @@ // InstalledSnaps returns the list of all installed snaps suitable for // ValidationSets checks. -func InstalledSnaps(st *state.State) ([]*snapasserts.InstalledSnap, error) { - var snaps []*snapasserts.InstalledSnap +func InstalledSnaps(st *state.State) (snaps []*snapasserts.InstalledSnap, ignoreValidation map[string]bool, err error) { all, err := All(st) if err != nil { - return nil, err + return nil, nil, err } + ignoreValidation = make(map[string]bool) for _, snapState := range all { cur, err := snapState.CurrentInfo() if err != nil { - return nil, err + return nil, nil, err + } + snaps = append(snaps, snapasserts.NewInstalledSnap(snapState.InstanceName(), + snapState.CurrentSideInfo().SnapID, cur.Revision)) + if snapState.IgnoreValidation { + ignoreValidation[snapState.InstanceName()] = true } - snaps = append(snaps, - snapasserts.NewInstalledSnap(snapState.InstanceName(), - snapState.CurrentSideInfo().SnapID, - cur.Revision)) } - return snaps, nil + return snaps, ignoreValidation, nil } // NumSnaps returns the number of installed snaps. diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstate_install_test.go snapd-2.54.2+21.10/overlord/snapstate/snapstate_install_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstate_install_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/snapstate_install_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2020 Canonical Ltd + * Copyright (C) 2016-2021 Canonical Ltd * * This 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,12 +21,16 @@ import ( "context" + "errors" "fmt" "io/ioutil" + "os" + "os/exec" "path/filepath" "time" . "gopkg.in/check.v1" + "gopkg.in/tomb.v2" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/asserts/assertstest" @@ -56,26 +60,39 @@ "github.com/snapcore/snapd/testutil" ) -func verifyInstallTasks(c *C, opts, discards int, ts *state.TaskSet, st *state.State) { - kinds := taskKinds(ts.Tasks()) - - expected := []string{ - "prerequisites", - "download-snap", - "validate-snap", - "mount-snap", +func expectedDoInstallTasks(typ snap.Type, opts, discards int, startTasks []string, filterOut map[string]bool) []string { + if !release.OnClassic { + switch typ { + case snap.TypeGadget: + opts |= updatesGadget + case snap.TypeKernel: + opts |= updatesGadgetAssets + case snap.TypeSnapd: + opts |= updatesBootConfig + } + } + if startTasks == nil { + startTasks = []string{ + "prerequisites", + "download-snap", + "validate-snap", + "mount-snap", + } } + expected := startTasks if opts&unlinkBefore != 0 { expected = append(expected, + "run-hook[pre-refresh]", "stop-snap-services", "remove-aliases", "unlink-current-snap", ) } + if opts&(updatesGadget|updatesGadgetAssets) != 0 { + expected = append(expected, "update-gadget-assets") + } if opts&updatesGadget != 0 { - expected = append(expected, - "update-gadget-assets", - "update-gadget-cmdline") + expected = append(expected, "update-gadget-cmdline") } expected = append(expected, "copy-snap-data", @@ -89,9 +106,12 @@ if opts&updatesBootConfig != 0 { expected = append(expected, "update-managed-boot-config") } - expected = append(expected, - "run-hook[install]", - "start-snap-services") + if opts&unlinkBefore != 0 { + expected = append(expected, "run-hook[post-refresh]") + } else { + expected = append(expected, "run-hook[install]") + } + expected = append(expected, "start-snap-services") for i := 0; i < discards; i++ { expected = append(expected, "clear-snap", @@ -112,7 +132,28 @@ "run-hook[check-health]", ) + filtered := make([]string, 0, len(expected)) + for _, k := range expected { + if !filterOut[k] { + filtered = append(filtered, k) + } + } + + return filtered +} + +func verifyInstallTasks(c *C, typ snap.Type, opts, discards int, ts *state.TaskSet) { + kinds := taskKinds(ts.Tasks()) + + expected := expectedDoInstallTasks(typ, opts, discards, nil, nil) + c.Assert(kinds, DeepEquals, expected) + + if opts&noLastBeforeModificationsEdge == 0 { + te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge) + c.Assert(te, NotNil) + c.Assert(te.Kind(), Equals, "validate-snap") + } } func (s *snapmgrTestSuite) TestInstallDevModeConfinementFiltering(c *C) { @@ -177,7 +218,7 @@ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, 0, snapstate.Flags{}) c.Assert(err, IsNil) - verifyInstallTasks(c, 0, 0, ts, s.state) + verifyInstallTasks(c, snap.TypeApp, 0, 0, ts) c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) } @@ -220,7 +261,7 @@ ts, err := snapstate.Install(context.Background(), s.state, "snapd", opts, 0, snapstate.Flags{}) c.Assert(err, IsNil) - verifyInstallTasks(c, noConfigure, 0, ts, s.state) + verifyInstallTasks(c, snap.TypeSnapd, noConfigure, 0, ts) snapsup, err := snapstate.TaskSnapSetup(ts.Tasks()[0]) c.Assert(err, IsNil) @@ -238,7 +279,7 @@ ts, err := snapstate.Install(context.Background(), s.state, "snapd", opts, 0, snapstate.Flags{}) c.Assert(err, IsNil) - verifyInstallTasks(c, noConfigure|updatesBootConfig, 0, ts, s.state) + verifyInstallTasks(c, snap.TypeSnapd, noConfigure, 0, ts) snapsup, err := snapstate.TaskSnapSetup(ts.Tasks()[0]) c.Assert(err, IsNil) @@ -256,7 +297,7 @@ c.Assert(err, IsNil) c.Check(snapsup.CohortKey, Equals, "what") - verifyInstallTasks(c, 0, 0, ts, s.state) + verifyInstallTasks(c, snap.TypeApp, 0, 0, ts) c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) } @@ -273,7 +314,7 @@ ts, err := snapstate.InstallWithDeviceContext(context.Background(), s.state, "some-snap", opts, 0, snapstate.Flags{}, deviceCtx, "") c.Assert(err, IsNil) - verifyInstallTasks(c, 0, 0, ts, s.state) + verifyInstallTasks(c, snap.TypeApp, 0, 0, ts) c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) } @@ -320,7 +361,6 @@ } snapsup := &snapstate.SnapSetup{SideInfo: &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)}} _, err := snapstate.DoInstall(s.state, snapst, snapsup, 0, "", nil) - c.Assert(err, NotNil) c.Assert(err, ErrorMatches, `cannot update disabled snap "some-snap"`) } @@ -504,7 +544,6 @@ snapsup := &snapstate.SnapSetup{SideInfo: &snap.SideInfo{RealName: "system", SnapID: "some-snap-id", Revision: snap.R(1)}} _, err := snapstate.DoInstall(s.state, nil, snapsup, 0, "", nil) - c.Assert(err, NotNil) c.Assert(err, ErrorMatches, `cannot install reserved snap name 'system'`) } @@ -593,15 +632,11 @@ ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{Classic: true}) c.Assert(err, IsNil) - c.Assert(err, IsNil) - chg := s.state.NewChange("install", "install snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -628,10 +663,8 @@ chg := s.state.NewChange("install", "install snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -656,10 +689,8 @@ chg := s.state.NewChange("install", "install snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -687,10 +718,8 @@ chg.AddAll(ts) } - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -809,15 +838,11 @@ ts, _, err := snapstate.InstallPath(s.state, &snap.SideInfo{RealName: "some-snap"}, mockSnap, "", "", snapstate.Flags{Classic: true}) c.Assert(err, IsNil) - c.Assert(err, IsNil) - chg := s.state.NewChange("install", "install snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -852,15 +877,11 @@ ts, _, err := snapstate.InstallPath(s.state, &snap.SideInfo{RealName: "some-snap"}, mockSnap, "", "edge", snapstate.Flags{}) c.Assert(err, IsNil) - c.Assert(err, IsNil) - chg := s.state.NewChange("install", "install snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -929,10 +950,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Assert(chg.Err(), IsNil) @@ -1108,9 +1127,7 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Assert(chg.Err(), IsNil) @@ -1293,10 +1310,8 @@ terr.JoinLane(last.Lanes()[0]) chg.AddTask(terr) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() mountTask := tasks[len(tasks)-11] c.Assert(mountTask.Kind(), Equals, "mount-snap") @@ -1390,6 +1405,11 @@ name: "some-snap", }, { + op: "auto-connect:Undoing", + name: "some-snap", + revno: snap.R(11), + }, + { op: "discard-namespace", name: "some-snap", }, @@ -1441,10 +1461,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Assert(chg.Err(), IsNil) @@ -1608,10 +1626,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Assert(chg.Err(), IsNil) @@ -1769,10 +1785,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Assert(chg.Err(), IsNil) @@ -1827,10 +1841,8 @@ c.Check(info.SideInfo.RealName, Equals, "mock") c.Check(info.Version, Equals, "1.0") - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -1940,10 +1952,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -2060,10 +2070,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -2160,10 +2168,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure only local install was run, i.e. first actions are pseudo-action current c.Assert(s.fakeBackend.ops.Ops(), HasLen, 9) @@ -2247,10 +2253,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Assert(chg.Err(), IsNil) @@ -2475,10 +2479,8 @@ c.Assert(err, IsNil) chg2.AddAll(ts2) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure all our tasks ran and core was only installed once c.Assert(chg1.Err(), IsNil) @@ -2541,12 +2543,16 @@ c.Assert(err, IsNil) chg2.AddAll(ts2) - // we use our own settle as we need a bigger timeout s.state.Unlock() + defer s.state.Lock() + + // we use our own settle as we need a bigger timeout err = s.o.Settle(testutil.HostScaledTimeout(15 * time.Second)) - s.state.Lock() c.Assert(err, IsNil) + s.state.Lock() + defer s.state.Unlock() + // ensure expected change states c.Check(chg1.Status(), Equals, state.ErrorStatus) c.Check(chg2.Status(), Equals, state.DoneStatus) @@ -2575,7 +2581,6 @@ Channel: "", Revision: snap.R(21), }) - } } @@ -2659,6 +2664,8 @@ c.Check(prereq.AtTime().IsZero(), Equals, true) s.state.Unlock() + defer s.state.Lock() + defer s.se.Stop() // start running the change, this will trigger the @@ -2671,11 +2678,13 @@ // change is not ready yet, because the prerequists triggered // a state.Retry{} because of the conflicting change c.Assert(chg.IsReady(), Equals, false) + s.state.Lock() + defer s.state.Unlock() + // marked for retry c.Check(prereq.AtTime().IsZero(), Equals, false) c.Check(prereq.Status().Ready(), Equals, false) - s.state.Unlock() // retry interval is 10ms so 20ms should be plenty of time time.Sleep(20 * time.Millisecond) @@ -2683,8 +2692,6 @@ // chg got retried, core is now installed, things are good c.Assert(chg.IsReady(), Equals, true) - s.state.Lock() - // ensure all our tasks ran c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -2728,10 +2735,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Assert(chg.Err(), IsNil) @@ -2892,10 +2897,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Assert(chg.Err(), IsNil) @@ -3233,10 +3236,8 @@ terr.JoinLane(last.Lanes()[0]) chg.AddTask(terr) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() mountTask := tasks[len(tasks)-11] c.Assert(mountTask.Kind(), Equals, "mount-snap") @@ -3259,10 +3260,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Assert(chg.Err(), IsNil) @@ -3310,7 +3309,7 @@ c.Check(s.fakeStore.seenPrivacyKeys["privacy-key"], Equals, true) for i, ts := range tts { - verifyInstallTasks(c, 0, 0, ts, s.state) + verifyInstallTasks(c, snap.TypeApp, 0, 0, ts) // check that tasksets are in separate lanes for _, t := range ts.Tasks() { c.Assert(t.Lanes(), DeepEquals, []int{i + 1}) @@ -3398,10 +3397,8 @@ s.fakeBackend.copySnapDataFailTrigger = filepath.Join(dirs.SnapMountDir, "some-snap/11") - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -3477,6 +3474,7 @@ func (s *snapmgrTestSuite) TestSideInfoPaid(c *C) { s.state.Lock() defer s.state.Unlock() + opts := &snapstate.RevisionOptions{Channel: "channel-for-paid"} ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{}) c.Assert(err, IsNil) @@ -3484,10 +3482,8 @@ chg := s.state.NewChange("install", "install paid snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // verify snap has paid sideinfo var snapst snapstate.SnapState @@ -3500,6 +3496,7 @@ func (s *snapmgrTestSuite) TestSideInfoPrivate(c *C) { s.state.Lock() defer s.state.Unlock() + opts := &snapstate.RevisionOptions{Channel: "channel-for-private"} ts, err := snapstate.Install(context.Background(), s.state, "some-snap", opts, s.user.ID, snapstate.Flags{}) c.Assert(err, IsNil) @@ -3507,10 +3504,8 @@ chg := s.state.NewChange("install", "install private snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // verify snap has private sideinfo var snapst snapstate.SnapState @@ -3690,10 +3685,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), ErrorMatches, "cannot perform the following tasks:\n.*Download snap \"snap-content-slot\" \\(11\\) from channel \"stable\" \\(boom\\).*") c.Assert(chg.IsReady(), Equals, true) @@ -3708,10 +3701,7 @@ } type validationSetsSuite struct { - testutil.BaseTest - state *state.State - fakeStore *fakeStore - fakeBackend *fakeSnappyBackend + snapmgrBaseTest storeSigning *assertstest.StoreStack dev1acct *asserts.Account acct1Key *asserts.AccountKey @@ -3721,18 +3711,7 @@ var _ = Suite(&validationSetsSuite{}) func (s *validationSetsSuite) SetUpTest(c *C) { - s.BaseTest.SetUpTest(c) - dirs.SetRootDir(c.MkDir()) - s.state = state.New(nil) - - r := snapstatetest.MockDeviceModel(DefaultModel()) - s.AddCleanup(r) - - s.fakeBackend = &fakeSnappyBackend{} - s.fakeStore = &fakeStore{ - fakeBackend: s.fakeBackend, - state: s.state, - } + s.snapmgrBaseTest.SetUpTest(c) s.storeSigning = assertstest.NewStoreStack("can0nical", nil) s.dev1acct = assertstest.NewAccount(s.storeSigning, "developer1", nil, "") @@ -3741,30 +3720,6 @@ s.acct1Key = assertstest.NewAccountKey(s.storeSigning, s.dev1acct, nil, dev1PrivKey.PublicKey(), "") s.dev1Signing = assertstest.NewSigningDB(s.dev1acct.AccountID(), dev1PrivKey) c.Assert(s.storeSigning.Add(s.acct1Key), IsNil) - - oldAutomaticSnapshot := snapstate.AutomaticSnapshot - snapstate.AutomaticSnapshot = func(st *state.State, instanceName string) (ts *state.TaskSet, err error) { - task := st.NewTask("save-snapshot", "...") - ts = state.NewTaskSet(task) - return ts, nil - } - s.AddCleanup(func() { - snapstate.AutomaticSnapshot = oldAutomaticSnapshot - }) - - oldAutoAliases := snapstate.AutoAliases - snapstate.AutoAliases = func(st *state.State, info *snap.Info) (map[string]string, error) { - return nil, nil - } - s.AddCleanup(func() { - snapstate.AutoAliases = oldAutoAliases - }) - - s.state.Lock() - defer s.state.Unlock() - snapstate.ReplaceStore(s.state, s.fakeStore) - s.state.Set("seeded", true) - s.state.Set("refresh-privacy-key", "privacy-key") } func (s *validationSetsSuite) mockValidationSetAssert(c *C, name, sequence string, snaps ...interface{}) asserts.Assertion { @@ -3991,8 +3946,69 @@ c.Assert(s.fakeBackend.ops[1:], DeepEquals, expectedOps) } +func (s *validationSetsSuite) testInstallSnapRequiredByValidationSetWithBase(c *C, presenceForBase string) error { + restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { + vs := snapasserts.NewValidationSets() + someSnap := map[string]interface{}{ + "id": "yOqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap-with-base", + "presence": "required", + } + // base snap is invalid + someBase := map[string]interface{}{ + "id": "aOqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-base", + "presence": presenceForBase, + } + vsa1 := s.mockValidationSetAssert(c, "bar", "1", someSnap, someBase) + vs.Add(vsa1.(*asserts.ValidationSet)) + return vs, nil + }) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + tr := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + Current: 1, + } + assertstate.UpdateValidationSet(s.state, &tr) + + ts, err := snapstate.Install(context.Background(), s.state, "some-snap-with-base", &snapstate.RevisionOptions{Channel: "channel-for-base/stable"}, 0, snapstate.Flags{}) + c.Assert(err, IsNil) + chg := s.state.NewChange("install", "...") + chg.AddAll(ts) + + s.state.Unlock() + defer s.state.Lock() + defer s.se.Stop() + err = s.o.Settle(testutil.HostScaledTimeout(5 * time.Second)) + c.Assert(err, IsNil) + + s.state.Lock() + defer s.state.Unlock() + + return chg.Err() +} + +func (s *validationSetsSuite) TestInstallSnapRequiredByValidationSetWithInvalidBase(c *C) { + err := s.testInstallSnapRequiredByValidationSetWithBase(c, "invalid") + c.Check(err, ErrorMatches, `cannot perform the following tasks: +.*Ensure prerequisites for "some-snap-with-base" are available \(cannot install snap base "some-base": cannot install snap "some-base" due to enforcing rules of validation set 16/foo/bar/1\)`) +} + +func (s *validationSetsSuite) TestInstallSnapRequiredByValidationSetWithRequiredBase(c *C) { + err := s.testInstallSnapRequiredByValidationSetWithBase(c, "required") + c.Check(err, IsNil) +} + func (s *snapmgrTestSuite) TestInstallWithOutdatedPrereq(c *C) { s.state.Lock() + defer s.state.Unlock() + snapstate.ReplaceStore(s.state, contentStore{fakeStore: s.fakeStore, state: s.state}) info := &snap.SideInfo{ @@ -4010,12 +4026,9 @@ ts, err := snapstate.Install(context.Background(), s.state, "snap-content-plug", nil, s.user.ID, snapstate.Flags{}) c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() s.settle(c) - s.state.Lock() - defer s.state.Unlock() c.Assert(chg.Err(), IsNil) c.Assert(chg.Status(), Equals, state.DoneStatus) @@ -4104,6 +4117,8 @@ func (s *snapmgrTestSuite) TestInstallPrereqIgnoreConflictInSameChange(c *C) { s.state.Lock() + defer s.state.Unlock() + snapstate.ReplaceStore(s.state, contentStore{fakeStore: s.fakeStore, state: s.state}) repo := interfaces.NewRepository() @@ -4128,10 +4143,7 @@ c.Check(installTasks.Tasks(), Not(HasLen), 0) chg.AddAll(installTasks) - s.state.Unlock() s.settle(c) - s.state.Lock() - defer s.state.Unlock() // check that the prereq task wasn't retried prereqTask := findStrictlyOnePrereqTask(c, chg) @@ -4204,14 +4216,569 @@ chg := s.state.NewChange("install", "test: install") chg.AddAll(ts) - s.state.Unlock() - defer s.state.Lock() s.settle(c) - s.state.Lock() - defer s.state.Unlock() c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{ {macaroon: s.user.StoreMacaroon, name: "snap-content-plug", target: filepath.Join(dirs.SnapBlobDir, "snap-content-plug_11.snap")}, {macaroon: s.user.StoreMacaroon, name: "snap-content-slot", target: filepath.Join(dirs.SnapBlobDir, "snap-content-slot_11.snap")}, }) } + +func (s *snapmgrTestSuite) TestInstallDeduplicatesSnapNames(c *C) { + s.state.Lock() + defer s.state.Unlock() + + installed, ts, err := snapstate.InstallMany(s.state, []string{"some-snap", "some-base", "some-snap", "some-base"}, s.user.ID) + c.Assert(err, IsNil) + c.Check(installed, testutil.DeepUnsortedMatches, []string{"some-snap", "some-base"}) + c.Check(ts, HasLen, 2) +} + +type installFn func(info *snap.SideInfo) (*state.TaskSet, error) + +func (s *snapmgrTestSuite) TestCorrectNumRevisionsIfNoneAdded(c *C) { + // different paths to install a revision already stored in the state + installFuncs := []installFn{ + func(si *snap.SideInfo) (*state.TaskSet, error) { + yaml := "name: some-snap\nversion: 1.0\nepoch: 1*" + path := snaptest.MakeTestSnapWithFiles(c, yaml, nil) + ts, _, err := snapstate.InstallPath(s.state, si, path, "some-snap", "", snapstate.Flags{}) + return ts, err + }, func(si *snap.SideInfo) (*state.TaskSet, error) { + return snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Revision: si.Revision}, s.user.ID, snapstate.Flags{}) + }, + } + + for _, fn := range installFuncs { + s.testRetainCorrectNumRevisions(c, fn) + } +} + +func (s *snapmgrTestSuite) testRetainCorrectNumRevisions(c *C, installFn installFn) { + s.state.Lock() + defer s.state.Unlock() + + si := &snap.SideInfo{ + RealName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(1), + } + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + TrackingChannel: "latest/stable", + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + SnapType: "app", + }) + + tr := config.NewTransaction(s.state) + c.Assert(tr.Set("core", "refresh.retain", 1), IsNil) + tr.Commit() + + // install already stored revision + ts, err := installFn(si) + c.Assert(err, IsNil) + c.Assert(ts, NotNil) + chg := s.state.NewChange("install", "") + chg.AddAll(ts) + + s.settle(c) + c.Assert(chg.Err(), IsNil) + + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) + c.Assert(snapst.Sequence, DeepEquals, []*snap.SideInfo{si}) +} + +func (s *snapmgrTestSuite) TestInstallPathMany(c *C) { + s.state.Lock() + defer s.state.Unlock() + + var paths []string + var sideInfos []*snap.SideInfo + + snapNames := []string{"some-snap", "other-snap"} + for _, name := range snapNames { + yaml := fmt.Sprintf(`name: %s +version: 1.0 +epoch: 1 +`, name) + paths = append(paths, makeTestSnap(c, yaml)) + si := &snap.SideInfo{ + RealName: name, + Revision: snap.R("3"), + } + sideInfos = append(sideInfos, si) + } + + tss, err := snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, 0, nil) + c.Assert(err, IsNil) + c.Assert(tss, HasLen, 2) + + chg := s.state.NewChange("install", "install local snaps") + for _, ts := range tss { + chg.AddAll(ts) + } + + defer s.se.Stop() + s.settle(c) + + c.Assert(chg.Err(), IsNil) + c.Assert(chg.IsReady(), Equals, true) + + for _, name := range snapNames { + var snapst snapstate.SnapState + err = snapstate.Get(s.state, name, &snapst) + c.Assert(err, IsNil) + c.Check(snapst.Current, Equals, snap.R("3")) + } +} + +func (s *snapmgrTestSuite) TestInstallPathManyWithOneFailing(c *C) { + s.state.Lock() + defer s.state.Unlock() + + var paths []string + var sideInfos []*snap.SideInfo + + snapNames := []string{"some-snap", "other-snap"} + for _, name := range snapNames { + yaml := fmt.Sprintf(`name: %s +version: 1.0 +epoch: 1 +`, name) + paths = append(paths, makeTestSnap(c, yaml)) + sideInfos = append(sideInfos, &snap.SideInfo{RealName: name}) + } + + s.o.TaskRunner().AddHandler("fail", func(*state.Task, *tomb.Tomb) error { + return errors.New("expected") + }, nil) + + tss, err := snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, 0, nil) + c.Assert(err, IsNil) + c.Assert(tss, HasLen, 2) + + // fail installation of 'other-snap' which shouldn't affect 'some-snap' + failingTask := s.state.NewTask("fail", "expected failure") + snapThreeLanes := tss[1].Tasks()[0].Lanes() + for _, lane := range snapThreeLanes { + failingTask.JoinLane(lane) + } + tss[1].AddTask(failingTask) + + chg := s.state.NewChange("install", "install local snaps") + for _, ts := range tss { + chg.AddAll(ts) + } + + defer s.se.Stop() + s.settle(c) + + c.Assert(chg.Err(), NotNil) + c.Assert(chg.IsReady(), Equals, true) + + // some-snap is installed + err = snapstate.Get(s.state, "some-snap", &snapstate.SnapState{}) + c.Assert(err, IsNil) + + // other-snap is not + err = snapstate.Get(s.state, "other-snap", &snapstate.SnapState{}) + c.Assert(errors.Is(err, state.ErrNoState), Equals, true) +} + +func (s *snapmgrTestSuite) TestInstallPathManyAsUpdate(c *C) { + s.state.Lock() + defer s.state.Unlock() + + var paths []string + var sideInfos []*snap.SideInfo + + snapNames := []string{"some-snap", "other-snap"} + for _, name := range snapNames { + si := &snap.SideInfo{ + RealName: name, + Revision: snap.R("1"), + } + snapstate.Set(s.state, name, &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + SnapType: "app", + }) + + yaml := fmt.Sprintf(`name: %s +version: 1.0 +epoch: 1 +`, name) + + newSi := &snap.SideInfo{ + RealName: name, + Revision: snap.R("2"), + } + path, _ := snaptest.MakeTestSnapInfoWithFiles(c, yaml, nil, newSi) + + paths = append(paths, path) + sideInfos = append(sideInfos, newSi) + } + + tss, err := snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, 0, nil) + c.Assert(err, IsNil) + c.Assert(tss, HasLen, 2) + + chg := s.state.NewChange("install", "install local snaps") + for _, ts := range tss { + chg.AddAll(ts) + } + + defer s.se.Stop() + s.settle(c) + + c.Assert(chg.Err(), IsNil) + c.Assert(chg.IsReady(), Equals, true) + + for _, name := range snapNames { + var snapst snapstate.SnapState + err = snapstate.Get(s.state, name, &snapst) + c.Assert(err, IsNil) + c.Check(snapst.Current, Equals, snap.R("2")) + } +} + +func (s *snapmgrTestSuite) TestInstallPathManyDiskSpaceError(c *C) { + restore := snapstate.MockOsutilCheckFreeSpace(func(string, uint64) error { return &osutil.NotEnoughDiskSpaceError{} }) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + var paths []string + var sideInfos []*snap.SideInfo + + snapNames := []string{"some-snap", "other-snap"} + for _, name := range snapNames { + yaml := fmt.Sprintf(`name: %s +version: 1.0 +epoch: 1 +`, name) + paths = append(paths, makeTestSnap(c, yaml)) + si := &snap.SideInfo{ + RealName: name, + Revision: snap.R("1"), + } + sideInfos = append(sideInfos, si) + } + tr := config.NewTransaction(s.state) + c.Assert(tr.Set("core", "experimental.check-disk-space-install", true), IsNil) + tr.Commit() + + _, err := snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, 0, nil) + diskSpaceErr, ok := err.(*snapstate.InsufficientSpaceError) + c.Assert(ok, Equals, true) + c.Check(diskSpaceErr, ErrorMatches, `insufficient space in .* to perform "install" change for the following snaps: some-snap, other-snap`) + c.Check(diskSpaceErr.Path, Equals, filepath.Join(dirs.GlobalRootDir, "/var/lib/snapd")) + c.Check(diskSpaceErr.Snaps, DeepEquals, snapNames) +} + +func (s *snapmgrTestSuite) TestInstallPathManyClassic(c *C) { + restore := maybeMockClassicSupport(c) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + var paths []string + var sideInfos []*snap.SideInfo + + snapNames := []string{"some-snap", "other-snap"} + for _, name := range snapNames { + yaml := fmt.Sprintf(`name: %s +version: 1.0 +epoch: 1 +confinement: classic +`, name) + paths = append(paths, makeTestSnap(c, yaml)) + si := &snap.SideInfo{ + RealName: name, + Revision: snap.R("1"), + } + sideInfos = append(sideInfos, si) + } + + tts, err := snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, s.user.ID, &snapstate.Flags{Classic: true}) + c.Assert(err, IsNil) + c.Assert(tts, HasLen, 2) + + for i := range paths { + snapsup, err := snapstate.TaskSnapSetup(tts[i].Tasks()[0]) + c.Assert(err, IsNil) + c.Check(snapsup.Classic, Equals, true) + } +} + +func (s *snapmgrTestSuite) TestInstallPathManyDevMode(c *C) { + s.state.Lock() + defer s.state.Unlock() + + var paths []string + var sideInfos []*snap.SideInfo + + snapNames := []string{"some-snap", "other-snap"} + for _, name := range snapNames { + yaml := fmt.Sprintf(`name: %s +version: 1.0 +epoch: 1 +confinement: devmode +`, name) + paths = append(paths, makeTestSnap(c, yaml)) + si := &snap.SideInfo{ + RealName: name, + Revision: snap.R("1"), + } + sideInfos = append(sideInfos, si) + } + + tts, err := snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, s.user.ID, &snapstate.Flags{DevMode: true}) + c.Assert(err, IsNil) + c.Assert(tts, HasLen, 2) + + for i := range paths { + snapsup, err := snapstate.TaskSnapSetup(tts[i].Tasks()[0]) + c.Assert(err, IsNil) + c.Check(snapsup.DevMode, Equals, true) + } +} + +func (s *snapmgrTestSuite) TestInstallPathManyMissingClassic(c *C) { + s.state.Lock() + defer s.state.Unlock() + + var paths []string + var sideInfos []*snap.SideInfo + + snapNames := []string{"some-snap", "other-snap"} + for _, name := range snapNames { + yaml := fmt.Sprintf(`name: %s +version: 1.0 +epoch: 1 +confinement: classic +`, name) + paths = append(paths, makeTestSnap(c, yaml)) + si := &snap.SideInfo{ + RealName: name, + Revision: snap.R("1"), + } + sideInfos = append(sideInfos, si) + } + + _, err := snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, s.user.ID, nil) + c.Assert(err, ErrorMatches, `snap "some-snap" requires classic confinement`) +} + +func (s *snapmgrTestSuite) TestInstallPathManyFailOnEpochMismatch(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{{RealName: "some-snap", Revision: snap.R(-1)}}, + Current: snap.R(-1), + }) + yaml := `name: some-snap +version: 1.0 +epoch: 42 +` + path := makeTestSnap(c, yaml) + si := &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(-2), + } + + _, err := snapstate.InstallPathMany(context.Background(), s.state, []*snap.SideInfo{si}, []string{path}, s.user.ID, nil) + c.Assert(err, ErrorMatches, `cannot refresh "some-snap" to local snap with epoch 42, because it can't read the current epoch of 1\*`) +} + +func (s *snapmgrTestSuite) TestInstallPathManyClassicAsUpdate(c *C) { + restore := release.MockReleaseInfo(&release.OS{ID: "ubuntu"}) + defer restore() + // this needs doing because dirs depends on the release info + dirs.SetRootDir(dirs.GlobalRootDir) + + restore = snapstate.MockSnapReadInfo(func(name string, si *snap.SideInfo) (*snap.Info, error) { + return &snap.Info{SuggestedName: name, Confinement: "classic"}, nil + }) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + var paths []string + var sideInfos []*snap.SideInfo + + snapNames := []string{"some-snap", "other-snap"} + for _, name := range snapNames { + si := &snap.SideInfo{ + RealName: name, + Revision: snap.R("1"), + } + snapstate.Set(s.state, name, &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + Flags: snapstate.Flags{Classic: true}, + }) + yaml := fmt.Sprintf(`name: %s +version: 1.0 +confinement: classic +`, name) + paths = append(paths, makeTestSnap(c, yaml)) + + si = &snap.SideInfo{ + RealName: name, + Revision: snap.R("2"), + } + sideInfos = append(sideInfos, si) + } + + checkClassicInstall := func(tss []*state.TaskSet, err error, expectClassic bool) { + c.Assert(err, IsNil) + c.Check(tss, HasLen, 2) + + for i := range paths { + snapsup, err := snapstate.TaskSnapSetup(tss[i].Tasks()[0]) + c.Assert(err, IsNil) + c.Check(snapsup.Classic, Equals, expectClassic) + } + + if c.Failed() { + c.FailNow() + } + } + + // works with --classic + tss, err := snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, s.user.ID, &snapstate.Flags{Classic: true}) + checkClassicInstall(tss, err, true) + + // works without --classic + tss, err = snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, s.user.ID, nil) + checkClassicInstall(tss, err, true) + + // devmode overrides the snapsetup classic flag + tss, err = snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, s.user.ID, &snapstate.Flags{DevMode: true}) + checkClassicInstall(tss, err, false) + + // jailmode overrides it too (you need to provide both) + tss, err = snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, s.user.ID, &snapstate.Flags{JailMode: true}) + checkClassicInstall(tss, err, false) + + // jailmode and classic together gets you both + tss, err = snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, s.user.ID, &snapstate.Flags{JailMode: true, Classic: true}) + checkClassicInstall(tss, err, true) +} + +func (s *snapmgrTestSuite) TestInstallPathManyValidateContainer(c *C) { + s.state.Lock() + defer s.state.Unlock() + + path, si := mkInvalidSnap(c) + _, err := snapstate.InstallPathMany(context.Background(), s.state, []*snap.SideInfo{si}, []string{path}, s.user.ID, nil) + c.Assert(err, ErrorMatches, fmt.Sprintf(".*%s.*", snap.ErrBadModes)) +} + +func mkInvalidSnap(c *C) (string, *snap.SideInfo) { + si := &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R("1"), + } + yaml := []byte(`name: some-snap +version: 1 +`) + + dstDir := c.MkDir() + c.Assert(os.Chmod(dstDir, 0700), IsNil) + + c.Assert(os.Mkdir(filepath.Join(dstDir, "meta"), 0700), IsNil) + c.Assert(ioutil.WriteFile(filepath.Join(dstDir, "meta", "snap.yaml"), yaml, 0700), IsNil) + + // snapdir has /meta/snap.yaml, but / is 0700 + brokenSnap := filepath.Join(c.MkDir(), "broken.snap") + out, err := exec.Command("mksquashfs", dstDir, brokenSnap).CombinedOutput() + if err != nil { + c.Log(out) + c.Error(err) + c.FailNow() + } + + return brokenSnap, si +} + +func (s *snapmgrTestSuite) TestInstallPathManyWithLocalPrereqAndBaseNoStore(c *C) { + s.state.Lock() + defer s.state.Unlock() + + tr := config.NewTransaction(s.state) + c.Assert(tr.Set("core", "experimental.check-disk-space-install", true), IsNil) + tr.Commit() + + // use the real disk check since it also includes store checks + restore := snapstate.MockInstallSize(snapstate.InstallSize) + defer restore() + + // no core, we'll install it as well + snapstate.Set(s.state, "core", nil) + + var paths []string + var sideInfos []*snap.SideInfo + + snapNames := []string{"some-snap", "prereq-snap", "core"} + yamls := []string{ + `name: some-snap +version: 1.0 +base: core +plugs: + myplug: + interface: content + content: mycontent + default-provider: prereq-snap +`, + `name: prereq-snap +version: 1.0 +base: core +slots: + myslot: + interface: content + content: mycontent`, + `name: core +version: 1.0 +type: base +`, + } + + for i, name := range snapNames { + paths = append(paths, makeTestSnap(c, yamls[i])) + si := &snap.SideInfo{ + RealName: name, + Revision: snap.R("1"), + } + sideInfos = append(sideInfos, si) + } + + tss, err := snapstate.InstallPathMany(context.Background(), s.state, sideInfos, paths, 0, nil) + c.Assert(err, IsNil) + c.Assert(tss, HasLen, 3) + + chg := s.state.NewChange("install", "install local snaps") + for _, ts := range tss { + chg.AddAll(ts) + } + + defer s.se.Stop() + s.settle(c) + + c.Assert(chg.Err(), IsNil) + c.Assert(chg.IsReady(), Equals, true) + + op := s.fakeBackend.ops.First("storesvc-snap-action") + c.Assert(op, IsNil) +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstate_remove_test.go snapd-2.54.2+21.10/overlord/snapstate/snapstate_remove_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstate_remove_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/snapstate_remove_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -253,10 +253,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -400,9 +398,7 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -552,9 +548,7 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -662,10 +656,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -833,10 +825,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(len(s.fakeBackend.ops), Equals, 2) expected := fakeOps{ @@ -881,6 +871,55 @@ c.Check(snapst.Sequence, HasLen, 2) } +func (s *snapmgrTestSuite) TestRemoveOneRevisionDropsRevertStatus(c *C) { + si3 := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(3), + } + + si5 := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(5), + } + + si7 := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si5, &si3, &si7}, + Current: si7.Revision, + RevertStatus: map[int]snapstate.RevertStatus{ + 3: snapstate.NotBlocked, + 5: snapstate.NotBlocked, + }, + SnapType: "app", + }) + + chg := s.state.NewChange("remove", "remove a snap") + ts, err := snapstate.Remove(s.state, "some-snap", snap.R(3), nil) + c.Assert(err, IsNil) + chg.AddAll(ts) + + defer s.se.Stop() + s.settle(c) + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) + c.Check(snapst.Sequence, HasLen, 2) + // revert status of revision 3 got dropped + c.Check(snapst.RevertStatus, DeepEquals, map[int]snapstate.RevertStatus{ + 5: snapstate.NotBlocked, + }) +} + func (s *snapmgrTestSuite) TestRemoveLastRevisionRunThrough(c *C) { si := snap.SideInfo{ RealName: "some-snap", @@ -902,10 +941,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(len(s.fakeBackend.ops), Equals, 9) expected := fakeOps{ @@ -1136,10 +1173,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // verify snaps in the system state var snapst snapstate.SnapState @@ -1189,10 +1224,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // verify snaps in the system state var snapst snapstate.SnapState @@ -1367,20 +1400,20 @@ fakeSnappyBackend } -func (f *snapdBackend) RemoveSnapData(info *snap.Info) error { +func (f *snapdBackend) RemoveSnapData(info *snap.Info, opts *dirs.SnapDirOptions) error { dir := snap.DataDir(info.SnapName(), info.Revision) if err := os.Remove(dir); err != nil { return fmt.Errorf("unexpected error: %v", err) } - return f.fakeSnappyBackend.RemoveSnapData(info) + return f.fakeSnappyBackend.RemoveSnapData(info, nil) } -func (f *snapdBackend) RemoveSnapCommonData(info *snap.Info) error { +func (f *snapdBackend) RemoveSnapCommonData(info *snap.Info, opts *dirs.SnapDirOptions) error { dir := snap.CommonDataDir(info.SnapName()) if err := os.Remove(dir); err != nil { return fmt.Errorf("unexpected error: %v", err) } - return f.fakeSnappyBackend.RemoveSnapCommonData(info) + return f.fakeSnappyBackend.RemoveSnapCommonData(info, nil) } func isUndone(c *C, tasks []*state.Task, kind string, numExpected int) { @@ -1460,10 +1493,8 @@ // are still present. injectError(c, chg, "clear-snap", snap.Revision{N: 1}) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.ErrorStatus) isUndone(c, chg.Tasks(), "unlink-snap", 1) @@ -1540,10 +1571,8 @@ // current rev 1), before discarding the snap completely. injectError(c, chg, "discard-snap", snap.Revision{N: 1}) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.ErrorStatus) isUndone(c, chg.Tasks(), "unlink-snap", 1) @@ -1620,6 +1649,11 @@ st.Lock() defer st.Unlock() + // enable gate-auto-refresh-hook feature + tr := config.NewTransaction(s.state) + tr.Set("core", "experimental.gate-auto-refresh-hook", true) + tr.Commit() + for _, sn := range []string{"some-snap", "another-snap", "foo-snap"} { si := snap.SideInfo{ RealName: sn, @@ -1642,8 +1676,10 @@ } st.Set("refresh-candidates", rc) - c.Assert(snapstate.HoldRefresh(st, "some-snap", 0, "foo-snap"), IsNil) - c.Assert(snapstate.HoldRefresh(st, "another-snap", 0, "some-snap"), IsNil) + _, err := snapstate.HoldRefresh(st, "some-snap", 0, "foo-snap") + c.Assert(err, IsNil) + _, err = snapstate.HoldRefresh(st, "another-snap", 0, "some-snap") + c.Assert(err, IsNil) held, err := snapstate.HeldSnaps(st) c.Assert(err, IsNil) @@ -1657,10 +1693,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // verify snaps in the system state var snapst snapstate.SnapState @@ -1699,7 +1733,8 @@ rc := map[string]*snapstate.RefreshCandidate{"some-snap": {}} st.Set("refresh-candidates", rc) - c.Assert(snapstate.HoldRefresh(st, "some-snap", 0, "some-snap"), IsNil) + _, err := snapstate.HoldRefresh(st, "some-snap", 0, "some-snap") + c.Assert(err, IsNil) held, err := snapstate.HeldSnaps(st) c.Assert(err, IsNil) @@ -1710,10 +1745,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // verify snap in the system state var snapst snapstate.SnapState @@ -1909,3 +1942,33 @@ c.Check(ts, NotNil) c.Check(err, IsNil) } + +func (s *snapmgrTestSuite) TestRemoveDeduplicatesSnapNames(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{{ + RealName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(1), + }}, + Current: snap.R(1), + Active: true, + }) + + snapstate.Set(s.state, "some-base", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{{ + RealName: "some-base", + SnapID: "some-base-id", + Revision: snap.R(1), + }}, + Current: snap.R(1), + Active: true, + }) + + removed, ts, err := snapstate.RemoveMany(s.state, []string{"some-snap", "some-base", "some-snap", "some-base"}) + c.Assert(err, IsNil) + c.Check(removed, testutil.DeepUnsortedMatches, []string{"some-snap", "some-base"}) + c.Check(ts, HasLen, 2) +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstatetest/restart.go snapd-2.54.2+21.10/overlord/snapstate/snapstatetest/restart.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstatetest/restart.go 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/snapstatetest/restart.go 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,44 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2016-2021 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + * + */ + +package snapstatetest + +import ( + "github.com/snapcore/snapd/overlord/restart" + "github.com/snapcore/snapd/overlord/state" +) + +// MockRestartHandler mocks a restart.Handler based on a function +// to witness the restart requests. +type MockRestartHandler func(restart.RestartType) + +func (h MockRestartHandler) HandleRestart(t restart.RestartType) { + if h == nil { + return + } + h(t) +} + +func (h MockRestartHandler) RebootAsExpected(*state.State) error { + return nil +} + +func (h MockRestartHandler) RebootDidNotHappen(*state.State) error { + panic("internal error: mocking should not invoke RebootDidNotHappen") +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstate_test.go snapd-2.54.2+21.10/overlord/snapstate/snapstate_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstate_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/snapstate_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2018 Canonical Ltd + * Copyright (C) 2016-2021 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -51,6 +51,7 @@ "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/hookstate" "github.com/snapcore/snapd/overlord/ifacestate/ifacerepo" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/servicestate" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/backend" @@ -67,7 +68,7 @@ func TestSnapManager(t *testing.T) { TestingT(t) } -type snapmgrTestSuite struct { +type snapmgrBaseTest struct { testutil.BaseTest o *overlord.Overlord state *state.State @@ -84,21 +85,42 @@ user3 *auth.UserState } -func (s *snapmgrTestSuite) settle(c *C) { +// state must be locked by caller +func (s *snapmgrBaseTest) settle(c *C) { + s.state.Unlock() + defer s.state.Lock() + err := s.o.Settle(testutil.HostScaledTimeout(5 * time.Second)) - c.Assert(err, IsNil) + if err != nil { + s.state.Lock() + defer s.state.Unlock() + c.Error(err) + s.logTasks(c) + c.FailNow() + } } -var _ = Suite(&snapmgrTestSuite{}) +func (s *snapmgrBaseTest) logTasks(c *C) { + for _, chg := range s.state.Changes() { + c.Logf("\nChange %q (%s):", chg.Summary(), chg.Status()) + + for _, t := range chg.Tasks() { + c.Logf("\t%s - %s", t.Summary(), t.Status()) + } + } +} var fakeRevDateEpoch = time.Date(2018, 1, 0, 0, 0, 0, 0, time.UTC) -func (s *snapmgrTestSuite) SetUpTest(c *C) { +func (s *snapmgrBaseTest) SetUpTest(c *C) { s.BaseTest.SetUpTest(c) dirs.SetRootDir(c.MkDir()) s.o = overlord.Mock() s.state = s.o.State() + s.state.Lock() + restart.Init(s.state, "boot-id-0", nil) + s.state.Unlock() s.BaseTest.AddCleanup(snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {})) @@ -241,7 +263,7 @@ })) } -func (s *snapmgrTestSuite) TearDownTest(c *C) { +func (s *snapmgrBaseTest) TearDownTest(c *C) { s.BaseTest.TearDownTest(c) snapstate.ValidateRefreshes = nil snapstate.AutoAliases = nil @@ -249,7 +271,7 @@ } type ForeignTaskTracker interface { - ForeignTask(kind string, status state.Status, snapsup *snapstate.SnapSetup) + ForeignTask(kind string, status state.Status, snapsup *snapstate.SnapSetup) error } func AddForeignTaskHandlers(runner *state.TaskRunner, tracker ForeignTaskTracker) { @@ -264,12 +286,10 @@ return err } - tracker.ForeignTask(kind, status, snapsup) - - return nil + return tracker.ForeignTask(kind, status, snapsup) } runner.AddHandler("setup-profiles", fakeHandler, fakeHandler) - runner.AddHandler("auto-connect", fakeHandler, nil) + runner.AddHandler("auto-connect", fakeHandler, fakeHandler) runner.AddHandler("auto-disconnect", fakeHandler, nil) runner.AddHandler("remove-profiles", fakeHandler, fakeHandler) runner.AddHandler("discard-conns", fakeHandler, fakeHandler) @@ -296,6 +316,12 @@ } +type snapmgrTestSuite struct { + snapmgrBaseTest +} + +var _ = Suite(&snapmgrTestSuite{}) + func (s *snapmgrTestSuite) TestCleanSnapStateGet(c *C) { snapst := snapstate.SnapState{ Sequence: []*snap.SideInfo{ @@ -398,12 +424,13 @@ const ( unlinkBefore = 1 << iota cleanupAfter - maybeCore runCoreConfigure doesReRefresh updatesGadget + updatesGadgetAssets updatesBootConfig noConfigure + noLastBeforeModificationsEdge ) func taskKinds(tasks []*state.Task) []string { @@ -546,10 +573,8 @@ chg := s.state.NewChange("revert", "revert snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() var snapst snapstate.SnapState err = snapstate.Get(s.state, "some-snap", &snapst) @@ -1006,10 +1031,8 @@ c.Assert(err, IsNil) disableChg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(disableChg.Err(), IsNil) c.Assert(disableChg.IsReady(), Equals, true) @@ -1049,10 +1072,8 @@ c.Assert(err, IsNil) disableChg.AddAll(disableTs) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(disableChg.Err(), IsNil) c.Assert(disableChg.IsReady(), Equals, true) @@ -1072,10 +1093,8 @@ c.Assert(err, IsNil) enableChg.AddAll(enableTs) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(enableChg.Err(), IsNil) c.Assert(enableChg.IsReady(), Equals, true) @@ -1115,10 +1134,8 @@ c.Assert(err, IsNil) disableChg.AddAll(disableTs) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(disableChg.Err(), IsNil) c.Assert(disableChg.IsReady(), Equals, true) @@ -1138,10 +1155,8 @@ c.Assert(err, IsNil) enableChg.AddAll(enableTs) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(enableChg.Err(), IsNil) c.Assert(enableChg.IsReady(), Equals, true) @@ -1183,10 +1198,8 @@ c.Assert(err, IsNil) disableChg.AddAll(disableTs) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(disableChg.Err(), IsNil) c.Assert(disableChg.IsReady(), Equals, true) @@ -1206,10 +1219,8 @@ c.Assert(err, IsNil) enableChg.AddAll(enableTs) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(enableChg.Err(), IsNil) c.Assert(enableChg.IsReady(), Equals, true) @@ -1243,10 +1254,8 @@ c.Assert(err, IsNil) disableChg.AddAll(disableTs) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(disableChg.Err(), IsNil) c.Assert(disableChg.IsReady(), Equals, true) @@ -1266,10 +1275,8 @@ c.Assert(err, IsNil) enableChg.AddAll(enableTs) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(enableChg.Err(), IsNil) c.Assert(enableChg.IsReady(), Equals, true) @@ -1320,11 +1327,9 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // config snapshot of rev. 2 has been made by 'revert' var cfgs map[string]interface{} c.Assert(s.state.Get("revision-config", &cfgs), IsNil) @@ -1458,10 +1463,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -1521,9 +1524,115 @@ Channel: "", Revision: snap.R(7), }) + c.Check(snapst.RevertStatus, HasLen, 0) c.Assert(snapst.Block(), DeepEquals, []snap.Revision{snap.R(7)}) } +func (s *snapmgrTestSuite) TestRevertRevisionNotBlocked(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(7), + } + siOld := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(2), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + SnapType: "app", + Sequence: []*snap.SideInfo{&siOld, &si}, + Current: si.Revision, + }) + + chg := s.state.NewChange("revert", "revert a snap backwards") + ts, err := snapstate.Revert(s.state, "some-snap", snapstate.Flags{RevertStatus: snapstate.NotBlocked}) + c.Assert(err, IsNil) + chg.AddAll(ts) + + defer s.se.Stop() + s.settle(c) + + // verify that the R(2) version is active now and R(7) is still there + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) + + // last refresh time shouldn't be modified on revert. + c.Check(snapst.LastRefreshTime, IsNil) + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Current, Equals, snap.R(2)) + c.Assert(snapst.Sequence, HasLen, 2) + c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ + RealName: "some-snap", + Channel: "", + Revision: snap.R(2), + }) + c.Assert(snapst.Sequence[1], DeepEquals, &snap.SideInfo{ + RealName: "some-snap", + Channel: "", + Revision: snap.R(7), + }) + // we have reverted from rev 7 to rev 2, but rev 7 is marked as not blocked + // due to revert. + c.Check(snapst.RevertStatus, DeepEquals, map[int]snapstate.RevertStatus{ + 7: snapstate.NotBlocked, + }) + c.Assert(snapst.Block(), HasLen, 0) +} + +func (s *snapmgrTestSuite) TestRevertRevisionNotBlockedUndo(c *C) { + si := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(1), + } + si2 := snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(2), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + SnapType: "app", + Sequence: []*snap.SideInfo{&si, &si2}, + Current: si2.Revision, + RevertStatus: map[int]snapstate.RevertStatus{ + 3: snapstate.NotBlocked, + }, + }) + + chg := s.state.NewChange("revert", "install a revert") + ts, err := snapstate.Revert(s.state, "some-snap", snapstate.Flags{RevertStatus: snapstate.NotBlocked}) + c.Assert(err, IsNil) + tasks := ts.Tasks() + last := tasks[len(tasks)-1] + terr := s.state.NewTask("error-trigger", "provoking undo") + terr.WaitFor(last) + chg.AddAll(ts) + chg.AddTask(terr) + + defer s.se.Stop() + s.settle(c) + + // verify snaps in the system state + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) + + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Sequence, HasLen, 2) + c.Assert(snapst.Current, Equals, snap.R(2)) + c.Check(snapst.RevertStatus, DeepEquals, map[int]snapstate.RevertStatus{ + 3: snapstate.NotBlocked, + }) +} + func (s *snapmgrTestSuite) TestRevertWithBaseRunThrough(c *C) { si := snap.SideInfo{ RealName: "some-snap-with-base", @@ -1569,10 +1678,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -1655,10 +1762,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -1752,10 +1857,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(s.fakeBackend.ops.Ops(), HasLen, 7) @@ -1796,10 +1899,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -1882,10 +1983,8 @@ terr.WaitFor(last) chg.AddTask(terr) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -1926,6 +2025,11 @@ name: "some-snap", }, { + op: "auto-connect:Undoing", + name: "some-snap", + revno: snap.R(1), + }, + { op: "unlink-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/1"), }, @@ -1983,10 +2087,8 @@ s.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "some-snap/1") - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -2100,10 +2202,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -2178,10 +2278,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -2265,9 +2363,7 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -2345,9 +2441,7 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -2528,9 +2622,7 @@ c.Assert(err, IsNil, comment) chg.AddAll(ts) - s.state.Unlock() s.settle(c) - s.state.Lock() // switch is not really really doing anything backend related c.Assert(s.fakeBackend.ops, HasLen, 0, comment) @@ -2583,10 +2675,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // switch is not really really doing anything backend related c.Assert(s.fakeBackend.ops, HasLen, 0) @@ -2655,10 +2745,8 @@ chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(chg.Status(), Equals, state.UndoneStatus) c.Assert(errReported, Equals, 0) @@ -2685,10 +2773,8 @@ chg.AddAll(ts) s.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "some-snap/11") - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // no failure report was generated } @@ -3088,9 +3174,7 @@ chg.AddTask(terr) // run the changes - s.state.Unlock() s.settle(c) - s.state.Lock() s.verifyRefreshLast(c) } @@ -3260,7 +3344,7 @@ task := st.NewTask("auto-connect", "...") // not restarting - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) si := &snap.SideInfo{RealName: "some-app"} snaptest.MockSnap(c, "name: some-app\nversion: 1", si) snapsup := &snapstate.SnapSetup{SideInfo: si} @@ -3268,7 +3352,7 @@ c.Check(err, IsNil) // restarting ... we always wait - state.MockRestarting(st, state.RestartDaemon) + restart.MockPending(st, restart.RestartDaemon) err = snapstate.FinishRestart(task, snapsup) c.Check(err, FitsTypeOf, &state.Retry{}) } @@ -3327,7 +3411,7 @@ snapsup := &snapstate.SnapSetup{SideInfo: si, Type: snapInfo.SnapType} // restarting - state.MockRestarting(st, state.RestartUnset) + restart.MockPending(st, restart.RestartUnset) c.Assert(snapstate.FinishRestart(task, snapsup), IsNil) c.Check(generateWrappersCalled, Equals, tc.expectedWrappersCall, Commentf("#%d: %v", i, tc)) @@ -4327,7 +4411,7 @@ c.Assert(tsl, HasLen, 3) // 1. install core - verifyInstallTasks(c, runCoreConfigure|maybeCore, 0, tsl[0], s.state) + verifyInstallTasks(c, snap.TypeOS, runCoreConfigure, 0, tsl[0]) // 2 transition-connections verifyTransitionConnectionsTasks(c, tsl[1]) // 3 remove-ubuntu-core @@ -4381,10 +4465,8 @@ chg.AddAll(ts) } - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Assert(chg.Err(), IsNil) @@ -4572,10 +4654,8 @@ chg.AddAll(ts) } - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Assert(chg.Err(), IsNil) @@ -4656,10 +4736,8 @@ SnapType: "os", }) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(s.state.Changes(), HasLen, 1) c.Check(s.state.Changes()[0].Kind(), Equals, "transition-ubuntu-core") @@ -4679,10 +4757,8 @@ SnapType: "os", }) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(s.state.Changes(), HasLen, 0) // not counted as a try @@ -4705,20 +4781,17 @@ // tried 3h ago, no retry s.state.Set("ubuntu-core-transition-last-retry-time", time.Now().Add(-3*time.Hour)) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(s.state.Changes(), HasLen, 0) // tried 7h ago, retry s.state.Set("ubuntu-core-transition-last-retry-time", time.Now().Add(-7*time.Hour)) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() + c.Check(s.state.Changes(), HasLen, 1) var t time.Time @@ -4739,10 +4812,8 @@ chg := s.state.NewChange("unrelated-change", "unfinished change blocks core transition") chg.SetStatus(state.DoStatus) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(s.state.Changes(), HasLen, 1) c.Check(s.state.Changes()[0].Kind(), Equals, "unrelated-change") @@ -4780,10 +4851,8 @@ // no snaps installed on this system (e.g. fresh classic) snapstate.Set(s.state, "core", nil) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(s.state.Changes(), HasLen, 0) } @@ -4804,10 +4873,8 @@ Current: snap.R(1), }) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(s.state.Changes(), HasLen, 1) } @@ -4823,10 +4890,8 @@ SnapType: "os", }) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(s.state.Changes(), HasLen, 0) } @@ -4845,10 +4910,8 @@ tr.Set("core", "experimental.snapd-snap", true) tr.Commit() - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(s.state.Changes(), HasLen, 1) chg := s.state.Changes()[0] @@ -4878,10 +4941,8 @@ tr.Set("core", "experimental.snapd-snap", true) tr.Commit() - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(s.state.Changes(), HasLen, 1) chg := s.state.Changes()[0] @@ -4890,7 +4951,9 @@ c.Assert(chg.IsReady(), Equals, true) c.Check(s.fakeStore.downloads, HasLen, 1) ts := state.NewTaskSet(chg.Tasks()...) - verifyInstallTasks(c, noConfigure, 0, ts, s.state) + // task set was reconstituted from change tasks, so edges information is + // lost + verifyInstallTasks(c, snap.TypeSnapd, noConfigure|noLastBeforeModificationsEdge, 0, ts) // ensure preferences from the core snap got transferred over var snapst snapstate.SnapState @@ -4909,20 +4972,17 @@ // tried 3h ago, no retry s.state.Set("snapd-transition-last-retry-time", time.Now().Add(-3*time.Hour)) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(s.state.Changes(), HasLen, 0) // tried 7h ago, retry s.state.Set("snapd-transition-last-retry-time", time.Now().Add(-7*time.Hour)) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() + c.Check(s.state.Changes(), HasLen, 1) var t time.Time @@ -5035,10 +5095,8 @@ snapstate.Get(s.state, name, &snapst1) c.Assert(snapst1.DevMode, Equals, true) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() var snapst2 snapstate.SnapState snapstate.Get(s.state, name, &snapst2) @@ -5055,11 +5113,12 @@ defer r() c.Assert(sandbox.ForceDevMode(), Equals, true) - defer s.se.Stop() - s.settle(c) s.state.Lock() defer s.state.Unlock() + defer s.se.Stop() + s.settle(c) + var n int s.state.Get("fix-forced-devmode", &n) c.Check(n, Equals, 1) @@ -5089,10 +5148,8 @@ snapstate.Get(s.state, "core", &snapst1) c.Assert(snapst1.DevMode, Equals, true) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() var snapst2 snapstate.SnapState snapstate.Get(s.state, "core", &snapst2) @@ -5910,10 +5967,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(len(s.fakeBackend.ops) >= 1, Equals, true) storeAction := s.fakeBackend.ops[0] @@ -6102,7 +6157,7 @@ } deviceCtx := &snapstatetest.TrivialDeviceContext{DeviceModel: model} s.state.Lock() - ch, err := snapstate.ResolveChannel(s.state, tc.snap, tc.cur, tc.new, deviceCtx) + ch, err := snapstate.ResolveChannel(tc.snap, tc.cur, tc.new, deviceCtx) s.state.Unlock() comment := Commentf("tc %d: %#v", i, tc) if tc.err != "" { @@ -6126,7 +6181,7 @@ c.Assert(err, IsNil) c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) - verifyInstallTasks(c, updatesGadget, 0, ts, s.state) + verifyInstallTasks(c, snap.TypeGadget, 0, 0, ts) } func (s *snapmgrTestSuite) TestGadgetUpdateTaskAddedOnRefresh(c *C) { @@ -6150,7 +6205,32 @@ c.Assert(err, IsNil) c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) - verifyUpdateTasks(c, unlinkBefore|cleanupAfter|doesReRefresh|updatesGadget, 0, ts, s.state) + verifyUpdateTasks(c, snap.TypeGadget, doesReRefresh, 0, ts) + +} + +func (s *snapmgrTestSuite) TestGadgetUpdateTaskAddedOnKernelRefresh(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "brand-kernel", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "brand-kernel", SnapID: "brand-kernel-id", Revision: snap.R(1)}, + }, + Current: snap.R(1), + SnapType: "kernel", + }) + + // and on update + ts, err := snapstate.Update(s.state, "brand-kernel", &snapstate.RevisionOptions{}, 0, snapstate.Flags{}) + c.Assert(err, IsNil) + + c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) + verifyUpdateTasks(c, snap.TypeKernel, doesReRefresh, 0, ts) } @@ -6231,10 +6311,8 @@ t.Set("snap-setup", sup) chg.AddTask(t) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(chg.Status(), Equals, state.DoneStatus) @@ -6298,10 +6376,8 @@ terr.JoinLane(t.Lanes()[0]) chg.AddTask(terr) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(chg.Status(), Equals, state.ErrorStatus) c.Check(t.Status(), Equals, state.UndoneStatus) @@ -6360,10 +6436,8 @@ terr.JoinLane(t.Lanes()[0]) chg.AddTask(terr) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(chg.Status(), Equals, state.ErrorStatus) c.Check(t.Status(), Equals, state.UndoneStatus) @@ -6429,10 +6503,8 @@ return nil } - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.IsReady(), Equals, true) c.Assert(chg.Err(), ErrorMatches, `(?s)cannot perform the following tasks:.*- +\(start-snap-services mock error\).*`) @@ -6533,10 +6605,8 @@ c.Assert(err, IsNil) installChg.AddAll(installTs) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(installChg.Err(), IsNil) c.Assert(installChg.IsReady(), Equals, true) @@ -6575,10 +6645,8 @@ c.Assert(err, IsNil) updateChg.AddAll(updateTs) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(updateChg.Err(), IsNil) c.Assert(updateChg.IsReady(), Equals, true) @@ -6622,10 +6690,8 @@ c.Assert(err, IsNil) installChg.AddAll(installTs) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(installChg.Err(), IsNil) c.Assert(installChg.IsReady(), Equals, true) @@ -6654,10 +6720,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // various backend operations, but no unlink-current-snap expected := fakeOps{ @@ -6813,9 +6877,10 @@ st.Lock() defer st.Unlock() - snaps, err := snapstate.InstalledSnaps(st) + snaps, ignoreValidation, err := snapstate.InstalledSnaps(st) c.Assert(err, IsNil) c.Check(snaps, HasLen, 0) + c.Check(ignoreValidation, HasLen, 0) snapstate.Set(st, "foo", &snapstate.SnapState{ Active: true, @@ -6825,10 +6890,322 @@ snaptest.MockSnap(c, string(`name: foo version: 1`), &snap.SideInfo{Revision: snap.R("13")}) - snaps, err = snapstate.InstalledSnaps(st) + snapstate.Set(st, "bar", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{{RealName: "bar", Revision: snap.R(5), SnapID: "bar-id"}}, + Current: snap.R(5), + Flags: snapstate.Flags{IgnoreValidation: true}, + }) + snaptest.MockSnap(c, string(`name: bar +version: 1`), &snap.SideInfo{Revision: snap.R("5")}) + + snaps, ignoreValidation, err = snapstate.InstalledSnaps(st) c.Assert(err, IsNil) - c.Check(snaps, HasLen, 1) - c.Check(snaps[0].SnapName(), Equals, "foo") - c.Check(snaps[0].ID(), Equals, "foo-id") - c.Check(snaps[0].Revision, Equals, snap.R("23")) + c.Check(snaps, testutil.DeepUnsortedMatches, []*snapasserts.InstalledSnap{ + snapasserts.NewInstalledSnap("foo", "foo-id", snap.R("23")), + snapasserts.NewInstalledSnap("bar", "bar-id", snap.R("5"))}) + + c.Check(ignoreValidation, DeepEquals, map[string]bool{"bar": true}) +} + +func (s *snapmgrTestSuite) addSnapsForRemodel(c *C) { + si := &snap.SideInfo{ + RealName: "some-base", Revision: snap.R(1), + } + snaptest.MockSnapCurrent(c, "name: some-base\nversion: 1.0\ntype: base\n", si) + snapstate.Set(s.state, "some-base", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + SnapType: "base", + }) + + si = &snap.SideInfo{ + RealName: "some-kernel", Revision: snap.R(2), + } + snaptest.MockSnapCurrent(c, "name: some-kernel\nversion: 1.0\ntype: kernel\n", si) + snapstate.Set(s.state, "some-kernel", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + SnapType: "kernel", + }) + si = &snap.SideInfo{ + RealName: "some-gadget", Revision: snap.R(3), + } + snaptest.MockSnapCurrent(c, "name: some-gadget\nversion: 1.0\ntype: gadget\n", si) + snapstate.Set(s.state, "some-gadget", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + SnapType: "gadget", + }) +} + +var nonReLinkKinds = []string{ + "copy-snap-data", + "setup-profiles", + "auto-connect", + "set-auto-aliases", + "setup-aliases", + "run-hook[install]", + "start-snap-services", + "run-hook[configure]", + "run-hook[check-health]", +} + +func kindsToSet(kinds []string) map[string]bool { + s := make(map[string]bool, len(kinds)) + for _, k := range kinds { + s[k] = true + } + return s +} + +func (s *snapmgrTestSuite) TestRemodelLinkNewBaseOrKernelHappy(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + s.BaseTest.AddCleanup(snapstate.MockSnapReadInfo(snap.ReadInfo)) + s.state.Lock() + defer s.state.Unlock() + s.addSnapsForRemodel(c) + + ts, err := snapstate.LinkNewBaseOrKernel(s.state, "some-kernel") + c.Assert(err, IsNil) + tasks := ts.Tasks() + c.Check(taskKinds(tasks), DeepEquals, expectedDoInstallTasks(snap.TypeKernel, 0, 0, []string{"prepare-snap"}, kindsToSet(nonReLinkKinds))) + c.Assert(tasks, HasLen, 3) + tPrepare := tasks[0] + tUpdateGadgetAssets := tasks[1] + tLink := tasks[2] + c.Assert(tPrepare.Kind(), Equals, "prepare-snap") + c.Assert(tPrepare.Summary(), Equals, `Prepare snap "some-kernel" (2) for remodel`) + c.Assert(tPrepare.Has("snap-setup"), Equals, true) + c.Assert(tUpdateGadgetAssets.Kind(), Equals, "update-gadget-assets") + c.Assert(tUpdateGadgetAssets.Summary(), Equals, `Update assets from kernel "some-kernel" (2) for remodel`) + c.Assert(tUpdateGadgetAssets.WaitTasks(), DeepEquals, []*state.Task{tPrepare}) + c.Assert(tLink.Kind(), Equals, "link-snap") + c.Assert(tLink.Summary(), Equals, `Make snap "some-kernel" (2) available to the system during remodel`) + c.Assert(tLink.WaitTasks(), DeepEquals, []*state.Task{tUpdateGadgetAssets}) + + ts, err = snapstate.LinkNewBaseOrKernel(s.state, "some-base") + c.Assert(err, IsNil) + tasks = ts.Tasks() + c.Check(taskKinds(tasks), DeepEquals, expectedDoInstallTasks(snap.TypeBase, 0, 0, []string{"prepare-snap"}, kindsToSet(nonReLinkKinds))) + c.Assert(tasks, HasLen, 2) + tPrepare = tasks[0] + tLink = tasks[1] + c.Assert(tPrepare.Kind(), Equals, "prepare-snap") + c.Assert(tPrepare.Summary(), Equals, `Prepare snap "some-base" (1) for remodel`) + c.Assert(tPrepare.Has("snap-setup"), Equals, true) + c.Assert(tLink.Kind(), Equals, "link-snap") + c.Assert(tLink.Summary(), Equals, `Make snap "some-base" (1) available to the system during remodel`) +} + +func (s *snapmgrTestSuite) TestRemodelLinkNewBaseOrKernelBadType(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + s.BaseTest.AddCleanup(snapstate.MockSnapReadInfo(snap.ReadInfo)) + s.state.Lock() + defer s.state.Unlock() + s.addSnapsForRemodel(c) + + si := &snap.SideInfo{RealName: "some-snap", Revision: snap.R(3)} + snaptest.MockSnapCurrent(c, "name: snap-gadget\nversion: 1.0\n", si) + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + SnapType: "app", + }) + ts, err := snapstate.LinkNewBaseOrKernel(s.state, "some-snap") + c.Assert(err, ErrorMatches, `internal error: cannot link type app`) + c.Assert(ts, IsNil) + + ts, err = snapstate.LinkNewBaseOrKernel(s.state, "some-gadget") + c.Assert(err, ErrorMatches, `internal error: cannot link type gadget`) + c.Assert(ts, IsNil) +} + +func (s *snapmgrTestSuite) TestRemodelAddLinkNewBaseOrKernel(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + s.BaseTest.AddCleanup(snapstate.MockSnapReadInfo(snap.ReadInfo)) + s.state.Lock() + defer s.state.Unlock() + + // try a kernel snap first + si := &snap.SideInfo{RealName: "some-kernel", Revision: snap.R(2)} + tPrepare := s.state.NewTask("prepare-snap", "dummy task") + snapsup := &snapstate.SnapSetup{ + SideInfo: si, + Type: "kernel", + } + tPrepare.Set("snap-setup", snapsup) + tDummy := s.state.NewTask("dummy-task", "dummy task") + ts := state.NewTaskSet(tPrepare, tDummy) + + tsNew, err := snapstate.AddLinkNewBaseOrKernel(s.state, ts) + c.Assert(err, IsNil) + c.Assert(tsNew, NotNil) + tasks := tsNew.Tasks() + c.Check(taskKinds(tasks), DeepEquals, expectedDoInstallTasks(snap.TypeKernel, 0, 0, []string{"prepare-snap", "dummy-task"}, kindsToSet(nonReLinkKinds))) + // since this is the kernel, we have our task + dummy task + update-gadget-assets + link-snap + c.Assert(tasks, HasLen, 4) + tUpdateGadgetAssets := tasks[2] + tLink := tasks[3] + c.Assert(tUpdateGadgetAssets.Kind(), Equals, "update-gadget-assets") + c.Assert(tUpdateGadgetAssets.Summary(), Equals, `Update assets from kernel "some-kernel" (2) for remodel`) + c.Assert(tUpdateGadgetAssets.WaitTasks(), DeepEquals, []*state.Task{ + tDummy, + }) + c.Assert(tLink.Kind(), Equals, "link-snap") + c.Assert(tLink.Summary(), Equals, `Make snap "some-kernel" (2) available to the system during remodel`) + c.Assert(tLink.WaitTasks(), DeepEquals, []*state.Task{ + // waits for last task in the set + tUpdateGadgetAssets, + }) + for _, tsk := range []*state.Task{tLink, tUpdateGadgetAssets} { + var ssID string + c.Assert(tsk.Get("snap-setup-task", &ssID), IsNil) + c.Assert(ssID, Equals, tPrepare.ID()) + } + + // try with base snap + si = &snap.SideInfo{RealName: "some-base", Revision: snap.R(1)} + tPrepare = s.state.NewTask("prepare-snap", "dummy task") + tPrepare.Set("snap-setup", &snapstate.SnapSetup{ + SideInfo: si, + Type: "base", + }) + ts = state.NewTaskSet(tPrepare) + tsNew, err = snapstate.AddLinkNewBaseOrKernel(s.state, ts) + c.Assert(err, IsNil) + c.Assert(tsNew, NotNil) + tasks = tsNew.Tasks() + c.Check(taskKinds(tasks), DeepEquals, expectedDoInstallTasks(snap.TypeBase, 0, 0, []string{"prepare-snap"}, kindsToSet(nonReLinkKinds))) + // since this is the base, we have our task + link-snap only + c.Assert(tasks, HasLen, 2) + tLink = tasks[1] + c.Assert(tLink.Kind(), Equals, "link-snap") + c.Assert(tLink.Summary(), Equals, `Make snap "some-base" (1) available to the system during remodel`) + var ssID string + c.Assert(tLink.Get("snap-setup-task", &ssID), IsNil) + c.Assert(ssID, Equals, tPrepare.ID()) + + // but bails when there is no task with snap setup + ts = state.NewTaskSet() + tsNew, err = snapstate.AddLinkNewBaseOrKernel(s.state, ts) + c.Assert(err, ErrorMatches, `internal error: cannot identify task with snap-setup`) + c.Assert(tsNew, IsNil) +} + +func (s *snapmgrTestSuite) TestRemodelSwitchNewGadget(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + s.BaseTest.AddCleanup(snapstate.MockSnapReadInfo(snap.ReadInfo)) + s.state.Lock() + defer s.state.Unlock() + s.addSnapsForRemodel(c) + + ts, err := snapstate.SwitchToNewGadget(s.state, "some-gadget") + c.Assert(err, IsNil) + tasks := ts.Tasks() + c.Check(taskKinds(tasks), DeepEquals, expectedDoInstallTasks(snap.TypeGadget, 0, 0, []string{"prepare-snap"}, kindsToSet(append(nonReLinkKinds, "link-snap")))) + c.Assert(tasks, HasLen, 3) + tPrepare := tasks[0] + tUpdateGadgetAssets := tasks[1] + tUpdateGadgetCmdline := tasks[2] + c.Assert(tPrepare.Kind(), Equals, "prepare-snap") + c.Assert(tPrepare.Summary(), Equals, `Prepare snap "some-gadget" (3) for remodel`) + c.Assert(tPrepare.Has("snap-setup"), Equals, true) + c.Assert(tUpdateGadgetAssets.Kind(), Equals, "update-gadget-assets") + c.Assert(tUpdateGadgetAssets.Summary(), Equals, `Update assets from gadget "some-gadget" (3) for remodel`) + c.Assert(tUpdateGadgetAssets.WaitTasks(), DeepEquals, []*state.Task{tPrepare}) + c.Assert(tUpdateGadgetCmdline.Kind(), Equals, "update-gadget-cmdline") + c.Assert(tUpdateGadgetCmdline.Summary(), Equals, `Update kernel command line from gadget "some-gadget" (3) for remodel`) + c.Assert(tUpdateGadgetCmdline.WaitTasks(), DeepEquals, []*state.Task{tUpdateGadgetAssets}) +} + +func (s *snapmgrTestSuite) TestRemodelSwitchNewGadgetBadType(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + s.BaseTest.AddCleanup(snapstate.MockSnapReadInfo(snap.ReadInfo)) + s.state.Lock() + defer s.state.Unlock() + s.addSnapsForRemodel(c) + + si := &snap.SideInfo{RealName: "some-snap", Revision: snap.R(3)} + snaptest.MockSnapCurrent(c, "name: snap-gadget\nversion: 1.0\n", si) + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + SnapType: "app", + }) + ts, err := snapstate.SwitchToNewGadget(s.state, "some-snap") + c.Assert(err, ErrorMatches, `internal error: cannot link type app`) + c.Assert(ts, IsNil) + ts, err = snapstate.SwitchToNewGadget(s.state, "some-kernel") + c.Assert(err, ErrorMatches, `internal error: cannot link type kernel`) + c.Assert(ts, IsNil) + ts, err = snapstate.SwitchToNewGadget(s.state, "some-base") + c.Assert(err, ErrorMatches, `internal error: cannot link type base`) + c.Assert(ts, IsNil) +} + +func (s *snapmgrTestSuite) TestRemodelAddGadgetAssetTasks(c *C) { + restore := release.MockOnClassic(false) + defer restore() + + s.BaseTest.AddCleanup(snapstate.MockSnapReadInfo(snap.ReadInfo)) + s.state.Lock() + defer s.state.Unlock() + + si := &snap.SideInfo{RealName: "some-gadget", Revision: snap.R(3)} + tPrepare := s.state.NewTask("prepare-snap", "dummy task") + snapsup := &snapstate.SnapSetup{ + SideInfo: si, + Type: "gadget", + } + tPrepare.Set("snap-setup", snapsup) + tDummy := s.state.NewTask("dummy-task", "dummy task") + ts := state.NewTaskSet(tPrepare, tDummy) + + tsNew, err := snapstate.AddGadgetAssetsTasks(s.state, ts) + c.Assert(err, IsNil) + c.Assert(tsNew, NotNil) + tasks := tsNew.Tasks() + c.Check(taskKinds(tasks), DeepEquals, expectedDoInstallTasks(snap.TypeGadget, 0, 0, []string{"prepare-snap", "dummy-task"}, kindsToSet(append(nonReLinkKinds, "link-snap")))) + // since this is the gadget, we have our task + dummy task + update assets + update cmdline + c.Assert(tasks, HasLen, 4) + tUpdateGadgetAssets := tasks[2] + tUpdateGadgetCmdline := tasks[3] + c.Assert(tUpdateGadgetAssets.Kind(), Equals, "update-gadget-assets") + c.Assert(tUpdateGadgetAssets.Summary(), Equals, `Update assets from gadget "some-gadget" (3) for remodel`) + c.Assert(tUpdateGadgetAssets.WaitTasks(), DeepEquals, []*state.Task{ + // waits for the last task in the set + tDummy, + }) + c.Assert(tUpdateGadgetCmdline.Kind(), Equals, "update-gadget-cmdline") + c.Assert(tUpdateGadgetCmdline.Summary(), Equals, `Update kernel command line from gadget "some-gadget" (3) for remodel`) + c.Assert(tUpdateGadgetCmdline.WaitTasks(), DeepEquals, []*state.Task{ + tUpdateGadgetAssets, + }) + for _, tsk := range []*state.Task{tUpdateGadgetAssets, tUpdateGadgetCmdline} { + var ssID string + c.Assert(tsk.Get("snap-setup-task", &ssID), IsNil) + c.Assert(ssID, Equals, tPrepare.ID()) + } + + // but bails when there is no task with snap setup + ts = state.NewTaskSet() + tsNew, err = snapstate.AddGadgetAssetsTasks(s.state, ts) + c.Assert(err, ErrorMatches, `internal error: cannot identify task with snap-setup`) + c.Assert(tsNew, IsNil) } diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstate_try_test.go snapd-2.54.2+21.10/overlord/snapstate/snapstate_try_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstate_try_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/snapstate_try_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -72,10 +72,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstate_update_test.go snapd-2.54.2+21.10/overlord/snapstate/snapstate_update_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/snapstate_update_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/snapstate_update_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2020 Canonical Ltd + * Copyright (C) 2016-2021 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -26,6 +26,7 @@ "os" "path/filepath" "sort" + "strings" "time" . "gopkg.in/check.v1" @@ -36,6 +37,7 @@ "github.com/snapcore/snapd/dirs" "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/ifacetest" + "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/osutil" "github.com/snapcore/snapd/overlord/assertstate" "github.com/snapcore/snapd/overlord/auth" @@ -44,6 +46,7 @@ _ "github.com/snapcore/snapd/overlord/configstate" "github.com/snapcore/snapd/overlord/configstate/config" "github.com/snapcore/snapd/overlord/ifacestate/ifacerepo" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/snapstate" "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" "github.com/snapcore/snapd/overlord/state" @@ -54,73 +57,25 @@ "github.com/snapcore/snapd/testutil" ) -func verifyUpdateTasks(c *C, opts, discards int, ts *state.TaskSet, st *state.State) { +func verifyUpdateTasks(c *C, typ snap.Type, opts, discards int, ts *state.TaskSet) { kinds := taskKinds(ts.Tasks()) - expected := []string{ - "prerequisites", - "download-snap", - "validate-snap", - "mount-snap", - } - expected = append(expected, "run-hook[pre-refresh]") - if opts&unlinkBefore != 0 { - expected = append(expected, - "stop-snap-services", - ) - } - if opts&unlinkBefore != 0 { - expected = append(expected, - "remove-aliases", - "unlink-current-snap", - ) - } - if opts&updatesGadget != 0 { - expected = append(expected, - "update-gadget-assets", - "update-gadget-cmdline") - } - expected = append(expected, - "copy-snap-data", - "setup-profiles", - "link-snap", - ) - if opts&maybeCore != 0 { - expected = append(expected, "setup-profiles") - } - expected = append(expected, - "auto-connect", - "set-auto-aliases", - "setup-aliases", - "run-hook[post-refresh]", - "start-snap-services") - - c.Assert(ts.Tasks()[len(expected)-2].Summary(), Matches, `Run post-refresh hook of .*`) - for i := 0; i < discards; i++ { - expected = append(expected, - "clear-snap", - "discard-snap", - ) - } - if opts&cleanupAfter != 0 { - expected = append(expected, - "cleanup", - ) - } - expected = append(expected, - "run-hook[configure]", - "run-hook[check-health]", - ) + expected := expectedDoInstallTasks(typ, unlinkBefore|cleanupAfter|opts, discards, nil, nil) if opts&doesReRefresh != 0 { expected = append(expected, "check-rerefresh") } c.Assert(kinds, DeepEquals, expected) + + te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge) + c.Assert(te, NotNil) + c.Assert(te.Kind(), Equals, "validate-snap") } func (s *snapmgrTestSuite) TestUpdateDoesGC(c *C) { s.state.Lock() defer s.state.Unlock() + restore := release.MockOnClassic(false) defer restore() @@ -141,10 +96,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure garbage collection runs as the last tasks expectedTail := fakeOps{ @@ -229,9 +182,7 @@ c.Assert(err, IsNil, comment) chg.AddAll(ts) - s.state.Unlock() s.settle(c) - s.state.Lock() // switch is not really really doing anything backend related c.Assert(s.fakeBackend.ops.Ops(), DeepEquals, []string{ @@ -294,7 +245,7 @@ ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{}) c.Assert(err, IsNil) - verifyUpdateTasks(c, unlinkBefore|cleanupAfter|doesReRefresh, 2, ts, s.state) + verifyUpdateTasks(c, snap.TypeApp, doesReRefresh, 2, ts) // and ensure that it will remove the revisions after "current" // (si3, si4) @@ -341,10 +292,9 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() + expected := fakeOps{ { op: "remove-snap-aliases", @@ -467,10 +417,8 @@ } chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() var snapst snapstate.SnapState err = snapstate.Get(s.state, "some-snap", &snapst) @@ -696,7 +644,7 @@ ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{}) c.Assert(err, IsNil) - verifyUpdateTasks(c, unlinkBefore|cleanupAfter|doesReRefresh, 0, ts, s.state) + verifyUpdateTasks(c, snap.TypeApp, doesReRefresh, 0, ts) c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) c.Check(validateCalled, Equals, true) @@ -731,10 +679,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{{ @@ -874,10 +820,13 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() + // local modifications, edge must be set + te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge) + c.Assert(te, NotNil) + c.Assert(te.Kind(), Equals, "validate-snap") + defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -1069,6 +1018,66 @@ c.Check(snapstate.AuxStoreInfoFilename("services-snap-id"), testutil.FilePresent) } +func (s *snapmgrTestSuite) TestUpdateDropsRevertStatus(c *C) { + si := snap.SideInfo{ + RealName: "services-snap", + Revision: snap.R(7), + SnapID: "services-snap-id", + } + snaptest.MockSnap(c, `name: services-snap`, &si) + + s.state.Lock() + defer s.state.Unlock() + + si2 := snap.SideInfo{ + RealName: "services-snap", + Revision: snap.R(11), + SnapID: "services-snap-id", + } + snapstate.Set(s.state, "services-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si, &si2}, + Current: si.Revision, + RevertStatus: map[int]snapstate.RevertStatus{ + 11: snapstate.NotBlocked, + }, + SnapType: "app", + TrackingChannel: "latest/stable", + CohortKey: "embattled", + }) + + chg := s.state.NewChange("refresh", "refresh a snap") + ts, err := snapstate.Update(s.state, "services-snap", &snapstate.RevisionOptions{ + Channel: "some-channel", + CohortKey: "some-cohort", + }, s.user.ID, snapstate.Flags{}) + c.Assert(err, IsNil) + chg.AddAll(ts) + + defer s.se.Stop() + s.settle(c) + + // verify snaps in the system state + var snapst snapstate.SnapState + c.Assert(snapstate.Get(s.state, "services-snap", &snapst), IsNil) + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Current, Equals, snap.R(11)) + c.Assert(snapst.Sequence, HasLen, 2) + c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ + RealName: "services-snap", + SnapID: "services-snap-id", + Channel: "", + Revision: snap.R(7), + }) + c.Assert(snapst.Sequence[1], DeepEquals, &snap.SideInfo{ + RealName: "services-snap", + Channel: "some-channel", + SnapID: "services-snap-id", + Revision: snap.R(11), + }) + c.Check(snapst.RevertStatus, HasLen, 0) +} + func (s *snapmgrTestSuite) TestUpdateResetsHoldState(c *C) { si := snap.SideInfo{ RealName: "some-snap", @@ -1108,7 +1117,8 @@ tr.Commit() // pretend that the snap was held during last auto-refresh - c.Assert(snapstate.HoldRefresh(s.state, "gating-snap", 0, "some-snap", "other-snap"), IsNil) + _, err := snapstate.HoldRefresh(s.state, "gating-snap", 0, "some-snap", "other-snap") + c.Assert(err, IsNil) // sanity check held, err := snapstate.HeldSnaps(s.state) c.Assert(err, IsNil) @@ -1164,9 +1174,7 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -1375,10 +1383,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{ {macaroon: s.user.StoreMacaroon, name: "some-base", target: filepath.Join(dirs.SnapBlobDir, "some-base_11.snap")}, @@ -1421,10 +1427,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{ {macaroon: s.user.StoreMacaroon, name: "some-snap", target: filepath.Join(dirs.SnapBlobDir, "some-snap_11.snap")}, @@ -1458,10 +1462,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{ {macaroon: s.user.StoreMacaroon, name: "snap-content-plug", target: filepath.Join(dirs.SnapBlobDir, "snap-content-plug_11.snap")}, @@ -1519,10 +1521,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{ {macaroon: s.user.StoreMacaroon, name: "snap-content-plug", target: filepath.Join(dirs.SnapBlobDir, "snap-content-plug_11.snap")}, @@ -1548,10 +1548,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus) c.Assert(chg.Err(), IsNil) @@ -1606,10 +1604,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(chg.Status(), Equals, state.DoneStatus) @@ -1731,10 +1727,8 @@ } c.Check(updated, HasLen, 3) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus) c.Assert(chg.Err(), IsNil) @@ -1839,10 +1833,8 @@ } c.Check(updated, HasLen, 3) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus) c.Assert(chg.Err(), IsNil) @@ -1966,10 +1958,8 @@ } c.Check(updated, HasLen, 3) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus) c.Assert(chg.Err(), IsNil) @@ -2060,10 +2050,8 @@ s.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "/some-snap/11") - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -2277,10 +2265,8 @@ terr.JoinLane(last.Lanes()[0]) chg.AddTask(terr) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(chg.Status(), Equals, state.ErrorStatus) c.Check(errorTaskExecuted, Equals, true) @@ -2319,11 +2305,9 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() cfgs = nil // config copy of rev. 1 has been made c.Assert(s.state.Get("revision-config", &cfgs), IsNil) @@ -2368,10 +2352,8 @@ terr.JoinLane(last.Lanes()[0]) chg.AddTask(terr) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ { @@ -2472,6 +2454,11 @@ name: "some-snap", }, { + op: "auto-connect:Undoing", + name: "some-snap", + revno: snap.R(11), + }, + { op: "unlink-snap", path: filepath.Join(dirs.SnapMountDir, "some-snap/11"), }, @@ -2571,10 +2558,8 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Status(), Equals, state.DoneStatus) c.Assert(chg.Err(), IsNil) @@ -2730,10 +2715,12 @@ chg := s.state.NewChange("refresh", "refresh a snap") chg.AddAll(ts) - s.state.Unlock() + // no local modifications, hence no edge + te := ts.MaybeEdge(snapstate.LastBeforeLocalModificationsEdge) + c.Assert(te, IsNil) + defer s.se.Stop() s.settle(c) - s.state.Lock() expected := fakeOps{ // we just expect the "storesvc-snap-action" ops, we @@ -2874,10 +2861,8 @@ chg := s.state.NewChange("refresh", "refresh a snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // verify snapSetup info var snapsup snapstate.SnapSetup @@ -3041,10 +3026,8 @@ chg := s.state.NewChange("refresh", "refresh snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // verify snap has IgnoreValidation set var snapst snapstate.SnapState @@ -3090,10 +3073,8 @@ chg = s.state.NewChange("refresh", "refresh snaps") chg.AddAll(tts[0]) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() snapst = snapstate.SnapState{} err = snapstate.Get(s.state, "some-snap", &snapst) @@ -3144,10 +3125,8 @@ chg = s.state.NewChange("refresh", "refresh snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() snapst = snapstate.SnapState{} err = snapstate.Get(s.state, "some-snap", &snapst) @@ -3237,10 +3216,8 @@ chg := s.state.NewChange("refresh", "refresh snaps") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Assert(chg.Err(), IsNil) @@ -3275,9 +3252,7 @@ chg.AddAll(ts) } - s.state.Unlock() s.settle(c) - s.state.Lock() // ensure all our tasks ran c.Assert(chg.Err(), IsNil) @@ -3391,7 +3366,7 @@ ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "channel-for-7"}, s.user.ID, snapstate.Flags{Amend: true}) c.Assert(err, IsNil) - verifyUpdateTasks(c, unlinkBefore|cleanupAfter|doesReRefresh, 0, ts, s.state) + verifyUpdateTasks(c, snap.TypeApp, doesReRefresh, 0, ts) // ensure we go from local to store revision-7 var snapsup snapstate.SnapSetup @@ -3544,6 +3519,50 @@ }) } +func (s *snapmgrTestSuite) TestAllUpdateRevisionNotBlocked(c *C) { + // update-all *should* set the block list + si7 := snap.SideInfo{ + RealName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(7), + } + si11 := snap.SideInfo{ + RealName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(11), + } + + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{&si7, &si11}, + Current: si7.Revision, + RevertStatus: map[int]snapstate.RevertStatus{ + si7.Revision.N: snapstate.NotBlocked, + }, + }) + + updates, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, s.user.ID, nil) + c.Check(err, IsNil) + c.Check(updates, HasLen, 0) + + c.Assert(s.fakeBackend.ops, HasLen, 2) + c.Check(s.fakeBackend.ops[0], DeepEquals, fakeOp{ + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + InstanceName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(7), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 7), + Block: []snap.Revision{snap.R(11)}, + Epoch: snap.E("1*"), + }}, + userID: 1, + }) +} + func (s *snapmgrTestSuite) TestUpdateManyPartialFailureCheckRerefreshDone(c *C) { s.state.Lock() defer s.state.Unlock() @@ -3603,10 +3622,8 @@ checkIsAutoRefresh(c, chg.Tasks(), true) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // not updated var snapst snapstate.SnapState @@ -4095,7 +4112,7 @@ ts, err := snapstate.UpdateWithDeviceContext(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{}, deviceCtx, "") c.Assert(err, IsNil) - verifyUpdateTasks(c, unlinkBefore|cleanupAfter|doesReRefresh, 0, ts, s.state) + verifyUpdateTasks(c, snap.TypeApp, doesReRefresh, 0, ts) c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) c.Check(validateCalled, Equals, true) @@ -4126,7 +4143,7 @@ opts := &snapstate.RevisionOptions{Channel: "some-channel", Revision: snap.R(11)} ts, err := snapstate.UpdateWithDeviceContext(s.state, "some-snap", opts, 0, snapstate.Flags{}, deviceCtx, "") c.Assert(err, IsNil) - verifyUpdateTasks(c, unlinkBefore|cleanupAfter|doesReRefresh, 0, ts, s.state) + verifyUpdateTasks(c, snap.TypeApp, doesReRefresh, 0, ts) c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) } @@ -4209,10 +4226,8 @@ chg := s.state.NewChange("refresh", "refresh snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -4283,10 +4298,8 @@ chg := s.state.NewChange("refresh", "refresh snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // verify snap is in classic var snapst snapstate.SnapState @@ -4414,11 +4427,11 @@ c.Assert(err, IsNil) // ensure edges information is still there - te, err := ts.Edge(snapstate.DownloadAndChecksDoneEdge) + te, err := ts.Edge(snapstate.LastBeforeLocalModificationsEdge) c.Assert(te, NotNil) c.Assert(err, IsNil) - verifyUpdateTasks(c, unlinkBefore|cleanupAfter|doesReRefresh, expectedDiscards, ts, s.state) + verifyUpdateTasks(c, snap.TypeApp, doesReRefresh, expectedDiscards, ts) c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) } @@ -4441,7 +4454,7 @@ ts, err := snapstate.Update(s.state, "some-snap", nil, 0, snapstate.Flags{}) c.Assert(err, IsNil) - verifyUpdateTasks(c, unlinkBefore|cleanupAfter|doesReRefresh, 3, ts, s.state) + verifyUpdateTasks(c, snap.TypeApp, doesReRefresh, 3, ts) c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())) } @@ -4486,7 +4499,7 @@ c.Check(updates, DeepEquals, []string{"some-snap"}) ts := tts[0] - verifyUpdateTasks(c, unlinkBefore|cleanupAfter, 3, ts, s.state) + verifyUpdateTasks(c, snap.TypeApp, 0, 3, ts) // check that the tasks are in non-default lane for _, t := range ts.Tasks() { @@ -4495,7 +4508,7 @@ c.Assert(s.state.TaskCount(), Equals, len(ts.Tasks())+1) // 1==rerefresh // ensure edges information is still there - te, err := ts.Edge(snapstate.DownloadAndChecksDoneEdge) + te, err := ts.Edge(snapstate.LastBeforeLocalModificationsEdge) c.Assert(te, NotNil) c.Assert(err, IsNil) @@ -4559,10 +4572,8 @@ // refresh of some-snap fails on link-snap s.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "/some-snap/11") - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Check(chg.Err(), ErrorMatches, ".*cannot perform the following tasks:\n- Make snap \"some-snap\" \\(11\\) available to the system.*") c.Check(chg.IsReady(), Equals, true) @@ -4913,7 +4924,7 @@ c.Assert(tts, HasLen, 2) verifyLastTasksetIsReRefresh(c, tts) c.Check(updates, DeepEquals, []string{"some-snap"}) - verifyUpdateTasks(c, unlinkBefore|cleanupAfter, 0, tts[0], s.state) + verifyUpdateTasks(c, snap.TypeApp, 0, 0, tts[0]) c.Check(validateCalled, Equals, true) } @@ -4979,8 +4990,8 @@ c.Assert(snapsup.InstanceName(), Equals, "some-snap") c.Assert(snapsupInstance.InstanceName(), Equals, "some-snap_instance") - verifyUpdateTasks(c, unlinkBefore|cleanupAfter, 3, tts[0], s.state) - verifyUpdateTasks(c, unlinkBefore|cleanupAfter, 1, tts[1], s.state) + verifyUpdateTasks(c, snap.TypeApp, 0, 3, tts[0]) + verifyUpdateTasks(c, snap.TypeApp, 0, 1, tts[1]) } func (s *snapmgrTestSuite) TestParallelInstanceUpdateManyValidateRefreshes(c *C) { @@ -5037,8 +5048,8 @@ verifyLastTasksetIsReRefresh(c, tts) sort.Strings(updates) c.Check(updates, DeepEquals, []string{"some-snap", "some-snap_instance"}) - verifyUpdateTasks(c, unlinkBefore|cleanupAfter, 0, tts[0], s.state) - verifyUpdateTasks(c, unlinkBefore|cleanupAfter, 0, tts[1], s.state) + verifyUpdateTasks(c, snap.TypeApp, 0, 0, tts[0]) + verifyUpdateTasks(c, snap.TypeApp, 0, 0, tts[1]) c.Check(validateCalled, Equals, true) } @@ -5218,10 +5229,8 @@ } } - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -5277,10 +5286,8 @@ } } - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -5336,10 +5343,8 @@ } } - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -5393,10 +5398,8 @@ chg.AddTask(t1) chg.AddTask(t2) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -5447,10 +5450,8 @@ t.Set("snap-setup", snapsup) chg.AddTask(t) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -5500,11 +5501,9 @@ c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // config of rev. 1 has been stored in per-revision map var cfgs map[string]interface{} c.Assert(s.state.Get("revision-config", &cfgs), IsNil) @@ -5565,10 +5564,8 @@ chg.AddAll(ts) } - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // content consumer snap fails to download c.Assert(chg.Err(), ErrorMatches, "cannot perform the following tasks:\n.*Download snap \"snap-content-slot\" \\(11\\) from channel \"latest/stable\" \\(boom\\).*") @@ -5622,10 +5619,8 @@ s.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "some-snap/11") - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() // verify we generated a failure report c.Check(n, Equals, 1) @@ -5674,10 +5669,10 @@ ts, err = snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Channel: "some-channel"}, s.user.ID, snapstate.Flags{}) c.Assert(err, IsNil) chg.AddAll(ts) - s.state.Unlock() + defer s.se.Stop() s.settle(c) - s.state.Lock() + // verify that we excluded this field from the bugreport c.Check(n, Equals, 2) c.Check(errExtra, DeepEquals, map[string]string{ @@ -5762,10 +5757,8 @@ chg := s.state.NewChange("refresh", "refresh snap") chg.AddAll(ts) - s.state.Unlock() defer s.se.Stop() s.settle(c) - s.state.Lock() c.Assert(chg.Err(), IsNil) c.Assert(chg.IsReady(), Equals, true) @@ -5859,6 +5852,7 @@ func (s *snapmgrTestSuite) TestUpdateSnapAndOutdatedPrereq(c *C) { s.state.Lock() + defer s.state.Unlock() updateSnaps := []string{"outdated-consumer", "outdated-producer"} for _, snapName := range updateSnaps { @@ -5882,13 +5876,8 @@ for _, ts := range tss { chg.AddAll(ts) } - s.state.Unlock() - s.settle(c) - s.state.Lock() - defer s.state.Unlock() - c.Assert(chg.Err(), IsNil) c.Assert(chg.Status(), Equals, state.DoneStatus) @@ -6002,8 +5991,9 @@ c.Assert(prereqTask.AtTime().IsZero(), Equals, false) } -func (s *snapmgrTestSuite) TestUpdatePrereqNoRetryWithIfFails(c *C) { +func (s *snapmgrTestSuite) TestUpdateNoRetryIfPrereqTaskFails(c *C) { s.state.Lock() + defer s.state.Unlock() snapstate.Set(s.state, "outdated-producer", &snapstate.SnapState{ Sequence: []*snap.SideInfo{{ @@ -6033,10 +6023,7 @@ updateChg := s.state.NewChange("refresh-snap", "test: update snap") updateChg.AddAll(updateTasks) - s.state.Unlock() s.settle(c) - s.state.Lock() - defer s.state.Unlock() prereqTask := findStrictlyOnePrereqTask(c, updateChg) @@ -6193,9 +6180,8 @@ }) snaptest.MockSnap(c, `name: some-snap`, si) - fi, err := os.Stat(snap.MountFile("some-snap", si.Revision)) - c.Assert(err, IsNil) - refreshedDate := fi.ModTime() + refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1) + ts, err := snapstate.Update(s.state, "some-snap", nil, 0, snapstate.Flags{}) c.Assert(err, IsNil) @@ -6212,7 +6198,7 @@ InstanceName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1), - Epoch: snap.E("0"), + Epoch: snap.E("1*"), RefreshedDate: refreshedDate, }}}, { op: "storesvc-snap-action:action", @@ -6264,9 +6250,7 @@ }) snaptest.MockSnap(c, `name: some-snap`, si) - fi, err := os.Stat(snap.MountFile("some-snap", si.Revision)) - c.Assert(err, IsNil) - refreshedDate := fi.ModTime() + refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1) ts, err := snapstate.Update(s.state, "some-snap", nil, 0, snapstate.Flags{}) c.Assert(err, IsNil) @@ -6284,7 +6268,7 @@ InstanceName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1), - Epoch: snap.E("0"), + Epoch: snap.E("1*"), RefreshedDate: refreshedDate, }}}, { op: "storesvc-snap-action:action", @@ -6335,6 +6319,8 @@ }) snaptest.MockSnap(c, `name: some-snap`, si) + refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1) + ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Revision: snap.R(11)}, 0, snapstate.Flags{}) c.Assert(err, IsNil) @@ -6345,10 +6331,6 @@ // new snap revision from the store c.Check(snapsup.Revision(), Equals, snap.R(11)) - fi, err := os.Stat(snap.MountFile("some-snap", si.Revision)) - c.Assert(err, IsNil) - refreshedDate := fi.ModTime() - c.Assert(s.fakeBackend.ops, HasLen, 2) expectedOps := fakeOps{{ op: "storesvc-snap-action", @@ -6356,7 +6338,7 @@ InstanceName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1), - Epoch: snap.E("0"), + Epoch: snap.E("1*"), RefreshedDate: refreshedDate, }}}, { op: "storesvc-snap-action:action", @@ -6407,10 +6389,6 @@ }) snaptest.MockSnap(c, `name: some-snap`, si) - fi, err := os.Stat(snap.MountFile("some-snap", si.Revision)) - c.Assert(err, IsNil) - refreshedDate := fi.ModTime() - ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Revision: snap.R(11)}, 0, snapstate.Flags{}) c.Assert(err, IsNil) @@ -6426,8 +6404,8 @@ InstanceName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1), - Epoch: snap.E("0"), - RefreshedDate: refreshedDate, + Epoch: snap.E("1*"), + RefreshedDate: fakeRevDateEpoch.AddDate(0, 0, 1), }}, }, { op: "storesvc-snap-action:action", @@ -6490,7 +6468,7 @@ c.Assert(s.fakeBackend.ops, HasLen, 0) } -func (s *validationSetsSuite) TestUpdateToRevisionSnapRequiredByValidationWronRevisionError(c *C) { +func (s *validationSetsSuite) TestUpdateToRevisionSnapRequiredByValidationWrongRevisionError(c *C) { restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { vs := snapasserts.NewValidationSets() someSnap := map[string]interface{}{ @@ -6528,7 +6506,9 @@ c.Assert(err, ErrorMatches, `cannot update snap "some-snap" to revision 11 without --ignore-validation, revision 5 is required by validation sets: 16/foo/bar/2`) } -func (s *validationSetsSuite) TestUpdateManyRequiredByValidationSetAlreadyAtCorrectRevisionNoop(c *C) { +// test that updating to a revision that is different than the revision required +// by a validation set is possible if --ignore-validation flag is passed. +func (s *validationSetsSuite) TestUpdateToWrongRevisionIgnoreValidation(c *C) { restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { vs := snapasserts.NewValidationSets() someSnap := map[string]interface{}{ @@ -6554,37 +6534,112 @@ } assertstate.UpdateValidationSet(s.state, &tr) + // revision 1 is already installed; it doesn't match the required revision 5 + // but that's not relevant for the test (we could have installed it with + // --ignore-validation before, and that's reflected by IgnoreValidation flag + // in the snapstate). + si := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)} snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ - Active: true, - Sequence: []*snap.SideInfo{ - {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(5)}, - }, - Current: snap.R(5), + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: snap.R(1), SnapType: "app", + Flags: snapstate.Flags{ + IgnoreValidation: true, + }, }) - names, _, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, 0, nil) - c.Assert(err, IsNil) - c.Check(names, HasLen, 0) - c.Assert(s.fakeBackend.ops, HasLen, 0) -} + snaptest.MockSnap(c, `name: some-snap`, si) -func (s *validationSetsSuite) TestUpdateManyRequiredByValidationSetsCohortIgnored(c *C) { - restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { - vs := snapasserts.NewValidationSets() - someSnap := map[string]interface{}{ - "id": "yOqKhntON3vR7kwEbVPsILm7bUViPDzx", - "name": "some-snap", - "presence": "required", - "revision": "5", - } - vsa1 := s.mockValidationSetAssert(c, "bar", "2", someSnap) - vs.Add(vsa1.(*asserts.ValidationSet)) - return vs, nil - }) - defer restore() + refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1) - s.state.Lock() - defer s.state.Unlock() + // revision 5 is required and requesting revision 11 would fail + // without --ignore-validation. + revOpts := &snapstate.RevisionOptions{Revision: snap.R(11)} + _, err := snapstate.Update(s.state, "some-snap", revOpts, 0, snapstate.Flags{IgnoreValidation: true}) + c.Assert(err, IsNil) + + c.Assert(s.fakeBackend.ops, HasLen, 2) + expectedOps := fakeOps{{ + op: "storesvc-snap-action", + curSnaps: []store.CurrentSnap{{ + InstanceName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(1), + Epoch: snap.E("1*"), + RefreshedDate: refreshedDate, + IgnoreValidation: true, + }}, + }, { + op: "storesvc-snap-action:action", + action: store.SnapAction{ + Action: "refresh", + InstanceName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(11), + Flags: store.SnapActionIgnoreValidation, + }, + revno: snap.R(11), + }} + c.Assert(s.fakeBackend.ops, DeepEquals, expectedOps) +} + +func (s *validationSetsSuite) TestUpdateManyRequiredByValidationSetAlreadyAtCorrectRevisionNoop(c *C) { + restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { + vs := snapasserts.NewValidationSets() + someSnap := map[string]interface{}{ + "id": "yOqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap", + "presence": "required", + "revision": "5", + } + vsa1 := s.mockValidationSetAssert(c, "bar", "2", someSnap) + vs.Add(vsa1.(*asserts.ValidationSet)) + return vs, nil + }) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + tr := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + Current: 2, + } + assertstate.UpdateValidationSet(s.state, &tr) + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(5)}, + }, + Current: snap.R(5), + SnapType: "app", + }) + names, _, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap"}, 0, nil) + c.Assert(err, IsNil) + c.Check(names, HasLen, 0) + c.Assert(s.fakeBackend.ops, HasLen, 0) +} + +func (s *validationSetsSuite) TestUpdateManyRequiredByValidationSetsCohortIgnored(c *C) { + restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { + vs := snapasserts.NewValidationSets() + someSnap := map[string]interface{}{ + "id": "yOqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap", + "presence": "required", + "revision": "5", + } + vsa1 := s.mockValidationSetAssert(c, "bar", "2", someSnap) + vs.Add(vsa1.(*asserts.ValidationSet)) + return vs, nil + }) + defer restore() + + s.state.Lock() + defer s.state.Unlock() tr := assertstate.ValidationSetTracking{ AccountID: "foo", @@ -6604,14 +6659,12 @@ }) snaptest.MockSnap(c, `name: some-snap`, si) + refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1) + names, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, 0, &snapstate.Flags{}) c.Assert(err, IsNil) c.Check(names, DeepEquals, []string{"some-snap"}) - fi, err := os.Stat(snap.MountFile("some-snap", si.Revision)) - c.Assert(err, IsNil) - refreshedDate := fi.ModTime() - c.Assert(s.fakeBackend.ops, HasLen, 2) expectedOps := fakeOps{{ op: "storesvc-snap-action", @@ -6619,7 +6672,7 @@ InstanceName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1), - Epoch: snap.E("0"), + Epoch: snap.E("1*"), RefreshedDate: refreshedDate, }}, }, { @@ -6674,14 +6727,11 @@ }) snaptest.MockSnap(c, `name: some-snap`, si) + refreshedDate := fakeRevDateEpoch.AddDate(0, 0, 1) names, _, err := snapstate.UpdateMany(context.Background(), s.state, nil, 0, &snapstate.Flags{}) c.Assert(err, IsNil) c.Check(names, DeepEquals, []string{"some-snap"}) - fi, err := os.Stat(snap.MountFile("some-snap", si.Revision)) - c.Assert(err, IsNil) - refreshedDate := fi.ModTime() - c.Assert(s.fakeBackend.ops, HasLen, 2) expectedOps := fakeOps{{ op: "storesvc-snap-action", @@ -6689,7 +6739,7 @@ InstanceName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1), - Epoch: snap.E("0"), + Epoch: snap.E("1*"), RefreshedDate: refreshedDate, IgnoreValidation: true, }}, @@ -6800,14 +6850,796 @@ chg := s.state.NewChange("update", "test: update") chg.AddAll(ts) - s.state.Unlock() - defer s.state.Lock() s.settle(c) + + c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{ + {macaroon: s.user.StoreMacaroon, name: "outdated-consumer", target: filepath.Join(dirs.SnapBlobDir, "outdated-consumer_11.snap")}, + {macaroon: s.user.StoreMacaroon, name: "outdated-producer", target: filepath.Join(dirs.SnapBlobDir, "outdated-producer_11.snap")}, + }) +} + +func (s *validationSetsSuite) testUpdateManyValidationSetsPartialFailure(c *C) *state.Change { + logbuf, rest := logger.MockLogger() + defer rest() + + restore := snapstate.MockEnforcedValidationSets(func(st *state.State) (*snapasserts.ValidationSets, error) { + vs := snapasserts.NewValidationSets() + snap1 := map[string]interface{}{ + "id": "aaqKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-snap", + "presence": "required", + } + snap2 := map[string]interface{}{ + "id": "bgtKhntON3vR7kwEbVPsILm7bUViPDzx", + "name": "some-other-snap", + "presence": "required", + } + vsa1 := s.mockValidationSetAssert(c, "bar", "2", snap1, snap2) + vs.Add(vsa1.(*asserts.ValidationSet)) + return vs, nil + }) + defer restore() + s.state.Lock() defer s.state.Unlock() + tr := assertstate.ValidationSetTracking{ + AccountID: "foo", + Name: "bar", + Mode: assertstate.Enforce, + Current: 2, + } + assertstate.UpdateValidationSet(s.state, &tr) + + si1 := &snap.SideInfo{RealName: "some-snap", SnapID: "some-snap-id", Revision: snap.R(1)} + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si1}, + Current: snap.R(1), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-snap`, si1) + + si2 := &snap.SideInfo{RealName: "some-other-snap", SnapID: "some-other-snap-id", Revision: snap.R(1)} + snapstate.Set(s.state, "some-other-snap", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si2}, + Current: snap.R(1), + SnapType: "app", + }) + snaptest.MockSnap(c, `name: some-other-snap`, si2) + + s.fakeBackend.linkSnapFailTrigger = filepath.Join(dirs.SnapMountDir, "/some-other-snap/11") + + names, tss, err := snapstate.UpdateMany(context.Background(), s.state, nil, s.user.ID, &snapstate.Flags{}) + c.Assert(err, IsNil) + c.Check(names, DeepEquals, []string{"some-other-snap", "some-snap"}) + c.Check(logbuf.String(), Equals, "") + chg := s.state.NewChange("update", "") + for _, ts := range tss { + chg.AddAll(ts) + } + + s.settle(c) + + return chg +} + +func (s *validationSetsSuite) TestUpdateManyValidationSetsPartialFailureNothingToRestore(c *C) { + var refreshed []string + restoreMaybeRestoreValidationSetsAndRevertSnaps := snapstate.MockMaybeRestoreValidationSetsAndRevertSnaps(func(st *state.State, refreshedSnaps []string) ([]*state.TaskSet, error) { + refreshed = refreshedSnaps + // nothing to restore + return nil, nil + }) + defer restoreMaybeRestoreValidationSetsAndRevertSnaps() + + var addCurrentTrackingToValidationSetsStackCalled int + restoreAddCurrentTrackingToValidationSetsStack := snapstate.MockAddCurrentTrackingToValidationSetsStack(func(st *state.State) error { + addCurrentTrackingToValidationSetsStackCalled++ + return nil + }) + defer restoreAddCurrentTrackingToValidationSetsStack() + + s.testUpdateManyValidationSetsPartialFailure(c) + + // only some-snap was successfully refreshed, this also confirms that + // mockMaybeRestoreValidationSetsAndRevertSnaps was called. + c.Check(refreshed, DeepEquals, []string{"some-snap"}) + + // validation sets history update was attempted (could be a no-op if + // maybeRestoreValidationSetsAndRevertSnaps restored last tracking + // data). + c.Check(addCurrentTrackingToValidationSetsStackCalled, Equals, 1) +} + +func (s *validationSetsSuite) TestUpdateManyValidationSetsPartialFailureRevertTasks(c *C) { + var refreshed []string + restoreMaybeRestoreValidationSetsAndRevertSnaps := snapstate.MockMaybeRestoreValidationSetsAndRevertSnaps(func(st *state.State, refreshedSnaps []string) ([]*state.TaskSet, error) { + refreshed = refreshedSnaps + ts := state.NewTaskSet(st.NewTask("fake-revert-task", "")) + return []*state.TaskSet{ts}, nil + }) + defer restoreMaybeRestoreValidationSetsAndRevertSnaps() + + var addCurrentTrackingToValidationSetsStackCalled int + restoreAddCurrentTrackingToValidationSetsStack := snapstate.MockAddCurrentTrackingToValidationSetsStack(func(st *state.State) error { + addCurrentTrackingToValidationSetsStackCalled++ + return nil + }) + defer restoreAddCurrentTrackingToValidationSetsStack() + + chg := s.testUpdateManyValidationSetsPartialFailure(c) + + // only some-snap was successfully refreshed, this also confirms that + // mockMaybeRestoreValidationSetsAndRevertSnaps was called. + c.Check(refreshed, DeepEquals, []string{"some-snap"}) + + s.state.Lock() + defer s.state.Unlock() + + // check that a fake revert task returned by maybeRestoreValidationSetsAndRevertSnaps + // got injected into the refresh change. + var seen bool + for _, t := range chg.Tasks() { + if t.Kind() == "fake-revert-task" { + seen = true + break + } + } + c.Check(seen, Equals, true) + + // we haven't updated validation sets history + c.Check(addCurrentTrackingToValidationSetsStackCalled, Equals, 0) +} + +func (s *snapmgrTestSuite) TestUpdatePrerequisiteBackwardsCompat(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "outdated-producer", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{{ + RealName: "outdated-producer", + SnapID: "outdated-producer-id", + Revision: snap.R(1), + }}, + Current: snap.R(1), + Active: true, + }) + snapstate.Set(s.state, "outdated-consumer", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{{ + RealName: "outdated-consumer", + SnapID: "outdated-consumer-id", + Revision: snap.R(1), + }}, + Current: snap.R(1), + Active: true, + }) + + tasks, err := snapstate.Update(s.state, "outdated-consumer", nil, s.user.ID, snapstate.Flags{}) + c.Assert(err, IsNil) + c.Check(tasks, Not(HasLen), 0) + chg := s.state.NewChange("update", "test: update snap") + chg.AddAll(tasks) + + prereqTask := findStrictlyOnePrereqTask(c, chg) + + var snapsup snapstate.SnapSetup + err = prereqTask.Get("snap-setup", &snapsup) + c.Assert(err, IsNil) + + // mimic a task serialized by an "old" snapd without PrereqContentAttrs + // The new code shouldn't update the prereq since it doesn't have the content attrs + snapsup.PrereqContentAttrs = nil + prereqTask.Set("snap-setup", &snapsup) + + s.settle(c) + + // the producer wasn't updated since there were no content attributes c.Check(s.fakeStore.downloads, DeepEquals, []fakeDownload{ {macaroon: s.user.StoreMacaroon, name: "outdated-consumer", target: filepath.Join(dirs.SnapBlobDir, "outdated-consumer_11.snap")}, - {macaroon: s.user.StoreMacaroon, name: "outdated-producer", target: filepath.Join(dirs.SnapBlobDir, "outdated-producer_11.snap")}, }) } + +func (s *snapmgrTestSuite) TestUpdateDeduplicatesSnapNames(c *C) { + s.state.Lock() + defer s.state.Unlock() + + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{{ + RealName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(1), + }}, + Current: snap.R(1), + Active: true, + }) + + snapstate.Set(s.state, "some-base", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{{ + RealName: "some-base", + SnapID: "some-base-id", + Revision: snap.R(1), + }}, + Current: snap.R(1), + Active: true, + }) + + updated, _, err := snapstate.UpdateMany(context.Background(), s.state, []string{"some-snap", "some-base", "some-snap", "some-base"}, s.user.ID, nil) + c.Assert(err, IsNil) + c.Check(updated, testutil.DeepUnsortedMatches, []string{"some-snap", "some-base"}) +} + +func (s *snapmgrTestSuite) TestUpdateWithHiddenSnapDataDir(c *C) { + restore := snapstate.MockGetSnapDirOptions(func(*state.State) (*dirs.SnapDirOptions, error) { + return &dirs.SnapDirOptions{HiddenSnapDataDir: true}, nil + }) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + info := &snap.SideInfo{ + Revision: snap.R(1), + SnapID: "some-snap-id", + RealName: "some-snap", + } + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Sequence: []*snap.SideInfo{info}, + Current: info.Revision, + Active: true, + }) + + chg := s.state.NewChange("update", "update a snap") + ts, err := snapstate.Update(s.state, "some-snap", nil, s.user.ID, snapstate.Flags{}) + c.Assert(err, IsNil) + chg.AddAll(ts) + + s.settle(c) + c.Assert(chg.Err(), IsNil) + c.Assert(chg.Status(), Equals, state.DoneStatus) + + c.Assert(s.fakeBackend.ops.MustFindOp(c, "copy-data").dirOpts, DeepEquals, &dirs.SnapDirOptions{HiddenSnapDataDir: true}) +} + +func (s *snapmgrTestSuite) TestUndoInstallAfterDeletingRevisions(c *C) { + s.state.Lock() + defer s.state.Unlock() + + // 1 and 2 should be deleted, 3 is target revision and 4 is current + seq := []*snap.SideInfo{ + { + RealName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(1), + }, + { + RealName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(2), + }, + { + RealName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(3), + }, + { + RealName: "some-snap", + SnapID: "some-snap-id", + Revision: snap.R(4), + }, + } + snapstate.Set(s.state, "some-snap", &snapstate.SnapState{ + Active: true, + Sequence: seq, + Current: seq[len(seq)-1].Revision, + }) + + tr := config.NewTransaction(s.state) + // remove the first two revisions so the old-candidate-index+1 (in undoLinkSnap) would be out of bounds if we didn't + // account for discarded revisions + c.Assert(tr.Set("core", "refresh.retain", 1), IsNil) + tr.Commit() + + s.o.TaskRunner().AddHandler("fail", func(*state.Task, *tomb.Tomb) error { + return errors.New("expected") + }, nil) + + // install already stored revision + ts, err := snapstate.Update(s.state, "some-snap", &snapstate.RevisionOptions{Revision: seq[len(seq)-2].Revision}, s.user.ID, snapstate.Flags{NoReRefresh: true}) + c.Assert(err, IsNil) + c.Assert(ts, NotNil) + chg := s.state.NewChange("refresh", "") + chg.AddAll(ts) + + // make update fail after removing old snaps + failTask := s.state.NewTask("fail", "expected") + disc := findLastTask(chg, "discard-snap") + for _, lane := range disc.Lanes() { + failTask.JoinLane(lane) + } + failTask.WaitFor(disc) + chg.AddTask(failTask) + + s.settle(c) + + c.Assert(chg.Status(), Equals, state.ErrorStatus) + + var snapst snapstate.SnapState + err = snapstate.Get(s.state, "some-snap", &snapst) + c.Assert(err, IsNil) + c.Assert(snapst.Sequence, DeepEquals, seq[2:]) + + linkTask := findLastTask(chg, "link-snap") + c.Check(linkTask.Status(), Equals, state.UndoneStatus) + + var oldRevsBeforeCand []snap.Revision + c.Assert(linkTask.Get("old-revs-before-cand", &oldRevsBeforeCand), IsNil) + c.Assert(oldRevsBeforeCand, DeepEquals, []snap.Revision{seq[0].Revision, seq[1].Revision}) +} + +func findLastTask(chg *state.Change, kind string) *state.Task { + var last *state.Task + + for _, task := range chg.Tasks() { + if task.Kind() == kind { + last = task + } + } + + return last +} + +func (s *snapmgrTestSuite) TestUpdateBaseKernelSingleRebootHappy(c *C) { + restore := release.MockOnClassic(false) + defer restore() + restore = snapstate.MockRevisionDate(nil) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + var restartRequested []restart.RestartType + restart.Init(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) { + restartRequested = append(restartRequested, t) + })) + + restore = snapstatetest.MockDeviceModel(MakeModel(map[string]interface{}{ + "kernel": "kernel", + "base": "core18", + })) + defer restore() + + siKernel := snap.SideInfo{ + RealName: "kernel", + Revision: snap.R(7), + SnapID: "kernel-id", + } + siBase := snap.SideInfo{ + RealName: "core18", + Revision: snap.R(7), + SnapID: "core18-snap-id", + } + for _, si := range []*snap.SideInfo{&siKernel, &siBase} { + snaptest.MockSnap(c, fmt.Sprintf(`name: %s`, si.RealName), si) + typ := "kernel" + if si.RealName == "core18" { + typ = "base" + } + snapstate.Set(s.state, si.RealName, &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + TrackingChannel: "latest/stable", + SnapType: typ, + }) + } + + chg := s.state.NewChange("refresh", "refresh kernel and base") + affected, tss, err := snapstate.UpdateMany(context.Background(), s.state, + []string{"kernel", "core18"}, s.user.ID, &snapstate.Flags{}) + c.Assert(err, IsNil) + c.Assert(affected, DeepEquals, []string{"core18", "kernel"}) + snapTasks := make(map[string]*state.Task) + var kernelTs, baseTs *state.TaskSet + for _, ts := range tss { + chg.AddAll(ts) + for _, tsk := range ts.Tasks() { + switch tsk.Kind() { + // setup-profiles should appear right before link-snap, + // while set-auto-aliase appears right after + // auto-connect + case "link-snap", "auto-connect", "setup-profiles", "set-auto-aliases": + snapsup, err := snapstate.TaskSnapSetup(tsk) + c.Assert(err, IsNil) + snapTasks[fmt.Sprintf("%s@%s", tsk.Kind(), snapsup.Type)] = tsk + if tsk.Kind() == "link-snap" { + opts := 0 + if snapsup.Type == snap.TypeBase { + opts |= noConfigure + baseTs = ts + } else if snapsup.Type == snap.TypeKernel { + kernelTs = ts + } + verifyUpdateTasks(c, snapsup.Type, opts, 0, ts) + } + } + } + } + + c.Assert(snapTasks, HasLen, 8) + linkSnapKernel := snapTasks["link-snap@kernel"] + autoConnectKernel := snapTasks["auto-connect@kernel"] + linkSnapBase := snapTasks["link-snap@base"] + autoConnectBase := snapTasks["auto-connect@base"] + c.Assert(kernelTs, NotNil) + c.Assert(baseTs, NotNil) + c.Assert(kernelTs.MaybeEdge(snapstate.BeforeMaybeRebootEdge), Equals, snapTasks["setup-profiles@kernel"]) + c.Assert(kernelTs.MaybeEdge(snapstate.MaybeRebootEdge), Equals, linkSnapKernel) + c.Assert(kernelTs.MaybeEdge(snapstate.MaybeRebootWaitEdge), Equals, autoConnectKernel) + c.Assert(kernelTs.MaybeEdge(snapstate.AfterMaybeRebootWaitEdge), Equals, snapTasks["set-auto-aliases@kernel"]) + + c.Assert(baseTs.MaybeEdge(snapstate.BeforeMaybeRebootEdge), Equals, snapTasks["setup-profiles@base"]) + c.Assert(baseTs.MaybeEdge(snapstate.MaybeRebootEdge), Equals, linkSnapBase) + c.Assert(baseTs.MaybeEdge(snapstate.MaybeRebootWaitEdge), Equals, autoConnectBase) + c.Assert(baseTs.MaybeEdge(snapstate.AfterMaybeRebootWaitEdge), Equals, snapTasks["set-auto-aliases@base"]) + + c.Assert(linkSnapBase.WaitTasks(), DeepEquals, []*state.Task{ + snapTasks["setup-profiles@base"], snapTasks["setup-profiles@kernel"], + }) + c.Assert(linkSnapKernel.WaitTasks(), DeepEquals, []*state.Task{ + snapTasks["setup-profiles@kernel"], linkSnapBase, + }) + c.Assert(autoConnectKernel.WaitTasks(), DeepEquals, []*state.Task{ + snapTasks["link-snap@kernel"], autoConnectBase, + }) + c.Assert(autoConnectBase.WaitTasks(), DeepEquals, []*state.Task{ + snapTasks["link-snap@base"], snapTasks["link-snap@kernel"], + }) + c.Assert(snapTasks["set-auto-aliases@kernel"].WaitTasks(), DeepEquals, []*state.Task{ + autoConnectKernel, + }) + c.Assert(snapTasks["set-auto-aliases@base"].WaitTasks(), DeepEquals, []*state.Task{ + autoConnectBase, autoConnectKernel, + }) + + var cannotReboot bool + // link-snap of base cannot issue a reboot + c.Assert(linkSnapBase.Get("cannot-reboot", &cannotReboot), IsNil) + c.Assert(cannotReboot, Equals, true) + // but the link-snap of the kernel can issue a reboot + c.Assert(linkSnapKernel.Get("cannot-reboot", &cannotReboot), Equals, state.ErrNoState) + + // have fake backend indicate a need to reboot for both snaps + s.fakeBackend.linkSnapMaybeReboot = true + s.fakeBackend.linkSnapRebootFor = map[string]bool{ + "kernel": true, + "core18": true, + } + + defer s.se.Stop() + s.settle(c) + + c.Check(chg.Status(), Equals, state.DoneStatus) + // a single system restart was requested + c.Check(restartRequested, DeepEquals, []restart.RestartType{ + restart.RestartSystem, + }) + + for _, name := range []string{"kernel", "core18"} { + snapID := "kernel-id" + if name == "core18" { + snapID = "core18-snap-id" + } + var snapst snapstate.SnapState + err = snapstate.Get(s.state, name, &snapst) + c.Assert(err, IsNil) + + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Sequence, HasLen, 2) + c.Assert(snapst.Sequence[0], DeepEquals, &snap.SideInfo{ + RealName: name, + SnapID: snapID, + Channel: "", + Revision: snap.R(7), + }) + c.Assert(snapst.Sequence[1], DeepEquals, &snap.SideInfo{ + RealName: name, + Channel: "latest/stable", + SnapID: snapID, + Revision: snap.R(11), + }) + } + + // ops come in semi random order, but we know that link and auto-connect + // operations will be done in a specific order, + ops := make([]string, 0, 8) + for _, op := range s.fakeBackend.ops { + if op.op == "link-snap" { + split := strings.Split(op.path, "/") + c.Assert(len(split) > 2, Equals, true) + ops = append(ops, filepath.Join(split[len(split)-2:]...)) + } else if op.op == "cleanup-trash" { + ops = append(ops, fmt.Sprintf("%s-%s", op.op, op.name)) + } else if strings.HasPrefix(op.op, "auto-connect:") || strings.HasPrefix(op.op, "setup-profiles:") { + ops = append(ops, fmt.Sprintf("%s-%s/%s", op.op, op.name, op.revno)) + } + } + c.Assert(ops, HasLen, 8) + c.Assert(ops[0:2], testutil.DeepUnsortedMatches, []string{ + "setup-profiles:Doing-kernel/11", "setup-profiles:Doing-core18/11", + }) + c.Assert(ops[2:6], DeepEquals, []string{ + "core18/11", "kernel/11", + "auto-connect:Doing-core18/11", "auto-connect:Doing-kernel/11", + }) + c.Assert(ops[6:], testutil.DeepUnsortedMatches, []string{ + "cleanup-trash-core18", "cleanup-trash-kernel", + }) +} + +func (s *snapmgrTestSuite) TestUpdateBaseKernelSingleRebootUnsupportedWithCoreHappy(c *C) { + restore := release.MockOnClassic(false) + defer restore() + restore = snapstate.MockRevisionDate(nil) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + var restartRequested []restart.RestartType + restart.Init(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) { + restartRequested = append(restartRequested, t) + })) + + restore = snapstatetest.MockDeviceModel(DefaultModel()) + defer restore() + + siKernel := snap.SideInfo{ + RealName: "kernel", + Revision: snap.R(7), + SnapID: "kernel-id", + } + siCore := snap.SideInfo{ + RealName: "core", + Revision: snap.R(7), + SnapID: "core-snap-id", + } + for _, si := range []*snap.SideInfo{&siKernel, &siCore} { + snaptest.MockSnap(c, fmt.Sprintf(`name: %s`, si.RealName), si) + typ := "kernel" + if si.RealName == "core18" { + typ = "base" + } + snapstate.Set(s.state, si.RealName, &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + TrackingChannel: "latest/stable", + SnapType: typ, + }) + } + + chg := s.state.NewChange("refresh", "refresh kernel and base") + affected, tss, err := snapstate.UpdateMany(context.Background(), s.state, + []string{"kernel", "core"}, s.user.ID, &snapstate.Flags{}) + c.Assert(err, IsNil) + c.Assert(affected, DeepEquals, []string{"core", "kernel"}) + snapTasks := make(map[string]*state.Task) + var kernelTs, coreTs *state.TaskSet + for _, ts := range tss { + chg.AddAll(ts) + for _, tsk := range ts.Tasks() { + switch tsk.Kind() { + // setup-profiles should appear right before link-snap + case "link-snap", "auto-connect", "setup-profiles", "set-auto-aliases": + snapsup, err := snapstate.TaskSnapSetup(tsk) + c.Assert(err, IsNil) + snapTasks[fmt.Sprintf("%s@%s", tsk.Kind(), snapsup.Type)] = tsk + if tsk.Kind() == "link-snap" { + opts := 0 + verifyUpdateTasks(c, snapsup.Type, opts, 0, ts) + if snapsup.Type == snap.TypeOS { + coreTs = ts + } else if snapsup.Type == snap.TypeKernel { + kernelTs = ts + } + } + } + } + } + + c.Assert(snapTasks, HasLen, 8) + linkSnapKernel := snapTasks["link-snap@kernel"] + autoConnectKernel := snapTasks["auto-connect@kernel"] + linkSnapBase := snapTasks["link-snap@os"] + autoConnectBase := snapTasks["auto-connect@os"] + c.Assert(kernelTs, NotNil) + c.Assert(coreTs, NotNil) + c.Assert(kernelTs.MaybeEdge(snapstate.BeforeMaybeRebootEdge), Equals, snapTasks["setup-profiles@kernel"]) + c.Assert(kernelTs.MaybeEdge(snapstate.MaybeRebootEdge), Equals, linkSnapKernel) + c.Assert(kernelTs.MaybeEdge(snapstate.MaybeRebootWaitEdge), Equals, autoConnectKernel) + c.Assert(kernelTs.MaybeEdge(snapstate.AfterMaybeRebootWaitEdge), Equals, snapTasks["set-auto-aliases@kernel"]) + + c.Assert(coreTs.MaybeEdge(snapstate.BeforeMaybeRebootEdge), Equals, snapTasks["setup-profiles@os"]) + c.Assert(coreTs.MaybeEdge(snapstate.MaybeRebootEdge), Equals, linkSnapBase) + c.Assert(coreTs.MaybeEdge(snapstate.MaybeRebootWaitEdge), Equals, autoConnectBase) + c.Assert(coreTs.MaybeEdge(snapstate.AfterMaybeRebootWaitEdge), Equals, snapTasks["set-auto-aliases@os"]) + + c.Assert(coreTs, NotNil) + + c.Assert(linkSnapBase.WaitTasks(), DeepEquals, []*state.Task{ + snapTasks["setup-profiles@os"], + }) + c.Assert(autoConnectBase.WaitTasks(), DeepEquals, []*state.Task{ + snapTasks["link-snap@os"], + }) + // kernel tasks have an implicit dependency on all "core" tasks + c.Assert(linkSnapKernel.WaitTasks(), DeepEquals, append([]*state.Task{ + snapTasks["setup-profiles@kernel"], + }, coreTs.Tasks()...)) + c.Assert(autoConnectKernel.WaitTasks(), DeepEquals, append([]*state.Task{ + snapTasks["link-snap@kernel"], + }, coreTs.Tasks()...)) + // have fake backend indicate a need to reboot for both snaps + s.fakeBackend.linkSnapMaybeReboot = true + s.fakeBackend.linkSnapRebootFor = map[string]bool{ + "kernel": true, + "core": true, + } + + defer s.se.Stop() + s.settle(c) + + c.Check(chg.Status(), Equals, state.DoneStatus) + // when updating both kernel that uses core as base, and "core" we have two reboots + c.Check(restartRequested, DeepEquals, []restart.RestartType{ + restart.RestartSystem, + restart.RestartSystem, + }) + + for _, name := range []string{"kernel", "core"} { + snapID := "kernel-id" + if name == "core" { + snapID = "core-snap-id" + } + var snapst snapstate.SnapState + err = snapstate.Get(s.state, name, &snapst) + c.Assert(err, IsNil) + + c.Assert(snapst.Active, Equals, true) + c.Assert(snapst.Sequence, HasLen, 2) + c.Assert(snapst.Sequence[1], DeepEquals, &snap.SideInfo{ + RealName: name, + Channel: "latest/stable", + SnapID: snapID, + Revision: snap.R(11), + }) + } +} + +func (s *snapmgrTestSuite) TestUpdateBaseKernelSingleRebootUndone(c *C) { + restore := release.MockOnClassic(false) + defer restore() + restore = snapstate.MockRevisionDate(nil) + defer restore() + + s.state.Lock() + defer s.state.Unlock() + + var restartRequested []restart.RestartType + restart.Init(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) { + restartRequested = append(restartRequested, t) + })) + + restore = snapstatetest.MockDeviceModel(ModelWithBase("core18")) + defer restore() + + // use services-snap here to make sure services would be stopped/started appropriately + siKernel := snap.SideInfo{ + RealName: "kernel", + Revision: snap.R(7), + SnapID: "kernel-id", + } + siBase := snap.SideInfo{ + RealName: "core18", + Revision: snap.R(7), + SnapID: "core18-snap-id", + } + for _, si := range []*snap.SideInfo{&siKernel, &siBase} { + snaptest.MockSnap(c, fmt.Sprintf(`name: %s`, si.RealName), si) + typ := "kernel" + if si.RealName == "core18" { + typ = "base" + } + snapstate.Set(s.state, si.RealName, &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{si}, + Current: si.Revision, + TrackingChannel: "latest/stable", + SnapType: typ, + }) + } + + chg := s.state.NewChange("refresh", "refresh kernel and base") + affected, tss, err := snapstate.UpdateMany(context.Background(), s.state, + []string{"kernel", "core18"}, s.user.ID, &snapstate.Flags{}) + c.Assert(err, IsNil) + c.Assert(affected, DeepEquals, []string{"core18", "kernel"}) + var autoConnectBase, autoConnectKernel *state.Task + for _, ts := range tss { + chg.AddAll(ts) + for _, tsk := range ts.Tasks() { + if tsk.Kind() == "auto-connect" { + snapsup, err := snapstate.TaskSnapSetup(tsk) + c.Assert(err, IsNil) + if snapsup.Type == "kernel" { + autoConnectKernel = tsk + } else { + autoConnectBase = tsk + } + break + } + } + } + // verify auto connect of kernel waits for base + waitsForBase := false + for _, tsk := range autoConnectKernel.WaitTasks() { + if tsk == autoConnectBase { + waitsForBase = true + } + } + c.Assert(waitsForBase, Equals, true, Commentf("auto-connect of kernel does not wait for base")) + + // have fake backend indicate a need to reboot for both snaps + s.fakeBackend.linkSnapMaybeReboot = true + s.fakeBackend.linkSnapRebootFor = map[string]bool{ + "kernel": true, + "core18": true, + } + errInjected := 0 + s.fakeBackend.maybeInjectErr = func(op *fakeOp) error { + if op.op == "auto-connect:Doing" && op.name == "kernel" { + errInjected++ + return fmt.Errorf("auto-connect-kernel mock error") + } + return nil + } + + defer s.se.Stop() + s.settle(c) + + c.Check(chg.Status(), Equals, state.ErrorStatus) + c.Check(chg.Err(), ErrorMatches, `(?s).*\(auto-connect-kernel mock error\)`) + c.Check(restartRequested, DeepEquals, []restart.RestartType{ + // do path + restart.RestartSystem, + // undo + restart.RestartSystem, + restart.RestartSystem, + }) + c.Check(errInjected, Equals, 1) + + // ops come in semi random order, but we know that link and auto-connect + // operations will be done in a specific order, + ops := make([]string, 0, 7) + for _, op := range s.fakeBackend.ops { + if op.op == "link-snap" { + split := strings.Split(op.path, "/") + c.Assert(len(split) > 2, Equals, true) + ops = append(ops, filepath.Join(split[len(split)-2:]...)) + } else if strings.HasPrefix(op.op, "auto-connect:") { + ops = append(ops, fmt.Sprintf("%s-%s/%s", op.op, op.name, op.revno)) + } + } + c.Assert(ops, HasLen, 7) + c.Assert(ops[0:5], DeepEquals, []string{ + // link snaps + "core18/11", "kernel/11", + "auto-connect:Doing-core18/11", + "auto-connect:Doing-kernel/11", // fails + "auto-connect:Undoing-core18/11", + }) + // those run unordered + c.Assert(ops[5:], testutil.DeepUnsortedMatches, []string{"core18/7", "kernel/7"}) +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/storehelpers.go snapd-2.54.2+21.10/overlord/snapstate/storehelpers.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/storehelpers.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/storehelpers.go 2022-01-06 21:25:16.000000000 +0000 @@ -93,7 +93,7 @@ } // installSize returns total download size of snaps and their prerequisites -// (bases and default content providers), querying the store as neccessarry, +// (bases and default content providers), querying the store as necessary, // potentially more than once. It assumes the initial list of snaps already has // download infos set. // The state must be locked by the caller. @@ -113,6 +113,12 @@ accountedSnaps[snap.InstanceName] = true } + // if the prerequisites are included in the install, don't query the store + // for info on them + for _, snap := range snaps { + accountedSnaps[snap.InstanceName()] = true + } + var prereqs []string resolveBaseAndContentProviders := func(inst minimalInstallInfo) { @@ -434,9 +440,7 @@ return store.SnapActionResult{}, e } -func updateToRevisionInfo(st *state.State, snapst *SnapState, revision snap.Revision, userID int, deviceCtx DeviceContext) (*snap.Info, error) { - // TODO: support ignore-validation? - +func updateToRevisionInfo(st *state.State, snapst *SnapState, revision snap.Revision, userID int, flags Flags, deviceCtx DeviceContext) (*snap.Info, error) { curSnaps, err := currentSnaps(st) if err != nil { return nil, err @@ -460,36 +464,43 @@ Revision: revision, } - enforcedSets, err := EnforcedValidationSets(st) - if err != nil { - return nil, err - } - if enforcedSets != nil { - requiredValsets, requiredRevision, err := enforcedSets.CheckPresenceRequired(naming.Snap(curInfo.InstanceName())) + var storeFlags store.SnapActionFlags + if !flags.IgnoreValidation { + enforcedSets, err := EnforcedValidationSets(st) if err != nil { return nil, err } - if !requiredRevision.Unset() { - if revision != requiredRevision { - return nil, fmt.Errorf("cannot update snap %q to revision %s without --ignore-validation, revision %s is required by validation sets: %s", - curInfo.InstanceName(), revision, requiredRevision, strings.Join(requiredValsets, ",")) - } - // note, not checking if required revision matches snapst.Current because - // this is already indirectly prevented by infoForUpdate(). - - // specific revision is required, reset cohort in current snaps - for _, sn := range curSnaps { - if sn.InstanceName == curInfo.InstanceName() { - sn.CohortKey = "" - break + if enforcedSets != nil { + requiredValsets, requiredRevision, err := enforcedSets.CheckPresenceRequired(naming.Snap(curInfo.InstanceName())) + if err != nil { + return nil, err + } + if !requiredRevision.Unset() { + if revision != requiredRevision { + return nil, fmt.Errorf("cannot update snap %q to revision %s without --ignore-validation, revision %s is required by validation sets: %s", + curInfo.InstanceName(), revision, requiredRevision, strings.Join(requiredValsets, ",")) + } + // note, not checking if required revision matches snapst.Current because + // this is already indirectly prevented by infoForUpdate(). + + // specific revision is required, reset cohort in current snaps + for _, sn := range curSnaps { + if sn.InstanceName == curInfo.InstanceName() { + sn.CohortKey = "" + break + } } } + if len(requiredValsets) > 0 { + setActionValidationSetsAndRequiredRevision(action, requiredValsets, requiredRevision) + } } - if len(requiredValsets) > 0 { - setActionValidationSetsAndRequiredRevision(action, requiredValsets, requiredRevision) - } + } else { + storeFlags = store.SnapActionIgnoreValidation } + action.Flags = storeFlags + theStore := Store(st, deviceCtx) st.Unlock() // calls to the store should be done without holding the state lock res, _, err := theStore.SnapAction(context.TODO(), curSnaps, []*store.SnapAction{action}, nil, user, opts) diff -Nru snapd-2.53+21.10ubuntu1/overlord/snapstate/storehelpers_test.go snapd-2.54.2+21.10/overlord/snapstate/storehelpers_test.go --- snapd-2.53+21.10ubuntu1/overlord/snapstate/storehelpers_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/snapstate/storehelpers_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -351,3 +351,135 @@ _, err := snapstate.InstallSize(st, []snapstate.MinimalInstallInfo{snapstate.InstallSnapInfo{Info: snap1}}, 0) c.Assert(err, ErrorMatches, `internal error: download info missing.*`) } + +func (s *snapmgrTestSuite) TestInstallSizeWithPrereqNoStore(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + repo := interfaces.NewRepository() + ifacerepo.Replace(st, repo) + + s.setupInstallSizeStore() + + snap1 := snaptest.MockSnap(c, `name: some-snap +version: 1.0 +epoch: 1 +base: core +plugs: + myplug: + interface: content + content: mycontent + content-provider: some-snap2`, &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(1), + }) + snap1.Size = snap1Size + + snap2 := snaptest.MockSnap(c, `name: some-snap2 +version: 1.0 +epoch: 1 +base: core +slots: + myslot: + interface: content + content: mycontent`, &snap.SideInfo{ + RealName: "some-snap2", + Revision: snap.R(1), + }) + snap2.Size = snap2Size + + // core is already installed + s.mockCoreSnap(c) + + sz, err := snapstate.InstallSize(st, []snapstate.MinimalInstallInfo{ + snapstate.InstallSnapInfo{Info: snap1}, snapstate.InstallSnapInfo{Info: snap2}}, 0) + c.Assert(err, IsNil) + c.Check(sz, Equals, uint64(snap1Size+snap2Size)) + + // no call to the store is made + c.Assert(s.fakeStore.fakeBackend.ops, HasLen, 0) +} + +func (s *snapmgrTestSuite) TestInstallSizeWithPrereqAndCoreNoStore(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + repo := interfaces.NewRepository() + ifacerepo.Replace(st, repo) + + s.setupInstallSizeStore() + + snap1 := snaptest.MockSnap(c, `name: some-snap +version: 1.0 +epoch: 1 +base: core +plugs: + myplug: + interface: content + content: mycontent + content-provider: some-snap2`, &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(1), + }) + snap1.Size = snap1Size + + snap2 := snaptest.MockSnap(c, `name: some-snap2 +version: 1.0 +epoch: 1 +base: core +slots: + myslot: + interface: content + content: mycontent`, &snap.SideInfo{ + RealName: "some-snap2", + Revision: snap.R(1), + }) + snap2.Size = snap2Size + + core := snaptest.MockSnap(c, `name: core +version: 1.0 +epoch: 1 +type: os`, &snap.SideInfo{ + RealName: "core", + Revision: snap.R(1), + }) + core.Size = someBaseSize + + sz, err := snapstate.InstallSize(st, []snapstate.MinimalInstallInfo{ + snapstate.InstallSnapInfo{Info: snap1}, snapstate.InstallSnapInfo{Info: snap2}, snapstate.InstallSnapInfo{Info: core}}, 0) + c.Assert(err, IsNil) + c.Check(sz, Equals, uint64(snap1Size+snap2Size+someBaseSize)) + + // no call to the store is made + c.Assert(s.fakeStore.fakeBackend.ops, HasLen, 0) +} + +func (s *snapmgrTestSuite) TestInstallSizeRemotePrereq(c *C) { + st := s.state + st.Lock() + defer st.Unlock() + + repo := interfaces.NewRepository() + ifacerepo.Replace(st, repo) + + s.setupInstallSizeStore() + + snap1 := snaptest.MockSnap(c, snapYamlWithContentPlug1, &snap.SideInfo{ + RealName: "some-snap", + Revision: snap.R(1), + }) + snap1.Size = snap1Size + + s.mockCoreSnap(c) + + sz, err := snapstate.InstallSize(st, []snapstate.MinimalInstallInfo{ + snapstate.InstallSnapInfo{Info: snap1}}, 0) + c.Assert(err, IsNil) + c.Check(sz, Equals, uint64(snap1Size+snapContentSlotSize+someBaseSize)) + + // the prereq's size info is fetched from the store + op := s.fakeStore.fakeBackend.ops.MustFindOp(c, "storesvc-snap-action:action") + c.Assert(op.action.InstanceName, Equals, "snap-content-slot") +} diff -Nru snapd-2.53+21.10ubuntu1/overlord/standby/export_test.go snapd-2.54.2+21.10/overlord/standby/export_test.go --- snapd-2.53+21.10ubuntu1/overlord/standby/export_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/standby/export_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -20,18 +20,8 @@ import ( "time" - - "github.com/snapcore/snapd/overlord/state" ) func (m *StandbyOpinions) SetStartTime(t time.Time) { m.startTime = t } - -func MockStateRequestRestart(newStateRequestRestart func(*state.State, state.RestartType)) (restore func()) { - oldStateRequestRestart := stateRequestRestart - stateRequestRestart = newStateRequestRestart - return func() { - stateRequestRestart = oldStateRequestRestart - } -} diff -Nru snapd-2.53+21.10ubuntu1/overlord/standby/standby.go snapd-2.54.2+21.10/overlord/standby/standby.go --- snapd-2.53+21.10ubuntu1/overlord/standby/standby.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/standby/standby.go 2022-01-06 21:25:16.000000000 +0000 @@ -23,14 +23,13 @@ "time" "github.com/snapcore/snapd/logger" + "github.com/snapcore/snapd/overlord/restart" "github.com/snapcore/snapd/overlord/state" ) var standbyWait = 5 * time.Second var maxWait = 5 * time.Minute -var stateRequestRestart = (*state.State).RequestRestart - type Opinionator interface { CanStandby() bool } @@ -99,7 +98,9 @@ timer := time.NewTimer(wait) for { if m.CanStandby() { - stateRequestRestart(m.state, state.RestartSocket) + m.state.Lock() + restart.Request(m.state, restart.RestartSocket) + m.state.Unlock() } select { case <-timer.C: diff -Nru snapd-2.53+21.10ubuntu1/overlord/standby/standby_test.go snapd-2.54.2+21.10/overlord/standby/standby_test.go --- snapd-2.53+21.10ubuntu1/overlord/standby/standby_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/standby/standby_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,6 +1,6 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2018 Canonical Ltd + * Copyright (C) 2018-2021 Canonical Ltd * * This 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,6 +23,8 @@ . "gopkg.in/check.v1" + "github.com/snapcore/snapd/overlord/restart" + "github.com/snapcore/snapd/overlord/snapstate/snapstatetest" "github.com/snapcore/snapd/overlord/standby" "github.com/snapcore/snapd/overlord/state" ) @@ -126,11 +128,13 @@ ch2 := make(chan struct{}) defer standby.MockStandbyWait(time.Millisecond)() - defer standby.MockStateRequestRestart(func(_ *state.State, t state.RestartType) { - c.Check(t, Equals, state.RestartSocket) + s.state.Lock() + restart.Init(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) { + c.Check(t, Equals, restart.RestartSocket) n++ ch2 <- struct{}{} - })() + })) + s.state.Unlock() m := standby.New(s.state) m.AddOpinion(opine(func() bool { @@ -156,9 +160,11 @@ func (s *standbySuite) TestStopWaits(c *C) { defer standby.MockStandbyWait(time.Millisecond)() - defer standby.MockStateRequestRestart(func(*state.State, state.RestartType) { + s.state.Lock() + restart.Init(s.state, "boot-id-0", snapstatetest.MockRestartHandler(func(t restart.RestartType) { c.Fatal("request restart should have not been called") - })() + })) + s.state.Unlock() ch := make(chan struct{}) opineReady := make(chan struct{}) diff -Nru snapd-2.53+21.10ubuntu1/overlord/state/copy.go snapd-2.54.2+21.10/overlord/state/copy.go --- snapd-2.53+21.10ubuntu1/overlord/state/copy.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/state/copy.go 2022-01-06 21:25:16.000000000 +0000 @@ -47,10 +47,6 @@ panic("cannot use EnsureBefore in checkpointOnlyBackend") } -func (b *checkpointOnlyBackend) RequestRestart(t RestartType) { - panic("cannot use RequestRestart in checkpointOnlyBackend") -} - // copyData will copy the given subkeys specifier from srcData to dstData. // // The subkeys is constructed from a dotted path like "user.auth". This copy diff -Nru snapd-2.53+21.10ubuntu1/overlord/state/state.go snapd-2.54.2+21.10/overlord/state/state.go --- snapd-2.53+21.10ubuntu1/overlord/state/state.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/state/state.go 2022-01-06 21:25:16.000000000 +0000 @@ -1,7 +1,7 @@ // -*- Mode: Go; indent-tabs-mode: t -*- /* - * Copyright (C) 2016-2020 Canonical Ltd + * Copyright (C) 2016-2021 Canonical Ltd * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU General Public License version 3 as @@ -39,8 +39,6 @@ type Backend interface { Checkpoint(data []byte) error EnsureBefore(d time.Duration) - // TODO: take flags to ask for reboot vs restart? - RequestRestart(t RestartType) } type customData map[string]*json.RawMessage @@ -74,25 +72,6 @@ data[key] = &entryJSON } -type RestartType int - -const ( - RestartUnset RestartType = iota - RestartDaemon - RestartSystem - // RestartSystemNow is like RestartSystem but action is immediate - RestartSystemNow - // RestartSocket will restart the daemon so that it goes into - // socket activation mode. - RestartSocket - // Stop just stops the daemon (used with image pre-seeding) - StopDaemon - // RestartSystemHaltNow will shutdown --halt the system asap - RestartSystemHaltNow - // RestartSystemPoweroffNow will shutdown --poweroff the system asap - RestartSystemPoweroffNow -) - // State represents an evolving system state that persists across restarts. // // The State is concurrency-safe, and all reads and writes to it must be @@ -118,10 +97,6 @@ modified bool cache map[interface{}]interface{} - - restarting RestartType - restartLck sync.Mutex - bootID string } // New returns a new empty state. @@ -263,70 +238,6 @@ } } -// RequestRestart asks for a restart of the managing process. -// The state needs to be locked to request a RestartSystem. -func (s *State) RequestRestart(t RestartType) { - if s.backend != nil { - switch t { - case RestartSystem, RestartSystemNow, RestartSystemHaltNow, RestartSystemPoweroffNow: - if s.bootID == "" { - panic("internal error: cannot request a system restart if current boot ID was not provided via VerifyReboot") - } - s.Set("system-restart-from-boot-id", s.bootID) - } - s.restartLck.Lock() - s.restarting = t - s.restartLck.Unlock() - s.backend.RequestRestart(t) - } -} - -// Restarting returns whether a restart was requested with RequestRestart and of which type. -func (s *State) Restarting() (bool, RestartType) { - s.restartLck.Lock() - defer s.restartLck.Unlock() - return s.restarting != RestartUnset, s.restarting -} - -var ErrExpectedReboot = errors.New("expected reboot did not happen") - -// VerifyReboot checks if the state remembers that a system restart was -// requested and whether it succeeded based on the provided current -// boot id. It returns ErrExpectedReboot if the expected reboot did -// not happen yet. It must be called early in the usage of state and -// before an RequestRestart with RestartSystem is attempted. -// It must be called with the state lock held. -func (s *State) VerifyReboot(curBootID string) error { - var fromBootID string - err := s.Get("system-restart-from-boot-id", &fromBootID) - if err != nil && err != ErrNoState { - return err - } - s.bootID = curBootID - if fromBootID == "" { - return nil - } - if fromBootID == curBootID { - return ErrExpectedReboot - } - // we rebooted alright - s.ClearReboot() - return nil -} - -// ClearReboot clears state information about tracking requested reboots. -func (s *State) ClearReboot() { - s.Set("system-restart-from-boot-id", nil) -} - -func MockRestarting(s *State, restarting RestartType) RestartType { - s.restartLck.Lock() - defer s.restartLck.Unlock() - old := s.restarting - s.restarting = restarting - return old -} - // ErrNoState represents the case of no state entry for a given key. var ErrNoState = errors.New("no state entry for key") diff -Nru snapd-2.53+21.10ubuntu1/overlord/state/state_test.go snapd-2.54.2+21.10/overlord/state/state_test.go --- snapd-2.53+21.10ubuntu1/overlord/state/state_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/state/state_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -185,10 +185,9 @@ } type fakeStateBackend struct { - checkpoints [][]byte - error func() error - ensureBefore time.Duration - restartRequested bool + checkpoints [][]byte + error func() error + ensureBefore time.Duration } func (b *fakeStateBackend) Checkpoint(data []byte) error { @@ -203,10 +202,6 @@ b.ensureBefore = d } -func (b *fakeStateBackend) RequestRestart(t state.RestartType) { - b.restartRequested = true -} - func (ss *stateSuite) TestImplicitCheckpointAndRead(c *C) { b := new(fakeStateBackend) st := state.New(b) @@ -980,66 +975,6 @@ c.Check(chg.Status(), Equals, state.HoldStatus) } -func (ss *stateSuite) TestRequestRestart(c *C) { - b := new(fakeStateBackend) - st := state.New(b) - - ok, t := st.Restarting() - c.Check(ok, Equals, false) - c.Check(t, Equals, state.RestartUnset) - - st.RequestRestart(state.RestartDaemon) - - c.Check(b.restartRequested, Equals, true) - - ok, t = st.Restarting() - c.Check(ok, Equals, true) - c.Check(t, Equals, state.RestartDaemon) -} - -func (ss *stateSuite) TestRequestRestartSystemAndVerifyReboot(c *C) { - b := new(fakeStateBackend) - st := state.New(b) - - st.Lock() - err := st.VerifyReboot("boot-id-1") - st.Unlock() - c.Assert(err, IsNil) - - ok, t := st.Restarting() - c.Check(ok, Equals, false) - c.Check(t, Equals, state.RestartUnset) - - st.Lock() - st.RequestRestart(state.RestartSystem) - st.Unlock() - - c.Check(b.restartRequested, Equals, true) - - ok, t = st.Restarting() - c.Check(ok, Equals, true) - c.Check(t, Equals, state.RestartSystem) - - var fromBootID string - st.Lock() - c.Check(st.Get("system-restart-from-boot-id", &fromBootID), IsNil) - st.Unlock() - c.Check(fromBootID, Equals, "boot-id-1") - - st.Lock() - err = st.VerifyReboot("boot-id-1") - st.Unlock() - c.Check(err, Equals, state.ErrExpectedReboot) - - st.Lock() - err = st.VerifyReboot("boot-id-2") - st.Unlock() - c.Assert(err, IsNil) - st.Lock() - c.Check(st.Get("system-restart-from-boot-id", &fromBootID), Equals, state.ErrNoState) - st.Unlock() -} - func (ss *stateSuite) TestReadStateInitsCache(c *C) { st, err := state.ReadState(nil, bytes.NewBufferString("{}")) c.Assert(err, IsNil) diff -Nru snapd-2.53+21.10ubuntu1/overlord/state/taskrunner_test.go snapd-2.54.2+21.10/overlord/state/taskrunner_test.go --- snapd-2.53+21.10ubuntu1/overlord/state/taskrunner_test.go 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/overlord/state/taskrunner_test.go 2022-01-06 21:25:16.000000000 +0000 @@ -33,6 +33,7 @@ "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/overlord/state" + "github.com/snapcore/snapd/testutil" ) type taskRunnerSuite struct{} @@ -58,8 +59,6 @@ } } -func (b *stateBackend) RequestRestart(t state.RestartType) {} - func ensureChange(c *C, r *state.TaskRunner, sb *stateBackend, chg *state.Change) { for i := 0; i < 20; i++ { sb.ensureBefore = time.Hour @@ -343,6 +342,206 @@ } } +func (ts *taskRunnerSuite) TestAbortAcrossLanesDescendantTask(c *C) { + + // () + // t11(1) -> t12(1) => t15(1) + // \ / + // => t13(1,2) => t14(1,2) => t23(1,2) => t24(1,2) + // / \ + // t21(2) -> t22(2) => t25(2) + // + names := strings.Fields("t11 t12 t13 t14 t15 t21 t22 t23 t24 t25") + + sb := &stateBackend{} + st := state.New(sb) + r := state.NewTaskRunner(st) + defer r.Stop() + + st.Lock() + defer st.Unlock() + + c.Assert(len(st.Tasks()), Equals, 0) + + chg := st.NewChange("install", "...") + tasks := make(map[string]*state.Task) + for _, name := range names { + tasks[name] = st.NewTask("do", name) + chg.AddTask(tasks[name]) + } + tasks["t12"].WaitFor(tasks["t11"]) + tasks["t13"].WaitFor(tasks["t12"]) + tasks["t14"].WaitFor(tasks["t13"]) + tasks["t15"].WaitFor(tasks["t14"]) + for lane, names := range map[int][]string{ + 1: {"t11", "t12", "t13", "t14", "t15", "t23", "t24"}, + 2: {"t21", "t22", "t23", "t24", "t25", "t13", "t14"}, + } { + for _, name := range names { + tasks[name].JoinLane(lane) + } + } + + tasks["t22"].WaitFor(tasks["t21"]) + tasks["t23"].WaitFor(tasks["t22"]) + tasks["t24"].WaitFor(tasks["t23"]) + tasks["t25"].WaitFor(tasks["t24"]) + + tasks["t13"].WaitFor(tasks["t22"]) + tasks["t15"].WaitFor(tasks["t24"]) + tasks["t23"].WaitFor(tasks["t14"]) + + ch := make(chan string, 256) + do := func(task *state.Task, tomb *tomb.Tomb) error { + c.Logf("do %q", task.Summary()) + label := task.Summary() + if label == "t15" { + ch <- fmt.Sprintf("t15:error") + return fmt.Errorf("mock error") + } + ch <- fmt.Sprintf("%s:do", label) + return nil + } + undo := func(task *state.Task, tomb *tomb.Tomb) error { + c.Logf("undo %q", task.Summary()) + label := task.Summary() + ch <- fmt.Sprintf("%s:undo", label) + return nil + } + r.AddHandler("do", do, undo) + + c.Logf("-----") + + st.Unlock() + ensureChange(c, r, sb, chg) + st.Lock() + close(ch) + var sequence []string + for event := range ch { + sequence = append(sequence, event) + } + for _, name := range names { + task := tasks[name] + c.Logf("%5s %5s lanes: %v status: %v", task.ID(), task.Summary(), task.Lanes(), task.Status()) + } + c.Assert(sequence[:4], testutil.DeepUnsortedMatches, []string{ + "t11:do", "t12:do", + "t21:do", "t22:do", + }) + c.Assert(sequence[4:8], DeepEquals, []string{ + "t13:do", "t14:do", "t23:do", "t24:do", + }) + c.Assert(sequence[8:10], testutil.DeepUnsortedMatches, []string{ + "t25:do", + "t15:error", + }) + c.Assert(sequence[10:11], testutil.DeepUnsortedMatches, []string{ + "t25:undo", + }) + c.Assert(sequence[11:15], DeepEquals, []string{ + "t24:undo", "t23:undo", "t14:undo", "t13:undo", + }) + c.Assert(sequence[15:19], testutil.DeepUnsortedMatches, []string{ + "t21:undo", "t22:undo", + "t12:undo", "t11:undo", + }) +} + +func (ts *taskRunnerSuite) TestAbortAcrossLanesStriclyOrderedTasks(c *C) { + + // () + // t11(1) -> t12(1) + // \ + // => t13(1,2) => t14(1,2) => t23(1,2) => t24(1,2) + // / + // t21(2) -> t22(2) + // + names := strings.Fields("t11 t12 t13 t14 t21 t22 t23 t24") + + sb := &stateBackend{} + st := state.New(sb) + r := state.NewTaskRunner(st) + defer r.Stop() + + st.Lock() + defer st.Unlock() + + c.Assert(len(st.Tasks()), Equals, 0) + + chg := st.NewChange("install", "...") + tasks := make(map[string]*state.Task) + for _, name := range names { + tasks[name] = st.NewTask("do", name) + chg.AddTask(tasks[name]) + } + tasks["t12"].WaitFor(tasks["t11"]) + tasks["t13"].WaitFor(tasks["t12"]) + tasks["t14"].WaitFor(tasks["t13"]) + for lane, names := range map[int][]string{ + 1: {"t11", "t12", "t13", "t14", "t23", "t24"}, + 2: {"t21", "t22", "t23", "t24", "t13", "t14"}, + } { + for _, name := range names { + tasks[name].JoinLane(lane) + } + } + + tasks["t22"].WaitFor(tasks["t21"]) + tasks["t23"].WaitFor(tasks["t22"]) + tasks["t24"].WaitFor(tasks["t23"]) + + tasks["t13"].WaitFor(tasks["t22"]) + tasks["t23"].WaitFor(tasks["t14"]) + + ch := make(chan string, 256) + do := func(task *state.Task, tomb *tomb.Tomb) error { + c.Logf("do %q", task.Summary()) + label := task.Summary() + if label == "t24" { + ch <- fmt.Sprintf("t24:error") + return fmt.Errorf("mock error") + } + ch <- fmt.Sprintf("%s:do", label) + return nil + } + undo := func(task *state.Task, tomb *tomb.Tomb) error { + c.Logf("undo %q", task.Summary()) + label := task.Summary() + ch <- fmt.Sprintf("%s:undo", label) + return nil + } + r.AddHandler("do", do, undo) + + c.Logf("-----") + + st.Unlock() + ensureChange(c, r, sb, chg) + st.Lock() + close(ch) + var sequence []string + for event := range ch { + sequence = append(sequence, event) + } + for _, name := range names { + task := tasks[name] + c.Logf("%5s %5s lanes: %v status: %v", task.ID(), task.Summary(), task.Lanes(), task.Status()) + } + c.Assert(sequence[:4], testutil.DeepUnsortedMatches, []string{ + "t11:do", "t12:do", + "t21:do", "t22:do", + }) + c.Assert(sequence[4:8], DeepEquals, []string{ + "t13:do", "t14:do", "t23:do", "t24:error", + }) + c.Assert(sequence[8:11], DeepEquals, []string{ + "t23:undo", "t14:undo", "t13:undo", + }) + c.Assert(sequence[11:], testutil.DeepUnsortedMatches, []string{ + "t21:undo", "t22:undo", + "t12:undo", "t11:undo", + }) +} + func (ts *taskRunnerSuite) TestExternalAbort(c *C) { sb := &stateBackend{} st := state.New(sb) diff -Nru snapd-2.53+21.10ubuntu1/packaging/amzn-2/snapd.spec snapd-2.54.2+21.10/packaging/amzn-2/snapd.spec --- snapd-2.53+21.10ubuntu1/packaging/amzn-2/snapd.spec 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/packaging/amzn-2/snapd.spec 2022-01-06 21:25:16.000000000 +0000 @@ -33,6 +33,11 @@ %global with_multilib 1 %endif +# Set if valgrind is to be run +%ifnarch ppc64le +%global with_valgrind 1 +%endif + %if ! %{with vendorized} %global with_bundled 0 %else @@ -97,7 +102,7 @@ %endif Name: snapd -Version: 2.53 +Version: 2.54.2 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -206,7 +211,9 @@ %if ! 0%{?rhel} BuildRequires: libseccomp-static %endif +%if 0%{?with_valgrind} BuildRequires: valgrind +%endif BuildRequires: %{_bindir}/rst2man %if 0%{?fedora} # ShellCheck in EPEL is too old... @@ -289,7 +296,7 @@ Provides: bundled(golang(github.com/kr/pretty)) Provides: bundled(golang(github.com/kr/text)) Provides: bundled(golang(github.com/mvo5/goconfigparser)) -Provides: bundled(golang(github.com/mvo5/libseccomp-golang)) +Provides: bundled(golang(github.com/seccomp/libseccomp-golang)) Provides: bundled(golang(github.com/snapcore/go-gettext)) Provides: bundled(golang(golang.org/x/crypto/openpgp/armor)) Provides: bundled(golang(golang.org/x/crypto/openpgp/packet)) @@ -534,8 +541,6 @@ %endif %if ! 0%{?with_bundled} -# We don't need mvo5 fork for seccomp, as we have seccomp 2.3.x -sed -e "s:github.com/mvo5/libseccomp-golang:github.com/seccomp/libseccomp-golang:g" -i cmd/snap-seccomp/*.go # We don't need the snapcore fork for bolt - it is just a fix on ppc sed -e "s:github.com/snapcore/bolt:github.com/boltdb/bolt:g" -i advisor/*.go errtracker/*.go %endif @@ -543,7 +548,7 @@ # We have to build snapd first to prevent the build from # building various things from the tree without additional # set tags. -%gobuild -o bin/snapd $GOFLAGS ./cmd/snapd +%gobuild -o bin/snapd $GOFLAGS %{import_path}/cmd/snapd BUILDTAGS="${BUILDTAGS} nomanagers" %gobuild -o bin/snap $GOFLAGS %{import_path}/cmd/snap %gobuild -o bin/snap-failure $GOFLAGS %{import_path}/cmd/snap-failure @@ -595,18 +600,21 @@ %if 0%{?with_selinux} --enable-selinux \ %endif +%if 0%{?rhel} == 7 + --disable-bpf \ +%endif --libexecdir=%{_libexecdir}/snapd/ \ --enable-nvidia-biarch \ %{?with_multilib:--with-32bit-libdir=%{_prefix}/lib} \ --with-snap-mount-dir=%{_sharedstatedir}/snapd/snap \ --enable-merged-usr -%make_build +%make_build %{!?with_valgrind:HAVE_VALGRIND=} popd # Build systemd units, dbus services, and env files pushd ./data -make BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" \ +make BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ SYSTEMDSYSTEMUNITDIR="%{_unitdir}" \ SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" @@ -691,7 +699,7 @@ # Install all systemd and dbus units, and env files pushd ./data -%make_install BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" \ +%make_install BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ SYSTEMDSYSTEMUNITDIR="%{_unitdir}" SYSTEMDUSERUNITDIR="%{_userunitdir}" \ SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" @@ -833,6 +841,7 @@ %{_datadir}/dbus-1/system.d/snapd.system-services.conf %{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy %{_datadir}/applications/io.snapcraft.SessionAgent.desktop +%{_datadir}/fish/vendor_conf.d/snapd.fish %{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop %config(noreplace) %{_sysconfdir}/sysconfig/snapd %dir %{_sharedstatedir}/snapd @@ -867,6 +876,8 @@ # this is typically owned by zsh, but we do not want to explicitly require zsh %dir %{_datadir}/zsh %dir %{_datadir}/zsh/site-functions +# similar case for fish +%dir %{_datadir}/fish/vendor_conf.d %files -n snap-confine %doc cmd/snap-confine/PORTING @@ -978,6 +989,450 @@ %changelog +* Thu Jan 06 2022 Ian Johnson +- New upstream release 2.54.2 + - tests: exclude interfaces-kernel-module load on arm + - tests: ensure that test-snapd-kernel-module-load is + removed + - tests: do not test microk8s-smoke on arm + - tests/core/failover: replace boot-state with snap debug boot-vars + - tests: use snap info|awk to extract tracking channel + - tests: fix remodel-kernel test when running on external devices + - .github/workflows/test.yaml: also check internal snapd version for + cleanliness + - packaging/ubuntu-16.04/rules: eliminate seccomp modification + - bootloader/assets/grub_*cfg_asset.go: update Copyright + - build-aux/snap/snapcraft.yaml: adjust comment about get-version + - .github/workflows/test.yaml: add check in github actions for dirty + snapd snaps + - build-aux/snap/snapcraft.yaml: use build-packages, don't fail + dirty builds + - data/selinux: allow poking /proc/xen + +* Mon Dec 20 2021 Michael Vogt +- New upstream release 2.54.1 + - buid-aux: set version before calling ./generate-packaging-dir + This fixes the "dirty" suffix in the auto-generated version + +* Fri Dec 17 2021 Michael Vogt +- New upstream release 2.54 + - interfaces/builtin/opengl.go: add boot_vga sys/devices file + - o/configstate/configcore: add tmpfs.size option + - tests: moving to manual opensuse 15.2 + - cmd/snap-device-helper: bring back the device type identification + behavior, but for remove action fallback only + - cmd/snap-failure: use snapd from the snapd snap if core is not + present + - tests/core/failover: enable the test on core18 + - o/devicestate: ensure proper order when remodel does a simple + switch-snap-channel + - builtin/interfaces: add shared memory interface + - overlord: extend kernel/base success and failover with bootenv + checks + - o/snapstate: check disk space w/o store if possible + - snap-bootstrap: Mount snaps read only + - gadget/install: do not re-create partitions using OnDiskVolume + after deletion + - many: fix formatting w/ latest go version + - devicestate,timeutil: improve logging of NTP sync + - tests/main/security-device-cgroups-helper: more debugs + - cmd/snap: print a placeholder for version of broken snaps + - o/snapstate: mock system with classic confinement support + - cmd: Fixup .clangd to use correct syntax + - tests: run spread tests in fedora-35 + - data/selinux: allow snapd to access /etc/modprobe.d + - mount-control: step 2 + - daemon: add multiple snap sideload to API + - tests/lib/pkgdb: install dbus-user-session during prepare, drop + dbus-x11 + - systemd: provide more detailed errors for unimplemented method in + emulation mode + - tests: avoid checking TRUST_TEST_KEYS on restore on remodel-base + test + - tests: retry umounting /var/lib/snapd/seed on uc20 on fsck-on-boot + test + - o/snapstate: add hide/expose snap data to backend + - interfaces: kernel-module-load + - snap: add support for `snap watch + --last={revert,enable,disable,switch}` + - tests/main/security-udev-input-subsystem: drop info from udev + - tests/core/kernel-and-base-single-reboot-failover, + tests/lib/fakestore: verify failover scenario + - tests/main/security-device-cgroups-helper: collect some debug info + when the test fails + - tests/nested/manual/core20-remodel: wait for device to have a + serial before starting a remodel + - tests/main/generic-unregister: test re-registration if not blocked + - o/snapstate, assertsate: validation sets/undo on partial failure + - tests: ensure snapd can be downloaded as a module + - snapdtool, many: support additional key/value flags in info file + - data/env: improve fish shell env setup + - usersession/client: provide a way for client to send messages to a + subset of users + - tests: verify that simultaneous refresh of kernel and base + triggers a single reboot only + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - asserts: change behavior of alternative attribute matcher + - configcore: relax validation rules for hostname + - cmd/snap-confine: do not include libglvnd libraries from the host + system + - overlord, tests: add managers and a spread test for UC20 to UC22 + remodel + - HACKING.md: adjust again for building the snapd snap + - systemd: add support for systemd unit alias names + - o/snapstate: add InstallPathMany + - gadget: allow EnsureLayoutCompatibility to ensure disk has all + laid out structsnow reject/fail: + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider (#11111) + - interfaces/interfaces/scsi_generic: add interface for scsi generic + de… (#10936) + - osutil/disks/mockdisk.go: add MockDevicePathToDiskMapping + - interfaces/microstack-support: set controlsDeviceCgroup to true + - network-setup-control: add netplan generate D-Bus rules + - interface/builtin/log_observe: allow to access /dev/kmsg + - .github/workflows/test.yaml: restore failing of spread tests on + errors (nested) + - gadget: tweaks to DiskStructureDeviceTraits + expand test cases + - tests/lib/nested.sh: allow tests to use their own core18 in extra- + snaps-path + - interfaces/browser-support: Update rules for Edge + - o/devicestate: during remodel first check pending download tasks + for snaps + - polkit: add a package to validate polkit policy files + - HACKING.md: document building the snapd snap and splicing it into + the core snap + - interfaces/udev: fix installing snaps inside lxd in 21.10 + - o/snapstate: refactor disk space checks + - tests: add (strict) microk8s smoke test + - osutil/strace: try to enable strace on more arches + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - tests/main/snapd-reexec-snapd-snap: improve debugging + - daemon: write formdata file parts to snaps dir + - systemd: add support for .target units + - tests: run snap-disconnect on uc16 + - many: add experimental setting to allow using ~/.snap/data instead + of ~/snap + - overlord/snapstate: perform a single reboot when updating boot + base and kernel + - kernel/fde: add DeviceUnlockKernelHookDeviceMapperBackResolver, + use w/ disks pkg + - o/devicestate: introduce DeviceManager.Unregister + - interfaces: allow receiving PropertiesChanged on the mpris plug + - tests: new tool used to retrieve data from mongo db + - daemon: amend ssh keys coming from the store + - tests: Include the tools from snapd-testing-tools project in + "$TESTSTOOLS" + - tests: new workflow step used to report spread error to mongodb + - interfaces/builtin/dsp: update proc files for ambarella flavor + - gadget: replace ondisk implementation with disks package, refactor + part calcs + - tests: Revert "tests: disable flaky uc18 tests until systemd is + fixed" + - Revert: "many: Vendor apparmor-3.0.3 into the snapd snap" + - asserts: rename "white box" to "clear box" (woke checker) + - many: Vendor apparmor-3.0.3 into the snapd snap + - tests: reorganize the debug-each on the spread.yaml + - packaging: sync with downstream packaging in Fedora and openSUSE + - tests: disable flaky uc18 tests until systemd is fixed + - data/env: provide profile setup for fish shell + - tests: use ubuntu-image 1.11 from stable channel + - gadget/gadget.go: include disk schema in the disk device volume + traits too + - tests/main/security-device-cgroups-strict-enforced: extend the + comments + - README.md: point at bugs.launchpad.net/snapd instead of snappy + project + - osutil/disks: introduce RegisterDeviceMapperBackResolver + use for + crypt-luks2 + - packaging: make postrm script robust against `rm` failures + - tests: print extra debug on auto-refresh-gating test failure + - o/assertstate, api: move enforcing/monitoring from api to + assertstate, save history + - tests: skip the test-snapd-timedate-control-consumer.date to avoid + NTP sync error + - gadget/install: use disks functions to implement deviceFromRole, + also rename + - tests: the `lxd` test is failing right now on 21.10 + - o/snapstate: account for deleted revs when undoing install + - interfaces/builtin/block_devices: allow blkid to print block + device attributes + - gadget: include size + sector-size in DiskVolumeDeviceTraits + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - o/snapstate/handlers: propagate read errors on "copy-snap-data" + - osutil/disks: add more fields to Partition, populate them during + discovery + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - o/snapstate: remove repeated test assertions + - tests: skip `snap advise-command` test if the store is overloaded + - cmd: create ~/snap dir with 0700 perms + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - github: leave a comment documenting reasons for pipefail + - github: enable pipefail when running spread + - osutil/disks: add DiskFromPartitionDeviceNode + - gadget, many: add model param to Update() + - cmd/snap-seccomp: add riscv64 support + - o/snapstate: maintain a RevertStatus map in SnapState + - tests: enable lxd tests on impish system + - tests: (partially) revert the memory limits PR#r10241 + - o/assertstate: functions for handling validation sets tracking + history + - tests: some improvements for the spread log parser + - interfaces/network-manager-observe: Update for libnm / dart + clients + - tests: add ntp related debug around "auto-refresh" test + - boot: expand on the fact that reseal taking modeenv is very + intentional + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - data/selinux: update the policy to allow snapd to talk to + org.freedesktop.timedate1 + - o/snapstate: keep old revision if install doesn't add new one + - overlord/state: add a unit test for a kernel+base refresh like + sequence + - desktop, usersession: observe notifications + - osutil/disks: add AllPhysicalDisks() + - timeutil,deviceutil: fix unit tests on systems without dbus or + without ntp-sync + - cmd/snap-bootstrap/README: explain all the things (well most of + them anyways) + - docs: add run-checks dependency install instruction + - o/snapstate: do not prune refresh-candidates if gate-auto-refresh- + hook feature is not enabled + - o/snapstate: test relink remodel helpers do a proper subset of + doInstall and rework the verify*Tasks helpers + - tests/main/mount-ns: make the test run early + - tests: add `--debug` to netplan apply + - many: wait for up to 10min for NTP synchronization before + autorefresh + - tests: initialize CHANGE_ID in _wait_autorefresh + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - tests: add more debug around qemu-nbd + - o/hookstate: print cohort with snapctl refresh --pending (#10985) + - tests: misc robustness changes + - o/snapstate: improve install/update tests (#10850) + - tests: clean up test tools + - spread.yaml: show `journalctl -e` for all suites on debug + - tests: give interfaces-udisks2 more time for the loop device to + appear + - tests: set memory limit for snapd + - tests: increase timeout/add debug around nbd0 mounting (up, see + LP:#1949513) + - snapstate: add debug message where a snap is mounted + - tests: give nbd0 more time to show up in preseed-lxd + - interfaces/dsp: add more ambarella things + - cmd/snap: improve snap disconnect arg parsing and err msg + - tests: disable nested lxd snapd testing + - tests: disable flaky "interfaces-udisks2" on ubuntu-18.04-32 + - o/snapstate: avoid validationSetsSuite repeating snapmgrTestSuite + - sandbox/cgroup: wait for start transient unit job to finish + - o/snapstate: fix task order, tweak errors, add unit tests for + remodel helpers + - osutil/disks: re-org methods for end of usable region, size + information + - build-aux: ensure that debian packaging matches build-base + - docs: update HACKING.md instructions for snapd 2.52 and later + - spread: run lxd tests with version from latest/edge + - interfaces: suppress denial of sys_module capability + - osutil/disks: add methods to replace gadget/ondisk functions + - tests: split test tools - part 1 + - tests: fix nested tests on uc20 + - data/selinux: allow snap-confine to read udev's database + - i/b/common_test: refactor AppArmor features test + - tests: run spread tests on debian 11 + - o/devicestate: copy timesyncd clock timestamp during install + - interfaces/builtin: do not probe parser features when apparmor + isn't available + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - tests: fix error message in run-checks + - tests: spread test for validation sets enforcing + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - o/snapstate: deduplicate snap names in remove/install/update + - tests/main/selinux-data-context: use session when performing + actions as test user + - packaging/opensuse: sync with openSUSE packaging, enable AppArmor + on 15.3+ + - interfaces: skip connection of netlink interface on older + systems + - asserts, o/snapstate: honor IgnoreValidation flag when checking + installed snaps + - tests/main/apparmor-batch-reload: fix fake apparmor_parser to + handle --preprocess + - sandbox/apparmor, interfaces/apparmor: detect bpf capability, + generate snippet for s-c + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - tests: test for enforcing with prerequisites + - tests/main/snapd-sigterm: fix race conditions + - spread: run lxd tests with version from latest/stable + - run-checks: remove --spread from help message + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests: ensure systemd-timesyncd is installed on debian + - interfaces/u2f-devices: add Nitrokey 3 + - tests: update the ubuntu-image channel to candidate + - osutil/disks/labels: simplify decoding algorithm + - tests: not testing lxd snap anymore on i386 architecture + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - cmd/snap: support --ignore-validation with snap install client + command + - tests/snapd-sigterm: be more robust against service restart + - tests: simplify mock script for apparmor_parser + - o/devicestate, o/servicestate: update gadget assets and cmdline + when remodeling + - tests/nested/manual/refresh-revert-fundamentals: re-enable + encryption + - osutil/disks: fix bug in BlkIDEncodeLabel, add BlkIDDecodeLabel + - gadget, osutil/disks: fix some bugs from prior PR'sin the dir. + - secboot: revert move to new version (revert #10715) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup + - many: mv MockDeviceNameDisksToPartitionMapping -> + MockDeviceNameToDiskMapping + - interfaces/builtin: Add '/com/canonical/dbusmenu' path access to + 'unity7' interface + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - osutil/disks, many: switch to defining Partitions directly for + MockDiskMapping + - tests: remove extra-snaps-assertions test + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - tests/nested/core/core20-create-recovery: fix passing of data to + curl + - daemon: allow enabling enforce mode + - daemon: use the syscall connection to get the socket credentials + - i/builtin/kubernetes_support: add access to Calico lock file + - osutil: ensure parent dir is opened and sync'd + - tests: using test-snapd-curl snap instead of http snap + - overlord: add managers unit test demonstrating cyclic dependency + between gadget and kernel updates + - gadget/ondisk.go: include the filesystem UUID in the returned + OnDiskVolume + - packaging: fixes for building on openSUSE + - o/configcore: allow hostnames up to 253 characters, with dot- + delimited elements + - gadget/ondisk.go: add listBlockDevices() to get all block devices + on a system + - gadget: add mapping trait types + functions to save/load + - interfaces: add polkit security backend + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - tests: merge coverage results + - tests: remove "features" from fde-setup.go example + - fde: add new device-setup support to fde-setup + - gadget: add `encryptedDevice` and add encryptedDeviceLUKS + - spread: use `bios: uefi` for uc20 + - client: fail fast on non-retryable errors + - tests: support running all spread tests with experimental features + - tests: check that a snap that doesn't have gate-auto-refresh hook + can call --proceed + - o/snapstate: support ignore-validation flag when updating to a + specific snap revision + - o/snapstate: test prereq update if started by old version + - tests/main: disable cgroup-devices-v1 and freezer tests on 21.10 + - tests/main/interfaces-many: run both variants on all possible + Ubuntu systems + - gadget: mv ensureLayoutCompatibility to gadget proper, add + gadgettest pkg + - many: replace state.State restart support with overlord/restart + - overlord: fix generated snap-revision assertions in remodel unit + tests + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.4 + - devicestate: mock devicestate.MockTimeutilIsNTPSynchronized to + avoid host env leaking into tests + - timeutil: return NoTimedate1Error if it can't connect to the + system bus + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.3 + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - configcore: relax validation rules for hostname + - o/devicestate: introduce DeviceManager.Unregister + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider + - many: wait for up to 10min for NTP synchronization before + autorefresh + - interfaces/interfaces/scsi_generic: add interface for scsi generic + devices + - interfaces/microstack-support: set controlsDeviceCgroup to true + - interface/builtin/log_observe: allow to access /dev/kmsg + - daemon: write formdata file parts to snaps dir + - spread: run lxd tests with version from latest/edge + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - interfaces/builtin/dsp: add proc files for monitoring Ambarella + DSP firmware + - interfaces/builtin/dsp: update proc file accordingly + +* Mon Nov 15 2021 Ian Johnson +- New upstream release 2.53.2 + - interfaces/builtin/block_devices: allow blkid to print block + device attributes/run/udev/data/b{major}:{minor} + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - interfaces/network-manager-observe: Update for libnm client + library + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - o/hookstate: print cohort with snapctl refresh --pending + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - tests: ensure systemd-timesyncd is installed on debian + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests/main/snapd-sigterm: flush, use retry + - tests/main/snapd-sigterm: fix race conditions + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - data/selinux: allow snap-confine to read udev's database + - interfaces/dsp: add more ambarella things* interfaces/dsp: add + more ambarella things + +* Thu Oct 21 2021 Ian Johnson +- New upstream release 2.53.1 + - spread: run lxd tests with version from latest/stable + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional (#10946) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup (2.53) + - interfaces/u2f-devices: add Nitrokey 3 + - Update the ubuntu-image channel to candidate + - Allow hostnames up to 253 characters, with dot-delimited elements + (as suggested by man 7 hostname). + - Disable i386 until it is possible to build snapd using lxd + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - tests/snapd-sigterm: be more robust against service restart + - tests: add a regression test for snapd hanging on SIGTERM + - daemon: use the syscall connection to get the socket + credentials + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - secboot: revert move to new version + * Tue Oct 05 2021 Michael Vogt - New upstream release 2.53 - overlord: fix generated snap-revision assertions in remodel unit diff -Nru snapd-2.53+21.10ubuntu1/packaging/arch/PKGBUILD snapd-2.54.2+21.10/packaging/arch/PKGBUILD --- snapd-2.53+21.10ubuntu1/packaging/arch/PKGBUILD 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/packaging/arch/PKGBUILD 2022-01-06 21:25:16.000000000 +0000 @@ -11,7 +11,7 @@ depends=('squashfs-tools' 'libseccomp' 'libsystemd' 'apparmor') optdepends=('bash-completion: bash completion support' 'xdg-desktop-portal: desktop integration') -pkgver=2.53 +pkgver=2.54.2 pkgrel=1 arch=('x86_64' 'i686' 'armv7h' 'aarch64') url="https://github.com/snapcore/snapd" diff -Nru snapd-2.53+21.10ubuntu1/packaging/centos-7/snapd.spec snapd-2.54.2+21.10/packaging/centos-7/snapd.spec --- snapd-2.53+21.10ubuntu1/packaging/centos-7/snapd.spec 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/packaging/centos-7/snapd.spec 2022-01-06 21:25:16.000000000 +0000 @@ -33,6 +33,11 @@ %global with_multilib 1 %endif +# Set if valgrind is to be run +%ifnarch ppc64le +%global with_valgrind 1 +%endif + %if ! %{with vendorized} %global with_bundled 0 %else @@ -97,7 +102,7 @@ %endif Name: snapd -Version: 2.53 +Version: 2.54.2 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -206,7 +211,9 @@ %if ! 0%{?rhel} BuildRequires: libseccomp-static %endif +%if 0%{?with_valgrind} BuildRequires: valgrind +%endif BuildRequires: %{_bindir}/rst2man %if 0%{?fedora} # ShellCheck in EPEL is too old... @@ -289,7 +296,7 @@ Provides: bundled(golang(github.com/kr/pretty)) Provides: bundled(golang(github.com/kr/text)) Provides: bundled(golang(github.com/mvo5/goconfigparser)) -Provides: bundled(golang(github.com/mvo5/libseccomp-golang)) +Provides: bundled(golang(github.com/seccomp/libseccomp-golang)) Provides: bundled(golang(github.com/snapcore/go-gettext)) Provides: bundled(golang(golang.org/x/crypto/openpgp/armor)) Provides: bundled(golang(golang.org/x/crypto/openpgp/packet)) @@ -534,8 +541,6 @@ %endif %if ! 0%{?with_bundled} -# We don't need mvo5 fork for seccomp, as we have seccomp 2.3.x -sed -e "s:github.com/mvo5/libseccomp-golang:github.com/seccomp/libseccomp-golang:g" -i cmd/snap-seccomp/*.go # We don't need the snapcore fork for bolt - it is just a fix on ppc sed -e "s:github.com/snapcore/bolt:github.com/boltdb/bolt:g" -i advisor/*.go errtracker/*.go %endif @@ -543,7 +548,7 @@ # We have to build snapd first to prevent the build from # building various things from the tree without additional # set tags. -%gobuild -o bin/snapd $GOFLAGS ./cmd/snapd +%gobuild -o bin/snapd $GOFLAGS %{import_path}/cmd/snapd BUILDTAGS="${BUILDTAGS} nomanagers" %gobuild -o bin/snap $GOFLAGS %{import_path}/cmd/snap %gobuild -o bin/snap-failure $GOFLAGS %{import_path}/cmd/snap-failure @@ -595,18 +600,21 @@ %if 0%{?with_selinux} --enable-selinux \ %endif +%if 0%{?rhel} == 7 + --disable-bpf \ +%endif --libexecdir=%{_libexecdir}/snapd/ \ --enable-nvidia-biarch \ %{?with_multilib:--with-32bit-libdir=%{_prefix}/lib} \ --with-snap-mount-dir=%{_sharedstatedir}/snapd/snap \ --enable-merged-usr -%make_build +%make_build %{!?with_valgrind:HAVE_VALGRIND=} popd # Build systemd units, dbus services, and env files pushd ./data -make BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" \ +make BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ SYSTEMDSYSTEMUNITDIR="%{_unitdir}" \ SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" @@ -691,7 +699,7 @@ # Install all systemd and dbus units, and env files pushd ./data -%make_install BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" \ +%make_install BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ SYSTEMDSYSTEMUNITDIR="%{_unitdir}" SYSTEMDUSERUNITDIR="%{_userunitdir}" \ SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" @@ -833,6 +841,7 @@ %{_datadir}/dbus-1/system.d/snapd.system-services.conf %{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy %{_datadir}/applications/io.snapcraft.SessionAgent.desktop +%{_datadir}/fish/vendor_conf.d/snapd.fish %{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop %config(noreplace) %{_sysconfdir}/sysconfig/snapd %dir %{_sharedstatedir}/snapd @@ -867,6 +876,8 @@ # this is typically owned by zsh, but we do not want to explicitly require zsh %dir %{_datadir}/zsh %dir %{_datadir}/zsh/site-functions +# similar case for fish +%dir %{_datadir}/fish/vendor_conf.d %files -n snap-confine %doc cmd/snap-confine/PORTING @@ -978,6 +989,450 @@ %changelog +* Thu Jan 06 2022 Ian Johnson +- New upstream release 2.54.2 + - tests: exclude interfaces-kernel-module load on arm + - tests: ensure that test-snapd-kernel-module-load is + removed + - tests: do not test microk8s-smoke on arm + - tests/core/failover: replace boot-state with snap debug boot-vars + - tests: use snap info|awk to extract tracking channel + - tests: fix remodel-kernel test when running on external devices + - .github/workflows/test.yaml: also check internal snapd version for + cleanliness + - packaging/ubuntu-16.04/rules: eliminate seccomp modification + - bootloader/assets/grub_*cfg_asset.go: update Copyright + - build-aux/snap/snapcraft.yaml: adjust comment about get-version + - .github/workflows/test.yaml: add check in github actions for dirty + snapd snaps + - build-aux/snap/snapcraft.yaml: use build-packages, don't fail + dirty builds + - data/selinux: allow poking /proc/xen + +* Mon Dec 20 2021 Michael Vogt +- New upstream release 2.54.1 + - buid-aux: set version before calling ./generate-packaging-dir + This fixes the "dirty" suffix in the auto-generated version + +* Fri Dec 17 2021 Michael Vogt +- New upstream release 2.54 + - interfaces/builtin/opengl.go: add boot_vga sys/devices file + - o/configstate/configcore: add tmpfs.size option + - tests: moving to manual opensuse 15.2 + - cmd/snap-device-helper: bring back the device type identification + behavior, but for remove action fallback only + - cmd/snap-failure: use snapd from the snapd snap if core is not + present + - tests/core/failover: enable the test on core18 + - o/devicestate: ensure proper order when remodel does a simple + switch-snap-channel + - builtin/interfaces: add shared memory interface + - overlord: extend kernel/base success and failover with bootenv + checks + - o/snapstate: check disk space w/o store if possible + - snap-bootstrap: Mount snaps read only + - gadget/install: do not re-create partitions using OnDiskVolume + after deletion + - many: fix formatting w/ latest go version + - devicestate,timeutil: improve logging of NTP sync + - tests/main/security-device-cgroups-helper: more debugs + - cmd/snap: print a placeholder for version of broken snaps + - o/snapstate: mock system with classic confinement support + - cmd: Fixup .clangd to use correct syntax + - tests: run spread tests in fedora-35 + - data/selinux: allow snapd to access /etc/modprobe.d + - mount-control: step 2 + - daemon: add multiple snap sideload to API + - tests/lib/pkgdb: install dbus-user-session during prepare, drop + dbus-x11 + - systemd: provide more detailed errors for unimplemented method in + emulation mode + - tests: avoid checking TRUST_TEST_KEYS on restore on remodel-base + test + - tests: retry umounting /var/lib/snapd/seed on uc20 on fsck-on-boot + test + - o/snapstate: add hide/expose snap data to backend + - interfaces: kernel-module-load + - snap: add support for `snap watch + --last={revert,enable,disable,switch}` + - tests/main/security-udev-input-subsystem: drop info from udev + - tests/core/kernel-and-base-single-reboot-failover, + tests/lib/fakestore: verify failover scenario + - tests/main/security-device-cgroups-helper: collect some debug info + when the test fails + - tests/nested/manual/core20-remodel: wait for device to have a + serial before starting a remodel + - tests/main/generic-unregister: test re-registration if not blocked + - o/snapstate, assertsate: validation sets/undo on partial failure + - tests: ensure snapd can be downloaded as a module + - snapdtool, many: support additional key/value flags in info file + - data/env: improve fish shell env setup + - usersession/client: provide a way for client to send messages to a + subset of users + - tests: verify that simultaneous refresh of kernel and base + triggers a single reboot only + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - asserts: change behavior of alternative attribute matcher + - configcore: relax validation rules for hostname + - cmd/snap-confine: do not include libglvnd libraries from the host + system + - overlord, tests: add managers and a spread test for UC20 to UC22 + remodel + - HACKING.md: adjust again for building the snapd snap + - systemd: add support for systemd unit alias names + - o/snapstate: add InstallPathMany + - gadget: allow EnsureLayoutCompatibility to ensure disk has all + laid out structsnow reject/fail: + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider (#11111) + - interfaces/interfaces/scsi_generic: add interface for scsi generic + de… (#10936) + - osutil/disks/mockdisk.go: add MockDevicePathToDiskMapping + - interfaces/microstack-support: set controlsDeviceCgroup to true + - network-setup-control: add netplan generate D-Bus rules + - interface/builtin/log_observe: allow to access /dev/kmsg + - .github/workflows/test.yaml: restore failing of spread tests on + errors (nested) + - gadget: tweaks to DiskStructureDeviceTraits + expand test cases + - tests/lib/nested.sh: allow tests to use their own core18 in extra- + snaps-path + - interfaces/browser-support: Update rules for Edge + - o/devicestate: during remodel first check pending download tasks + for snaps + - polkit: add a package to validate polkit policy files + - HACKING.md: document building the snapd snap and splicing it into + the core snap + - interfaces/udev: fix installing snaps inside lxd in 21.10 + - o/snapstate: refactor disk space checks + - tests: add (strict) microk8s smoke test + - osutil/strace: try to enable strace on more arches + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - tests/main/snapd-reexec-snapd-snap: improve debugging + - daemon: write formdata file parts to snaps dir + - systemd: add support for .target units + - tests: run snap-disconnect on uc16 + - many: add experimental setting to allow using ~/.snap/data instead + of ~/snap + - overlord/snapstate: perform a single reboot when updating boot + base and kernel + - kernel/fde: add DeviceUnlockKernelHookDeviceMapperBackResolver, + use w/ disks pkg + - o/devicestate: introduce DeviceManager.Unregister + - interfaces: allow receiving PropertiesChanged on the mpris plug + - tests: new tool used to retrieve data from mongo db + - daemon: amend ssh keys coming from the store + - tests: Include the tools from snapd-testing-tools project in + "$TESTSTOOLS" + - tests: new workflow step used to report spread error to mongodb + - interfaces/builtin/dsp: update proc files for ambarella flavor + - gadget: replace ondisk implementation with disks package, refactor + part calcs + - tests: Revert "tests: disable flaky uc18 tests until systemd is + fixed" + - Revert: "many: Vendor apparmor-3.0.3 into the snapd snap" + - asserts: rename "white box" to "clear box" (woke checker) + - many: Vendor apparmor-3.0.3 into the snapd snap + - tests: reorganize the debug-each on the spread.yaml + - packaging: sync with downstream packaging in Fedora and openSUSE + - tests: disable flaky uc18 tests until systemd is fixed + - data/env: provide profile setup for fish shell + - tests: use ubuntu-image 1.11 from stable channel + - gadget/gadget.go: include disk schema in the disk device volume + traits too + - tests/main/security-device-cgroups-strict-enforced: extend the + comments + - README.md: point at bugs.launchpad.net/snapd instead of snappy + project + - osutil/disks: introduce RegisterDeviceMapperBackResolver + use for + crypt-luks2 + - packaging: make postrm script robust against `rm` failures + - tests: print extra debug on auto-refresh-gating test failure + - o/assertstate, api: move enforcing/monitoring from api to + assertstate, save history + - tests: skip the test-snapd-timedate-control-consumer.date to avoid + NTP sync error + - gadget/install: use disks functions to implement deviceFromRole, + also rename + - tests: the `lxd` test is failing right now on 21.10 + - o/snapstate: account for deleted revs when undoing install + - interfaces/builtin/block_devices: allow blkid to print block + device attributes + - gadget: include size + sector-size in DiskVolumeDeviceTraits + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - o/snapstate/handlers: propagate read errors on "copy-snap-data" + - osutil/disks: add more fields to Partition, populate them during + discovery + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - o/snapstate: remove repeated test assertions + - tests: skip `snap advise-command` test if the store is overloaded + - cmd: create ~/snap dir with 0700 perms + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - github: leave a comment documenting reasons for pipefail + - github: enable pipefail when running spread + - osutil/disks: add DiskFromPartitionDeviceNode + - gadget, many: add model param to Update() + - cmd/snap-seccomp: add riscv64 support + - o/snapstate: maintain a RevertStatus map in SnapState + - tests: enable lxd tests on impish system + - tests: (partially) revert the memory limits PR#r10241 + - o/assertstate: functions for handling validation sets tracking + history + - tests: some improvements for the spread log parser + - interfaces/network-manager-observe: Update for libnm / dart + clients + - tests: add ntp related debug around "auto-refresh" test + - boot: expand on the fact that reseal taking modeenv is very + intentional + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - data/selinux: update the policy to allow snapd to talk to + org.freedesktop.timedate1 + - o/snapstate: keep old revision if install doesn't add new one + - overlord/state: add a unit test for a kernel+base refresh like + sequence + - desktop, usersession: observe notifications + - osutil/disks: add AllPhysicalDisks() + - timeutil,deviceutil: fix unit tests on systems without dbus or + without ntp-sync + - cmd/snap-bootstrap/README: explain all the things (well most of + them anyways) + - docs: add run-checks dependency install instruction + - o/snapstate: do not prune refresh-candidates if gate-auto-refresh- + hook feature is not enabled + - o/snapstate: test relink remodel helpers do a proper subset of + doInstall and rework the verify*Tasks helpers + - tests/main/mount-ns: make the test run early + - tests: add `--debug` to netplan apply + - many: wait for up to 10min for NTP synchronization before + autorefresh + - tests: initialize CHANGE_ID in _wait_autorefresh + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - tests: add more debug around qemu-nbd + - o/hookstate: print cohort with snapctl refresh --pending (#10985) + - tests: misc robustness changes + - o/snapstate: improve install/update tests (#10850) + - tests: clean up test tools + - spread.yaml: show `journalctl -e` for all suites on debug + - tests: give interfaces-udisks2 more time for the loop device to + appear + - tests: set memory limit for snapd + - tests: increase timeout/add debug around nbd0 mounting (up, see + LP:#1949513) + - snapstate: add debug message where a snap is mounted + - tests: give nbd0 more time to show up in preseed-lxd + - interfaces/dsp: add more ambarella things + - cmd/snap: improve snap disconnect arg parsing and err msg + - tests: disable nested lxd snapd testing + - tests: disable flaky "interfaces-udisks2" on ubuntu-18.04-32 + - o/snapstate: avoid validationSetsSuite repeating snapmgrTestSuite + - sandbox/cgroup: wait for start transient unit job to finish + - o/snapstate: fix task order, tweak errors, add unit tests for + remodel helpers + - osutil/disks: re-org methods for end of usable region, size + information + - build-aux: ensure that debian packaging matches build-base + - docs: update HACKING.md instructions for snapd 2.52 and later + - spread: run lxd tests with version from latest/edge + - interfaces: suppress denial of sys_module capability + - osutil/disks: add methods to replace gadget/ondisk functions + - tests: split test tools - part 1 + - tests: fix nested tests on uc20 + - data/selinux: allow snap-confine to read udev's database + - i/b/common_test: refactor AppArmor features test + - tests: run spread tests on debian 11 + - o/devicestate: copy timesyncd clock timestamp during install + - interfaces/builtin: do not probe parser features when apparmor + isn't available + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - tests: fix error message in run-checks + - tests: spread test for validation sets enforcing + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - o/snapstate: deduplicate snap names in remove/install/update + - tests/main/selinux-data-context: use session when performing + actions as test user + - packaging/opensuse: sync with openSUSE packaging, enable AppArmor + on 15.3+ + - interfaces: skip connection of netlink interface on older + systems + - asserts, o/snapstate: honor IgnoreValidation flag when checking + installed snaps + - tests/main/apparmor-batch-reload: fix fake apparmor_parser to + handle --preprocess + - sandbox/apparmor, interfaces/apparmor: detect bpf capability, + generate snippet for s-c + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - tests: test for enforcing with prerequisites + - tests/main/snapd-sigterm: fix race conditions + - spread: run lxd tests with version from latest/stable + - run-checks: remove --spread from help message + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests: ensure systemd-timesyncd is installed on debian + - interfaces/u2f-devices: add Nitrokey 3 + - tests: update the ubuntu-image channel to candidate + - osutil/disks/labels: simplify decoding algorithm + - tests: not testing lxd snap anymore on i386 architecture + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - cmd/snap: support --ignore-validation with snap install client + command + - tests/snapd-sigterm: be more robust against service restart + - tests: simplify mock script for apparmor_parser + - o/devicestate, o/servicestate: update gadget assets and cmdline + when remodeling + - tests/nested/manual/refresh-revert-fundamentals: re-enable + encryption + - osutil/disks: fix bug in BlkIDEncodeLabel, add BlkIDDecodeLabel + - gadget, osutil/disks: fix some bugs from prior PR'sin the dir. + - secboot: revert move to new version (revert #10715) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup + - many: mv MockDeviceNameDisksToPartitionMapping -> + MockDeviceNameToDiskMapping + - interfaces/builtin: Add '/com/canonical/dbusmenu' path access to + 'unity7' interface + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - osutil/disks, many: switch to defining Partitions directly for + MockDiskMapping + - tests: remove extra-snaps-assertions test + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - tests/nested/core/core20-create-recovery: fix passing of data to + curl + - daemon: allow enabling enforce mode + - daemon: use the syscall connection to get the socket credentials + - i/builtin/kubernetes_support: add access to Calico lock file + - osutil: ensure parent dir is opened and sync'd + - tests: using test-snapd-curl snap instead of http snap + - overlord: add managers unit test demonstrating cyclic dependency + between gadget and kernel updates + - gadget/ondisk.go: include the filesystem UUID in the returned + OnDiskVolume + - packaging: fixes for building on openSUSE + - o/configcore: allow hostnames up to 253 characters, with dot- + delimited elements + - gadget/ondisk.go: add listBlockDevices() to get all block devices + on a system + - gadget: add mapping trait types + functions to save/load + - interfaces: add polkit security backend + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - tests: merge coverage results + - tests: remove "features" from fde-setup.go example + - fde: add new device-setup support to fde-setup + - gadget: add `encryptedDevice` and add encryptedDeviceLUKS + - spread: use `bios: uefi` for uc20 + - client: fail fast on non-retryable errors + - tests: support running all spread tests with experimental features + - tests: check that a snap that doesn't have gate-auto-refresh hook + can call --proceed + - o/snapstate: support ignore-validation flag when updating to a + specific snap revision + - o/snapstate: test prereq update if started by old version + - tests/main: disable cgroup-devices-v1 and freezer tests on 21.10 + - tests/main/interfaces-many: run both variants on all possible + Ubuntu systems + - gadget: mv ensureLayoutCompatibility to gadget proper, add + gadgettest pkg + - many: replace state.State restart support with overlord/restart + - overlord: fix generated snap-revision assertions in remodel unit + tests + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.4 + - devicestate: mock devicestate.MockTimeutilIsNTPSynchronized to + avoid host env leaking into tests + - timeutil: return NoTimedate1Error if it can't connect to the + system bus + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.3 + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - configcore: relax validation rules for hostname + - o/devicestate: introduce DeviceManager.Unregister + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider + - many: wait for up to 10min for NTP synchronization before + autorefresh + - interfaces/interfaces/scsi_generic: add interface for scsi generic + devices + - interfaces/microstack-support: set controlsDeviceCgroup to true + - interface/builtin/log_observe: allow to access /dev/kmsg + - daemon: write formdata file parts to snaps dir + - spread: run lxd tests with version from latest/edge + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - interfaces/builtin/dsp: add proc files for monitoring Ambarella + DSP firmware + - interfaces/builtin/dsp: update proc file accordingly + +* Mon Nov 15 2021 Ian Johnson +- New upstream release 2.53.2 + - interfaces/builtin/block_devices: allow blkid to print block + device attributes/run/udev/data/b{major}:{minor} + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - interfaces/network-manager-observe: Update for libnm client + library + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - o/hookstate: print cohort with snapctl refresh --pending + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - tests: ensure systemd-timesyncd is installed on debian + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests/main/snapd-sigterm: flush, use retry + - tests/main/snapd-sigterm: fix race conditions + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - data/selinux: allow snap-confine to read udev's database + - interfaces/dsp: add more ambarella things* interfaces/dsp: add + more ambarella things + +* Thu Oct 21 2021 Ian Johnson +- New upstream release 2.53.1 + - spread: run lxd tests with version from latest/stable + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional (#10946) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup (2.53) + - interfaces/u2f-devices: add Nitrokey 3 + - Update the ubuntu-image channel to candidate + - Allow hostnames up to 253 characters, with dot-delimited elements + (as suggested by man 7 hostname). + - Disable i386 until it is possible to build snapd using lxd + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - tests/snapd-sigterm: be more robust against service restart + - tests: add a regression test for snapd hanging on SIGTERM + - daemon: use the syscall connection to get the socket + credentials + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - secboot: revert move to new version + * Tue Oct 05 2021 Michael Vogt - New upstream release 2.53 - overlord: fix generated snap-revision assertions in remodel unit diff -Nru snapd-2.53+21.10ubuntu1/packaging/centos-8/snapd.spec snapd-2.54.2+21.10/packaging/centos-8/snapd.spec --- snapd-2.53+21.10ubuntu1/packaging/centos-8/snapd.spec 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/packaging/centos-8/snapd.spec 2022-01-06 21:25:16.000000000 +0000 @@ -33,6 +33,11 @@ %global with_multilib 1 %endif +# Set if valgrind is to be run +%ifnarch ppc64le +%global with_valgrind 1 +%endif + %if ! %{with vendorized} %global with_bundled 0 %else @@ -97,7 +102,7 @@ %endif Name: snapd -Version: 2.53 +Version: 2.54.2 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -206,7 +211,9 @@ %if ! 0%{?rhel} BuildRequires: libseccomp-static %endif +%if 0%{?with_valgrind} BuildRequires: valgrind +%endif BuildRequires: %{_bindir}/rst2man %if 0%{?fedora} # ShellCheck in EPEL is too old... @@ -289,7 +296,7 @@ Provides: bundled(golang(github.com/kr/pretty)) Provides: bundled(golang(github.com/kr/text)) Provides: bundled(golang(github.com/mvo5/goconfigparser)) -Provides: bundled(golang(github.com/mvo5/libseccomp-golang)) +Provides: bundled(golang(github.com/seccomp/libseccomp-golang)) Provides: bundled(golang(github.com/snapcore/go-gettext)) Provides: bundled(golang(golang.org/x/crypto/openpgp/armor)) Provides: bundled(golang(golang.org/x/crypto/openpgp/packet)) @@ -534,8 +541,6 @@ %endif %if ! 0%{?with_bundled} -# We don't need mvo5 fork for seccomp, as we have seccomp 2.3.x -sed -e "s:github.com/mvo5/libseccomp-golang:github.com/seccomp/libseccomp-golang:g" -i cmd/snap-seccomp/*.go # We don't need the snapcore fork for bolt - it is just a fix on ppc sed -e "s:github.com/snapcore/bolt:github.com/boltdb/bolt:g" -i advisor/*.go errtracker/*.go %endif @@ -543,7 +548,7 @@ # We have to build snapd first to prevent the build from # building various things from the tree without additional # set tags. -%gobuild -o bin/snapd $GOFLAGS ./cmd/snapd +%gobuild -o bin/snapd $GOFLAGS %{import_path}/cmd/snapd BUILDTAGS="${BUILDTAGS} nomanagers" %gobuild -o bin/snap $GOFLAGS %{import_path}/cmd/snap %gobuild -o bin/snap-failure $GOFLAGS %{import_path}/cmd/snap-failure @@ -595,18 +600,21 @@ %if 0%{?with_selinux} --enable-selinux \ %endif +%if 0%{?rhel} == 7 + --disable-bpf \ +%endif --libexecdir=%{_libexecdir}/snapd/ \ --enable-nvidia-biarch \ %{?with_multilib:--with-32bit-libdir=%{_prefix}/lib} \ --with-snap-mount-dir=%{_sharedstatedir}/snapd/snap \ --enable-merged-usr -%make_build +%make_build %{!?with_valgrind:HAVE_VALGRIND=} popd # Build systemd units, dbus services, and env files pushd ./data -make BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" \ +make BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ SYSTEMDSYSTEMUNITDIR="%{_unitdir}" \ SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" @@ -691,7 +699,7 @@ # Install all systemd and dbus units, and env files pushd ./data -%make_install BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" \ +%make_install BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ SYSTEMDSYSTEMUNITDIR="%{_unitdir}" SYSTEMDUSERUNITDIR="%{_userunitdir}" \ SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" @@ -833,6 +841,7 @@ %{_datadir}/dbus-1/system.d/snapd.system-services.conf %{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy %{_datadir}/applications/io.snapcraft.SessionAgent.desktop +%{_datadir}/fish/vendor_conf.d/snapd.fish %{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop %config(noreplace) %{_sysconfdir}/sysconfig/snapd %dir %{_sharedstatedir}/snapd @@ -867,6 +876,8 @@ # this is typically owned by zsh, but we do not want to explicitly require zsh %dir %{_datadir}/zsh %dir %{_datadir}/zsh/site-functions +# similar case for fish +%dir %{_datadir}/fish/vendor_conf.d %files -n snap-confine %doc cmd/snap-confine/PORTING @@ -978,6 +989,450 @@ %changelog +* Thu Jan 06 2022 Ian Johnson +- New upstream release 2.54.2 + - tests: exclude interfaces-kernel-module load on arm + - tests: ensure that test-snapd-kernel-module-load is + removed + - tests: do not test microk8s-smoke on arm + - tests/core/failover: replace boot-state with snap debug boot-vars + - tests: use snap info|awk to extract tracking channel + - tests: fix remodel-kernel test when running on external devices + - .github/workflows/test.yaml: also check internal snapd version for + cleanliness + - packaging/ubuntu-16.04/rules: eliminate seccomp modification + - bootloader/assets/grub_*cfg_asset.go: update Copyright + - build-aux/snap/snapcraft.yaml: adjust comment about get-version + - .github/workflows/test.yaml: add check in github actions for dirty + snapd snaps + - build-aux/snap/snapcraft.yaml: use build-packages, don't fail + dirty builds + - data/selinux: allow poking /proc/xen + +* Mon Dec 20 2021 Michael Vogt +- New upstream release 2.54.1 + - buid-aux: set version before calling ./generate-packaging-dir + This fixes the "dirty" suffix in the auto-generated version + +* Fri Dec 17 2021 Michael Vogt +- New upstream release 2.54 + - interfaces/builtin/opengl.go: add boot_vga sys/devices file + - o/configstate/configcore: add tmpfs.size option + - tests: moving to manual opensuse 15.2 + - cmd/snap-device-helper: bring back the device type identification + behavior, but for remove action fallback only + - cmd/snap-failure: use snapd from the snapd snap if core is not + present + - tests/core/failover: enable the test on core18 + - o/devicestate: ensure proper order when remodel does a simple + switch-snap-channel + - builtin/interfaces: add shared memory interface + - overlord: extend kernel/base success and failover with bootenv + checks + - o/snapstate: check disk space w/o store if possible + - snap-bootstrap: Mount snaps read only + - gadget/install: do not re-create partitions using OnDiskVolume + after deletion + - many: fix formatting w/ latest go version + - devicestate,timeutil: improve logging of NTP sync + - tests/main/security-device-cgroups-helper: more debugs + - cmd/snap: print a placeholder for version of broken snaps + - o/snapstate: mock system with classic confinement support + - cmd: Fixup .clangd to use correct syntax + - tests: run spread tests in fedora-35 + - data/selinux: allow snapd to access /etc/modprobe.d + - mount-control: step 2 + - daemon: add multiple snap sideload to API + - tests/lib/pkgdb: install dbus-user-session during prepare, drop + dbus-x11 + - systemd: provide more detailed errors for unimplemented method in + emulation mode + - tests: avoid checking TRUST_TEST_KEYS on restore on remodel-base + test + - tests: retry umounting /var/lib/snapd/seed on uc20 on fsck-on-boot + test + - o/snapstate: add hide/expose snap data to backend + - interfaces: kernel-module-load + - snap: add support for `snap watch + --last={revert,enable,disable,switch}` + - tests/main/security-udev-input-subsystem: drop info from udev + - tests/core/kernel-and-base-single-reboot-failover, + tests/lib/fakestore: verify failover scenario + - tests/main/security-device-cgroups-helper: collect some debug info + when the test fails + - tests/nested/manual/core20-remodel: wait for device to have a + serial before starting a remodel + - tests/main/generic-unregister: test re-registration if not blocked + - o/snapstate, assertsate: validation sets/undo on partial failure + - tests: ensure snapd can be downloaded as a module + - snapdtool, many: support additional key/value flags in info file + - data/env: improve fish shell env setup + - usersession/client: provide a way for client to send messages to a + subset of users + - tests: verify that simultaneous refresh of kernel and base + triggers a single reboot only + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - asserts: change behavior of alternative attribute matcher + - configcore: relax validation rules for hostname + - cmd/snap-confine: do not include libglvnd libraries from the host + system + - overlord, tests: add managers and a spread test for UC20 to UC22 + remodel + - HACKING.md: adjust again for building the snapd snap + - systemd: add support for systemd unit alias names + - o/snapstate: add InstallPathMany + - gadget: allow EnsureLayoutCompatibility to ensure disk has all + laid out structsnow reject/fail: + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider (#11111) + - interfaces/interfaces/scsi_generic: add interface for scsi generic + de… (#10936) + - osutil/disks/mockdisk.go: add MockDevicePathToDiskMapping + - interfaces/microstack-support: set controlsDeviceCgroup to true + - network-setup-control: add netplan generate D-Bus rules + - interface/builtin/log_observe: allow to access /dev/kmsg + - .github/workflows/test.yaml: restore failing of spread tests on + errors (nested) + - gadget: tweaks to DiskStructureDeviceTraits + expand test cases + - tests/lib/nested.sh: allow tests to use their own core18 in extra- + snaps-path + - interfaces/browser-support: Update rules for Edge + - o/devicestate: during remodel first check pending download tasks + for snaps + - polkit: add a package to validate polkit policy files + - HACKING.md: document building the snapd snap and splicing it into + the core snap + - interfaces/udev: fix installing snaps inside lxd in 21.10 + - o/snapstate: refactor disk space checks + - tests: add (strict) microk8s smoke test + - osutil/strace: try to enable strace on more arches + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - tests/main/snapd-reexec-snapd-snap: improve debugging + - daemon: write formdata file parts to snaps dir + - systemd: add support for .target units + - tests: run snap-disconnect on uc16 + - many: add experimental setting to allow using ~/.snap/data instead + of ~/snap + - overlord/snapstate: perform a single reboot when updating boot + base and kernel + - kernel/fde: add DeviceUnlockKernelHookDeviceMapperBackResolver, + use w/ disks pkg + - o/devicestate: introduce DeviceManager.Unregister + - interfaces: allow receiving PropertiesChanged on the mpris plug + - tests: new tool used to retrieve data from mongo db + - daemon: amend ssh keys coming from the store + - tests: Include the tools from snapd-testing-tools project in + "$TESTSTOOLS" + - tests: new workflow step used to report spread error to mongodb + - interfaces/builtin/dsp: update proc files for ambarella flavor + - gadget: replace ondisk implementation with disks package, refactor + part calcs + - tests: Revert "tests: disable flaky uc18 tests until systemd is + fixed" + - Revert: "many: Vendor apparmor-3.0.3 into the snapd snap" + - asserts: rename "white box" to "clear box" (woke checker) + - many: Vendor apparmor-3.0.3 into the snapd snap + - tests: reorganize the debug-each on the spread.yaml + - packaging: sync with downstream packaging in Fedora and openSUSE + - tests: disable flaky uc18 tests until systemd is fixed + - data/env: provide profile setup for fish shell + - tests: use ubuntu-image 1.11 from stable channel + - gadget/gadget.go: include disk schema in the disk device volume + traits too + - tests/main/security-device-cgroups-strict-enforced: extend the + comments + - README.md: point at bugs.launchpad.net/snapd instead of snappy + project + - osutil/disks: introduce RegisterDeviceMapperBackResolver + use for + crypt-luks2 + - packaging: make postrm script robust against `rm` failures + - tests: print extra debug on auto-refresh-gating test failure + - o/assertstate, api: move enforcing/monitoring from api to + assertstate, save history + - tests: skip the test-snapd-timedate-control-consumer.date to avoid + NTP sync error + - gadget/install: use disks functions to implement deviceFromRole, + also rename + - tests: the `lxd` test is failing right now on 21.10 + - o/snapstate: account for deleted revs when undoing install + - interfaces/builtin/block_devices: allow blkid to print block + device attributes + - gadget: include size + sector-size in DiskVolumeDeviceTraits + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - o/snapstate/handlers: propagate read errors on "copy-snap-data" + - osutil/disks: add more fields to Partition, populate them during + discovery + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - o/snapstate: remove repeated test assertions + - tests: skip `snap advise-command` test if the store is overloaded + - cmd: create ~/snap dir with 0700 perms + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - github: leave a comment documenting reasons for pipefail + - github: enable pipefail when running spread + - osutil/disks: add DiskFromPartitionDeviceNode + - gadget, many: add model param to Update() + - cmd/snap-seccomp: add riscv64 support + - o/snapstate: maintain a RevertStatus map in SnapState + - tests: enable lxd tests on impish system + - tests: (partially) revert the memory limits PR#r10241 + - o/assertstate: functions for handling validation sets tracking + history + - tests: some improvements for the spread log parser + - interfaces/network-manager-observe: Update for libnm / dart + clients + - tests: add ntp related debug around "auto-refresh" test + - boot: expand on the fact that reseal taking modeenv is very + intentional + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - data/selinux: update the policy to allow snapd to talk to + org.freedesktop.timedate1 + - o/snapstate: keep old revision if install doesn't add new one + - overlord/state: add a unit test for a kernel+base refresh like + sequence + - desktop, usersession: observe notifications + - osutil/disks: add AllPhysicalDisks() + - timeutil,deviceutil: fix unit tests on systems without dbus or + without ntp-sync + - cmd/snap-bootstrap/README: explain all the things (well most of + them anyways) + - docs: add run-checks dependency install instruction + - o/snapstate: do not prune refresh-candidates if gate-auto-refresh- + hook feature is not enabled + - o/snapstate: test relink remodel helpers do a proper subset of + doInstall and rework the verify*Tasks helpers + - tests/main/mount-ns: make the test run early + - tests: add `--debug` to netplan apply + - many: wait for up to 10min for NTP synchronization before + autorefresh + - tests: initialize CHANGE_ID in _wait_autorefresh + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - tests: add more debug around qemu-nbd + - o/hookstate: print cohort with snapctl refresh --pending (#10985) + - tests: misc robustness changes + - o/snapstate: improve install/update tests (#10850) + - tests: clean up test tools + - spread.yaml: show `journalctl -e` for all suites on debug + - tests: give interfaces-udisks2 more time for the loop device to + appear + - tests: set memory limit for snapd + - tests: increase timeout/add debug around nbd0 mounting (up, see + LP:#1949513) + - snapstate: add debug message where a snap is mounted + - tests: give nbd0 more time to show up in preseed-lxd + - interfaces/dsp: add more ambarella things + - cmd/snap: improve snap disconnect arg parsing and err msg + - tests: disable nested lxd snapd testing + - tests: disable flaky "interfaces-udisks2" on ubuntu-18.04-32 + - o/snapstate: avoid validationSetsSuite repeating snapmgrTestSuite + - sandbox/cgroup: wait for start transient unit job to finish + - o/snapstate: fix task order, tweak errors, add unit tests for + remodel helpers + - osutil/disks: re-org methods for end of usable region, size + information + - build-aux: ensure that debian packaging matches build-base + - docs: update HACKING.md instructions for snapd 2.52 and later + - spread: run lxd tests with version from latest/edge + - interfaces: suppress denial of sys_module capability + - osutil/disks: add methods to replace gadget/ondisk functions + - tests: split test tools - part 1 + - tests: fix nested tests on uc20 + - data/selinux: allow snap-confine to read udev's database + - i/b/common_test: refactor AppArmor features test + - tests: run spread tests on debian 11 + - o/devicestate: copy timesyncd clock timestamp during install + - interfaces/builtin: do not probe parser features when apparmor + isn't available + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - tests: fix error message in run-checks + - tests: spread test for validation sets enforcing + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - o/snapstate: deduplicate snap names in remove/install/update + - tests/main/selinux-data-context: use session when performing + actions as test user + - packaging/opensuse: sync with openSUSE packaging, enable AppArmor + on 15.3+ + - interfaces: skip connection of netlink interface on older + systems + - asserts, o/snapstate: honor IgnoreValidation flag when checking + installed snaps + - tests/main/apparmor-batch-reload: fix fake apparmor_parser to + handle --preprocess + - sandbox/apparmor, interfaces/apparmor: detect bpf capability, + generate snippet for s-c + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - tests: test for enforcing with prerequisites + - tests/main/snapd-sigterm: fix race conditions + - spread: run lxd tests with version from latest/stable + - run-checks: remove --spread from help message + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests: ensure systemd-timesyncd is installed on debian + - interfaces/u2f-devices: add Nitrokey 3 + - tests: update the ubuntu-image channel to candidate + - osutil/disks/labels: simplify decoding algorithm + - tests: not testing lxd snap anymore on i386 architecture + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - cmd/snap: support --ignore-validation with snap install client + command + - tests/snapd-sigterm: be more robust against service restart + - tests: simplify mock script for apparmor_parser + - o/devicestate, o/servicestate: update gadget assets and cmdline + when remodeling + - tests/nested/manual/refresh-revert-fundamentals: re-enable + encryption + - osutil/disks: fix bug in BlkIDEncodeLabel, add BlkIDDecodeLabel + - gadget, osutil/disks: fix some bugs from prior PR'sin the dir. + - secboot: revert move to new version (revert #10715) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup + - many: mv MockDeviceNameDisksToPartitionMapping -> + MockDeviceNameToDiskMapping + - interfaces/builtin: Add '/com/canonical/dbusmenu' path access to + 'unity7' interface + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - osutil/disks, many: switch to defining Partitions directly for + MockDiskMapping + - tests: remove extra-snaps-assertions test + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - tests/nested/core/core20-create-recovery: fix passing of data to + curl + - daemon: allow enabling enforce mode + - daemon: use the syscall connection to get the socket credentials + - i/builtin/kubernetes_support: add access to Calico lock file + - osutil: ensure parent dir is opened and sync'd + - tests: using test-snapd-curl snap instead of http snap + - overlord: add managers unit test demonstrating cyclic dependency + between gadget and kernel updates + - gadget/ondisk.go: include the filesystem UUID in the returned + OnDiskVolume + - packaging: fixes for building on openSUSE + - o/configcore: allow hostnames up to 253 characters, with dot- + delimited elements + - gadget/ondisk.go: add listBlockDevices() to get all block devices + on a system + - gadget: add mapping trait types + functions to save/load + - interfaces: add polkit security backend + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - tests: merge coverage results + - tests: remove "features" from fde-setup.go example + - fde: add new device-setup support to fde-setup + - gadget: add `encryptedDevice` and add encryptedDeviceLUKS + - spread: use `bios: uefi` for uc20 + - client: fail fast on non-retryable errors + - tests: support running all spread tests with experimental features + - tests: check that a snap that doesn't have gate-auto-refresh hook + can call --proceed + - o/snapstate: support ignore-validation flag when updating to a + specific snap revision + - o/snapstate: test prereq update if started by old version + - tests/main: disable cgroup-devices-v1 and freezer tests on 21.10 + - tests/main/interfaces-many: run both variants on all possible + Ubuntu systems + - gadget: mv ensureLayoutCompatibility to gadget proper, add + gadgettest pkg + - many: replace state.State restart support with overlord/restart + - overlord: fix generated snap-revision assertions in remodel unit + tests + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.4 + - devicestate: mock devicestate.MockTimeutilIsNTPSynchronized to + avoid host env leaking into tests + - timeutil: return NoTimedate1Error if it can't connect to the + system bus + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.3 + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - configcore: relax validation rules for hostname + - o/devicestate: introduce DeviceManager.Unregister + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider + - many: wait for up to 10min for NTP synchronization before + autorefresh + - interfaces/interfaces/scsi_generic: add interface for scsi generic + devices + - interfaces/microstack-support: set controlsDeviceCgroup to true + - interface/builtin/log_observe: allow to access /dev/kmsg + - daemon: write formdata file parts to snaps dir + - spread: run lxd tests with version from latest/edge + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - interfaces/builtin/dsp: add proc files for monitoring Ambarella + DSP firmware + - interfaces/builtin/dsp: update proc file accordingly + +* Mon Nov 15 2021 Ian Johnson +- New upstream release 2.53.2 + - interfaces/builtin/block_devices: allow blkid to print block + device attributes/run/udev/data/b{major}:{minor} + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - interfaces/network-manager-observe: Update for libnm client + library + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - o/hookstate: print cohort with snapctl refresh --pending + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - tests: ensure systemd-timesyncd is installed on debian + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests/main/snapd-sigterm: flush, use retry + - tests/main/snapd-sigterm: fix race conditions + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - data/selinux: allow snap-confine to read udev's database + - interfaces/dsp: add more ambarella things* interfaces/dsp: add + more ambarella things + +* Thu Oct 21 2021 Ian Johnson +- New upstream release 2.53.1 + - spread: run lxd tests with version from latest/stable + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional (#10946) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup (2.53) + - interfaces/u2f-devices: add Nitrokey 3 + - Update the ubuntu-image channel to candidate + - Allow hostnames up to 253 characters, with dot-delimited elements + (as suggested by man 7 hostname). + - Disable i386 until it is possible to build snapd using lxd + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - tests/snapd-sigterm: be more robust against service restart + - tests: add a regression test for snapd hanging on SIGTERM + - daemon: use the syscall connection to get the socket + credentials + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - secboot: revert move to new version + * Tue Oct 05 2021 Michael Vogt - New upstream release 2.53 - overlord: fix generated snap-revision assertions in remodel unit diff -Nru snapd-2.53+21.10ubuntu1/packaging/debian-sid/changelog snapd-2.54.2+21.10/packaging/debian-sid/changelog --- snapd-2.53+21.10ubuntu1/packaging/debian-sid/changelog 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/packaging/debian-sid/changelog 2022-01-06 21:25:16.000000000 +0000 @@ -1,3 +1,468 @@ +snapd (2.54.2-1) unstable; urgency=medium + + * New upstream release, LP: #1955137 + - tests: exclude interfaces-kernel-module load on arm + - tests: ensure that test-snapd-kernel-module-load is + removed + - tests: do not test microk8s-smoke on arm + - tests/core/failover: replace boot-state with snap debug boot-vars + - tests: use snap info|awk to extract tracking channel + - tests: fix remodel-kernel test when running on external devices + - .github/workflows/test.yaml: also check internal snapd version for + cleanliness + - packaging/ubuntu-16.04/rules: eliminate seccomp modification + - bootloader/assets/grub_*cfg_asset.go: update Copyright + - build-aux/snap/snapcraft.yaml: adjust comment about get-version + - .github/workflows/test.yaml: add check in github actions for dirty + snapd snaps + - build-aux/snap/snapcraft.yaml: use build-packages, don't fail + dirty builds + - data/selinux: allow poking /proc/xen + + -- Ian Johnson Thu, 06 Jan 2022 15:25:16 -0600 + +snapd (2.54.1-1) unstable; urgency=medium + + * New upstream release, LP: #1955137 + - buid-aux: set version before calling ./generate-packaging-dir + This fixes the "dirty" suffix in the auto-generated version + + -- Michael Vogt Mon, 20 Dec 2021 10:06:09 +0100 + +snapd (2.54-1) unstable; urgency=medium + + * New upstream release, LP: #1955137 + - interfaces/builtin/opengl.go: add boot_vga sys/devices file + - o/configstate/configcore: add tmpfs.size option + - tests: moving to manual opensuse 15.2 + - cmd/snap-device-helper: bring back the device type identification + behavior, but for remove action fallback only + - cmd/snap-failure: use snapd from the snapd snap if core is not + present + - tests/core/failover: enable the test on core18 + - o/devicestate: ensure proper order when remodel does a simple + switch-snap-channel + - builtin/interfaces: add shared memory interface + - overlord: extend kernel/base success and failover with bootenv + checks + - o/snapstate: check disk space w/o store if possible + - snap-bootstrap: Mount snaps read only + - gadget/install: do not re-create partitions using OnDiskVolume + after deletion + - many: fix formatting w/ latest go version + - devicestate,timeutil: improve logging of NTP sync + - tests/main/security-device-cgroups-helper: more debugs + - cmd/snap: print a placeholder for version of broken snaps + - o/snapstate: mock system with classic confinement support + - cmd: Fixup .clangd to use correct syntax + - tests: run spread tests in fedora-35 + - data/selinux: allow snapd to access /etc/modprobe.d + - mount-control: step 2 + - daemon: add multiple snap sideload to API + - tests/lib/pkgdb: install dbus-user-session during prepare, drop + dbus-x11 + - systemd: provide more detailed errors for unimplemented method in + emulation mode + - tests: avoid checking TRUST_TEST_KEYS on restore on remodel-base + test + - tests: retry umounting /var/lib/snapd/seed on uc20 on fsck-on-boot + test + - o/snapstate: add hide/expose snap data to backend + - interfaces: kernel-module-load + - snap: add support for `snap watch + --last={revert,enable,disable,switch}` + - tests/main/security-udev-input-subsystem: drop info from udev + - tests/core/kernel-and-base-single-reboot-failover, + tests/lib/fakestore: verify failover scenario + - tests/main/security-device-cgroups-helper: collect some debug info + when the test fails + - tests/nested/manual/core20-remodel: wait for device to have a + serial before starting a remodel + - tests/main/generic-unregister: test re-registration if not blocked + - o/snapstate, assertsate: validation sets/undo on partial failure + - tests: ensure snapd can be downloaded as a module + - snapdtool, many: support additional key/value flags in info file + - data/env: improve fish shell env setup + - usersession/client: provide a way for client to send messages to a + subset of users + - tests: verify that simultaneous refresh of kernel and base + triggers a single reboot only + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - asserts: change behavior of alternative attribute matcher + - configcore: relax validation rules for hostname + - cmd/snap-confine: do not include libglvnd libraries from the host + system + - overlord, tests: add managers and a spread test for UC20 to UC22 + remodel + - HACKING.md: adjust again for building the snapd snap + - systemd: add support for systemd unit alias names + - o/snapstate: add InstallPathMany + - gadget: allow EnsureLayoutCompatibility to ensure disk has all + laid out structsnow reject/fail: + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider (#11111) + - interfaces/interfaces/scsi_generic: add interface for scsi generic + de… (#10936) + - osutil/disks/mockdisk.go: add MockDevicePathToDiskMapping + - interfaces/microstack-support: set controlsDeviceCgroup to true + - network-setup-control: add netplan generate D-Bus rules + - interface/builtin/log_observe: allow to access /dev/kmsg + - .github/workflows/test.yaml: restore failing of spread tests on + errors (nested) + - gadget: tweaks to DiskStructureDeviceTraits + expand test cases + - tests/lib/nested.sh: allow tests to use their own core18 in extra- + snaps-path + - interfaces/browser-support: Update rules for Edge + - o/devicestate: during remodel first check pending download tasks + for snaps + - polkit: add a package to validate polkit policy files + - HACKING.md: document building the snapd snap and splicing it into + the core snap + - interfaces/udev: fix installing snaps inside lxd in 21.10 + - o/snapstate: refactor disk space checks + - tests: add (strict) microk8s smoke test + - osutil/strace: try to enable strace on more arches + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - tests/main/snapd-reexec-snapd-snap: improve debugging + - daemon: write formdata file parts to snaps dir + - systemd: add support for .target units + - tests: run snap-disconnect on uc16 + - many: add experimental setting to allow using ~/.snap/data instead + of ~/snap + - overlord/snapstate: perform a single reboot when updating boot + base and kernel + - kernel/fde: add DeviceUnlockKernelHookDeviceMapperBackResolver, + use w/ disks pkg + - o/devicestate: introduce DeviceManager.Unregister + - interfaces: allow receiving PropertiesChanged on the mpris plug + - tests: new tool used to retrieve data from mongo db + - daemon: amend ssh keys coming from the store + - tests: Include the tools from snapd-testing-tools project in + "$TESTSTOOLS" + - tests: new workflow step used to report spread error to mongodb + - interfaces/builtin/dsp: update proc files for ambarella flavor + - gadget: replace ondisk implementation with disks package, refactor + part calcs + - tests: Revert "tests: disable flaky uc18 tests until systemd is + fixed" + - Revert: "many: Vendor apparmor-3.0.3 into the snapd snap" + - asserts: rename "white box" to "clear box" (woke checker) + - many: Vendor apparmor-3.0.3 into the snapd snap + - tests: reorganize the debug-each on the spread.yaml + - packaging: sync with downstream packaging in Fedora and openSUSE + - tests: disable flaky uc18 tests until systemd is fixed + - data/env: provide profile setup for fish shell + - tests: use ubuntu-image 1.11 from stable channel + - gadget/gadget.go: include disk schema in the disk device volume + traits too + - tests/main/security-device-cgroups-strict-enforced: extend the + comments + - README.md: point at bugs.launchpad.net/snapd instead of snappy + project + - osutil/disks: introduce RegisterDeviceMapperBackResolver + use for + crypt-luks2 + - packaging: make postrm script robust against `rm` failures + - tests: print extra debug on auto-refresh-gating test failure + - o/assertstate, api: move enforcing/monitoring from api to + assertstate, save history + - tests: skip the test-snapd-timedate-control-consumer.date to avoid + NTP sync error + - gadget/install: use disks functions to implement deviceFromRole, + also rename + - tests: the `lxd` test is failing right now on 21.10 + - o/snapstate: account for deleted revs when undoing install + - interfaces/builtin/block_devices: allow blkid to print block + device attributes + - gadget: include size + sector-size in DiskVolumeDeviceTraits + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - o/snapstate/handlers: propagate read errors on "copy-snap-data" + - osutil/disks: add more fields to Partition, populate them during + discovery + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - o/snapstate: remove repeated test assertions + - tests: skip `snap advise-command` test if the store is overloaded + - cmd: create ~/snap dir with 0700 perms + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - github: leave a comment documenting reasons for pipefail + - github: enable pipefail when running spread + - osutil/disks: add DiskFromPartitionDeviceNode + - gadget, many: add model param to Update() + - cmd/snap-seccomp: add riscv64 support + - o/snapstate: maintain a RevertStatus map in SnapState + - tests: enable lxd tests on impish system + - tests: (partially) revert the memory limits PR#r10241 + - o/assertstate: functions for handling validation sets tracking + history + - tests: some improvements for the spread log parser + - interfaces/network-manager-observe: Update for libnm / dart + clients + - tests: add ntp related debug around "auto-refresh" test + - boot: expand on the fact that reseal taking modeenv is very + intentional + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - data/selinux: update the policy to allow snapd to talk to + org.freedesktop.timedate1 + - o/snapstate: keep old revision if install doesn't add new one + - overlord/state: add a unit test for a kernel+base refresh like + sequence + - desktop, usersession: observe notifications + - osutil/disks: add AllPhysicalDisks() + - timeutil,deviceutil: fix unit tests on systems without dbus or + without ntp-sync + - cmd/snap-bootstrap/README: explain all the things (well most of + them anyways) + - docs: add run-checks dependency install instruction + - o/snapstate: do not prune refresh-candidates if gate-auto-refresh- + hook feature is not enabled + - o/snapstate: test relink remodel helpers do a proper subset of + doInstall and rework the verify*Tasks helpers + - tests/main/mount-ns: make the test run early + - tests: add `--debug` to netplan apply + - many: wait for up to 10min for NTP synchronization before + autorefresh + - tests: initialize CHANGE_ID in _wait_autorefresh + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - tests: add more debug around qemu-nbd + - o/hookstate: print cohort with snapctl refresh --pending (#10985) + - tests: misc robustness changes + - o/snapstate: improve install/update tests (#10850) + - tests: clean up test tools + - spread.yaml: show `journalctl -e` for all suites on debug + - tests: give interfaces-udisks2 more time for the loop device to + appear + - tests: set memory limit for snapd + - tests: increase timeout/add debug around nbd0 mounting (up, see + LP:#1949513) + - snapstate: add debug message where a snap is mounted + - tests: give nbd0 more time to show up in preseed-lxd + - interfaces/dsp: add more ambarella things + - cmd/snap: improve snap disconnect arg parsing and err msg + - tests: disable nested lxd snapd testing + - tests: disable flaky "interfaces-udisks2" on ubuntu-18.04-32 + - o/snapstate: avoid validationSetsSuite repeating snapmgrTestSuite + - sandbox/cgroup: wait for start transient unit job to finish + - o/snapstate: fix task order, tweak errors, add unit tests for + remodel helpers + - osutil/disks: re-org methods for end of usable region, size + information + - build-aux: ensure that debian packaging matches build-base + - docs: update HACKING.md instructions for snapd 2.52 and later + - spread: run lxd tests with version from latest/edge + - interfaces: suppress denial of sys_module capability + - osutil/disks: add methods to replace gadget/ondisk functions + - tests: split test tools - part 1 + - tests: fix nested tests on uc20 + - data/selinux: allow snap-confine to read udev's database + - i/b/common_test: refactor AppArmor features test + - tests: run spread tests on debian 11 + - o/devicestate: copy timesyncd clock timestamp during install + - interfaces/builtin: do not probe parser features when apparmor + isn't available + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - tests: fix error message in run-checks + - tests: spread test for validation sets enforcing + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - o/snapstate: deduplicate snap names in remove/install/update + - tests/main/selinux-data-context: use session when performing + actions as test user + - packaging/opensuse: sync with openSUSE packaging, enable AppArmor + on 15.3+ + - interfaces: skip connection of netlink interface on older + systems + - asserts, o/snapstate: honor IgnoreValidation flag when checking + installed snaps + - tests/main/apparmor-batch-reload: fix fake apparmor_parser to + handle --preprocess + - sandbox/apparmor, interfaces/apparmor: detect bpf capability, + generate snippet for s-c + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - tests: test for enforcing with prerequisites + - tests/main/snapd-sigterm: fix race conditions + - spread: run lxd tests with version from latest/stable + - run-checks: remove --spread from help message + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests: ensure systemd-timesyncd is installed on debian + - interfaces/u2f-devices: add Nitrokey 3 + - tests: update the ubuntu-image channel to candidate + - osutil/disks/labels: simplify decoding algorithm + - tests: not testing lxd snap anymore on i386 architecture + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - cmd/snap: support --ignore-validation with snap install client + command + - tests/snapd-sigterm: be more robust against service restart + - tests: simplify mock script for apparmor_parser + - o/devicestate, o/servicestate: update gadget assets and cmdline + when remodeling + - tests/nested/manual/refresh-revert-fundamentals: re-enable + encryption + - osutil/disks: fix bug in BlkIDEncodeLabel, add BlkIDDecodeLabel + - gadget, osutil/disks: fix some bugs from prior PR'sin the dir. + - secboot: revert move to new version (revert #10715) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup + - many: mv MockDeviceNameDisksToPartitionMapping -> + MockDeviceNameToDiskMapping + - interfaces/builtin: Add '/com/canonical/dbusmenu' path access to + 'unity7' interface + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - osutil/disks, many: switch to defining Partitions directly for + MockDiskMapping + - tests: remove extra-snaps-assertions test + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - tests/nested/core/core20-create-recovery: fix passing of data to + curl + - daemon: allow enabling enforce mode + - daemon: use the syscall connection to get the socket credentials + - i/builtin/kubernetes_support: add access to Calico lock file + - osutil: ensure parent dir is opened and sync'd + - tests: using test-snapd-curl snap instead of http snap + - overlord: add managers unit test demonstrating cyclic dependency + between gadget and kernel updates + - gadget/ondisk.go: include the filesystem UUID in the returned + OnDiskVolume + - packaging: fixes for building on openSUSE + - o/configcore: allow hostnames up to 253 characters, with dot- + delimited elements + - gadget/ondisk.go: add listBlockDevices() to get all block devices + on a system + - gadget: add mapping trait types + functions to save/load + - interfaces: add polkit security backend + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - tests: merge coverage results + - tests: remove "features" from fde-setup.go example + - fde: add new device-setup support to fde-setup + - gadget: add `encryptedDevice` and add encryptedDeviceLUKS + - spread: use `bios: uefi` for uc20 + - client: fail fast on non-retryable errors + - tests: support running all spread tests with experimental features + - tests: check that a snap that doesn't have gate-auto-refresh hook + can call --proceed + - o/snapstate: support ignore-validation flag when updating to a + specific snap revision + - o/snapstate: test prereq update if started by old version + - tests/main: disable cgroup-devices-v1 and freezer tests on 21.10 + - tests/main/interfaces-many: run both variants on all possible + Ubuntu systems + - gadget: mv ensureLayoutCompatibility to gadget proper, add + gadgettest pkg + - many: replace state.State restart support with overlord/restart + - overlord: fix generated snap-revision assertions in remodel unit + tests + + -- Michael Vogt Fri, 17 Dec 2021 15:49:18 +0100 + +snapd (2.53.4-1) unstable; urgency=medium + + * New upstream release, LP: #1929842 + - devicestate: mock devicestate.MockTimeutilIsNTPSynchronized to + avoid host env leaking into tests + - timeutil: return NoTimedate1Error if it can't connect to the + system bus + + -- Ian Johnson Thu, 02 Dec 2021 17:16:48 -0600 + +snapd (2.53.3-1) unstable; urgency=medium + + * New upstream release, LP: #1929842 + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - configcore: relax validation rules for hostname + - o/devicestate: introduce DeviceManager.Unregister + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider + - many: wait for up to 10min for NTP synchronization before + autorefresh + - interfaces/interfaces/scsi_generic: add interface for scsi generic + devices + - interfaces/microstack-support: set controlsDeviceCgroup to true + - interface/builtin/log_observe: allow to access /dev/kmsg + - daemon: write formdata file parts to snaps dir + - spread: run lxd tests with version from latest/edge + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - interfaces/builtin/dsp: add proc files for monitoring Ambarella + DSP firmware + - interfaces/builtin/dsp: update proc file accordingly + + -- Ian Johnson Thu, 02 Dec 2021 11:42:15 -0600 + +snapd (2.53.2-1) unstable; urgency=medium + + * New upstream release, LP: #1946127 + - interfaces/builtin/block_devices: allow blkid to print block + device attributes/run/udev/data/b{major}:{minor} + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - interfaces/network-manager-observe: Update for libnm client + library + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - o/hookstate: print cohort with snapctl refresh --pending + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - tests: ensure systemd-timesyncd is installed on debian + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests/main/snapd-sigterm: flush, use retry + - tests/main/snapd-sigterm: fix race conditions + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - data/selinux: allow snap-confine to read udev's database + - interfaces/dsp: add more ambarella things* interfaces/dsp: add + more ambarella things + + -- Ian Johnson Mon, 15 Nov 2021 16:09:09 -0600 + +snapd (2.53.1-1) unstable; urgency=medium + + * New upstream release, LP: #1946127 + - spread: run lxd tests with version from latest/stable + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional (#10946) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup (2.53) + - interfaces/u2f-devices: add Nitrokey 3 + - Update the ubuntu-image channel to candidate + - Allow hostnames up to 253 characters, with dot-delimited elements + (as suggested by man 7 hostname). + - Disable i386 until it is possible to build snapd using lxd + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - tests/snapd-sigterm: be more robust against service restart + - tests: add a regression test for snapd hanging on SIGTERM + - daemon: use the syscall connection to get the socket + credentials + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - secboot: revert move to new version + + -- Ian Johnson Thu, 21 Oct 2021 11:55:31 -0500 + snapd (2.53-1) unstable; urgency=medium * New upstream release, LP: #1946127 diff -Nru snapd-2.53+21.10ubuntu1/packaging/debian-sid/control snapd-2.54.2+21.10/packaging/debian-sid/control --- snapd-2.53+21.10ubuntu1/packaging/debian-sid/control 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/packaging/debian-sid/control 2022-01-06 21:25:16.000000000 +0000 @@ -95,7 +95,8 @@ systemd, udev, ${misc:Depends}, - ${shlibs:Depends} + ${shlibs:Depends}, + default-dbus-session-bus | dbus-session-bus Replaces: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9), snap-confine (<< 2.23), ubuntu-core-launcher (<< 2.22), snapd-xdg-open (<= 0.0.0) Breaks: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9), snap-confine (<< 2.23), ubuntu-core-launcher (<< 2.22), snapd-xdg-open (<= 0.0.0), ${snapd:Breaks} Recommends: gnupg diff -Nru snapd-2.53+21.10ubuntu1/packaging/debian-sid/patches/0001-cmd-snap-seccomp-use-upstream-seccomp-package.patch snapd-2.54.2+21.10/packaging/debian-sid/patches/0001-cmd-snap-seccomp-use-upstream-seccomp-package.patch --- snapd-2.53+21.10ubuntu1/packaging/debian-sid/patches/0001-cmd-snap-seccomp-use-upstream-seccomp-package.patch 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/packaging/debian-sid/patches/0001-cmd-snap-seccomp-use-upstream-seccomp-package.patch 1970-01-01 00:00:00.000000000 +0000 @@ -1,79 +0,0 @@ -From b699fd17fa7ec5191901ea116369b1b963b58e14 Mon Sep 17 00:00:00 2001 -From: Zygmunt Krynicki -Date: Thu, 17 Jan 2019 15:48:46 +0200 -Subject: [PATCH 1/9] cmd/snap-seccomp: use upstream seccomp package - -Upstream snapd uses a fork that carries additional compatibility patch -required to build snapd for Ubuntu 14.04. This patch is not required with -the latest snapshot of the upstream seccomp golang bindings but they are -neither released upstream nor backported (in their entirety) to Ubuntu -14.04. - -The forked seccomp library is not packaged in Debian. As such, to build -snapd, we need to switch to the regular, non-forked package name. - -Signed-off-by: Zygmunt Krynicki -Signed-off-by: Maciej Borzecki ---- - cmd/snap-seccomp/main.go | 3 +-- - cmd/snap-seccomp/main_test.go | 2 +- - cmd/snap-seccomp/versioninfo.go | 2 +- - cmd/snap-seccomp/versioninfo_test.go | 2 +- - 4 files changed, 4 insertions(+), 5 deletions(-) - -diff --git a/cmd/snap-seccomp/main.go b/cmd/snap-seccomp/main.go -index 1f56f5876f62637cd99e3adab177d15fa8a941af..f7b5b8b62b5c511f3710a6bdd23367c0641144ae 100644 ---- a/cmd/snap-seccomp/main.go -+++ b/cmd/snap-seccomp/main.go -@@ -180,8 +180,7 @@ import ( - "strings" - "syscall" - -- // FIXME: we want github.com/seccomp/libseccomp-golang but that will not work with trusty because libseccomp-golang checks for the seccomp version and errors if it find one < 2.2.0 -- "github.com/mvo5/libseccomp-golang" -+ "github.com/seccomp/libseccomp-golang" - - "github.com/snapcore/snapd/arch" - "github.com/snapcore/snapd/osutil" -diff --git a/cmd/snap-seccomp/main_test.go b/cmd/snap-seccomp/main_test.go -index 90590e5d89901e7298fbfe37c818ff6d6d2c7874..6933649ea743d9422321f772967c8dfe2054c432 100644 ---- a/cmd/snap-seccomp/main_test.go -+++ b/cmd/snap-seccomp/main_test.go -@@ -30,7 +30,7 @@ import ( - "strings" - "testing" - -- "github.com/mvo5/libseccomp-golang" -+ "github.com/seccomp/libseccomp-golang" - . "gopkg.in/check.v1" - - "github.com/snapcore/snapd/arch" -diff --git a/cmd/snap-seccomp/versioninfo.go b/cmd/snap-seccomp/versioninfo.go -index b206d9b8c732d8b116e700f0e343ad4377fb19d2..2ddba5fd0becfbbc1eae1ac92b3711a8e4c69704 100644 ---- a/cmd/snap-seccomp/versioninfo.go -+++ b/cmd/snap-seccomp/versioninfo.go -@@ -25,7 +25,7 @@ import ( - "os" - "strings" - -- "github.com/mvo5/libseccomp-golang" -+ "github.com/seccomp/libseccomp-golang" - - "github.com/snapcore/snapd/cmd/snap-seccomp/syscalls" - "github.com/snapcore/snapd/osutil" -diff --git a/cmd/snap-seccomp/versioninfo_test.go b/cmd/snap-seccomp/versioninfo_test.go -index fadfaf10ca4aa2a631f7d8b0a342fde2b2436412..ea20c306c5031bbd6eab912a35070436fc52e74d 100644 ---- a/cmd/snap-seccomp/versioninfo_test.go -+++ b/cmd/snap-seccomp/versioninfo_test.go -@@ -23,7 +23,7 @@ import ( - "fmt" - "strings" - -- "github.com/mvo5/libseccomp-golang" -+ "github.com/seccomp/libseccomp-golang" - . "gopkg.in/check.v1" - - main "github.com/snapcore/snapd/cmd/snap-seccomp" --- -2.32.0 - diff -Nru snapd-2.53+21.10ubuntu1/packaging/debian-sid/patches/series snapd-2.54.2+21.10/packaging/debian-sid/patches/series --- snapd-2.53+21.10ubuntu1/packaging/debian-sid/patches/series 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/packaging/debian-sid/patches/series 2022-01-06 21:25:16.000000000 +0000 @@ -1,4 +1,3 @@ -0001-cmd-snap-seccomp-use-upstream-seccomp-package.patch 0002-cmd-snap-seccomp-skip-tests-that-fail-on-4.19.patch 0003-cmd-snap-seccomp-skip-tests-that-use-m32.patch 0004-cmd-snap-skip-tests-depending-on-text-wrapping.patch diff -Nru snapd-2.53+21.10ubuntu1/packaging/debian-sid/snapd.postrm snapd-2.54.2+21.10/packaging/debian-sid/snapd.postrm --- snapd-2.53+21.10ubuntu1/packaging/debian-sid/snapd.postrm 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/packaging/debian-sid/snapd.postrm 2022-01-06 21:25:16.000000000 +0000 @@ -79,6 +79,7 @@ fi # modules rm -f "/etc/modules-load.d/snap.${snap}.conf" + rm -f "/etc/modprobe.d/snap.${snap}.conf" # timer and socket units find /etc/systemd/system -name "snap.${snap}.*.timer" -o -name "snap.${snap}.*.socket" | while read -r f; do systemctl_stop "$(basename "$f")" diff -Nru snapd-2.53+21.10ubuntu1/packaging/fedora/snapd.spec snapd-2.54.2+21.10/packaging/fedora/snapd.spec --- snapd-2.53+21.10ubuntu1/packaging/fedora/snapd.spec 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/packaging/fedora/snapd.spec 2022-01-06 21:25:16.000000000 +0000 @@ -33,6 +33,11 @@ %global with_multilib 1 %endif +# Set if valgrind is to be run +%ifnarch ppc64le +%global with_valgrind 1 +%endif + %if ! %{with vendorized} %global with_bundled 0 %else @@ -97,7 +102,7 @@ %endif Name: snapd -Version: 2.53 +Version: 2.54.2 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -206,7 +211,9 @@ %if ! 0%{?rhel} BuildRequires: libseccomp-static %endif +%if 0%{?with_valgrind} BuildRequires: valgrind +%endif BuildRequires: %{_bindir}/rst2man %if 0%{?fedora} # ShellCheck in EPEL is too old... @@ -289,7 +296,7 @@ Provides: bundled(golang(github.com/kr/pretty)) Provides: bundled(golang(github.com/kr/text)) Provides: bundled(golang(github.com/mvo5/goconfigparser)) -Provides: bundled(golang(github.com/mvo5/libseccomp-golang)) +Provides: bundled(golang(github.com/seccomp/libseccomp-golang)) Provides: bundled(golang(github.com/snapcore/go-gettext)) Provides: bundled(golang(golang.org/x/crypto/openpgp/armor)) Provides: bundled(golang(golang.org/x/crypto/openpgp/packet)) @@ -534,8 +541,6 @@ %endif %if ! 0%{?with_bundled} -# We don't need mvo5 fork for seccomp, as we have seccomp 2.3.x -sed -e "s:github.com/mvo5/libseccomp-golang:github.com/seccomp/libseccomp-golang:g" -i cmd/snap-seccomp/*.go # We don't need the snapcore fork for bolt - it is just a fix on ppc sed -e "s:github.com/snapcore/bolt:github.com/boltdb/bolt:g" -i advisor/*.go errtracker/*.go %endif @@ -543,7 +548,7 @@ # We have to build snapd first to prevent the build from # building various things from the tree without additional # set tags. -%gobuild -o bin/snapd $GOFLAGS ./cmd/snapd +%gobuild -o bin/snapd $GOFLAGS %{import_path}/cmd/snapd BUILDTAGS="${BUILDTAGS} nomanagers" %gobuild -o bin/snap $GOFLAGS %{import_path}/cmd/snap %gobuild -o bin/snap-failure $GOFLAGS %{import_path}/cmd/snap-failure @@ -595,18 +600,21 @@ %if 0%{?with_selinux} --enable-selinux \ %endif +%if 0%{?rhel} == 7 + --disable-bpf \ +%endif --libexecdir=%{_libexecdir}/snapd/ \ --enable-nvidia-biarch \ %{?with_multilib:--with-32bit-libdir=%{_prefix}/lib} \ --with-snap-mount-dir=%{_sharedstatedir}/snapd/snap \ --enable-merged-usr -%make_build +%make_build %{!?with_valgrind:HAVE_VALGRIND=} popd # Build systemd units, dbus services, and env files pushd ./data -make BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" \ +make BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ SYSTEMDSYSTEMUNITDIR="%{_unitdir}" \ SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" @@ -691,7 +699,7 @@ # Install all systemd and dbus units, and env files pushd ./data -%make_install BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" \ +%make_install BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ SYSTEMDSYSTEMUNITDIR="%{_unitdir}" SYSTEMDUSERUNITDIR="%{_userunitdir}" \ SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" @@ -833,6 +841,7 @@ %{_datadir}/dbus-1/system.d/snapd.system-services.conf %{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy %{_datadir}/applications/io.snapcraft.SessionAgent.desktop +%{_datadir}/fish/vendor_conf.d/snapd.fish %{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop %config(noreplace) %{_sysconfdir}/sysconfig/snapd %dir %{_sharedstatedir}/snapd @@ -867,6 +876,8 @@ # this is typically owned by zsh, but we do not want to explicitly require zsh %dir %{_datadir}/zsh %dir %{_datadir}/zsh/site-functions +# similar case for fish +%dir %{_datadir}/fish/vendor_conf.d %files -n snap-confine %doc cmd/snap-confine/PORTING @@ -978,6 +989,450 @@ %changelog +* Thu Jan 06 2022 Ian Johnson +- New upstream release 2.54.2 + - tests: exclude interfaces-kernel-module load on arm + - tests: ensure that test-snapd-kernel-module-load is + removed + - tests: do not test microk8s-smoke on arm + - tests/core/failover: replace boot-state with snap debug boot-vars + - tests: use snap info|awk to extract tracking channel + - tests: fix remodel-kernel test when running on external devices + - .github/workflows/test.yaml: also check internal snapd version for + cleanliness + - packaging/ubuntu-16.04/rules: eliminate seccomp modification + - bootloader/assets/grub_*cfg_asset.go: update Copyright + - build-aux/snap/snapcraft.yaml: adjust comment about get-version + - .github/workflows/test.yaml: add check in github actions for dirty + snapd snaps + - build-aux/snap/snapcraft.yaml: use build-packages, don't fail + dirty builds + - data/selinux: allow poking /proc/xen + +* Mon Dec 20 2021 Michael Vogt +- New upstream release 2.54.1 + - buid-aux: set version before calling ./generate-packaging-dir + This fixes the "dirty" suffix in the auto-generated version + +* Fri Dec 17 2021 Michael Vogt +- New upstream release 2.54 + - interfaces/builtin/opengl.go: add boot_vga sys/devices file + - o/configstate/configcore: add tmpfs.size option + - tests: moving to manual opensuse 15.2 + - cmd/snap-device-helper: bring back the device type identification + behavior, but for remove action fallback only + - cmd/snap-failure: use snapd from the snapd snap if core is not + present + - tests/core/failover: enable the test on core18 + - o/devicestate: ensure proper order when remodel does a simple + switch-snap-channel + - builtin/interfaces: add shared memory interface + - overlord: extend kernel/base success and failover with bootenv + checks + - o/snapstate: check disk space w/o store if possible + - snap-bootstrap: Mount snaps read only + - gadget/install: do not re-create partitions using OnDiskVolume + after deletion + - many: fix formatting w/ latest go version + - devicestate,timeutil: improve logging of NTP sync + - tests/main/security-device-cgroups-helper: more debugs + - cmd/snap: print a placeholder for version of broken snaps + - o/snapstate: mock system with classic confinement support + - cmd: Fixup .clangd to use correct syntax + - tests: run spread tests in fedora-35 + - data/selinux: allow snapd to access /etc/modprobe.d + - mount-control: step 2 + - daemon: add multiple snap sideload to API + - tests/lib/pkgdb: install dbus-user-session during prepare, drop + dbus-x11 + - systemd: provide more detailed errors for unimplemented method in + emulation mode + - tests: avoid checking TRUST_TEST_KEYS on restore on remodel-base + test + - tests: retry umounting /var/lib/snapd/seed on uc20 on fsck-on-boot + test + - o/snapstate: add hide/expose snap data to backend + - interfaces: kernel-module-load + - snap: add support for `snap watch + --last={revert,enable,disable,switch}` + - tests/main/security-udev-input-subsystem: drop info from udev + - tests/core/kernel-and-base-single-reboot-failover, + tests/lib/fakestore: verify failover scenario + - tests/main/security-device-cgroups-helper: collect some debug info + when the test fails + - tests/nested/manual/core20-remodel: wait for device to have a + serial before starting a remodel + - tests/main/generic-unregister: test re-registration if not blocked + - o/snapstate, assertsate: validation sets/undo on partial failure + - tests: ensure snapd can be downloaded as a module + - snapdtool, many: support additional key/value flags in info file + - data/env: improve fish shell env setup + - usersession/client: provide a way for client to send messages to a + subset of users + - tests: verify that simultaneous refresh of kernel and base + triggers a single reboot only + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - asserts: change behavior of alternative attribute matcher + - configcore: relax validation rules for hostname + - cmd/snap-confine: do not include libglvnd libraries from the host + system + - overlord, tests: add managers and a spread test for UC20 to UC22 + remodel + - HACKING.md: adjust again for building the snapd snap + - systemd: add support for systemd unit alias names + - o/snapstate: add InstallPathMany + - gadget: allow EnsureLayoutCompatibility to ensure disk has all + laid out structsnow reject/fail: + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider (#11111) + - interfaces/interfaces/scsi_generic: add interface for scsi generic + de… (#10936) + - osutil/disks/mockdisk.go: add MockDevicePathToDiskMapping + - interfaces/microstack-support: set controlsDeviceCgroup to true + - network-setup-control: add netplan generate D-Bus rules + - interface/builtin/log_observe: allow to access /dev/kmsg + - .github/workflows/test.yaml: restore failing of spread tests on + errors (nested) + - gadget: tweaks to DiskStructureDeviceTraits + expand test cases + - tests/lib/nested.sh: allow tests to use their own core18 in extra- + snaps-path + - interfaces/browser-support: Update rules for Edge + - o/devicestate: during remodel first check pending download tasks + for snaps + - polkit: add a package to validate polkit policy files + - HACKING.md: document building the snapd snap and splicing it into + the core snap + - interfaces/udev: fix installing snaps inside lxd in 21.10 + - o/snapstate: refactor disk space checks + - tests: add (strict) microk8s smoke test + - osutil/strace: try to enable strace on more arches + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - tests/main/snapd-reexec-snapd-snap: improve debugging + - daemon: write formdata file parts to snaps dir + - systemd: add support for .target units + - tests: run snap-disconnect on uc16 + - many: add experimental setting to allow using ~/.snap/data instead + of ~/snap + - overlord/snapstate: perform a single reboot when updating boot + base and kernel + - kernel/fde: add DeviceUnlockKernelHookDeviceMapperBackResolver, + use w/ disks pkg + - o/devicestate: introduce DeviceManager.Unregister + - interfaces: allow receiving PropertiesChanged on the mpris plug + - tests: new tool used to retrieve data from mongo db + - daemon: amend ssh keys coming from the store + - tests: Include the tools from snapd-testing-tools project in + "$TESTSTOOLS" + - tests: new workflow step used to report spread error to mongodb + - interfaces/builtin/dsp: update proc files for ambarella flavor + - gadget: replace ondisk implementation with disks package, refactor + part calcs + - tests: Revert "tests: disable flaky uc18 tests until systemd is + fixed" + - Revert: "many: Vendor apparmor-3.0.3 into the snapd snap" + - asserts: rename "white box" to "clear box" (woke checker) + - many: Vendor apparmor-3.0.3 into the snapd snap + - tests: reorganize the debug-each on the spread.yaml + - packaging: sync with downstream packaging in Fedora and openSUSE + - tests: disable flaky uc18 tests until systemd is fixed + - data/env: provide profile setup for fish shell + - tests: use ubuntu-image 1.11 from stable channel + - gadget/gadget.go: include disk schema in the disk device volume + traits too + - tests/main/security-device-cgroups-strict-enforced: extend the + comments + - README.md: point at bugs.launchpad.net/snapd instead of snappy + project + - osutil/disks: introduce RegisterDeviceMapperBackResolver + use for + crypt-luks2 + - packaging: make postrm script robust against `rm` failures + - tests: print extra debug on auto-refresh-gating test failure + - o/assertstate, api: move enforcing/monitoring from api to + assertstate, save history + - tests: skip the test-snapd-timedate-control-consumer.date to avoid + NTP sync error + - gadget/install: use disks functions to implement deviceFromRole, + also rename + - tests: the `lxd` test is failing right now on 21.10 + - o/snapstate: account for deleted revs when undoing install + - interfaces/builtin/block_devices: allow blkid to print block + device attributes + - gadget: include size + sector-size in DiskVolumeDeviceTraits + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - o/snapstate/handlers: propagate read errors on "copy-snap-data" + - osutil/disks: add more fields to Partition, populate them during + discovery + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - o/snapstate: remove repeated test assertions + - tests: skip `snap advise-command` test if the store is overloaded + - cmd: create ~/snap dir with 0700 perms + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - github: leave a comment documenting reasons for pipefail + - github: enable pipefail when running spread + - osutil/disks: add DiskFromPartitionDeviceNode + - gadget, many: add model param to Update() + - cmd/snap-seccomp: add riscv64 support + - o/snapstate: maintain a RevertStatus map in SnapState + - tests: enable lxd tests on impish system + - tests: (partially) revert the memory limits PR#r10241 + - o/assertstate: functions for handling validation sets tracking + history + - tests: some improvements for the spread log parser + - interfaces/network-manager-observe: Update for libnm / dart + clients + - tests: add ntp related debug around "auto-refresh" test + - boot: expand on the fact that reseal taking modeenv is very + intentional + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - data/selinux: update the policy to allow snapd to talk to + org.freedesktop.timedate1 + - o/snapstate: keep old revision if install doesn't add new one + - overlord/state: add a unit test for a kernel+base refresh like + sequence + - desktop, usersession: observe notifications + - osutil/disks: add AllPhysicalDisks() + - timeutil,deviceutil: fix unit tests on systems without dbus or + without ntp-sync + - cmd/snap-bootstrap/README: explain all the things (well most of + them anyways) + - docs: add run-checks dependency install instruction + - o/snapstate: do not prune refresh-candidates if gate-auto-refresh- + hook feature is not enabled + - o/snapstate: test relink remodel helpers do a proper subset of + doInstall and rework the verify*Tasks helpers + - tests/main/mount-ns: make the test run early + - tests: add `--debug` to netplan apply + - many: wait for up to 10min for NTP synchronization before + autorefresh + - tests: initialize CHANGE_ID in _wait_autorefresh + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - tests: add more debug around qemu-nbd + - o/hookstate: print cohort with snapctl refresh --pending (#10985) + - tests: misc robustness changes + - o/snapstate: improve install/update tests (#10850) + - tests: clean up test tools + - spread.yaml: show `journalctl -e` for all suites on debug + - tests: give interfaces-udisks2 more time for the loop device to + appear + - tests: set memory limit for snapd + - tests: increase timeout/add debug around nbd0 mounting (up, see + LP:#1949513) + - snapstate: add debug message where a snap is mounted + - tests: give nbd0 more time to show up in preseed-lxd + - interfaces/dsp: add more ambarella things + - cmd/snap: improve snap disconnect arg parsing and err msg + - tests: disable nested lxd snapd testing + - tests: disable flaky "interfaces-udisks2" on ubuntu-18.04-32 + - o/snapstate: avoid validationSetsSuite repeating snapmgrTestSuite + - sandbox/cgroup: wait for start transient unit job to finish + - o/snapstate: fix task order, tweak errors, add unit tests for + remodel helpers + - osutil/disks: re-org methods for end of usable region, size + information + - build-aux: ensure that debian packaging matches build-base + - docs: update HACKING.md instructions for snapd 2.52 and later + - spread: run lxd tests with version from latest/edge + - interfaces: suppress denial of sys_module capability + - osutil/disks: add methods to replace gadget/ondisk functions + - tests: split test tools - part 1 + - tests: fix nested tests on uc20 + - data/selinux: allow snap-confine to read udev's database + - i/b/common_test: refactor AppArmor features test + - tests: run spread tests on debian 11 + - o/devicestate: copy timesyncd clock timestamp during install + - interfaces/builtin: do not probe parser features when apparmor + isn't available + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - tests: fix error message in run-checks + - tests: spread test for validation sets enforcing + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - o/snapstate: deduplicate snap names in remove/install/update + - tests/main/selinux-data-context: use session when performing + actions as test user + - packaging/opensuse: sync with openSUSE packaging, enable AppArmor + on 15.3+ + - interfaces: skip connection of netlink interface on older + systems + - asserts, o/snapstate: honor IgnoreValidation flag when checking + installed snaps + - tests/main/apparmor-batch-reload: fix fake apparmor_parser to + handle --preprocess + - sandbox/apparmor, interfaces/apparmor: detect bpf capability, + generate snippet for s-c + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - tests: test for enforcing with prerequisites + - tests/main/snapd-sigterm: fix race conditions + - spread: run lxd tests with version from latest/stable + - run-checks: remove --spread from help message + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests: ensure systemd-timesyncd is installed on debian + - interfaces/u2f-devices: add Nitrokey 3 + - tests: update the ubuntu-image channel to candidate + - osutil/disks/labels: simplify decoding algorithm + - tests: not testing lxd snap anymore on i386 architecture + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - cmd/snap: support --ignore-validation with snap install client + command + - tests/snapd-sigterm: be more robust against service restart + - tests: simplify mock script for apparmor_parser + - o/devicestate, o/servicestate: update gadget assets and cmdline + when remodeling + - tests/nested/manual/refresh-revert-fundamentals: re-enable + encryption + - osutil/disks: fix bug in BlkIDEncodeLabel, add BlkIDDecodeLabel + - gadget, osutil/disks: fix some bugs from prior PR'sin the dir. + - secboot: revert move to new version (revert #10715) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup + - many: mv MockDeviceNameDisksToPartitionMapping -> + MockDeviceNameToDiskMapping + - interfaces/builtin: Add '/com/canonical/dbusmenu' path access to + 'unity7' interface + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - osutil/disks, many: switch to defining Partitions directly for + MockDiskMapping + - tests: remove extra-snaps-assertions test + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - tests/nested/core/core20-create-recovery: fix passing of data to + curl + - daemon: allow enabling enforce mode + - daemon: use the syscall connection to get the socket credentials + - i/builtin/kubernetes_support: add access to Calico lock file + - osutil: ensure parent dir is opened and sync'd + - tests: using test-snapd-curl snap instead of http snap + - overlord: add managers unit test demonstrating cyclic dependency + between gadget and kernel updates + - gadget/ondisk.go: include the filesystem UUID in the returned + OnDiskVolume + - packaging: fixes for building on openSUSE + - o/configcore: allow hostnames up to 253 characters, with dot- + delimited elements + - gadget/ondisk.go: add listBlockDevices() to get all block devices + on a system + - gadget: add mapping trait types + functions to save/load + - interfaces: add polkit security backend + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - tests: merge coverage results + - tests: remove "features" from fde-setup.go example + - fde: add new device-setup support to fde-setup + - gadget: add `encryptedDevice` and add encryptedDeviceLUKS + - spread: use `bios: uefi` for uc20 + - client: fail fast on non-retryable errors + - tests: support running all spread tests with experimental features + - tests: check that a snap that doesn't have gate-auto-refresh hook + can call --proceed + - o/snapstate: support ignore-validation flag when updating to a + specific snap revision + - o/snapstate: test prereq update if started by old version + - tests/main: disable cgroup-devices-v1 and freezer tests on 21.10 + - tests/main/interfaces-many: run both variants on all possible + Ubuntu systems + - gadget: mv ensureLayoutCompatibility to gadget proper, add + gadgettest pkg + - many: replace state.State restart support with overlord/restart + - overlord: fix generated snap-revision assertions in remodel unit + tests + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.4 + - devicestate: mock devicestate.MockTimeutilIsNTPSynchronized to + avoid host env leaking into tests + - timeutil: return NoTimedate1Error if it can't connect to the + system bus + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.3 + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - configcore: relax validation rules for hostname + - o/devicestate: introduce DeviceManager.Unregister + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider + - many: wait for up to 10min for NTP synchronization before + autorefresh + - interfaces/interfaces/scsi_generic: add interface for scsi generic + devices + - interfaces/microstack-support: set controlsDeviceCgroup to true + - interface/builtin/log_observe: allow to access /dev/kmsg + - daemon: write formdata file parts to snaps dir + - spread: run lxd tests with version from latest/edge + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - interfaces/builtin/dsp: add proc files for monitoring Ambarella + DSP firmware + - interfaces/builtin/dsp: update proc file accordingly + +* Mon Nov 15 2021 Ian Johnson +- New upstream release 2.53.2 + - interfaces/builtin/block_devices: allow blkid to print block + device attributes/run/udev/data/b{major}:{minor} + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - interfaces/network-manager-observe: Update for libnm client + library + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - o/hookstate: print cohort with snapctl refresh --pending + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - tests: ensure systemd-timesyncd is installed on debian + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests/main/snapd-sigterm: flush, use retry + - tests/main/snapd-sigterm: fix race conditions + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - data/selinux: allow snap-confine to read udev's database + - interfaces/dsp: add more ambarella things* interfaces/dsp: add + more ambarella things + +* Thu Oct 21 2021 Ian Johnson +- New upstream release 2.53.1 + - spread: run lxd tests with version from latest/stable + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional (#10946) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup (2.53) + - interfaces/u2f-devices: add Nitrokey 3 + - Update the ubuntu-image channel to candidate + - Allow hostnames up to 253 characters, with dot-delimited elements + (as suggested by man 7 hostname). + - Disable i386 until it is possible to build snapd using lxd + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - tests/snapd-sigterm: be more robust against service restart + - tests: add a regression test for snapd hanging on SIGTERM + - daemon: use the syscall connection to get the socket + credentials + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - secboot: revert move to new version + * Tue Oct 05 2021 Michael Vogt - New upstream release 2.53 - overlord: fix generated snap-revision assertions in remodel unit diff -Nru snapd-2.53+21.10ubuntu1/packaging/fedora-33/snapd.spec snapd-2.54.2+21.10/packaging/fedora-33/snapd.spec --- snapd-2.53+21.10ubuntu1/packaging/fedora-33/snapd.spec 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/packaging/fedora-33/snapd.spec 2022-01-06 21:25:16.000000000 +0000 @@ -33,6 +33,11 @@ %global with_multilib 1 %endif +# Set if valgrind is to be run +%ifnarch ppc64le +%global with_valgrind 1 +%endif + %if ! %{with vendorized} %global with_bundled 0 %else @@ -97,7 +102,7 @@ %endif Name: snapd -Version: 2.53 +Version: 2.54.2 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -206,7 +211,9 @@ %if ! 0%{?rhel} BuildRequires: libseccomp-static %endif +%if 0%{?with_valgrind} BuildRequires: valgrind +%endif BuildRequires: %{_bindir}/rst2man %if 0%{?fedora} # ShellCheck in EPEL is too old... @@ -289,7 +296,7 @@ Provides: bundled(golang(github.com/kr/pretty)) Provides: bundled(golang(github.com/kr/text)) Provides: bundled(golang(github.com/mvo5/goconfigparser)) -Provides: bundled(golang(github.com/mvo5/libseccomp-golang)) +Provides: bundled(golang(github.com/seccomp/libseccomp-golang)) Provides: bundled(golang(github.com/snapcore/go-gettext)) Provides: bundled(golang(golang.org/x/crypto/openpgp/armor)) Provides: bundled(golang(golang.org/x/crypto/openpgp/packet)) @@ -534,8 +541,6 @@ %endif %if ! 0%{?with_bundled} -# We don't need mvo5 fork for seccomp, as we have seccomp 2.3.x -sed -e "s:github.com/mvo5/libseccomp-golang:github.com/seccomp/libseccomp-golang:g" -i cmd/snap-seccomp/*.go # We don't need the snapcore fork for bolt - it is just a fix on ppc sed -e "s:github.com/snapcore/bolt:github.com/boltdb/bolt:g" -i advisor/*.go errtracker/*.go %endif @@ -543,7 +548,7 @@ # We have to build snapd first to prevent the build from # building various things from the tree without additional # set tags. -%gobuild -o bin/snapd $GOFLAGS ./cmd/snapd +%gobuild -o bin/snapd $GOFLAGS %{import_path}/cmd/snapd BUILDTAGS="${BUILDTAGS} nomanagers" %gobuild -o bin/snap $GOFLAGS %{import_path}/cmd/snap %gobuild -o bin/snap-failure $GOFLAGS %{import_path}/cmd/snap-failure @@ -595,18 +600,21 @@ %if 0%{?with_selinux} --enable-selinux \ %endif +%if 0%{?rhel} == 7 + --disable-bpf \ +%endif --libexecdir=%{_libexecdir}/snapd/ \ --enable-nvidia-biarch \ %{?with_multilib:--with-32bit-libdir=%{_prefix}/lib} \ --with-snap-mount-dir=%{_sharedstatedir}/snapd/snap \ --enable-merged-usr -%make_build +%make_build %{!?with_valgrind:HAVE_VALGRIND=} popd # Build systemd units, dbus services, and env files pushd ./data -make BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" \ +make BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ SYSTEMDSYSTEMUNITDIR="%{_unitdir}" \ SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" @@ -691,7 +699,7 @@ # Install all systemd and dbus units, and env files pushd ./data -%make_install BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" \ +%make_install BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ SYSTEMDSYSTEMUNITDIR="%{_unitdir}" SYSTEMDUSERUNITDIR="%{_userunitdir}" \ SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" @@ -833,6 +841,7 @@ %{_datadir}/dbus-1/system.d/snapd.system-services.conf %{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy %{_datadir}/applications/io.snapcraft.SessionAgent.desktop +%{_datadir}/fish/vendor_conf.d/snapd.fish %{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop %config(noreplace) %{_sysconfdir}/sysconfig/snapd %dir %{_sharedstatedir}/snapd @@ -867,6 +876,8 @@ # this is typically owned by zsh, but we do not want to explicitly require zsh %dir %{_datadir}/zsh %dir %{_datadir}/zsh/site-functions +# similar case for fish +%dir %{_datadir}/fish/vendor_conf.d %files -n snap-confine %doc cmd/snap-confine/PORTING @@ -978,6 +989,450 @@ %changelog +* Thu Jan 06 2022 Ian Johnson +- New upstream release 2.54.2 + - tests: exclude interfaces-kernel-module load on arm + - tests: ensure that test-snapd-kernel-module-load is + removed + - tests: do not test microk8s-smoke on arm + - tests/core/failover: replace boot-state with snap debug boot-vars + - tests: use snap info|awk to extract tracking channel + - tests: fix remodel-kernel test when running on external devices + - .github/workflows/test.yaml: also check internal snapd version for + cleanliness + - packaging/ubuntu-16.04/rules: eliminate seccomp modification + - bootloader/assets/grub_*cfg_asset.go: update Copyright + - build-aux/snap/snapcraft.yaml: adjust comment about get-version + - .github/workflows/test.yaml: add check in github actions for dirty + snapd snaps + - build-aux/snap/snapcraft.yaml: use build-packages, don't fail + dirty builds + - data/selinux: allow poking /proc/xen + +* Mon Dec 20 2021 Michael Vogt +- New upstream release 2.54.1 + - buid-aux: set version before calling ./generate-packaging-dir + This fixes the "dirty" suffix in the auto-generated version + +* Fri Dec 17 2021 Michael Vogt +- New upstream release 2.54 + - interfaces/builtin/opengl.go: add boot_vga sys/devices file + - o/configstate/configcore: add tmpfs.size option + - tests: moving to manual opensuse 15.2 + - cmd/snap-device-helper: bring back the device type identification + behavior, but for remove action fallback only + - cmd/snap-failure: use snapd from the snapd snap if core is not + present + - tests/core/failover: enable the test on core18 + - o/devicestate: ensure proper order when remodel does a simple + switch-snap-channel + - builtin/interfaces: add shared memory interface + - overlord: extend kernel/base success and failover with bootenv + checks + - o/snapstate: check disk space w/o store if possible + - snap-bootstrap: Mount snaps read only + - gadget/install: do not re-create partitions using OnDiskVolume + after deletion + - many: fix formatting w/ latest go version + - devicestate,timeutil: improve logging of NTP sync + - tests/main/security-device-cgroups-helper: more debugs + - cmd/snap: print a placeholder for version of broken snaps + - o/snapstate: mock system with classic confinement support + - cmd: Fixup .clangd to use correct syntax + - tests: run spread tests in fedora-35 + - data/selinux: allow snapd to access /etc/modprobe.d + - mount-control: step 2 + - daemon: add multiple snap sideload to API + - tests/lib/pkgdb: install dbus-user-session during prepare, drop + dbus-x11 + - systemd: provide more detailed errors for unimplemented method in + emulation mode + - tests: avoid checking TRUST_TEST_KEYS on restore on remodel-base + test + - tests: retry umounting /var/lib/snapd/seed on uc20 on fsck-on-boot + test + - o/snapstate: add hide/expose snap data to backend + - interfaces: kernel-module-load + - snap: add support for `snap watch + --last={revert,enable,disable,switch}` + - tests/main/security-udev-input-subsystem: drop info from udev + - tests/core/kernel-and-base-single-reboot-failover, + tests/lib/fakestore: verify failover scenario + - tests/main/security-device-cgroups-helper: collect some debug info + when the test fails + - tests/nested/manual/core20-remodel: wait for device to have a + serial before starting a remodel + - tests/main/generic-unregister: test re-registration if not blocked + - o/snapstate, assertsate: validation sets/undo on partial failure + - tests: ensure snapd can be downloaded as a module + - snapdtool, many: support additional key/value flags in info file + - data/env: improve fish shell env setup + - usersession/client: provide a way for client to send messages to a + subset of users + - tests: verify that simultaneous refresh of kernel and base + triggers a single reboot only + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - asserts: change behavior of alternative attribute matcher + - configcore: relax validation rules for hostname + - cmd/snap-confine: do not include libglvnd libraries from the host + system + - overlord, tests: add managers and a spread test for UC20 to UC22 + remodel + - HACKING.md: adjust again for building the snapd snap + - systemd: add support for systemd unit alias names + - o/snapstate: add InstallPathMany + - gadget: allow EnsureLayoutCompatibility to ensure disk has all + laid out structsnow reject/fail: + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider (#11111) + - interfaces/interfaces/scsi_generic: add interface for scsi generic + de… (#10936) + - osutil/disks/mockdisk.go: add MockDevicePathToDiskMapping + - interfaces/microstack-support: set controlsDeviceCgroup to true + - network-setup-control: add netplan generate D-Bus rules + - interface/builtin/log_observe: allow to access /dev/kmsg + - .github/workflows/test.yaml: restore failing of spread tests on + errors (nested) + - gadget: tweaks to DiskStructureDeviceTraits + expand test cases + - tests/lib/nested.sh: allow tests to use their own core18 in extra- + snaps-path + - interfaces/browser-support: Update rules for Edge + - o/devicestate: during remodel first check pending download tasks + for snaps + - polkit: add a package to validate polkit policy files + - HACKING.md: document building the snapd snap and splicing it into + the core snap + - interfaces/udev: fix installing snaps inside lxd in 21.10 + - o/snapstate: refactor disk space checks + - tests: add (strict) microk8s smoke test + - osutil/strace: try to enable strace on more arches + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - tests/main/snapd-reexec-snapd-snap: improve debugging + - daemon: write formdata file parts to snaps dir + - systemd: add support for .target units + - tests: run snap-disconnect on uc16 + - many: add experimental setting to allow using ~/.snap/data instead + of ~/snap + - overlord/snapstate: perform a single reboot when updating boot + base and kernel + - kernel/fde: add DeviceUnlockKernelHookDeviceMapperBackResolver, + use w/ disks pkg + - o/devicestate: introduce DeviceManager.Unregister + - interfaces: allow receiving PropertiesChanged on the mpris plug + - tests: new tool used to retrieve data from mongo db + - daemon: amend ssh keys coming from the store + - tests: Include the tools from snapd-testing-tools project in + "$TESTSTOOLS" + - tests: new workflow step used to report spread error to mongodb + - interfaces/builtin/dsp: update proc files for ambarella flavor + - gadget: replace ondisk implementation with disks package, refactor + part calcs + - tests: Revert "tests: disable flaky uc18 tests until systemd is + fixed" + - Revert: "many: Vendor apparmor-3.0.3 into the snapd snap" + - asserts: rename "white box" to "clear box" (woke checker) + - many: Vendor apparmor-3.0.3 into the snapd snap + - tests: reorganize the debug-each on the spread.yaml + - packaging: sync with downstream packaging in Fedora and openSUSE + - tests: disable flaky uc18 tests until systemd is fixed + - data/env: provide profile setup for fish shell + - tests: use ubuntu-image 1.11 from stable channel + - gadget/gadget.go: include disk schema in the disk device volume + traits too + - tests/main/security-device-cgroups-strict-enforced: extend the + comments + - README.md: point at bugs.launchpad.net/snapd instead of snappy + project + - osutil/disks: introduce RegisterDeviceMapperBackResolver + use for + crypt-luks2 + - packaging: make postrm script robust against `rm` failures + - tests: print extra debug on auto-refresh-gating test failure + - o/assertstate, api: move enforcing/monitoring from api to + assertstate, save history + - tests: skip the test-snapd-timedate-control-consumer.date to avoid + NTP sync error + - gadget/install: use disks functions to implement deviceFromRole, + also rename + - tests: the `lxd` test is failing right now on 21.10 + - o/snapstate: account for deleted revs when undoing install + - interfaces/builtin/block_devices: allow blkid to print block + device attributes + - gadget: include size + sector-size in DiskVolumeDeviceTraits + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - o/snapstate/handlers: propagate read errors on "copy-snap-data" + - osutil/disks: add more fields to Partition, populate them during + discovery + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - o/snapstate: remove repeated test assertions + - tests: skip `snap advise-command` test if the store is overloaded + - cmd: create ~/snap dir with 0700 perms + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - github: leave a comment documenting reasons for pipefail + - github: enable pipefail when running spread + - osutil/disks: add DiskFromPartitionDeviceNode + - gadget, many: add model param to Update() + - cmd/snap-seccomp: add riscv64 support + - o/snapstate: maintain a RevertStatus map in SnapState + - tests: enable lxd tests on impish system + - tests: (partially) revert the memory limits PR#r10241 + - o/assertstate: functions for handling validation sets tracking + history + - tests: some improvements for the spread log parser + - interfaces/network-manager-observe: Update for libnm / dart + clients + - tests: add ntp related debug around "auto-refresh" test + - boot: expand on the fact that reseal taking modeenv is very + intentional + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - data/selinux: update the policy to allow snapd to talk to + org.freedesktop.timedate1 + - o/snapstate: keep old revision if install doesn't add new one + - overlord/state: add a unit test for a kernel+base refresh like + sequence + - desktop, usersession: observe notifications + - osutil/disks: add AllPhysicalDisks() + - timeutil,deviceutil: fix unit tests on systems without dbus or + without ntp-sync + - cmd/snap-bootstrap/README: explain all the things (well most of + them anyways) + - docs: add run-checks dependency install instruction + - o/snapstate: do not prune refresh-candidates if gate-auto-refresh- + hook feature is not enabled + - o/snapstate: test relink remodel helpers do a proper subset of + doInstall and rework the verify*Tasks helpers + - tests/main/mount-ns: make the test run early + - tests: add `--debug` to netplan apply + - many: wait for up to 10min for NTP synchronization before + autorefresh + - tests: initialize CHANGE_ID in _wait_autorefresh + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - tests: add more debug around qemu-nbd + - o/hookstate: print cohort with snapctl refresh --pending (#10985) + - tests: misc robustness changes + - o/snapstate: improve install/update tests (#10850) + - tests: clean up test tools + - spread.yaml: show `journalctl -e` for all suites on debug + - tests: give interfaces-udisks2 more time for the loop device to + appear + - tests: set memory limit for snapd + - tests: increase timeout/add debug around nbd0 mounting (up, see + LP:#1949513) + - snapstate: add debug message where a snap is mounted + - tests: give nbd0 more time to show up in preseed-lxd + - interfaces/dsp: add more ambarella things + - cmd/snap: improve snap disconnect arg parsing and err msg + - tests: disable nested lxd snapd testing + - tests: disable flaky "interfaces-udisks2" on ubuntu-18.04-32 + - o/snapstate: avoid validationSetsSuite repeating snapmgrTestSuite + - sandbox/cgroup: wait for start transient unit job to finish + - o/snapstate: fix task order, tweak errors, add unit tests for + remodel helpers + - osutil/disks: re-org methods for end of usable region, size + information + - build-aux: ensure that debian packaging matches build-base + - docs: update HACKING.md instructions for snapd 2.52 and later + - spread: run lxd tests with version from latest/edge + - interfaces: suppress denial of sys_module capability + - osutil/disks: add methods to replace gadget/ondisk functions + - tests: split test tools - part 1 + - tests: fix nested tests on uc20 + - data/selinux: allow snap-confine to read udev's database + - i/b/common_test: refactor AppArmor features test + - tests: run spread tests on debian 11 + - o/devicestate: copy timesyncd clock timestamp during install + - interfaces/builtin: do not probe parser features when apparmor + isn't available + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - tests: fix error message in run-checks + - tests: spread test for validation sets enforcing + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - o/snapstate: deduplicate snap names in remove/install/update + - tests/main/selinux-data-context: use session when performing + actions as test user + - packaging/opensuse: sync with openSUSE packaging, enable AppArmor + on 15.3+ + - interfaces: skip connection of netlink interface on older + systems + - asserts, o/snapstate: honor IgnoreValidation flag when checking + installed snaps + - tests/main/apparmor-batch-reload: fix fake apparmor_parser to + handle --preprocess + - sandbox/apparmor, interfaces/apparmor: detect bpf capability, + generate snippet for s-c + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - tests: test for enforcing with prerequisites + - tests/main/snapd-sigterm: fix race conditions + - spread: run lxd tests with version from latest/stable + - run-checks: remove --spread from help message + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests: ensure systemd-timesyncd is installed on debian + - interfaces/u2f-devices: add Nitrokey 3 + - tests: update the ubuntu-image channel to candidate + - osutil/disks/labels: simplify decoding algorithm + - tests: not testing lxd snap anymore on i386 architecture + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - cmd/snap: support --ignore-validation with snap install client + command + - tests/snapd-sigterm: be more robust against service restart + - tests: simplify mock script for apparmor_parser + - o/devicestate, o/servicestate: update gadget assets and cmdline + when remodeling + - tests/nested/manual/refresh-revert-fundamentals: re-enable + encryption + - osutil/disks: fix bug in BlkIDEncodeLabel, add BlkIDDecodeLabel + - gadget, osutil/disks: fix some bugs from prior PR'sin the dir. + - secboot: revert move to new version (revert #10715) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup + - many: mv MockDeviceNameDisksToPartitionMapping -> + MockDeviceNameToDiskMapping + - interfaces/builtin: Add '/com/canonical/dbusmenu' path access to + 'unity7' interface + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - osutil/disks, many: switch to defining Partitions directly for + MockDiskMapping + - tests: remove extra-snaps-assertions test + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - tests/nested/core/core20-create-recovery: fix passing of data to + curl + - daemon: allow enabling enforce mode + - daemon: use the syscall connection to get the socket credentials + - i/builtin/kubernetes_support: add access to Calico lock file + - osutil: ensure parent dir is opened and sync'd + - tests: using test-snapd-curl snap instead of http snap + - overlord: add managers unit test demonstrating cyclic dependency + between gadget and kernel updates + - gadget/ondisk.go: include the filesystem UUID in the returned + OnDiskVolume + - packaging: fixes for building on openSUSE + - o/configcore: allow hostnames up to 253 characters, with dot- + delimited elements + - gadget/ondisk.go: add listBlockDevices() to get all block devices + on a system + - gadget: add mapping trait types + functions to save/load + - interfaces: add polkit security backend + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - tests: merge coverage results + - tests: remove "features" from fde-setup.go example + - fde: add new device-setup support to fde-setup + - gadget: add `encryptedDevice` and add encryptedDeviceLUKS + - spread: use `bios: uefi` for uc20 + - client: fail fast on non-retryable errors + - tests: support running all spread tests with experimental features + - tests: check that a snap that doesn't have gate-auto-refresh hook + can call --proceed + - o/snapstate: support ignore-validation flag when updating to a + specific snap revision + - o/snapstate: test prereq update if started by old version + - tests/main: disable cgroup-devices-v1 and freezer tests on 21.10 + - tests/main/interfaces-many: run both variants on all possible + Ubuntu systems + - gadget: mv ensureLayoutCompatibility to gadget proper, add + gadgettest pkg + - many: replace state.State restart support with overlord/restart + - overlord: fix generated snap-revision assertions in remodel unit + tests + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.4 + - devicestate: mock devicestate.MockTimeutilIsNTPSynchronized to + avoid host env leaking into tests + - timeutil: return NoTimedate1Error if it can't connect to the + system bus + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.3 + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - configcore: relax validation rules for hostname + - o/devicestate: introduce DeviceManager.Unregister + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider + - many: wait for up to 10min for NTP synchronization before + autorefresh + - interfaces/interfaces/scsi_generic: add interface for scsi generic + devices + - interfaces/microstack-support: set controlsDeviceCgroup to true + - interface/builtin/log_observe: allow to access /dev/kmsg + - daemon: write formdata file parts to snaps dir + - spread: run lxd tests with version from latest/edge + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - interfaces/builtin/dsp: add proc files for monitoring Ambarella + DSP firmware + - interfaces/builtin/dsp: update proc file accordingly + +* Mon Nov 15 2021 Ian Johnson +- New upstream release 2.53.2 + - interfaces/builtin/block_devices: allow blkid to print block + device attributes/run/udev/data/b{major}:{minor} + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - interfaces/network-manager-observe: Update for libnm client + library + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - o/hookstate: print cohort with snapctl refresh --pending + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - tests: ensure systemd-timesyncd is installed on debian + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests/main/snapd-sigterm: flush, use retry + - tests/main/snapd-sigterm: fix race conditions + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - data/selinux: allow snap-confine to read udev's database + - interfaces/dsp: add more ambarella things* interfaces/dsp: add + more ambarella things + +* Thu Oct 21 2021 Ian Johnson +- New upstream release 2.53.1 + - spread: run lxd tests with version from latest/stable + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional (#10946) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup (2.53) + - interfaces/u2f-devices: add Nitrokey 3 + - Update the ubuntu-image channel to candidate + - Allow hostnames up to 253 characters, with dot-delimited elements + (as suggested by man 7 hostname). + - Disable i386 until it is possible to build snapd using lxd + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - tests/snapd-sigterm: be more robust against service restart + - tests: add a regression test for snapd hanging on SIGTERM + - daemon: use the syscall connection to get the socket + credentials + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - secboot: revert move to new version + * Tue Oct 05 2021 Michael Vogt - New upstream release 2.53 - overlord: fix generated snap-revision assertions in remodel unit diff -Nru snapd-2.53+21.10ubuntu1/packaging/fedora-34/snapd.spec snapd-2.54.2+21.10/packaging/fedora-34/snapd.spec --- snapd-2.53+21.10ubuntu1/packaging/fedora-34/snapd.spec 2021-10-05 18:29:14.000000000 +0000 +++ snapd-2.54.2+21.10/packaging/fedora-34/snapd.spec 2022-01-06 21:25:16.000000000 +0000 @@ -33,6 +33,11 @@ %global with_multilib 1 %endif +# Set if valgrind is to be run +%ifnarch ppc64le +%global with_valgrind 1 +%endif + %if ! %{with vendorized} %global with_bundled 0 %else @@ -97,7 +102,7 @@ %endif Name: snapd -Version: 2.53 +Version: 2.54.2 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -206,7 +211,9 @@ %if ! 0%{?rhel} BuildRequires: libseccomp-static %endif +%if 0%{?with_valgrind} BuildRequires: valgrind +%endif BuildRequires: %{_bindir}/rst2man %if 0%{?fedora} # ShellCheck in EPEL is too old... @@ -289,7 +296,7 @@ Provides: bundled(golang(github.com/kr/pretty)) Provides: bundled(golang(github.com/kr/text)) Provides: bundled(golang(github.com/mvo5/goconfigparser)) -Provides: bundled(golang(github.com/mvo5/libseccomp-golang)) +Provides: bundled(golang(github.com/seccomp/libseccomp-golang)) Provides: bundled(golang(github.com/snapcore/go-gettext)) Provides: bundled(golang(golang.org/x/crypto/openpgp/armor)) Provides: bundled(golang(golang.org/x/crypto/openpgp/packet)) @@ -534,8 +541,6 @@ %endif %if ! 0%{?with_bundled} -# We don't need mvo5 fork for seccomp, as we have seccomp 2.3.x -sed -e "s:github.com/mvo5/libseccomp-golang:github.com/seccomp/libseccomp-golang:g" -i cmd/snap-seccomp/*.go # We don't need the snapcore fork for bolt - it is just a fix on ppc sed -e "s:github.com/snapcore/bolt:github.com/boltdb/bolt:g" -i advisor/*.go errtracker/*.go %endif @@ -543,7 +548,7 @@ # We have to build snapd first to prevent the build from # building various things from the tree without additional # set tags. -%gobuild -o bin/snapd $GOFLAGS ./cmd/snapd +%gobuild -o bin/snapd $GOFLAGS %{import_path}/cmd/snapd BUILDTAGS="${BUILDTAGS} nomanagers" %gobuild -o bin/snap $GOFLAGS %{import_path}/cmd/snap %gobuild -o bin/snap-failure $GOFLAGS %{import_path}/cmd/snap-failure @@ -595,18 +600,21 @@ %if 0%{?with_selinux} --enable-selinux \ %endif +%if 0%{?rhel} == 7 + --disable-bpf \ +%endif --libexecdir=%{_libexecdir}/snapd/ \ --enable-nvidia-biarch \ %{?with_multilib:--with-32bit-libdir=%{_prefix}/lib} \ --with-snap-mount-dir=%{_sharedstatedir}/snapd/snap \ --enable-merged-usr -%make_build +%make_build %{!?with_valgrind:HAVE_VALGRIND=} popd # Build systemd units, dbus services, and env files pushd ./data -make BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" \ +make BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ SYSTEMDSYSTEMUNITDIR="%{_unitdir}" \ SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" @@ -691,7 +699,7 @@ # Install all systemd and dbus units, and env files pushd ./data -%make_install BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" \ +%make_install BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ SYSTEMDSYSTEMUNITDIR="%{_unitdir}" SYSTEMDUSERUNITDIR="%{_userunitdir}" \ SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" @@ -833,6 +841,7 @@ %{_datadir}/dbus-1/system.d/snapd.system-services.conf %{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy %{_datadir}/applications/io.snapcraft.SessionAgent.desktop +%{_datadir}/fish/vendor_conf.d/snapd.fish %{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop %config(noreplace) %{_sysconfdir}/sysconfig/snapd %dir %{_sharedstatedir}/snapd @@ -867,6 +876,8 @@ # this is typically owned by zsh, but we do not want to explicitly require zsh %dir %{_datadir}/zsh %dir %{_datadir}/zsh/site-functions +# similar case for fish +%dir %{_datadir}/fish/vendor_conf.d %files -n snap-confine %doc cmd/snap-confine/PORTING @@ -978,6 +989,450 @@ %changelog +* Thu Jan 06 2022 Ian Johnson +- New upstream release 2.54.2 + - tests: exclude interfaces-kernel-module load on arm + - tests: ensure that test-snapd-kernel-module-load is + removed + - tests: do not test microk8s-smoke on arm + - tests/core/failover: replace boot-state with snap debug boot-vars + - tests: use snap info|awk to extract tracking channel + - tests: fix remodel-kernel test when running on external devices + - .github/workflows/test.yaml: also check internal snapd version for + cleanliness + - packaging/ubuntu-16.04/rules: eliminate seccomp modification + - bootloader/assets/grub_*cfg_asset.go: update Copyright + - build-aux/snap/snapcraft.yaml: adjust comment about get-version + - .github/workflows/test.yaml: add check in github actions for dirty + snapd snaps + - build-aux/snap/snapcraft.yaml: use build-packages, don't fail + dirty builds + - data/selinux: allow poking /proc/xen + +* Mon Dec 20 2021 Michael Vogt +- New upstream release 2.54.1 + - buid-aux: set version before calling ./generate-packaging-dir + This fixes the "dirty" suffix in the auto-generated version + +* Fri Dec 17 2021 Michael Vogt +- New upstream release 2.54 + - interfaces/builtin/opengl.go: add boot_vga sys/devices file + - o/configstate/configcore: add tmpfs.size option + - tests: moving to manual opensuse 15.2 + - cmd/snap-device-helper: bring back the device type identification + behavior, but for remove action fallback only + - cmd/snap-failure: use snapd from the snapd snap if core is not + present + - tests/core/failover: enable the test on core18 + - o/devicestate: ensure proper order when remodel does a simple + switch-snap-channel + - builtin/interfaces: add shared memory interface + - overlord: extend kernel/base success and failover with bootenv + checks + - o/snapstate: check disk space w/o store if possible + - snap-bootstrap: Mount snaps read only + - gadget/install: do not re-create partitions using OnDiskVolume + after deletion + - many: fix formatting w/ latest go version + - devicestate,timeutil: improve logging of NTP sync + - tests/main/security-device-cgroups-helper: more debugs + - cmd/snap: print a placeholder for version of broken snaps + - o/snapstate: mock system with classic confinement support + - cmd: Fixup .clangd to use correct syntax + - tests: run spread tests in fedora-35 + - data/selinux: allow snapd to access /etc/modprobe.d + - mount-control: step 2 + - daemon: add multiple snap sideload to API + - tests/lib/pkgdb: install dbus-user-session during prepare, drop + dbus-x11 + - systemd: provide more detailed errors for unimplemented method in + emulation mode + - tests: avoid checking TRUST_TEST_KEYS on restore on remodel-base + test + - tests: retry umounting /var/lib/snapd/seed on uc20 on fsck-on-boot + test + - o/snapstate: add hide/expose snap data to backend + - interfaces: kernel-module-load + - snap: add support for `snap watch + --last={revert,enable,disable,switch}` + - tests/main/security-udev-input-subsystem: drop info from udev + - tests/core/kernel-and-base-single-reboot-failover, + tests/lib/fakestore: verify failover scenario + - tests/main/security-device-cgroups-helper: collect some debug info + when the test fails + - tests/nested/manual/core20-remodel: wait for device to have a + serial before starting a remodel + - tests/main/generic-unregister: test re-registration if not blocked + - o/snapstate, assertsate: validation sets/undo on partial failure + - tests: ensure snapd can be downloaded as a module + - snapdtool, many: support additional key/value flags in info file + - data/env: improve fish shell env setup + - usersession/client: provide a way for client to send messages to a + subset of users + - tests: verify that simultaneous refresh of kernel and base + triggers a single reboot only + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - asserts: change behavior of alternative attribute matcher + - configcore: relax validation rules for hostname + - cmd/snap-confine: do not include libglvnd libraries from the host + system + - overlord, tests: add managers and a spread test for UC20 to UC22 + remodel + - HACKING.md: adjust again for building the snapd snap + - systemd: add support for systemd unit alias names + - o/snapstate: add InstallPathMany + - gadget: allow EnsureLayoutCompatibility to ensure disk has all + laid out structsnow reject/fail: + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider (#11111) + - interfaces/interfaces/scsi_generic: add interface for scsi generic + de… (#10936) + - osutil/disks/mockdisk.go: add MockDevicePathToDiskMapping + - interfaces/microstack-support: set controlsDeviceCgroup to true + - network-setup-control: add netplan generate D-Bus rules + - interface/builtin/log_observe: allow to access /dev/kmsg + - .github/workflows/test.yaml: restore failing of spread tests on + errors (nested) + - gadget: tweaks to DiskStructureDeviceTraits + expand test cases + - tests/lib/nested.sh: allow tests to use their own core18 in extra- + snaps-path + - interfaces/browser-support: Update rules for Edge + - o/devicestate: during remodel first check pending download tasks + for snaps + - polkit: add a package to validate polkit policy files + - HACKING.md: document building the snapd snap and splicing it into + the core snap + - interfaces/udev: fix installing snaps inside lxd in 21.10 + - o/snapstate: refactor disk space checks + - tests: add (strict) microk8s smoke test + - osutil/strace: try to enable strace on more arches + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - tests/main/snapd-reexec-snapd-snap: improve debugging + - daemon: write formdata file parts to snaps dir + - systemd: add support for .target units + - tests: run snap-disconnect on uc16 + - many: add experimental setting to allow using ~/.snap/data instead + of ~/snap + - overlord/snapstate: perform a single reboot when updating boot + base and kernel + - kernel/fde: add DeviceUnlockKernelHookDeviceMapperBackResolver, + use w/ disks pkg + - o/devicestate: introduce DeviceManager.Unregister + - interfaces: allow receiving PropertiesChanged on the mpris plug + - tests: new tool used to retrieve data from mongo db + - daemon: amend ssh keys coming from the store + - tests: Include the tools from snapd-testing-tools project in + "$TESTSTOOLS" + - tests: new workflow step used to report spread error to mongodb + - interfaces/builtin/dsp: update proc files for ambarella flavor + - gadget: replace ondisk implementation with disks package, refactor + part calcs + - tests: Revert "tests: disable flaky uc18 tests until systemd is + fixed" + - Revert: "many: Vendor apparmor-3.0.3 into the snapd snap" + - asserts: rename "white box" to "clear box" (woke checker) + - many: Vendor apparmor-3.0.3 into the snapd snap + - tests: reorganize the debug-each on the spread.yaml + - packaging: sync with downstream packaging in Fedora and openSUSE + - tests: disable flaky uc18 tests until systemd is fixed + - data/env: provide profile setup for fish shell + - tests: use ubuntu-image 1.11 from stable channel + - gadget/gadget.go: include disk schema in the disk device volume + traits too + - tests/main/security-device-cgroups-strict-enforced: extend the + comments + - README.md: point at bugs.launchpad.net/snapd instead of snappy + project + - osutil/disks: introduce RegisterDeviceMapperBackResolver + use for + crypt-luks2 + - packaging: make postrm script robust against `rm` failures + - tests: print extra debug on auto-refresh-gating test failure + - o/assertstate, api: move enforcing/monitoring from api to + assertstate, save history + - tests: skip the test-snapd-timedate-control-consumer.date to avoid + NTP sync error + - gadget/install: use disks functions to implement deviceFromRole, + also rename + - tests: the `lxd` test is failing right now on 21.10 + - o/snapstate: account for deleted revs when undoing install + - interfaces/builtin/block_devices: allow blkid to print block + device attributes + - gadget: include size + sector-size in DiskVolumeDeviceTraits + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - o/snapstate/handlers: propagate read errors on "copy-snap-data" + - osutil/disks: add more fields to Partition, populate them during + discovery + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - o/snapstate: remove repeated test assertions + - tests: skip `snap advise-command` test if the store is overloaded + - cmd: create ~/snap dir with 0700 perms + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - github: leave a comment documenting reasons for pipefail + - github: enable pipefail when running spread + - osutil/disks: add DiskFromPartitionDeviceNode + - gadget, many: add model param to Update() + - cmd/snap-seccomp: add riscv64 support + - o/snapstate: maintain a RevertStatus map in SnapState + - tests: enable lxd tests on impish system + - tests: (partially) revert the memory limits PR#r10241 + - o/assertstate: functions for handling validation sets tracking + history + - tests: some improvements for the spread log parser + - interfaces/network-manager-observe: Update for libnm / dart + clients + - tests: add ntp related debug around "auto-refresh" test + - boot: expand on the fact that reseal taking modeenv is very + intentional + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - data/selinux: update the policy to allow snapd to talk to + org.freedesktop.timedate1 + - o/snapstate: keep old revision if install doesn't add new one + - overlord/state: add a unit test for a kernel+base refresh like + sequence + - desktop, usersession: observe notifications + - osutil/disks: add AllPhysicalDisks() + - timeutil,deviceutil: fix unit tests on systems without dbus or + without ntp-sync + - cmd/snap-bootstrap/README: explain all the things (well most of + them anyways) + - docs: add run-checks dependency install instruction + - o/snapstate: do not prune refresh-candidates if gate-auto-refresh- + hook feature is not enabled + - o/snapstate: test relink remodel helpers do a proper subset of + doInstall and rework the verify*Tasks helpers + - tests/main/mount-ns: make the test run early + - tests: add `--debug` to netplan apply + - many: wait for up to 10min for NTP synchronization before + autorefresh + - tests: initialize CHANGE_ID in _wait_autorefresh + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - tests: add more debug around qemu-nbd + - o/hookstate: print cohort with snapctl refresh --pending (#10985) + - tests: misc robustness changes + - o/snapstate: improve install/update tests (#10850) + - tests: clean up test tools + - spread.yaml: show `journalctl -e` for all suites on debug + - tests: give interfaces-udisks2 more time for the loop device to + appear + - tests: set memory limit for snapd + - tests: increase timeout/add debug around nbd0 mounting (up, see + LP:#1949513) + - snapstate: add debug message where a snap is mounted + - tests: give nbd0 more time to show up in preseed-lxd + - interfaces/dsp: add more ambarella things + - cmd/snap: improve snap disconnect arg parsing and err msg + - tests: disable nested lxd snapd testing + - tests: disable flaky "interfaces-udisks2" on ubuntu-18.04-32 + - o/snapstate: avoid validationSetsSuite repeating snapmgrTestSuite + - sandbox/cgroup: wait for start transient unit job to finish + - o/snapstate: fix task order, tweak errors, add unit tests for + remodel helpers + - osutil/disks: re-org methods for end of usable region, size + information + - build-aux: ensure that debian packaging matches build-base + - docs: update HACKING.md instructions for snapd 2.52 and later + - spread: run lxd tests with version from latest/edge + - interfaces: suppress denial of sys_module capability + - osutil/disks: add methods to replace gadget/ondisk functions + - tests: split test tools - part 1 + - tests: fix nested tests on uc20 + - data/selinux: allow snap-confine to read udev's database + - i/b/common_test: refactor AppArmor features test + - tests: run spread tests on debian 11 + - o/devicestate: copy timesyncd clock timestamp during install + - interfaces/builtin: do not probe parser features when apparmor + isn't available + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - tests: fix error message in run-checks + - tests: spread test for validation sets enforcing + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - o/snapstate: deduplicate snap names in remove/install/update + - tests/main/selinux-data-context: use session when performing + actions as test user + - packaging/opensuse: sync with openSUSE packaging, enable AppArmor + on 15.3+ + - interfaces: skip connection of netlink interface on older + systems + - asserts, o/snapstate: honor IgnoreValidation flag when checking + installed snaps + - tests/main/apparmor-batch-reload: fix fake apparmor_parser to + handle --preprocess + - sandbox/apparmor, interfaces/apparmor: detect bpf capability, + generate snippet for s-c + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - tests: test for enforcing with prerequisites + - tests/main/snapd-sigterm: fix race conditions + - spread: run lxd tests with version from latest/stable + - run-checks: remove --spread from help message + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests: ensure systemd-timesyncd is installed on debian + - interfaces/u2f-devices: add Nitrokey 3 + - tests: update the ubuntu-image channel to candidate + - osutil/disks/labels: simplify decoding algorithm + - tests: not testing lxd snap anymore on i386 architecture + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - cmd/snap: support --ignore-validation with snap install client + command + - tests/snapd-sigterm: be more robust against service restart + - tests: simplify mock script for apparmor_parser + - o/devicestate, o/servicestate: update gadget assets and cmdline + when remodeling + - tests/nested/manual/refresh-revert-fundamentals: re-enable + encryption + - osutil/disks: fix bug in BlkIDEncodeLabel, add BlkIDDecodeLabel + - gadget, osutil/disks: fix some bugs from prior PR'sin the dir. + - secboot: revert move to new version (revert #10715) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup + - many: mv MockDeviceNameDisksToPartitionMapping -> + MockDeviceNameToDiskMapping + - interfaces/builtin: Add '/com/canonical/dbusmenu' path access to + 'unity7' interface + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - osutil/disks, many: switch to defining Partitions directly for + MockDiskMapping + - tests: remove extra-snaps-assertions test + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - tests/nested/core/core20-create-recovery: fix passing of data to + curl + - daemon: allow enabling enforce mode + - daemon: use the syscall connection to get the socket credentials + - i/builtin/kubernetes_support: add access to Calico lock file + - osutil: ensure parent dir is opened and sync'd + - tests: using test-snapd-curl snap instead of http snap + - overlord: add managers unit test demonstrating cyclic dependency + between gadget and kernel updates + - gadget/ondisk.go: include the filesystem UUID in the returned + OnDiskVolume + - packaging: fixes for building on openSUSE + - o/configcore: allow hostnames up to 253 characters, with dot- + delimited elements + - gadget/ondisk.go: add listBlockDevices() to get all block devices + on a system + - gadget: add mapping trait types + functions to save/load + - interfaces: add polkit security backend + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - tests: merge coverage results + - tests: remove "features" from fde-setup.go example + - fde: add new device-setup support to fde-setup + - gadget: add `encryptedDevice` and add encryptedDeviceLUKS + - spread: use `bios: uefi` for uc20 + - client: fail fast on non-retryable errors + - tests: support running all spread tests with experimental features + - tests: check that a snap that doesn't have gate-auto-refresh hook + can call --proceed + - o/snapstate: support ignore-validation flag when updating to a + specific snap revision + - o/snapstate: test prereq update if started by old version + - tests/main: disable cgroup-devices-v1 and freezer tests on 21.10 + - tests/main/interfaces-many: run both variants on all possible + Ubuntu systems + - gadget: mv ensureLayoutCompatibility to gadget proper, add + gadgettest pkg + - many: replace state.State restart support with overlord/restart + - overlord: fix generated snap-revision assertions in remodel unit + tests + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.4 + - devicestate: mock devicestate.MockTimeutilIsNTPSynchronized to + avoid host env leaking into tests + - timeutil: return NoTimedate1Error if it can't connect to the + system bus + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.3 + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - configcore: relax validation rules for hostname + - o/devicestate: introduce DeviceManager.Unregister + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider + - many: wait for up to 10min for NTP synchronization before + autorefresh + - interfaces/interfaces/scsi_generic: add interface for scsi generic + devices + - interfaces/microstack-support: set controlsDeviceCgroup to true + - interface/builtin/log_observe: allow to access /dev/kmsg + - daemon: write formdata file parts to snaps dir + - spread: run lxd tests with version from latest/edge + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - interfaces/builtin/dsp: add proc files for monitoring Ambarella + DSP firmware + - interfaces/builtin/dsp: update proc file accordingly + +* Mon Nov 15 2021 Ian Johnson +- New upstream release 2.53.2 + - interfaces/builtin/block_devices: allow blkid to print block + device attributes/run/udev/data/b{major}:{minor} + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - interfaces/network-manager-observe: Update for libnm client + library + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - o/hookstate: print cohort with snapctl refresh --pending + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - tests: ensure systemd-timesyncd is installed on debian + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests/main/snapd-sigterm: flush, use retry + - tests/main/snapd-sigterm: fix race conditions + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - data/selinux: allow snap-confine to read udev's database + - interfaces/dsp: add more ambarella things* interfaces/dsp: add + more ambarella things + +* Thu Oct 21 2021 Ian Johnson +- New upstream release 2.53.1 + - spread: run lxd tests with version from latest/stable + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional (#10946) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup (2.53) + - interfaces/u2f-devices: add Nitrokey 3 + - Update the ubuntu-image channel to candidate + - Allow hostnames up to 253 characters, with dot-delimited elements + (as suggested by man 7 hostname). + - Disable i386 until it is possible to build snapd using lxd + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - tests/snapd-sigterm: be more robust against service restart + - tests: add a regression test for snapd hanging on SIGTERM + - daemon: use the syscall connection to get the socket + credentials + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - secboot: revert move to new version + * Tue Oct 05 2021 Michael Vogt - New upstream release 2.53 - overlord: fix generated snap-revision assertions in remodel unit diff -Nru snapd-2.53+21.10ubuntu1/packaging/fedora-35/snapd.spec snapd-2.54.2+21.10/packaging/fedora-35/snapd.spec --- snapd-2.53+21.10ubuntu1/packaging/fedora-35/snapd.spec 1970-01-01 00:00:00.000000000 +0000 +++ snapd-2.54.2+21.10/packaging/fedora-35/snapd.spec 2022-01-06 21:25:16.000000000 +0000 @@ -0,0 +1,10488 @@ +# With Fedora, nothing is bundled. For everything else, bundling is used. +# To use bundled stuff, use "--with vendorized" on rpmbuild +%if 0%{?fedora} +%bcond_with vendorized +%else +%bcond_without vendorized +%endif + +# With Amazon Linux 2+, we're going to provide the /snap symlink by default, +# since classic snaps currently require it... :( +%if 0%{?amzn} >= 2 +%bcond_without snap_symlink +%else +%bcond_with snap_symlink +%endif + +# A switch to allow building the package with support for testkeys which +# are used for the spread test suite of snapd. +%bcond_with testkeys + +%global with_devel 1 +%global with_debug 1 +%global with_check 0 +%global with_unit_test 0 +%global with_test_keys 0 +%global with_selinux 1 + +# For the moment, we don't support all golang arches... +%global with_goarches 0 + +# Set if multilib is enabled for supported arches +%ifarch x86_64 aarch64 %{power64} s390x +%global with_multilib 1 +%endif + +# Set if valgrind is to be run +%ifnarch ppc64le +%global with_valgrind 1 +%endif + +%if ! %{with vendorized} +%global with_bundled 0 +%else +%global with_bundled 1 +%endif + +%if ! %{with testkeys} +%global with_test_keys 0 +%else +%global with_test_keys 1 +%endif + +%if 0%{?with_debug} +%global _dwz_low_mem_die_limit 0 +%else +%global debug_package %{nil} +%endif + +%global provider github +%global provider_tld com +%global project snapcore +%global repo snapd +# https://github.com/snapcore/snapd +%global provider_prefix %{provider}.%{provider_tld}/%{project}/%{repo} +%global import_path %{provider_prefix} + +%global snappy_svcs snapd.service snapd.socket snapd.autoimport.service snapd.seeded.service +%global snappy_user_svcs snapd.session-agent.service snapd.session-agent.socket + +# Until we have a way to add more extldflags to gobuild macro... +# Always use external linking when building static binaries. +%if 0%{?fedora} || 0%{?rhel} >= 8 +%define gobuild_static(o:) go build -buildmode pie -compiler gc -tags="rpm_crashtraceback ${BUILDTAGS:-}" -ldflags "${LDFLAGS:-} -B 0x$(head -c20 /dev/urandom|od -An -tx1|tr -d ' \\n') -linkmode external -extldflags '%__global_ldflags -static'" -a -v -x %{?**}; +%endif +%if 0%{?rhel} == 7 +# no pass PIE flags due to https://bugzilla.redhat.com/show_bug.cgi?id=1634486 +%define gobuild_static(o:) go build -compiler gc -tags="rpm_crashtraceback ${BUILDTAGS:-}" -ldflags "${LDFLAGS:-} -B 0x$(head -c20 /dev/urandom|od -An -tx1|tr -d ' \\n') -linkmode external -extldflags '%__global_ldflags -static'" -a -v -x %{?**}; +%endif + +# These macros are missing BUILDTAGS in RHEL 8, see RHBZ#1825138 +%if 0%{?rhel} == 8 +%define gobuild(o:) go build -buildmode pie -compiler gc -tags="rpm_crashtraceback ${BUILDTAGS:-}" -ldflags "${LDFLAGS:-} -B 0x$(head -c20 /dev/urandom|od -An -tx1|tr -d ' \\n') -linkmode external -extldflags '%__global_ldflags'" -a -v -x %{?**}; +%endif + +# These macros are not defined in RHEL 7 +%if 0%{?rhel} == 7 +%define gobuild(o:) go build -compiler gc -tags="rpm_crashtraceback ${BUILDTAGS:-}" -ldflags "${LDFLAGS:-} -B 0x$(head -c20 /dev/urandom|od -An -tx1|tr -d ' \\n') -linkmode external -extldflags '%__global_ldflags'" -a -v -x %{?**}; +%define gotest() go test -compiler gc -ldflags "${LDFLAGS:-}" %{?**}; +%endif + +# Compat path macros +%{!?_environmentdir: %global _environmentdir %{_prefix}/lib/environment.d} +%{!?_systemdgeneratordir: %global _systemdgeneratordir %{_prefix}/lib/systemd/system-generators} +%{!?_systemd_system_env_generator_dir: %global _systemd_system_env_generator_dir %{_prefix}/lib/systemd/system-environment-generators} + +# Fedora selinux-policy includes 'map' permission on a 'file' class. However, +# Amazon Linux 2 does not have the updated policy containing the fix for +# https://bugzilla.redhat.com/show_bug.cgi?id=1574383. +# For now disable SELinux on Amazon Linux 2 until it's fixed. +%if 0%{?amzn2} == 1 +%global with_selinux 0 +%endif + +Name: snapd +Version: 2.54.2 +Release: 0%{?dist} +Summary: A transactional software package manager +License: GPLv3 +URL: https://%{provider_prefix} +Source0: https://%{provider_prefix}/releases/download/%{version}/%{name}_%{version}.no-vendor.tar.xz +Source1: https://%{provider_prefix}/releases/download/%{version}/%{name}_%{version}.only-vendor.tar.xz + +%if 0%{?with_goarches} +# e.g. el6 has ppc64 arch without gcc-go, so EA tag is required +ExclusiveArch: %{?go_arches:%{go_arches}}%{!?go_arches:%{ix86} x86_64 %{arm}} +%else +# Verified arches from snapd upstream +ExclusiveArch: %{ix86} x86_64 %{arm} aarch64 ppc64le s390x +%endif + +# If go_compiler is not set to 1, there is no virtual provide. Use golang instead. +BuildRequires: make +BuildRequires: %{?go_compiler:compiler(go-compiler)}%{!?go_compiler:golang >= 1.9} +BuildRequires: systemd +%{?systemd_requires} + +Requires: snap-confine%{?_isa} = %{version}-%{release} +Requires: squashfs-tools + +%if 0%{?rhel} && 0%{?rhel} < 8 +# Rich dependencies not available, always pull in squashfuse +# snapd will use squashfs.ko instead of squashfuse if it's on the system +# NOTE: Amazon Linux 2 does not have squashfuse, squashfs.ko is part of the kernel package +%if ! 0%{?amzn2} +Requires: squashfuse +Requires: fuse +%endif +%else +# snapd will use squashfuse in the event that squashfs.ko isn't available (cloud instances, containers, etc.) +Requires: ((squashfuse and fuse) or kmod(squashfs.ko)) +%endif + +# bash-completion owns /usr/share/bash-completion/completions +Requires: bash-completion + +%if 0%{?with_selinux} +# Force the SELinux module to be installed +Requires: %{name}-selinux = %{version}-%{release} +%endif + +%if 0%{?fedora} && 0%{?fedora} < 30 +# snapd-login-service is no more +# Note: Remove when F29 is EOL +Obsoletes: %{name}-login-service < 1.33 +Provides: %{name}-login-service = 1.33 +Provides: %{name}-login-service%{?_isa} = 1.33 +%endif + +%if ! 0%{?with_bundled} +BuildRequires: golang(github.com/boltdb/bolt) +BuildRequires: golang(github.com/coreos/go-systemd/activation) +BuildRequires: golang(github.com/godbus/dbus) +BuildRequires: golang(github.com/godbus/dbus/introspect) +BuildRequires: golang(github.com/gorilla/mux) +BuildRequires: golang(github.com/jessevdk/go-flags) +BuildRequires: golang(github.com/juju/ratelimit) +BuildRequires: golang(github.com/kr/pretty) +BuildRequires: golang(github.com/kr/text) +BuildRequires: golang(github.com/mvo5/goconfigparser) +BuildRequires: golang(github.com/seccomp/libseccomp-golang) +BuildRequires: golang(github.com/snapcore/go-gettext) +BuildRequires: golang(golang.org/x/crypto/openpgp/armor) +BuildRequires: golang(golang.org/x/crypto/openpgp/packet) +BuildRequires: golang(golang.org/x/crypto/sha3) +BuildRequires: golang(golang.org/x/crypto/ssh/terminal) +BuildRequires: golang(golang.org/x/xerrors) +BuildRequires: golang(golang.org/x/xerrors/internal) +BuildRequires: golang(gopkg.in/check.v1) +BuildRequires: golang(gopkg.in/macaroon.v1) +BuildRequires: golang(gopkg.in/mgo.v2/bson) +BuildRequires: golang(gopkg.in/retry.v1) +BuildRequires: golang(gopkg.in/tomb.v2) +BuildRequires: golang(gopkg.in/yaml.v2) +BuildRequires: golang(gopkg.in/yaml.v3) +%endif + +%description +Snappy is a modern, cross-distribution, transactional package manager +designed for working with self-contained, immutable packages. + +%package -n snap-confine +Summary: Confinement system for snap applications +License: GPLv3 +BuildRequires: autoconf +BuildRequires: automake +BuildRequires: libtool +BuildRequires: gcc +BuildRequires: gettext +BuildRequires: gnupg +BuildRequires: pkgconfig(glib-2.0) +BuildRequires: pkgconfig(libcap) +BuildRequires: pkgconfig(libseccomp) +%if 0%{?with_selinux} +BuildRequires: pkgconfig(libselinux) +%endif +BuildRequires: pkgconfig(libudev) +BuildRequires: pkgconfig(systemd) +BuildRequires: pkgconfig(udev) +BuildRequires: xfsprogs-devel +BuildRequires: glibc-static +%if ! 0%{?rhel} +BuildRequires: libseccomp-static +%endif +%if 0%{?with_valgrind} +BuildRequires: valgrind +%endif +BuildRequires: %{_bindir}/rst2man +%if 0%{?fedora} +# ShellCheck in EPEL is too old... +BuildRequires: %{_bindir}/shellcheck +%endif + +# Ensures older version from split packaging is replaced +Obsoletes: snap-confine < 2.19 + +%description -n snap-confine +This package is used internally by snapd to apply confinement to +the started snap applications. + +%if 0%{?with_selinux} +%package selinux +Summary: SELinux module for snapd +License: GPLv2+ +BuildArch: noarch +BuildRequires: selinux-policy, selinux-policy-devel +Requires(post): selinux-policy-base >= %{_selinux_policy_version} +Requires(post): policycoreutils +%if 0%{?rhel} == 7 +Requires(post): policycoreutils-python +%else +Requires(post): policycoreutils-python-utils +%endif +Requires(pre): libselinux-utils +Requires(post): libselinux-utils + +%description selinux +This package provides the SELinux policy module to ensure snapd +runs properly under an environment with SELinux enabled. +%endif + +%if 0%{?with_devel} +%package devel +Summary: Development files for %{name} +BuildArch: noarch + +%if 0%{?with_check} && ! 0%{?with_bundled} +%endif + +%if ! 0%{?with_bundled} +Requires: golang(github.com/boltdb/bolt) +Requires: golang(github.com/coreos/go-systemd/activation) +Requires: golang(github.com/godbus/dbus) +Requires: golang(github.com/godbus/dbus/introspect) +Requires: golang(github.com/gorilla/mux) +Requires: golang(github.com/jessevdk/go-flags) +Requires: golang(github.com/juju/ratelimit) +Requires: golang(github.com/kr/pretty) +Requires: golang(github.com/kr/text) +Requires: golang(github.com/mvo5/goconfigparser) +Requires: golang(github.com/seccomp/libseccomp-golang) +Requires: golang(github.com/snapcore/go-gettext) +Requires: golang(golang.org/x/crypto/openpgp/armor) +Requires: golang(golang.org/x/crypto/openpgp/packet) +Requires: golang(golang.org/x/crypto/sha3) +Requires: golang(golang.org/x/crypto/ssh/terminal) +Requires: golang(golang.org/x/xerrors) +Requires: golang(golang.org/x/xerrors/internal) +Requires: golang(gopkg.in/check.v1) +Requires: golang(gopkg.in/macaroon.v1) +Requires: golang(gopkg.in/mgo.v2/bson) +Requires: golang(gopkg.in/retry.v1) +Requires: golang(gopkg.in/tomb.v2) +Requires: golang(gopkg.in/yaml.v2) +Requires: golang(gopkg.in/yaml.v3) +%else +# These Provides are unversioned because the sources in +# the bundled tarball are unversioned (they go by git commit) +# *sigh*... I hate golang... +Provides: bundled(golang(github.com/snapcore/bolt)) +Provides: bundled(golang(github.com/coreos/go-systemd/activation)) +Provides: bundled(golang(github.com/godbus/dbus)) +Provides: bundled(golang(github.com/godbus/dbus/introspect)) +Provides: bundled(golang(github.com/gorilla/mux)) +Provides: bundled(golang(github.com/jessevdk/go-flags)) +Provides: bundled(golang(github.com/juju/ratelimit)) +Provides: bundled(golang(github.com/kr/pretty)) +Provides: bundled(golang(github.com/kr/text)) +Provides: bundled(golang(github.com/mvo5/goconfigparser)) +Provides: bundled(golang(github.com/seccomp/libseccomp-golang)) +Provides: bundled(golang(github.com/snapcore/go-gettext)) +Provides: bundled(golang(golang.org/x/crypto/openpgp/armor)) +Provides: bundled(golang(golang.org/x/crypto/openpgp/packet)) +Provides: bundled(golang(golang.org/x/crypto/sha3)) +Provides: bundled(golang(golang.org/x/crypto/ssh/terminal)) +Provides: bundled(golang(golang.org/x/xerrors)) +Provides: bundled(golang(golang.org/x/xerrors/internal)) +Provides: bundled(golang(gopkg.in/check.v1)) +Provides: bundled(golang(gopkg.in/macaroon.v1)) +Provides: bundled(golang(gopkg.in/mgo.v2/bson)) +Provides: bundled(golang(gopkg.in/retry.v1)) +Provides: bundled(golang(gopkg.in/tomb.v2)) +Provides: bundled(golang(gopkg.in/yaml.v2)) +Provides: bundled(golang(gopkg.in/yaml.v3)) +%endif + +# Generated by gofed +Provides: golang(%{import_path}/advisor) = %{version}-%{release} +Provides: golang(%{import_path}/arch) = %{version}-%{release} +Provides: golang(%{import_path}/asserts) = %{version}-%{release} +Provides: golang(%{import_path}/asserts/assertstest) = %{version}-%{release} +Provides: golang(%{import_path}/asserts/internal) = %{version}-%{release} +Provides: golang(%{import_path}/asserts/signtool) = %{version}-%{release} +Provides: golang(%{import_path}/asserts/snapasserts) = %{version}-%{release} +Provides: golang(%{import_path}/asserts/sysdb) = %{version}-%{release} +Provides: golang(%{import_path}/asserts/systestkeys) = %{version}-%{release} +Provides: golang(%{import_path}/boot) = %{version}-%{release} +Provides: golang(%{import_path}/boot/boottest) = %{version}-%{release} +Provides: golang(%{import_path}/bootloader) = %{version}-%{release} +Provides: golang(%{import_path}/bootloader/androidbootenv) = %{version}-%{release} +Provides: golang(%{import_path}/bootloader/assets) = %{version}-%{release} +Provides: golang(%{import_path}/bootloader/assets/genasset) = %{version}-%{release} +Provides: golang(%{import_path}/bootloader/bootloadertest) = %{version}-%{release} +Provides: golang(%{import_path}/bootloader/efi) = %{version}-%{release} +Provides: golang(%{import_path}/bootloader/grubenv) = %{version}-%{release} +Provides: golang(%{import_path}/bootloader/lkenv) = %{version}-%{release} +Provides: golang(%{import_path}/bootloader/ubootenv) = %{version}-%{release} +Provides: golang(%{import_path}/client) = %{version}-%{release} +Provides: golang(%{import_path}/client/clientutil) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snap) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snap-bootstrap) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snap-bootstrap/triggerwatch) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snap-exec) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snap-failure) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snap-preseed) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snap-recovery-chooser) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snap-repair) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snap-seccomp) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snap-seccomp/syscalls) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snap-update-ns) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snapctl) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snapd) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snaplock) = %{version}-%{release} +Provides: golang(%{import_path}/cmd/snaplock/runinhibit) = %{version}-%{release} +Provides: golang(%{import_path}/daemon) = %{version}-%{release} +Provides: golang(%{import_path}/dbusutil) = %{version}-%{release} +Provides: golang(%{import_path}/dbusutil/dbustest) = %{version}-%{release} +Provides: golang(%{import_path}/desktop/notification) = %{version}-%{release} +Provides: golang(%{import_path}/desktop/notification/notificationtest) = %{version}-%{release} +Provides: golang(%{import_path}/dirs) = %{version}-%{release} +Provides: golang(%{import_path}/docs) = %{version}-%{release} +Provides: golang(%{import_path}/errtracker) = %{version}-%{release} +Provides: golang(%{import_path}/features) = %{version}-%{release} +Provides: golang(%{import_path}/gadget) = %{version}-%{release} +Provides: golang(%{import_path}/gadget/edition) = %{version}-%{release} +Provides: golang(%{import_path}/gadget/install) = %{version}-%{release} +Provides: golang(%{import_path}/gadget/internal) = %{version}-%{release} +Provides: golang(%{import_path}/gadget/quantity) = %{version}-%{release} +Provides: golang(%{import_path}/httputil) = %{version}-%{release} +Provides: golang(%{import_path}/i18n) = %{version}-%{release} +Provides: golang(%{import_path}/i18n/xgettext-go) = %{version}-%{release} +Provides: golang(%{import_path}/image) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces/apparmor) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces/backends) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces/builtin) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces/dbus) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces/hotplug) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces/ifacetest) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces/kmod) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces/mount) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces/policy) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces/seccomp) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces/systemd) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces/udev) = %{version}-%{release} +Provides: golang(%{import_path}/interfaces/utils) = %{version}-%{release} +Provides: golang(%{import_path}/jsonutil) = %{version}-%{release} +Provides: golang(%{import_path}/jsonutil/safejson) = %{version}-%{release} +Provides: golang(%{import_path}/kernel) = %{version}-%{release} +Provides: golang(%{import_path}/logger) = %{version}-%{release} +Provides: golang(%{import_path}/metautil) = %{version}-%{release} +Provides: golang(%{import_path}/netutil) = %{version}-%{release} +Provides: golang(%{import_path}/osutil) = %{version}-%{release} +Provides: golang(%{import_path}/osutil/disks) = %{version}-%{release} +Provides: golang(%{import_path}/osutil/mount) = %{version}-%{release} +Provides: golang(%{import_path}/osutil/squashfs) = %{version}-%{release} +Provides: golang(%{import_path}/osutil/strace) = %{version}-%{release} +Provides: golang(%{import_path}/osutil/sys) = %{version}-%{release} +Provides: golang(%{import_path}/osutil/udev/crawler) = %{version}-%{release} +Provides: golang(%{import_path}/osutil/udev/netlink) = %{version}-%{release} +Provides: golang(%{import_path}/overlord) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/assertstate) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/assertstate/assertstatetest) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/auth) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/cmdstate) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/configstate) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/configstate/config) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/configstate/configcore) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/configstate/proxyconf) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/configstate/settings) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/devicestate) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/devicestate/devicestatetest) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/devicestate/fde) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/devicestate/internal) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/healthstate) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/hookstate) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/hookstate/ctlcmd) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/hookstate/hooktest) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/ifacestate) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/ifacestate/ifacerepo) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/ifacestate/udevmonitor) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/patch) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/servicestate) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/snapshotstate) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/snapshotstate/backend) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/snapstate) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/snapstate/backend) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/snapstate/policy) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/snapstate/snapstatetest) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/standby) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/state) = %{version}-%{release} +Provides: golang(%{import_path}/overlord/storecontext) = %{version}-%{release} +Provides: golang(%{import_path}/polkit) = %{version}-%{release} +Provides: golang(%{import_path}/progress) = %{version}-%{release} +Provides: golang(%{import_path}/progress/progresstest) = %{version}-%{release} +Provides: golang(%{import_path}/randutil) = %{version}-%{release} +Provides: golang(%{import_path}/release) = %{version}-%{release} +Provides: golang(%{import_path}/sandbox) = %{version}-%{release} +Provides: golang(%{import_path}/sandbox/apparmor) = %{version}-%{release} +Provides: golang(%{import_path}/sandbox/cgroup) = %{version}-%{release} +Provides: golang(%{import_path}/sandbox/seccomp) = %{version}-%{release} +Provides: golang(%{import_path}/sandbox/selinux) = %{version}-%{release} +Provides: golang(%{import_path}/sanity) = %{version}-%{release} +Provides: golang(%{import_path}/secboot) = %{version}-%{release} +Provides: golang(%{import_path}/seed) = %{version}-%{release} +Provides: golang(%{import_path}/seed/internal) = %{version}-%{release} +Provides: golang(%{import_path}/seed/seedtest) = %{version}-%{release} +Provides: golang(%{import_path}/seed/seedwriter) = %{version}-%{release} +Provides: golang(%{import_path}/snap) = %{version}-%{release} +Provides: golang(%{import_path}/snap/channel) = %{version}-%{release} +Provides: golang(%{import_path}/snap/internal) = %{version}-%{release} +Provides: golang(%{import_path}/snap/naming) = %{version}-%{release} +Provides: golang(%{import_path}/snap/pack) = %{version}-%{release} +Provides: golang(%{import_path}/snap/snapdir) = %{version}-%{release} +Provides: golang(%{import_path}/snap/snapenv) = %{version}-%{release} +Provides: golang(%{import_path}/snap/snapfile) = %{version}-%{release} +Provides: golang(%{import_path}/snap/snaptest) = %{version}-%{release} +Provides: golang(%{import_path}/snap/squashfs) = %{version}-%{release} +Provides: golang(%{import_path}/snapdenv) = %{version}-%{release} +Provides: golang(%{import_path}/snapdtool) = %{version}-%{release} +Provides: golang(%{import_path}/spdx) = %{version}-%{release} +Provides: golang(%{import_path}/store) = %{version}-%{release} +Provides: golang(%{import_path}/store/storetest) = %{version}-%{release} +Provides: golang(%{import_path}/strutil) = %{version}-%{release} +Provides: golang(%{import_path}/strutil/chrorder) = %{version}-%{release} +Provides: golang(%{import_path}/strutil/quantity) = %{version}-%{release} +Provides: golang(%{import_path}/strutil/shlex) = %{version}-%{release} +Provides: golang(%{import_path}/sysconfig) = %{version}-%{release} +Provides: golang(%{import_path}/systemd) = %{version}-%{release} +Provides: golang(%{import_path}/testutil) = %{version}-%{release} +Provides: golang(%{import_path}/timeout) = %{version}-%{release} +Provides: golang(%{import_path}/timeutil) = %{version}-%{release} +Provides: golang(%{import_path}/timings) = %{version}-%{release} +Provides: golang(%{import_path}/usersession/agent) = %{version}-%{release} +Provides: golang(%{import_path}/usersession/autostart) = %{version}-%{release} +Provides: golang(%{import_path}/usersession/client) = %{version}-%{release} +Provides: golang(%{import_path}/usersession/userd) = %{version}-%{release} +Provides: golang(%{import_path}/usersession/userd/ui) = %{version}-%{release} +Provides: golang(%{import_path}/usersession/xdgopenproxy) = %{version}-%{release} +Provides: golang(%{import_path}/wrappers) = %{version}-%{release} +Provides: golang(%{import_path}/x11) = %{version}-%{release} + +%description devel +This package contains library source intended for +building other packages which use import path with +%{import_path} prefix. +%endif + +%if 0%{?with_unit_test} && 0%{?with_devel} +%package unit-test-devel +Summary: Unit tests for %{name} package + +%if 0%{?with_check} +#Here comes all BuildRequires: PACKAGE the unit tests +#in %%check section need for running +%endif + +# test subpackage tests code from devel subpackage +Requires: %{name}-devel = %{version}-%{release} + +%description unit-test-devel +This package contains unit tests for project +providing packages with %{import_path} prefix. +%endif + +%prep +%if ! 0%{?with_bundled} +%setup -q +# Ensure there's no bundled stuff accidentally leaking in... +rm -rf vendor/* +%else +# Extract each tarball properly +%setup -q -D -b 1 +%endif +# Apply patches +%autopatch -p1 + + +%build +# Generate version files +./mkversion.sh "%{version}-%{release}" + +# Build snapd +mkdir -p src/github.com/snapcore +ln -s ../../../ src/github.com/snapcore/snapd + +%if ! 0%{?with_bundled} +export GOPATH=$(pwd):%{gopath} +# FIXME: move spec file really to a go.mod world instead of this hack +rm -f go.mod +export GO111MODULE=off +#%else +#export GOPATH=$(pwd):$(pwd)/Godeps/_workspace:%{gopath} +%endif + +# see https://github.com/gofed/go-macros/blob/master/rpm/macros.d/macros.go-compilers-golang +BUILDTAGS= +%if 0%{?with_test_keys} +BUILDTAGS="withtestkeys nosecboot" +%else +BUILDTAGS="nosecboot" +%endif + +%if ! 0%{?with_bundled} +# We don't need the snapcore fork for bolt - it is just a fix on ppc +sed -e "s:github.com/snapcore/bolt:github.com/boltdb/bolt:g" -i advisor/*.go errtracker/*.go +%endif + +# We have to build snapd first to prevent the build from +# building various things from the tree without additional +# set tags. +%gobuild -o bin/snapd $GOFLAGS %{import_path}/cmd/snapd +BUILDTAGS="${BUILDTAGS} nomanagers" +%gobuild -o bin/snap $GOFLAGS %{import_path}/cmd/snap +%gobuild -o bin/snap-failure $GOFLAGS %{import_path}/cmd/snap-failure + +# To ensure things work correctly with base snaps, +# snap-exec, snap-update-ns, and snapctl need to be built statically +( +%if 0%{?rhel} >= 8 + # since 1.12.1, the go-toolset module is built with FIPS compliance that + # defaults to using libcrypto.so which gets loaded at runtime via dlopen(), + # disable that functionality for statically built binaries + BUILDTAGS="${BUILDTAGS} no_openssl" +%endif + %gobuild_static -o bin/snap-exec $GOFLAGS %{import_path}/cmd/snap-exec + %gobuild_static -o bin/snap-update-ns $GOFLAGS %{import_path}/cmd/snap-update-ns + %gobuild_static -o bin/snapctl $GOFLAGS %{import_path}/cmd/snapctl +) + +%if 0%{?rhel} +# There's no static link library for libseccomp in RHEL/CentOS... +sed -e "s/-Bstatic -lseccomp/-Bstatic/g" -i cmd/snap-seccomp/*.go +%endif +%gobuild -o bin/snap-seccomp $GOFLAGS %{import_path}/cmd/snap-seccomp + +%if 0%{?with_selinux} +( +%if 0%{?rhel} == 7 + M4PARAM='-D distro_rhel7' +%endif +%if 0%{?rhel} == 7 || 0%{?rhel} == 8 + # RHEL7 and RHEL8 are missing the BPF interfaces from their reference policy + M4PARAM="$M4PARAM -D no_bpf" +%endif + # Build SELinux module + cd ./data/selinux + # pass M4PARAM in env instead of as an override, so that make can still + # manipulate it freely, for more details see: + # https://www.gnu.org/software/make/manual/html_node/Override-Directive.html + M4PARAM="$M4PARAM" make SHARE="%{_datadir}" TARGETS="snappy" +) +%endif + +# Build snap-confine +pushd ./cmd +autoreconf --force --install --verbose +# FIXME: add --enable-caps-over-setuid as soon as possible (setuid discouraged!) +%configure \ + --disable-apparmor \ +%if 0%{?with_selinux} + --enable-selinux \ +%endif +%if 0%{?rhel} == 7 + --disable-bpf \ +%endif + --libexecdir=%{_libexecdir}/snapd/ \ + --enable-nvidia-biarch \ + %{?with_multilib:--with-32bit-libdir=%{_prefix}/lib} \ + --with-snap-mount-dir=%{_sharedstatedir}/snapd/snap \ + --enable-merged-usr + +%make_build %{!?with_valgrind:HAVE_VALGRIND=} +popd + +# Build systemd units, dbus services, and env files +pushd ./data +make BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ + SYSTEMDSYSTEMUNITDIR="%{_unitdir}" \ + SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ + SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" +popd + +%install +install -d -p %{buildroot}%{_bindir} +install -d -p %{buildroot}%{_libexecdir}/snapd +install -d -p %{buildroot}%{_mandir}/man8 +install -d -p %{buildroot}%{_environmentdir} +install -d -p %{buildroot}%{_systemdgeneratordir} +install -d -p %{buildroot}%{_systemd_system_env_generator_dir} +install -d -p %{buildroot}%{_unitdir} +install -d -p %{buildroot}%{_userunitdir} +install -d -p %{buildroot}%{_sysconfdir}/profile.d +install -d -p %{buildroot}%{_sysconfdir}/sysconfig +install -d -p %{buildroot}%{_sharedstatedir}/snapd/assertions +install -d -p %{buildroot}%{_sharedstatedir}/snapd/cookie +install -d -p %{buildroot}%{_sharedstatedir}/snapd/dbus-1/services +install -d -p %{buildroot}%{_sharedstatedir}/snapd/dbus-1/system-services +install -d -p %{buildroot}%{_sharedstatedir}/snapd/desktop/applications +install -d -p %{buildroot}%{_sharedstatedir}/snapd/device +install -d -p %{buildroot}%{_sharedstatedir}/snapd/hostfs +install -d -p %{buildroot}%{_sharedstatedir}/snapd/inhibit +install -d -p %{buildroot}%{_sharedstatedir}/snapd/lib/gl +install -d -p %{buildroot}%{_sharedstatedir}/snapd/lib/gl32 +install -d -p %{buildroot}%{_sharedstatedir}/snapd/lib/glvnd +install -d -p %{buildroot}%{_sharedstatedir}/snapd/lib/vulkan +install -d -p %{buildroot}%{_sharedstatedir}/snapd/mount +install -d -p %{buildroot}%{_sharedstatedir}/snapd/seccomp/bpf +install -d -p %{buildroot}%{_sharedstatedir}/snapd/snaps +install -d -p %{buildroot}%{_sharedstatedir}/snapd/snap/bin +install -d -p %{buildroot}%{_localstatedir}/snap +install -d -p %{buildroot}%{_localstatedir}/cache/snapd +install -d -p %{buildroot}%{_datadir}/polkit-1/actions +%if 0%{?with_selinux} +install -d -p %{buildroot}%{_datadir}/selinux/devel/include/contrib +install -d -p %{buildroot}%{_datadir}/selinux/packages +%endif + +# Install snap and snapd +install -p -m 0755 bin/snap %{buildroot}%{_bindir} +install -p -m 0755 bin/snap-exec %{buildroot}%{_libexecdir}/snapd +install -p -m 0755 bin/snap-failure %{buildroot}%{_libexecdir}/snapd +install -p -m 0755 bin/snapd %{buildroot}%{_libexecdir}/snapd +install -p -m 0755 bin/snap-update-ns %{buildroot}%{_libexecdir}/snapd +install -p -m 0755 bin/snap-seccomp %{buildroot}%{_libexecdir}/snapd +# Ensure /usr/bin/snapctl is a symlink to /usr/libexec/snapd/snapctl +install -p -m 0755 bin/snapctl %{buildroot}%{_libexecdir}/snapd/snapctl +ln -sf %{_libexecdir}/snapd/snapctl %{buildroot}%{_bindir}/snapctl + +%if 0%{?with_selinux} +# Install SELinux module +install -p -m 0644 data/selinux/snappy.if %{buildroot}%{_datadir}/selinux/devel/include/contrib +install -p -m 0644 data/selinux/snappy.pp.bz2 %{buildroot}%{_datadir}/selinux/packages +%endif + +# Install snap(8) man page +bin/snap help --man > %{buildroot}%{_mandir}/man8/snap.8 + +# Install the "info" data file with snapd version +install -m 644 -D data/info %{buildroot}%{_libexecdir}/snapd/info + +# Install bash completion for "snap" +install -m 644 -D data/completion/bash/snap %{buildroot}%{_datadir}/bash-completion/completions/snap +install -m 644 -D data/completion/bash/complete.sh %{buildroot}%{_libexecdir}/snapd +install -m 644 -D data/completion/bash/etelpmoc.sh %{buildroot}%{_libexecdir}/snapd +# Install zsh completion for "snap" +install -d -p %{buildroot}%{_datadir}/zsh/site-functions +install -m 644 -D data/completion/zsh/_snap %{buildroot}%{_datadir}/zsh/site-functions/_snap + +# Install snap-confine +pushd ./cmd +%make_install +# Undo the 0111 permissions, they are restored in the files section +chmod 0755 %{buildroot}%{_sharedstatedir}/snapd/void +# We don't use AppArmor +rm -rfv %{buildroot}%{_sysconfdir}/apparmor.d +# ubuntu-core-launcher is dead +rm -fv %{buildroot}%{_bindir}/ubuntu-core-launcher +popd + +# Install all systemd and dbus units, and env files +pushd ./data +%make_install BINDIR="%{_bindir}" LIBEXECDIR="%{_libexecdir}" DATADIR="%{_datadir}" \ + SYSTEMDSYSTEMUNITDIR="%{_unitdir}" SYSTEMDUSERUNITDIR="%{_userunitdir}" \ + SNAP_MOUNT_DIR="%{_sharedstatedir}/snapd/snap" \ + SNAPD_ENVIRONMENT_FILE="%{_sysconfdir}/sysconfig/snapd" +popd + +%if 0%{?rhel} == 7 +# Install kernel tweaks +# See: https://access.redhat.com/articles/3128691 +install -m 644 -D data/sysctl/rhel7-snap.conf %{buildroot}%{_sysctldir}/99-snap.conf +%endif + +# Remove snappy core specific units +rm -fv %{buildroot}%{_unitdir}/snapd.system-shutdown.service +rm -fv %{buildroot}%{_unitdir}/snapd.snap-repair.* +rm -fv %{buildroot}%{_unitdir}/snapd.core-fixup.* +rm -fv %{buildroot}%{_unitdir}/snapd.recovery-chooser-trigger.service + +# Remove snappy core specific scripts and binaries +rm %{buildroot}%{_libexecdir}/snapd/snapd.core-fixup.sh +rm %{buildroot}%{_libexecdir}/snapd/system-shutdown + +# Remove snapd apparmor service +rm -f %{buildroot}%{_unitdir}/snapd.apparmor.service +rm -f %{buildroot}%{_libexecdir}/snapd/snapd-apparmor + +# Install Polkit configuration +install -m 644 -D data/polkit/io.snapcraft.snapd.policy %{buildroot}%{_datadir}/polkit-1/actions + +# Disable re-exec by default +echo 'SNAP_REEXEC=0' > %{buildroot}%{_sysconfdir}/sysconfig/snapd + +# Create state.json and the README file to be ghosted +touch %{buildroot}%{_sharedstatedir}/snapd/state.json +touch %{buildroot}%{_sharedstatedir}/snapd/snap/README + +# When enabled, create a symlink for /snap to point to /var/lib/snapd/snap +%if %{with snap_symlink} +ln -sr %{buildroot}%{_sharedstatedir}/snapd/snap %{buildroot}/snap +%endif + +# source codes for building projects +%if 0%{?with_devel} +install -d -p %{buildroot}/%{gopath}/src/%{import_path}/ +echo "%%dir %%{gopath}/src/%%{import_path}/." >> devel.file-list +# find all *.go but no *_test.go files and generate devel.file-list +for file in $(find . -iname "*.go" -o -iname "*.s" \! -iname "*_test.go") ; do + echo "%%dir %%{gopath}/src/%%{import_path}/$(dirname $file)" >> devel.file-list + install -d -p %{buildroot}/%{gopath}/src/%{import_path}/$(dirname $file) + cp -pav $file %{buildroot}/%{gopath}/src/%{import_path}/$file + echo "%%{gopath}/src/%%{import_path}/$file" >> devel.file-list +done +%endif + +# testing files for this project +%if 0%{?with_unit_test} && 0%{?with_devel} +install -d -p %{buildroot}/%{gopath}/src/%{import_path}/ +# find all *_test.go files and generate unit-test.file-list +for file in $(find . -iname "*_test.go"); do + echo "%%dir %%{gopath}/src/%%{import_path}/$(dirname $file)" >> devel.file-list + install -d -p %{buildroot}/%{gopath}/src/%{import_path}/$(dirname $file) + cp -pav $file %{buildroot}/%{gopath}/src/%{import_path}/$file + echo "%%{gopath}/src/%%{import_path}/$file" >> unit-test-devel.file-list +done + +# Install additional testdata +install -d %{buildroot}/%{gopath}/src/%{import_path}/cmd/snap/test-data/ +cp -pav cmd/snap/test-data/* %{buildroot}/%{gopath}/src/%{import_path}/cmd/snap/test-data/ +echo "%%{gopath}/src/%%{import_path}/cmd/snap/test-data" >> unit-test-devel.file-list +%endif + +%if 0%{?with_devel} +sort -u -o devel.file-list devel.file-list +%endif + +%check +for binary in snap-exec snap-update-ns snapctl; do + ldd bin/$binary 2>&1 | grep 'not a dynamic executable' +done + +# snapd tests +%if 0%{?with_check} && 0%{?with_unit_test} && 0%{?with_devel} +%if ! 0%{?with_bundled} +export GOPATH=%{buildroot}/%{gopath}:%{gopath} +%else +export GOPATH=%{buildroot}/%{gopath}:$(pwd)/Godeps/_workspace:%{gopath} +%endif +# FIXME: we are in the go.mod world now but without this things fall apart +export GO111MODULE=off +%gotest %{import_path}/... +%endif + +# snap-confine tests (these always run!) +pushd ./cmd +make check +popd + +%files +#define license tag if not already defined +%{!?_licensedir:%global license %doc} +%license COPYING +%doc README.md docs/* +%{_bindir}/snap +%{_bindir}/snapctl +%{_environmentdir}/990-snapd.conf +%if 0%{?rhel} == 7 +%{_sysctldir}/99-snap.conf +%endif +%dir %{_libexecdir}/snapd +%{_libexecdir}/snapd/snapctl +%{_libexecdir}/snapd/snapd +%{_libexecdir}/snapd/snap-exec +%{_libexecdir}/snapd/snap-failure +%{_libexecdir}/snapd/info +%{_libexecdir}/snapd/snap-mgmt +%if 0%{?with_selinux} +%{_libexecdir}/snapd/snap-mgmt-selinux +%endif +%{_mandir}/man8/snap.8* +%{_datadir}/applications/snap-handle-link.desktop +%{_datadir}/bash-completion/completions/snap +%{_libexecdir}/snapd/complete.sh +%{_libexecdir}/snapd/etelpmoc.sh +%{_datadir}/zsh/site-functions/_snap +%{_libexecdir}/snapd/snapd.run-from-snap +%{_sysconfdir}/profile.d/snapd.sh +%{_mandir}/man8/snapd-env-generator.8* +%{_systemd_system_env_generator_dir}/snapd-env-generator +%{_unitdir}/snapd.socket +%{_unitdir}/snapd.service +%{_unitdir}/snapd.autoimport.service +%{_unitdir}/snapd.failure.service +%{_unitdir}/snapd.seeded.service +%{_userunitdir}/snapd.session-agent.service +%{_userunitdir}/snapd.session-agent.socket +%{_datadir}/dbus-1/services/io.snapcraft.Launcher.service +%{_datadir}/dbus-1/services/io.snapcraft.SessionAgent.service +%{_datadir}/dbus-1/services/io.snapcraft.Settings.service +%{_datadir}/dbus-1/session.d/snapd.session-services.conf +%{_datadir}/dbus-1/system.d/snapd.system-services.conf +%{_datadir}/polkit-1/actions/io.snapcraft.snapd.policy +%{_datadir}/applications/io.snapcraft.SessionAgent.desktop +%{_datadir}/fish/vendor_conf.d/snapd.fish +%{_sysconfdir}/xdg/autostart/snap-userd-autostart.desktop +%config(noreplace) %{_sysconfdir}/sysconfig/snapd +%dir %{_sharedstatedir}/snapd +%dir %{_sharedstatedir}/snapd/assertions +%dir %{_sharedstatedir}/snapd/cookie +%dir %{_sharedstatedir}/snapd/dbus-1 +%dir %{_sharedstatedir}/snapd/dbus-1/services +%dir %{_sharedstatedir}/snapd/dbus-1/system-services +%dir %{_sharedstatedir}/snapd/desktop +%dir %{_sharedstatedir}/snapd/desktop/applications +%dir %{_sharedstatedir}/snapd/device +%dir %{_sharedstatedir}/snapd/hostfs +%dir %{_sharedstatedir}/snapd/inhibit +%dir %{_sharedstatedir}/snapd/lib +%dir %{_sharedstatedir}/snapd/lib/gl +%dir %{_sharedstatedir}/snapd/lib/gl32 +%dir %{_sharedstatedir}/snapd/lib/glvnd +%dir %{_sharedstatedir}/snapd/lib/vulkan +%dir %{_sharedstatedir}/snapd/mount +%dir %{_sharedstatedir}/snapd/seccomp +%dir %{_sharedstatedir}/snapd/seccomp/bpf +%dir %{_sharedstatedir}/snapd/snaps +%dir %{_sharedstatedir}/snapd/snap +%ghost %dir %{_sharedstatedir}/snapd/snap/bin +%dir %{_localstatedir}/cache/snapd +%dir %{_localstatedir}/snap +%ghost %{_sharedstatedir}/snapd/state.json +%ghost %{_sharedstatedir}/snapd/snap/README +%if %{with snap_symlink} +/snap +%endif +# this is typically owned by zsh, but we do not want to explicitly require zsh +%dir %{_datadir}/zsh +%dir %{_datadir}/zsh/site-functions +# similar case for fish +%dir %{_datadir}/fish/vendor_conf.d + +%files -n snap-confine +%doc cmd/snap-confine/PORTING +%license COPYING +%dir %{_libexecdir}/snapd +# For now, we can't use caps +# FIXME: Switch to "%%attr(0755,root,root) %%caps(cap_sys_admin=pe)" asap! +%attr(4755,root,root) %{_libexecdir}/snapd/snap-confine +%{_libexecdir}/snapd/snap-device-helper +%{_libexecdir}/snapd/snap-discard-ns +%{_libexecdir}/snapd/snap-gdb-shim +%{_libexecdir}/snapd/snap-gdbserver-shim +%{_libexecdir}/snapd/snap-seccomp +%{_libexecdir}/snapd/snap-update-ns +%{_mandir}/man8/snap-confine.8* +%{_mandir}/man8/snap-discard-ns.8* +%{_systemdgeneratordir}/snapd-generator +%attr(0111,root,root) %{_sharedstatedir}/snapd/void + +%if 0%{?with_selinux} +%files selinux +%license data/selinux/COPYING +%doc data/selinux/README.md +%{_datadir}/selinux/packages/snappy.pp.bz2 +%{_datadir}/selinux/devel/include/contrib/snappy.if +%endif + +%if 0%{?with_devel} +%files devel -f devel.file-list +%license COPYING +%doc README.md +%dir %{gopath}/src/%{provider}.%{provider_tld}/%{project} +%endif + +%if 0%{?with_unit_test} && 0%{?with_devel} +%files unit-test-devel -f unit-test-devel.file-list +%license COPYING +%doc README.md +%endif + +%post +%if 0%{?rhel} == 7 +%sysctl_apply 99-snap.conf +%endif +%systemd_post %{snappy_svcs} +%systemd_user_post %{snappy_user_svcs} +# If install, test if snapd socket and timer are enabled. +# If enabled, then attempt to start them. This will silently fail +# in chroots or other environments where services aren't expected +# to be started. +if [ $1 -eq 1 ] ; then + if systemctl -q is-enabled snapd.socket > /dev/null 2>&1 ; then + systemctl start snapd.socket > /dev/null 2>&1 || : + fi +fi + +%preun +%systemd_preun %{snappy_svcs} +%systemd_user_preun %{snappy_user_svcs} + +# Remove all Snappy content if snapd is being fully uninstalled +if [ $1 -eq 0 ]; then + %{_libexecdir}/snapd/snap-mgmt --purge || : +fi + +%postun +%systemd_postun_with_restart %{snappy_svcs} +%systemd_user_postun_with_restart %{snappy_user_svcs} + +%if 0%{?with_selinux} +%triggerun -- snapd < 2.39 +# TODO: the trigger relies on a very specific snapd version that introduced SELinux +# mount context, figure out how to update the trigger condition to run when needed + +# Trigger on uninstall, with one version of the package being pre 2.38 see +# https://rpm-packaging-guide.github.io/#triggers-and-scriptlets for details +# when triggers are run +if [ "$1" -eq 2 -a "$2" -eq 1 ]; then + # Upgrade from pre 2.38 version + %{_libexecdir}/snapd/snap-mgmt-selinux --patch-selinux-mount-context=system_u:object_r:snappy_snap_t:s0 || : + + # snapd might have created fontconfig cache directory earlier, but with + # incorrect context due to bugs in the policy, make sure it gets the right one + # on upgrade when the new policy was introduced + if [ -d "%{_localstatedir}/cache/fontconfig" ]; then + restorecon -R %{_localstatedir}/cache/fontconfig || : + fi +elif [ "$1" -eq 1 -a "$2" -eq 2 ]; then + # Downgrade to a pre 2.38 version + %{_libexecdir}/snapd/snap-mgmt-selinux --remove-selinux-mount-context=system_u:object_r:snappy_snap_t:s0 || : +fi + +%pre selinux +%selinux_relabel_pre + +%post selinux +%selinux_modules_install %{_datadir}/selinux/packages/snappy.pp.bz2 +%selinux_relabel_post + +%posttrans selinux +%selinux_relabel_post + +%postun selinux +%selinux_modules_uninstall snappy +if [ $1 -eq 0 ]; then + %selinux_relabel_post +fi +%endif + + +%changelog +* Thu Jan 06 2022 Ian Johnson +- New upstream release 2.54.2 + - tests: exclude interfaces-kernel-module load on arm + - tests: ensure that test-snapd-kernel-module-load is + removed + - tests: do not test microk8s-smoke on arm + - tests/core/failover: replace boot-state with snap debug boot-vars + - tests: use snap info|awk to extract tracking channel + - tests: fix remodel-kernel test when running on external devices + - .github/workflows/test.yaml: also check internal snapd version for + cleanliness + - packaging/ubuntu-16.04/rules: eliminate seccomp modification + - bootloader/assets/grub_*cfg_asset.go: update Copyright + - build-aux/snap/snapcraft.yaml: adjust comment about get-version + - .github/workflows/test.yaml: add check in github actions for dirty + snapd snaps + - build-aux/snap/snapcraft.yaml: use build-packages, don't fail + dirty builds + - data/selinux: allow poking /proc/xen + +* Mon Dec 20 2021 Michael Vogt +- New upstream release 2.54.1 + - buid-aux: set version before calling ./generate-packaging-dir + This fixes the "dirty" suffix in the auto-generated version + +* Fri Dec 17 2021 Michael Vogt +- New upstream release 2.54 + - interfaces/builtin/opengl.go: add boot_vga sys/devices file + - o/configstate/configcore: add tmpfs.size option + - tests: moving to manual opensuse 15.2 + - cmd/snap-device-helper: bring back the device type identification + behavior, but for remove action fallback only + - cmd/snap-failure: use snapd from the snapd snap if core is not + present + - tests/core/failover: enable the test on core18 + - o/devicestate: ensure proper order when remodel does a simple + switch-snap-channel + - builtin/interfaces: add shared memory interface + - overlord: extend kernel/base success and failover with bootenv + checks + - o/snapstate: check disk space w/o store if possible + - snap-bootstrap: Mount snaps read only + - gadget/install: do not re-create partitions using OnDiskVolume + after deletion + - many: fix formatting w/ latest go version + - devicestate,timeutil: improve logging of NTP sync + - tests/main/security-device-cgroups-helper: more debugs + - cmd/snap: print a placeholder for version of broken snaps + - o/snapstate: mock system with classic confinement support + - cmd: Fixup .clangd to use correct syntax + - tests: run spread tests in fedora-35 + - data/selinux: allow snapd to access /etc/modprobe.d + - mount-control: step 2 + - daemon: add multiple snap sideload to API + - tests/lib/pkgdb: install dbus-user-session during prepare, drop + dbus-x11 + - systemd: provide more detailed errors for unimplemented method in + emulation mode + - tests: avoid checking TRUST_TEST_KEYS on restore on remodel-base + test + - tests: retry umounting /var/lib/snapd/seed on uc20 on fsck-on-boot + test + - o/snapstate: add hide/expose snap data to backend + - interfaces: kernel-module-load + - snap: add support for `snap watch + --last={revert,enable,disable,switch}` + - tests/main/security-udev-input-subsystem: drop info from udev + - tests/core/kernel-and-base-single-reboot-failover, + tests/lib/fakestore: verify failover scenario + - tests/main/security-device-cgroups-helper: collect some debug info + when the test fails + - tests/nested/manual/core20-remodel: wait for device to have a + serial before starting a remodel + - tests/main/generic-unregister: test re-registration if not blocked + - o/snapstate, assertsate: validation sets/undo on partial failure + - tests: ensure snapd can be downloaded as a module + - snapdtool, many: support additional key/value flags in info file + - data/env: improve fish shell env setup + - usersession/client: provide a way for client to send messages to a + subset of users + - tests: verify that simultaneous refresh of kernel and base + triggers a single reboot only + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - asserts: change behavior of alternative attribute matcher + - configcore: relax validation rules for hostname + - cmd/snap-confine: do not include libglvnd libraries from the host + system + - overlord, tests: add managers and a spread test for UC20 to UC22 + remodel + - HACKING.md: adjust again for building the snapd snap + - systemd: add support for systemd unit alias names + - o/snapstate: add InstallPathMany + - gadget: allow EnsureLayoutCompatibility to ensure disk has all + laid out structsnow reject/fail: + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider (#11111) + - interfaces/interfaces/scsi_generic: add interface for scsi generic + de… (#10936) + - osutil/disks/mockdisk.go: add MockDevicePathToDiskMapping + - interfaces/microstack-support: set controlsDeviceCgroup to true + - network-setup-control: add netplan generate D-Bus rules + - interface/builtin/log_observe: allow to access /dev/kmsg + - .github/workflows/test.yaml: restore failing of spread tests on + errors (nested) + - gadget: tweaks to DiskStructureDeviceTraits + expand test cases + - tests/lib/nested.sh: allow tests to use their own core18 in extra- + snaps-path + - interfaces/browser-support: Update rules for Edge + - o/devicestate: during remodel first check pending download tasks + for snaps + - polkit: add a package to validate polkit policy files + - HACKING.md: document building the snapd snap and splicing it into + the core snap + - interfaces/udev: fix installing snaps inside lxd in 21.10 + - o/snapstate: refactor disk space checks + - tests: add (strict) microk8s smoke test + - osutil/strace: try to enable strace on more arches + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - tests/main/snapd-reexec-snapd-snap: improve debugging + - daemon: write formdata file parts to snaps dir + - systemd: add support for .target units + - tests: run snap-disconnect on uc16 + - many: add experimental setting to allow using ~/.snap/data instead + of ~/snap + - overlord/snapstate: perform a single reboot when updating boot + base and kernel + - kernel/fde: add DeviceUnlockKernelHookDeviceMapperBackResolver, + use w/ disks pkg + - o/devicestate: introduce DeviceManager.Unregister + - interfaces: allow receiving PropertiesChanged on the mpris plug + - tests: new tool used to retrieve data from mongo db + - daemon: amend ssh keys coming from the store + - tests: Include the tools from snapd-testing-tools project in + "$TESTSTOOLS" + - tests: new workflow step used to report spread error to mongodb + - interfaces/builtin/dsp: update proc files for ambarella flavor + - gadget: replace ondisk implementation with disks package, refactor + part calcs + - tests: Revert "tests: disable flaky uc18 tests until systemd is + fixed" + - Revert: "many: Vendor apparmor-3.0.3 into the snapd snap" + - asserts: rename "white box" to "clear box" (woke checker) + - many: Vendor apparmor-3.0.3 into the snapd snap + - tests: reorganize the debug-each on the spread.yaml + - packaging: sync with downstream packaging in Fedora and openSUSE + - tests: disable flaky uc18 tests until systemd is fixed + - data/env: provide profile setup for fish shell + - tests: use ubuntu-image 1.11 from stable channel + - gadget/gadget.go: include disk schema in the disk device volume + traits too + - tests/main/security-device-cgroups-strict-enforced: extend the + comments + - README.md: point at bugs.launchpad.net/snapd instead of snappy + project + - osutil/disks: introduce RegisterDeviceMapperBackResolver + use for + crypt-luks2 + - packaging: make postrm script robust against `rm` failures + - tests: print extra debug on auto-refresh-gating test failure + - o/assertstate, api: move enforcing/monitoring from api to + assertstate, save history + - tests: skip the test-snapd-timedate-control-consumer.date to avoid + NTP sync error + - gadget/install: use disks functions to implement deviceFromRole, + also rename + - tests: the `lxd` test is failing right now on 21.10 + - o/snapstate: account for deleted revs when undoing install + - interfaces/builtin/block_devices: allow blkid to print block + device attributes + - gadget: include size + sector-size in DiskVolumeDeviceTraits + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - o/snapstate/handlers: propagate read errors on "copy-snap-data" + - osutil/disks: add more fields to Partition, populate them during + discovery + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - o/snapstate: remove repeated test assertions + - tests: skip `snap advise-command` test if the store is overloaded + - cmd: create ~/snap dir with 0700 perms + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - github: leave a comment documenting reasons for pipefail + - github: enable pipefail when running spread + - osutil/disks: add DiskFromPartitionDeviceNode + - gadget, many: add model param to Update() + - cmd/snap-seccomp: add riscv64 support + - o/snapstate: maintain a RevertStatus map in SnapState + - tests: enable lxd tests on impish system + - tests: (partially) revert the memory limits PR#r10241 + - o/assertstate: functions for handling validation sets tracking + history + - tests: some improvements for the spread log parser + - interfaces/network-manager-observe: Update for libnm / dart + clients + - tests: add ntp related debug around "auto-refresh" test + - boot: expand on the fact that reseal taking modeenv is very + intentional + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - data/selinux: update the policy to allow snapd to talk to + org.freedesktop.timedate1 + - o/snapstate: keep old revision if install doesn't add new one + - overlord/state: add a unit test for a kernel+base refresh like + sequence + - desktop, usersession: observe notifications + - osutil/disks: add AllPhysicalDisks() + - timeutil,deviceutil: fix unit tests on systems without dbus or + without ntp-sync + - cmd/snap-bootstrap/README: explain all the things (well most of + them anyways) + - docs: add run-checks dependency install instruction + - o/snapstate: do not prune refresh-candidates if gate-auto-refresh- + hook feature is not enabled + - o/snapstate: test relink remodel helpers do a proper subset of + doInstall and rework the verify*Tasks helpers + - tests/main/mount-ns: make the test run early + - tests: add `--debug` to netplan apply + - many: wait for up to 10min for NTP synchronization before + autorefresh + - tests: initialize CHANGE_ID in _wait_autorefresh + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - tests: add more debug around qemu-nbd + - o/hookstate: print cohort with snapctl refresh --pending (#10985) + - tests: misc robustness changes + - o/snapstate: improve install/update tests (#10850) + - tests: clean up test tools + - spread.yaml: show `journalctl -e` for all suites on debug + - tests: give interfaces-udisks2 more time for the loop device to + appear + - tests: set memory limit for snapd + - tests: increase timeout/add debug around nbd0 mounting (up, see + LP:#1949513) + - snapstate: add debug message where a snap is mounted + - tests: give nbd0 more time to show up in preseed-lxd + - interfaces/dsp: add more ambarella things + - cmd/snap: improve snap disconnect arg parsing and err msg + - tests: disable nested lxd snapd testing + - tests: disable flaky "interfaces-udisks2" on ubuntu-18.04-32 + - o/snapstate: avoid validationSetsSuite repeating snapmgrTestSuite + - sandbox/cgroup: wait for start transient unit job to finish + - o/snapstate: fix task order, tweak errors, add unit tests for + remodel helpers + - osutil/disks: re-org methods for end of usable region, size + information + - build-aux: ensure that debian packaging matches build-base + - docs: update HACKING.md instructions for snapd 2.52 and later + - spread: run lxd tests with version from latest/edge + - interfaces: suppress denial of sys_module capability + - osutil/disks: add methods to replace gadget/ondisk functions + - tests: split test tools - part 1 + - tests: fix nested tests on uc20 + - data/selinux: allow snap-confine to read udev's database + - i/b/common_test: refactor AppArmor features test + - tests: run spread tests on debian 11 + - o/devicestate: copy timesyncd clock timestamp during install + - interfaces/builtin: do not probe parser features when apparmor + isn't available + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - tests: fix error message in run-checks + - tests: spread test for validation sets enforcing + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - o/snapstate: deduplicate snap names in remove/install/update + - tests/main/selinux-data-context: use session when performing + actions as test user + - packaging/opensuse: sync with openSUSE packaging, enable AppArmor + on 15.3+ + - interfaces: skip connection of netlink interface on older + systems + - asserts, o/snapstate: honor IgnoreValidation flag when checking + installed snaps + - tests/main/apparmor-batch-reload: fix fake apparmor_parser to + handle --preprocess + - sandbox/apparmor, interfaces/apparmor: detect bpf capability, + generate snippet for s-c + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - tests: test for enforcing with prerequisites + - tests/main/snapd-sigterm: fix race conditions + - spread: run lxd tests with version from latest/stable + - run-checks: remove --spread from help message + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests: ensure systemd-timesyncd is installed on debian + - interfaces/u2f-devices: add Nitrokey 3 + - tests: update the ubuntu-image channel to candidate + - osutil/disks/labels: simplify decoding algorithm + - tests: not testing lxd snap anymore on i386 architecture + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - cmd/snap: support --ignore-validation with snap install client + command + - tests/snapd-sigterm: be more robust against service restart + - tests: simplify mock script for apparmor_parser + - o/devicestate, o/servicestate: update gadget assets and cmdline + when remodeling + - tests/nested/manual/refresh-revert-fundamentals: re-enable + encryption + - osutil/disks: fix bug in BlkIDEncodeLabel, add BlkIDDecodeLabel + - gadget, osutil/disks: fix some bugs from prior PR'sin the dir. + - secboot: revert move to new version (revert #10715) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup + - many: mv MockDeviceNameDisksToPartitionMapping -> + MockDeviceNameToDiskMapping + - interfaces/builtin: Add '/com/canonical/dbusmenu' path access to + 'unity7' interface + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - osutil/disks, many: switch to defining Partitions directly for + MockDiskMapping + - tests: remove extra-snaps-assertions test + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - tests/nested/core/core20-create-recovery: fix passing of data to + curl + - daemon: allow enabling enforce mode + - daemon: use the syscall connection to get the socket credentials + - i/builtin/kubernetes_support: add access to Calico lock file + - osutil: ensure parent dir is opened and sync'd + - tests: using test-snapd-curl snap instead of http snap + - overlord: add managers unit test demonstrating cyclic dependency + between gadget and kernel updates + - gadget/ondisk.go: include the filesystem UUID in the returned + OnDiskVolume + - packaging: fixes for building on openSUSE + - o/configcore: allow hostnames up to 253 characters, with dot- + delimited elements + - gadget/ondisk.go: add listBlockDevices() to get all block devices + on a system + - gadget: add mapping trait types + functions to save/load + - interfaces: add polkit security backend + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - tests: merge coverage results + - tests: remove "features" from fde-setup.go example + - fde: add new device-setup support to fde-setup + - gadget: add `encryptedDevice` and add encryptedDeviceLUKS + - spread: use `bios: uefi` for uc20 + - client: fail fast on non-retryable errors + - tests: support running all spread tests with experimental features + - tests: check that a snap that doesn't have gate-auto-refresh hook + can call --proceed + - o/snapstate: support ignore-validation flag when updating to a + specific snap revision + - o/snapstate: test prereq update if started by old version + - tests/main: disable cgroup-devices-v1 and freezer tests on 21.10 + - tests/main/interfaces-many: run both variants on all possible + Ubuntu systems + - gadget: mv ensureLayoutCompatibility to gadget proper, add + gadgettest pkg + - many: replace state.State restart support with overlord/restart + - overlord: fix generated snap-revision assertions in remodel unit + tests + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.4 + - devicestate: mock devicestate.MockTimeutilIsNTPSynchronized to + avoid host env leaking into tests + - timeutil: return NoTimedate1Error if it can't connect to the + system bus + +* Thu Dec 02 2021 Ian Johnson +- New upstream release 2.53.3 + - devicestate: Unregister deletes the device key pair as well + - daemon,tests: support forgetting device serial via API + - configcore: relax validation rules for hostname + - o/devicestate: introduce DeviceManager.Unregister + - packaging/ubuntu, packaging/debian: depend on dbus-session-bus + provider + - many: wait for up to 10min for NTP synchronization before + autorefresh + - interfaces/interfaces/scsi_generic: add interface for scsi generic + devices + - interfaces/microstack-support: set controlsDeviceCgroup to true + - interface/builtin/log_observe: allow to access /dev/kmsg + - daemon: write formdata file parts to snaps dir + - spread: run lxd tests with version from latest/edge + - cmd/libsnap-confine-private: fix snap-device-helper device allow + list modification on cgroup v2 + - interfaces/builtin/dsp: add proc files for monitoring Ambarella + DSP firmware + - interfaces/builtin/dsp: update proc file accordingly + +* Mon Nov 15 2021 Ian Johnson +- New upstream release 2.53.2 + - interfaces/builtin/block_devices: allow blkid to print block + device attributes/run/udev/data/b{major}:{minor} + - cmd/libsnap-confine-private: do not deny all devices when reusing + the device cgroup + - interfaces/builtin/time-control: allow pps access + - interfaces/u2f-devices: add Trezor and Trezor v2 keys + - interfaces: timezone-control, add permission for ListTimezones + DBus call + - interfaces/apparmor/template.go: allow udevadm from merged usr + systems + - interface/modem-manager: allow connecting to the mbim/qmi proxy + - interfaces/network-manager-observe: Update for libnm client + library + - cmd/snap-seccomp/syscalls: update syscalls to match libseccomp + abad8a8f4 + - sandbox/cgroup: freeze and thaw cgroups related to services and + scopes only + - o/hookstate: print cohort with snapctl refresh --pending + - cmd/snap-confine: lazy set up of device cgroup, only when devices + were assigned + - tests: ensure systemd-timesyncd is installed on debian + - tests/lib/pkgdb: install strace on Debian 11 and Sid + - tests/main/snapd-sigterm: flush, use retry + - tests/main/snapd-sigterm: fix race conditions + - release-tools/repack-debian-tarball.sh: fix c-vendor dir + - data/selinux: allow snap-confine to read udev's database + - interfaces/dsp: add more ambarella things* interfaces/dsp: add + more ambarella things + +* Thu Oct 21 2021 Ian Johnson +- New upstream release 2.53.1 + - spread: run lxd tests with version from latest/stable + - secboot: use latest secboot with tpm legacy platform and v2 fully + optional (#10946) + - cmd/snap-confine: die when snap process is outside of snap + specific cgroup (2.53) + - interfaces/u2f-devices: add Nitrokey 3 + - Update the ubuntu-image channel to candidate + - Allow hostnames up to 253 characters, with dot-delimited elements + (as suggested by man 7 hostname). + - Disable i386 until it is possible to build snapd using lxd + - o/snapstate, hookstate: print remaining hold time on snapctl + --hold + - tests/snapd-sigterm: be more robust against service restart + - tests: add a regression test for snapd hanging on SIGTERM + - daemon: use the syscall connection to get the socket + credentials + - interfaces/builtin/hardware-observer: add /proc/bus/input/devices + too + - cmd/snap-confine/snap-confine.apparmor.in: update ld rule for + s390x impish + - interface/modem-manager: add accept for MBIM/QMI proxy clients + - secboot: revert move to new version + +* Tue Oct 05 2021 Michael Vogt +- New upstream release 2.53 + - overlord: fix generated snap-revision assertions in remodel unit + tests + - snap-bootstrap: wait in `mountNonDataPartitionMatchingKernelDisk` + - interfaces/modem-manager: add access to PCIe modems + - overlord/devicestate: record recovery capable system on a + successful remodel + - o/snapstate: use device ctx in prerequisite install/update + - osutil/disks: support filtering by mount opts in + MountPointsForPartitionRoot + - many: support an API flag system-restart-immediate to make snap + ops proceed immediately with system restarts + - osutil/disks: add RootMountPointsForPartition + - overlord/devicestate, tests: enable UC20 remodel, add spread tests + - cmd/snap: improve snap run help message + - o/snapstate: support ignore validation flag on install/update + - osutil/disks: add Disk.FindMatchingPartitionWith{Fs,Part}Label + - desktop: implement gtk notification backend and provide minimal + notification api + - tests: use the latest cpu family for nested tests execution + - osutil/disks: add Partition struct and Disks.Partitions() + - o/snapstate: prevent install hang if prereq install fails + - osutil/disks: add Disk.KernelDevice{Node,Path} methods + - disks: add `Size(path)` helper + - tests: reset some mount units failing on ubuntu impish + - osutil/disks: add DiskFromDevicePath, other misc changes + - interfaces/apparmor: do not fail during initialization when there + is no AppArmor profile for snap-confine + - daemon: implement access checkers for themes API + - interfaces/seccomp: add clone3 to default template + - interfaces/u2f-devices: add GoTrust Idem Key + - o/snapstate: validation sets enforcing on update + - o/ifacestate: don't fail remove if disconnect hook fails + - tests: fix error trying to create the extra-snaps dir which + already exists + - devicestate: use EncryptionType + - cmd/libsnap-confine-private: workaround BPF memory accounting, + update apparmor profile + - tests: skip system-usernames-microk8s when TRUST_TEST_KEYS is + false + - interfaces/dsp: add a usb rule to the ambarella flavor + - interfaces/apparmor/template.go: allow inspection of dbus + mediation level + - tests/main/security-device-cgroups: fix when both variants run on + the same host + - cmd/snap-confine: update s-c apparmor profile to allow versioned + ld.so + - many: rename systemd.Kind to Backend for a bit more clarity + - cmd/libsnap-confine-private: fix set but unused variable in the + unit tests + - tests: fix netplan test on i386 architecture + - tests: fix lxd-mount-units test which is based on core20 in ubuntu + focal system + - osutil/disks: add new `CreateLinearMapperDevice` helper + - cmd/snap: wait while inhibition file is present + - tests: cleanup the job workspace as first step of the actions + workflow + - tests: use our own image for ubuntu impish + - o/snapstate: update default provider if missing required content + - o/assertstate, api: update validation set assertions only when + updating all snaps + - fde: add HasDeviceUnlock() helper + - secboot: move to new version + - o/ifacestate: don't lose connections if snaps are broken + - spread: display information about current device cgroup in debug + dump + - sysconfig: set TMPDIR in tests to avoid cluttering the real /tmp + - tests, interfaces/builtin: introduce 21.10 cgroupv2 variant, tweak + tests for cgroupv2, update builtin interfaces + - sysconfig/cloud-init: filter MAAS c-i config from ubuntu-seed on + grade signed + - usersession/client: refactor doMany() method + - interfaces/builtin/opengl.go: add libOpenGL.so* too + - o/assertstate: check installed snaps when refreshing validation + set assertions + - osutil: helper for injecting run time faults in snapd + - tests: update test nested tool part 2 + - libsnap-confine: use the pid parameter + - gadget/gadget.go: LaidOutSystemVolumeFromGadget -> + LaidOutVolumesFromGadget + - tests: update the time tolerance to fix the snapd-state test + - .github/workflows/test.yaml: revert #10809 + - tests: rename interfaces-hooks-misbehaving spread test to install- + hook-misbehaving + - data/selinux: update the policy to allow s-c to manipulate BPF map + and programs + - overlord/devicestate: make settle wait longer in remodel tests + - kernel/fde: mock systemd-run in unit test + - o/ifacestate: do not create stray task in batchConnectTasks if + there are no connections + - gadget: add VolumeName to Volume and VolumeStructure + - cmd/libsnap-confine-private: use root when necessary for BPF + related operations + - .github/workflows/test.yaml: bump action-build to 1.0.9 + - o/snapstate: enforce validation sets/enforce on InstallMany + - asserts, snapstate: return full validation set keys from + CheckPresenceRequired and CheckPresenceInvalid + - cmd/snap: only log translation warnings in debug/testing + - tests/main/preseed: update for new base snap of the lxd snap + - tests/nested/manual: use loop for checking for initialize-system + task done + - tests: add a local snap variant to testing prepare-image gating + support + - tests/main/security-device-cgroups-strict-enforced: demonstrate + device cgroup being enforced + - store: one more tweak for the test action timeout + - github: do not fail when codecov upload fails + - o/devicestate: fix flaky test remodel clash + - o/snapstate: add ChangeID to conflict error + - tests: fix regex of TestSnapActionTimeout test + - tests: fix tests for 21.10 + - tests: add test for store.SnapAction() request timeout + - tests: print user sessions info on debug-each + - packaging: backports of golang-go 1.13 are good enough + - sysconfig/cloudinit: add cloudDatasourcesInUseForDir + - cmd: build gdb shims as static binaries + - packaging/ubuntu: pass GO111MODULE to dh_auto_test + - cmd/libsnap-confine-private, tests, sandbox: remove warnings about + cgroup v2, drop forced devmode + - tests: increase memory quota in quota-groups-systemd-accounting + - tests: be more robust against a new day stepping in + - usersession/xdgopenproxy: move PortalLauncher class to own package + - interfaces/builtin: fix microstack unit tests on distros using + /usr/libexec + - cmd/snap-confine: handle CURRENT_TAGS on systems that support it + - cmd/libsnap-confine-private: device cgroup v2 support + - o/servicestate: Update task summary for restart action + - packaging, tests/lib/prepare-restore: build packages without + network access, fix building debs with go modules + - systemd: add AtLeast() method, add mocking in systemdtest + - systemd: use text.template to generate mount unit + - o/hookstate/ctlcmd: Implement snapctl refresh --show-lock command + - o/snapstate: optimize conflicts around snaps stored on + conditional-auto-refresh task + - tests/lib/prepare.sh: download core20 for UC20 runs via + BASE_CHANNEL + - mount-control: step 1 + - go: update go.mod dependencies + - o/snapstate: enforce validation sets on snap install + - tests: revert revert manual lxd removal + - tests: pre-cache snaps in classic and core systems + - tests/lib/nested.sh: split out additional helper for adding files + to VM imgs + - tests: update nested tool - part1 + - image/image_linux.go: add newline + - interfaces/block-devices: support to access the state of block + devices + - o/hookstate: require snap-refresh-control interface for snapctl + refresh --proceed + - build-aux: stage libgcc1 library into snapd snap + - configcore: add read-only netplan support + - tests: fix fakedevicesvc service already exists + - tests: fix interfaces-libvirt test + - tests: remove travis leftovers + - spread: bump delta ref to 2.52 + - packaging: ship the `snapd.apparmor.service` unit in debian + - packaging: remove duplicated `golang-go` build-dependency + - boot: record recovery capable systems in recovery bootenv + - tests: skip overlord tests on riscv64 due to timeouts. + - overlord/ifacestate: fix arguments in unit tests + - ifacestate: undo repository connection if doConnect fails + - many: remove unused parameters + - tests: failure of prereqs on content interface doesn't prevent + install + - tests/nested/manual/refresh-revert-fundamentals: fix variable use + - strutil: add Intersection() + - o/ifacestate: special-case system-files and force refreshing its + static attributes + - interface/builtin: add qualcomm-ipc-router interface for + AF_QIPCRTR socket protocol + - tests: new snapd-state tool + - codecov: fix files pathnames + - systemd: add mock systemd helper + - tests/nested/core/extra-snaps-assertions: fix the match pattern + - image,c/snap,tests: support enforcing validations in prepare-image + via --customize JSON validation enforce(|ignore) + - o/snapstate: enforce validation sets assertions when removing + snaps + - many: update deps + - interfaces/network-control: additional ethernet rule + - tests: use host-scaled settle timeout for hookstate tests + - many: move to go modules + - interfaces: no need for snapRefreshControlInterface struct + - interfaces: introduce snap-refresh-control interface + - tests: move interfaces-libvirt test back to 16.04 + - tests: bump the number of retries when waiting for /dev/nbd0p1 + - tests: add more space on ubuntu xenial + - spread: add 21.10 to qemu, remove 20.10 (EOL) + - packaging: add libfuse3-dev build dependency + - interfaces: add microstack-support interface + - wrappers: fix a bunch of duplicated service definitions in tests + - tests: use host-scaled timeout to avoid riscv64 test failure + - many: fix run-checks gofmt check + - tests: spread test for snapctl refresh --pending/--proceed from + the snap + - o/assertstate,daemon: refresh validation sets assertions with snap + declarations + - tests: migrate tests that are only executed on xenial to bionic + - tests: remove opensuse-15.1 and add opensuse-15.3 from spread runs + - packaging: update master changelog for 2.51.7 + - sysconfig/cloudinit: fix bug around error state of cloud-init + - interfaces, o/snapstate: introduce AffectsPlugOnRefresh flag + - interfaces/interfaces/ion-memory-control: add: add interface for + ion buf + - interfaces/dsp: add /dev/ambad into dsp interface + - tests: new spread log parser + - tests: check files and dirs are cleaned for each test + - o/hookstate/ctlcmd: unify the error message when context is + missing + - o/hookstate: support snapctl refresh --pending from snap + - many: remove unused/dead code + - cmd/libsnap-confine-private: add BPF support helpers + - interfaces/hardware-observe: add some dmi properties + - snapstate: abort kernel refresh if no gadget update can be found + - many: shellcheck fixes + - cmd/snap: add Size column to refresh --list + - packaging: build without dwarf debugging data + - snapstate: fix misleading `assumes` error message + - tests: fix restore in snapfuse spread tests + - o/assertstate: fix missing 'scheduled' header when auto refreshing + assertions + - o/snapstate: fail remove with invalid snap names + - o/hookstate/ctlcmd: correct err message if missing root + - .github/workflows/test.yaml: fix logic + - o/snapstate: don't hold some snaps if not all snaps can be held by + the given gating snap + - c-vendor.c: new c-vendor subdir + - store: make sure expectedZeroFields in tests gets updated + - overlord: add manager test for "assumes" checking + - store: deal correctly with "assumes" from the store raw yaml + - sysconfig/cloudinit.go: add functions for filtering cloud-init + config + - cgroup-support: allow to hide cgroupv2 warning via ENV + - gadget: Export mkfs functions for use in ubuntu-image + - tests: set to 10 minutes the kill timeout for tests failing on + slow boards + - .github/workflows/test.yaml: test github.events key + - i18n/xgettext-go: preserve already escaped quotes + - cmd/snap-seccomp/syscalls: update syscalls list to libseccomp + v2.2.0-428-g5c22d4b + - github: do not try to upload coverage when working with cached run + - tests/main/services-install-hook-can-run-svcs: shellcheck issue + fix + - interfaces/u2f-devices: add Nitrokey FIDO2 + - testutil: add DeepUnsortedMatches Checker + - cmd, packaging: import BPF headers from kernel, detect whether + host headers are usable + - tests: fix services-refresh-mode test + - tests: clean snaps.sh helper + - tests: fix timing issue on security-dev-input-event-denied test + - tests: update systems for sru validation + - .github/workflows: add codedov again + - secboot: remove duplicate import + - tests: stop the service when is active in test interfaces- + firewall-control test + - packaging: remove TEST_GITHUB_AUTOPKGTEST support + - packaging: merge 2.51.6 changelog back to master + - secboot: use half the mem for KDF in AddRecoveryKey + - secboot: switch main key KDF memory cost to 32KB + - tests: remove the test user just when it was installed on create- + user-2 test + - spread: temporarily fix the ownership of /home/ubuntu/.ssh on + 21.10 + - daemon, o/snapstate: handle IgnoreValidation flag on install (2/3) + - usersession/agent: refactor common JSON validation into own + function + - o/hookstate: allow snapctl refresh --proceed from snaps + - cmd/libsnap-confine-private: fix issues identified by coverity + - cmd/snap: print logs in local timezone + - packaging: changelog for 2.51.5 to master + - build-aux: build with go-1.13 in the snapcraft build too + - config: rename "virtual" config to "external" config + - devicestate: add `snap debug timings --ensure=install-system` + - interfaces/builtin/raw_usb: fix platform typo, fix access to usb + devices accessible through platform + - o/snapstate: remove commented out code + - cmd/snap-device-helper: reimplement snap-device-helper + - cmd/libsnap-confine-private: fix coverity issues in tests, tweak + uses of g_assert() + - o/devicestate/handlers_install.go: add workaround to create dirs + for install + - o/assertstate: implement ValidationSetAssertionForEnforce helper + - clang-format: stop breaking my includes + - o/snapstate: allow auto-refresh limited to snaps affected by a + specific gating snap + - tests: fix core-early-config test to use tests.nested tool + - sysconfig/cloudinit.go: measure (but don't use) gadget cloud-init + datasource + - c/snap,o/hookstate/ctlcmd: add JSON/string strict processing flags + to snap/snapctl + - corecfg: add "system.hostname" setting to the system settings + - wrappers: measure time to enable services in StartServices() + - configcore: fix early config timezone handling + - tests/nested/manual: enable serial assertions on testkeys nested + VM's + - configcore: fix a bunch of incorrect error returns + - .github/workflows/test.yaml: use snapcraft 4.x to build the snapd + snap + - packaging: merge 2.51.4 changelog back to master + - {device,snap}state: skip kernel extraction in seeding + - vendor: move to snapshot-4c814e1 branch and set fixed KDF options + - tests: use bigger storage on ubuntu 21.10 + - snap: support links map in snap.yaml (and later from the store + API) + - o/snapstate: add AffectedByRefreshCandidates helper + - configcore: register virtual config for timezone reading + - cmd/libsnap-confine-private: move device cgroup files, add helper + to deny a device + - tests: fix cached-results condition in github actions workflow + - interfaces/tee: add support for Qualcomm qseecom device node + - packaging: fix build failure on bionic and simplify rules + - o/snapstate: affectedByRefresh tweaks + - tests: update nested wait for snapd command + - interfaces/builtin: allow access to per-user GTK CSS overrides + - tests/main/snapd-snap: install 4.x snapcraft to build the snapd + snap + - snap/squashfs: handle squashfs-tools 4.5+ + - asserts/snapasserts: CheckPresenceInvalid and + CheckPresenceRequired methods + - cmd/snap-confine: refactor device cgroup handling to enable easier + v2 integration + - tests: skip udp protocol on latest ubuntus + - cmd/libsnap-confine-private: g_spawn_check_exit_status is + deprecated since glib 2.69 + - interfaces: s/specifc/specific/ + - github: enable gofmt for Go 1.13 jobs + - overlord/devicestate: UC20 specific set-model, managers tests + - o/devicestate, sysconfig: refactor cloud-init config permission + handling + - config: add "virtual" config via config.RegisterVirtualConfig + - packaging: switch ubuntu to use golang-1.13 + - snap: change `snap login --help` to not mention "buy" + - tests: removing Ubuntu 20.10, adding 21.04 nested in spread + - tests/many: remove lxd systemd unit to prevent unexpected + leftovers + - tests/main/services-install-hook-can-run-svcs: make variants more + obvious + - tests: force snapd-session-agent.socket to be re-generated + +* Tue Oct 05 2021 Michael Vogt +- New upstream release 2.52.1 + - snap-bootstrap: wait in `mountNonDataPartitionMatchingKernelDisk` + for the disk (if not present already) + - many: support an API flag system-restart-immediate to make snap + ops proceed immediately with system restarts + - cmd/libsnap-confine-private: g_spawn_check_exit_status is + deprecated since glib 2.69 + - interfaces/seccomp: add clone3 to default template + - interfaces/apparmor/template.go: allow inspection of dbus + mediation level + - interfaces/dsp: add a usb rule to the ambarella flavor + - cmd/snap-confine: update s-c apparmor profile to allow versioned + ld.so + - o/ifacestate: don't lose connections if snaps are broken + - interfaces/builtin/opengl.go: add libOpenGL.so* too + - interfaces/hardware-observe: add some dmi properties + - build-aux: stage libgcc1 library into snapd snap + - interfaces/block-devices: support to access the state of block + devices + - packaging: ship the `snapd.apparmor.service` unit in debian + +* Fri Sep 03 2021 Ian Johnson +- New upstream release 2.52 + - interface/builtin: add qualcomm-ipc-router interface for + AF_QIPCRTR socket protocol + - o/ifacestate: special-case system-files and force refreshing its + static attributes + - interfaces/network-control: additional ethernet rule + - packaging: update 2.52 changelog with 2.51.7 + - interfaces/interfaces/ion-memory-control: add: add interface for + ion buf + - packaging: merge 2.51.6 changelog back to 2.52 + - secboot: use half the mem for KDF in AddRecoveryKey + - secboot: switch main key KDF memory cost to 32KB + - many: merge release/2.51 change to release/2.52 + - .github/workflows/test.yaml: use snapcraft 4.x to build the snapd + snap + - o/servicestate: use snap app names for ExplicitServices of + ServiceAction + - tests/main/services-install-hook-can-run-svcs: add variant w/o + --enable + - o/servicestate: revert only start enabled services + - tests: adding Ubuntu 21.10 to spread test suite + - interface/modem-manager: add support for MBIM/QMI proxy clients + - cmd/snap/model: support storage-safety and snaps headers too + - o/assertstate: Implement EnforcedValidationSets helper + - tests: using retry tool for nested tests + - gadget: check for system-save with multi volumes if encrypting + correctly + - interfaces: make the service naming entirely internal to systemd + BE + - tests/lib/reset.sh: fix removing disabled snaps + - store/store_download.go: use system snap provided xdelta3 priority + + fallback + - packaging: merge changelog from 2.51.3 back to master + - overlord: only start enabled services + - interfaces/builtin: add sd-control interface + - tests/nested/cloud-init-{never-used,nocloud}-not-vuln: fix tests, + use 2.45 + - tests/lib/reset.sh: add workaround from refresh-vs-services tests + for all tests + - o/assertstate: check for conflicts when refreshing and committing + validation set asserts + - devicestate: add support to save timings from install mode + - tests: new tests.nested commands copy and wait-for + - install: add a bunch of nested timings + - tests: drop any-python wrapper + - store: set ResponseHeaderTimeout on the default transport + - tests: fix test-snapd-user-service-sockets test removing snap + - tests: moving nested_exec to nested.tests exec + - tests: add tests about services vs snapd refreshes + - client, cmd/snap, daemon: refactor REST API for quotas to match + CLI org + - c/snap,asserts: create/delete-key external keypair manager + interaction + - tests: revert disable of the delta download tests + - tests/main/system-usernames-microk8s: disable on centos 7 too + - boot: support device change + - o/snapstate: remove unused refreshSchedule argument for + isRefreshHeld helper + - daemon/api_quotas.go: handle conflicts, returning conflict + response + - tests: test for gate-auto-refresh hook error resulting in hold + - release: 2.51.2 + - snapstate/check_snap: add snap_microk8s to shared system- + usernames + - snapstate: remove temporary snap file for local revisions early + - interface: allows reading sd cards internal info from block- + devices interface + - tests: Renaming tool nested-state to tests.nested + - testutil: fix typo in json checker unit tests + - tests: ack assertions by default, add --noack option + - overlord/devicestate: try to pick alternative recovery labels + during remodel + - bootloader/assets: update recovery grub to allow system labels + generated by snapd + - tests: print serial log just once for nested tests + - tests: remove xenial 32 bits + - sandbox/cgroup: do not be so eager to fail when paths do not exist + - tests: run spread tests in ubuntu bionic 32bits + - c/snap,asserts: start supporting ExternalKeypairManager in the + snap key-related commands + - tests: refresh control spread test + - cmd/libsnap-confine-private: do not fail on ENOENT, better getline + error handling + - tests: disable delta download tests for now until the store is + fixed + - tests/nested/manual/preseed: fix for cloud images that ship + without core18 + - boot: properly handle tried system model + - tests/lib/store.sh: revert #10470 + - boot, seed/seedtest: tweak test helpers + - o/servicestate: TODO and fix preexisting typo + - o/servicestate: detect conflicts for quota group operations + - cmd/snap/quotas: adjust help texts for quota commands + - many/quotas: little adjustments + - tests: add spread test for classic snaps content slots + - o/snapstate: fix check-rerefresh task summary when refresh control + is used + - many: use changes + tasks for quota group operations + - tests: fix test snap-quota-groups when checking file + cgroupProcsFile + - asserts: introduce ExternalKeypairManager + - o/ifacestate: do not visit same halt tasks in waitChainSearch to + avoid cycles + - tests/lib/store.sh: fix make_snap_installable_with_id() + - overlord/devicestate, overlord/assertstate: use a temporary DB + when creating recovery systems + - corecfg: allow using `# snapd-edit: no` header to disable pi- + config# snapd-edit: no + - tests/main/interfaces-ssh-keys: tweak checks for openSUSE + Tumbleweed + - cmd/snap: prevent cycles in waitChainSearch with snap debug state + - o/snapstate: fix populating of affectedSnapInfo.AffectingSnaps for + marking self as affecting + - tests: new parameter used by retry tool to set env vars + - tests: support parameters for match-log on journal-state tool + - configcore: ignore system.pi-config.* setting on measured kernels + - sandbox/cgroup: support freezing groups with unified + hierarchy + - tests: fix preseed test to used core20 snap on latest systems + - testutil: introduce a checker which compares the type after having + passed them through a JSON marshaller + - store: tweak error message when store.Sections() download fails + - o/servicestate: stop setting DoneStatus prematurely for quota- + control + - cmd/libsnap-confine-private: bump max depth of groups hierarchy to + 32 + - many: turn Contact into an accessor + - store: make the log with download size a debug one + - cmd/snap-update-ns: Revert "cmd/snap-update-ns: add SRCDIR to + include search path" + - o/devicestate: move SystemMode method before first usage + - tests: skip tests when the sections cannot be retrieved + - boot: support resealing with a try model + - o/hookstate: dedicated handler for gate-auto-refresh hook + - tests: make sure the /root/snap dir is backed up on test snap- + user-dir-perms-fixed + - cmd/snap-confine: make mount ns use check cgroup v2 compatible + - snap: fix TestInstallNoPATH unit test failure when SUDO_UID is set + - cmd/libsnap-confine-private/cgroup-support.c: Fix typo + - cmd/snap-confine, cmd/snapd-generator: fix issues identified by + sparse + - o/snapstate: make conditional-auto-refresh conflict with other + tasks via affected snaps + - many: pass device/model info to configcore via sysconfig.Device + interface + - o/hookstate: return bool flag from Error function of hook handler + to ignore hook errors + - cmd/snap-update-ns: add SRCDIR to include search path + - tests: fix for tests/main/lxd-mount-units test and enable + ubuntu-21.04 + - overlord, o/devicestate: use a single test helper for resetting to + a post boot state + - HACKING.md: update instructions for go1.16+ + - tests: fix restore for security-dev-input-event-denied test + - o/servicestate: move SetStatus to doQuotaControl + - tests: fix classic-prepare-image test + - o/snapstate: prune gating information and refresh-candidates on + snap removal + - o/svcstate/svcstatetest, daemon/api_quotas: fix some tests, add + mock helper + - cmd: a bunch of tweaks and updates + - o/servicestate: refactor meter handling, eliminate some common + parameters + - o/hookstate/ctlcmd: allow snapctl refresh --pending --proceed + syntax. + - o/snapstate: prune refresh candidates in check-rerefresh + - osutil: pass --extrausers option to groupdel + - o/snapstate: remove refreshed snap from snaps-hold in + snapstate.doInstall + - tests/nested: add spread test for uc20 cloud.conf from gadgets + - boot: drop model from resealing and boostate + - o/servicestate, snap/quota: eliminate workaround for buggy + systemds, add spread test + - o/servicestate: introduce internal and servicestatetest + - o/servicestate/quota_control.go: enforce minimum of 4K for quota + groups + - overlord/servicestate: avoid unnecessary computation of disabled + services + - o/hookstate/ctlcmd: do not call ProceedWithRefresh immediately + from snapctl + - o/snapstate: prune hold state during autoRefreshPhase1 + - wrappers/services.go: do not restart disabled or inactive + services + - sysconfig/cloudinit.go: allow installing both gadget + ubuntu-seed + config + - spread: switch LXD back to latest/candidate channel + - interfaces/opengl: add support for Imagination PowerVR + - boot: decouple model from seal/reseal handling via an auxiliary + type + - spread, tests/main/lxd: no longer manual, switch to latest/stable + - github: try out golangci-lint + - tests: set lxd test to manual until failures are fixed + - tests: connect 30% of the interfaces on test interfaces-many-core- + provided + - packaging/debian-sid: update snap-seccomp patches for latest + master + - many: fix imports order (according to gci) + - o/snapstate: consider held snaps in autoRefreshPhase2 + - o/snapstate: unlock the state before calling backend in + undoStartSnapServices + - tests: replace "not MATCH" by NOMATCH in tests + - README.md: refer to new IRC server + - cmd/snap-preseed: provide more error info if snap-preseed fails + early on mount + - daemon: add a Daemon argument to AccessChecker.CheckAccess + - c/snap-bootstrap: add bind option with tests + - interfaces/builtin/netlink_driver_test.go: add test snippet + - overlord/devicestate: set up recovery system tasks when attempting + a remodel + - osutil,strutil,testutil: fix imports order (according to gci) + - release: merge 2.51.1 changelog + - cmd: fix imports order (according to gci) + - tests/lib/snaps/test-snapd-policy-app-consumer: remove dsp-control + interface + - o/servicestate: move handlers tests to quota_handlers_test.go file + instead + - interfaces: add netlink-driver interface + - interfaces: remove leftover debug print + - systemd: refactor property parsers for int values in + CurrentTasksCount, etc. + - tests: fix debug section for postrm-purge test + - tests/many: change all cloud-init passwords for ubuntu to use + plain_test_passwd + - asserts,interfaces,snap: fix imports order (according to gci) + - o/servicestate/quota_control_test.go: test the handlers directly + - tests: fix issue when checking the udev tag on test security- + device-cgroups + - many: introduce Store.SnapExists and use it in + /v2/accessories/themes + - o/snapstate: update LastRefreshTime in doLinkSnap handler + - o/hookstate: handle snapctl refresh --proceed and --hold + - boot: fix model inconsistency check in modeenv, extend unit tests + - overlord/servicestate: improve test robustness with locking + - tests: first part of the cleanup + - tests: new note in HACKING file to clarify about + yamlordereddictloader dependency + - daemon: make CheckAccess return an apiError + - overlord: fix imports ordering (according to gci) + - o/servicestate: add quotastate handlers + - boot: track model's sign key ID, prepare infra for tracking + candidate model + - daemon: have apiBaseSuite.errorReq return *apiError directly + - o/servicestate/service_control.go: add comment about + ExplicitServices + - interfaces: builtin: add dm-crypt interface to support external + storage encryption + - daemon: split out error response code from response*.go to + errors*.go + - interfaces/dsp: fix typo in udev rule + - daemon,o/devicestate: have DeviceManager.SystemMode take an + expectation on the system + - o/snapstate: add helpers for setting and querying holding time for + snaps + - many: fix quota groups for centos 7, amazon linux 2 w/ workaround + for buggy systemd + - overlord/servicestate: mv ensureSnapServicesForGroup to new file + - overlord/snapstate: lock the mutex before returning from stop snap + services undo + - daemon: drop resp completely in favor of using respJSON + consistently + - overlord/devicestate: support for snap downloads in recovery + system handlers + - daemon: introduce a separate findResponse, simplify SyncRespone + and drop Meta + - overlord/snapstate, overlord/devicestate: exclusive change + conflict check + - wrappers, packaging, snap-mgmt: handle removing slices on purge + too + - services: remember if acting on the entire snap + - store: extend context and action objects of SnapAction with + validation-sets + - o/snapstate: refresh control - autorefresh phase2 + - cmd/snap/quota: refactor quota CLI as per new design + - interfaces: opengl: change path for Xilinx zocl driver + - tests: update spread images for ubuntu-core-20 and ubuntu-21.04 + - o/servicestate/quota_control_test.go: change helper escaping + - o/configstate/configcore: support snap set system swap.size=... + - o/devicestate: require serial assertion before remodeling can be + started + - systemd: improve systemctl error reporting + - tests/core/remodel: use model assertions signed with valid keys + - daemon: use apiError for more of the code + - store: fix typo in snapActionResult struct json tag + - userd: mock `systemd --version` in privilegedDesktopLauncherSuite + - packaging/fedora: sync with downstream packaging + - daemon/api_quotas.go: include current memory usage information in + results + - daemon: introduce StructuredResponse and apiError + - o/patch: check if we have snapd snap with correct snap type + already in snapstate + - tests/main/snapd-snap: build the snapd snap on all platforms with + lxd + - tests: new commands for snaps-state tool + - tests/main/snap-quota-groups: add functional spread test for quota + groups + - interfaces/dsp: add /dev/cavalry into dsp interface + - cmd/snap/cmd_info_test.go: make test robust against TZ changes + - tests: moving to tests directories snaps built locally - part 2 + - usersession/userd: fix unit tests on systems using /var/lib/snapd + - sandbox/cgroup: wait for pid to be moved to the desired cgroup + - tests: fix snap-user-dir-perms-fixed vs format checks + - interfaces/desktop-launch: support confined snaps launching other + snaps + - features: enable dbus-activation by default + - usersession/autostart: change ~/snap perms to 0700 on startup + - cmd/snap-bootstrap/initramfs-mounts: mount ubuntu-data nosuid + - tests: new test static checker + - release-tool/changelog.py: misc fixes from real world usage + - release-tools/changelog.py: add function to generate github + release template + - spread, tests: Fedora 32 is EOL, drop it + - o/snapstate: bump max postponement from 60 to 95 days + - interfaces/apparmor: limit the number of jobs when running with a + single CPU + - packaging/fedora/snapd.spec: correct date format in changelog + - packaging: merge 2.51 changelog back to master + - packaging/ubuntu-16.04/changelog: add 2.50 and 2.50.1 changelogs, + placeholder for 2.51 + - interfaces: allow read access to /proc/tty/drivers to modem- + manager and ppp/dev/tty + +* Fri Aug 27 2021 Ian Johnson +- New upstream release 2.51.7 + - cmd/snap-seccomp/syscalls: update syscalls list to libseccomp + v2.2.0-428-g5c22d4b1 + - tests: cherry-pick shellcheck fix `bd730fd4` + - interfaces/dsp: add /dev/ambad into dsp interface + - many: shellcheck fixes + - snapstate: abort kernel refresh if no gadget update can be found + - overlord: add manager test for "assumes" checking + - store: deal correctly with "assumes" from the store raw yaml + +* Thu Aug 19 2021 Ian Johnson +- New upstream release 2.51.6 + - secboot: use half the mem for KDF in AddRecoveryKey + - secboot: switch main key KDF memory cost to 32KB + +* Mon Aug 16 2021 Ian Johnson +- New upstream release 2.51.5 + - snap/squashfs: handle squashfs-tools 4.5+ + - tests/core20-install-device-file-install-via-hook-hack: adjust + test for 2.51 + - o/devicestate/handlers_install.go: add workaround to create dirs + for install + - tests: fix linter warning + - tests: update other spread tests for new behaviour + - tests: ack assertions by default, add --noack option + - release-tools/changelog.py: also fix opensuse changelog date + format + - release-tools/changelog.py: fix typo in function name + - release-tools/changelog.py: fix fedora date format + - release-tools/changelog.py: handle case where we don't have a TZ + - release-tools/changelog.py: fix line length check + - release-tools/changelog.py: specify the LP bug for the release as + an arg too + - interface/modem-manager: add support for MBIM/QMI proxy + clients + - .github/workflows/test.yaml: use snapcraft 4.x to build the snapd + snap + +* Mon Aug 09 2021 Ian Johnson +- New upstream release 2.51.4 + - {device,snap}state: skip kernel extraction in seeding + - vendor: move to snapshot-4c814e1 branch and set fixed KDF options + - tests/interfaces/tee: fix HasLen check for udev snippets + - interfaces/tee: add support for Qualcomm qseecom device node + - gadget: check for system-save with multi volumes if encrypting + correctly + - gadget: drive-by: drop unnecessary/supported passthrough in test + gadget.yaml + +* Wed Jul 14 2021 Ian Johnson +- New upstream release 2.51.3 + - interfaces/builtin: add sd-control interface + - store: set ResponseHeaderTimeout on the default transport + +* Wed Jul 07 2021 Michael Vogt +- New upstream release 2.51.2 + - snapstate: remove temporary snap file for local revisions early + - interface: allows reading sd cards internal info from block- + devices interface + - o/ifacestate: do not visit same halt tasks in waitChainSearch to + avoid slow convergence (or unlikely cycles) + - corecfg: allow using `# snapd-edit: no` header to disable pi- + config + - configcore: ignore system.pi-config.* setting on measured kernels + - many: pass device/model info to configcore via sysconfig.Device + interface + - o/configstate/configcore: support snap set system swap.size=... + - store: make the log with download size a debug one + - interfaces/opengl: add support for Imagination PowerVR + +* Tue Jun 15 2021 Michael Vogt +- New upstream release 2.51.1 + - interfaces: add netlink-driver interface + - interfaces: builtin: add dm-crypt interface to support external + storage encryption + - interfaces/dsp: fix typo in udev rule + - overlord/snapstate: lock the mutex before returning from stop + snap services undo + - interfaces: opengl: change path for Xilinx zocl driver + - interfaces/dsp: add /dev/cavalry into dsp interface + - packaging/fedora/snapd.spec: correct date format in changelog + +* Thu May 27 2021 Ian Johnson +- New upstream release 2.51 + - cmd/snap: stacktraces debug endpoint + - secboot: deactivate volume again when model checker fails + - store: extra log message, a few minor cleanups + - packaging/debian-sid: update systemd patch + - snapstate: adjust update-gadget-assets user visible message + - tests/nested/core/core20-create-recovery: verify that recovery + system can be created at runtime + - gadget: support creating vfat partitions during bootstrap + - daemon/api_quotas.go: support updating quotas with ensure action + - daemon: tighten access to a couple of POST endpoints that should + be really be root-only + - seed/seedtest, overlord/devicestate: move seed validation helper + to seedtest + - overlord/hookstate/ctlcmd: remove unneeded parameter + - snap/quota: add CurrentMemoryUsage for current memory usage of a + quota group + - systemd: add CurrentMemoryUsage to get current memory usage for a + unit + - o/snapstate: introduce minimalInstallInfo interface + - o/hookstate: print pending info (ready, inhibited or none) + - osutil: a helper to find out the total amount of memory in the + system + - overlord, overlord/devicestate: allow for reloading modeenv in + devicemgr when testing + - daemon: refine access testing + - spread: disable unattended-upgrades on debian + - tests/lib/reset: make nc exit after a while when connection is + idle + - daemon: replace access control flags on commands with access + checkers + - release-tools/changelog.py: refactor regexp + file reading/writing + - packaging/debian-sid: update locale patch for the latest master + - overlord/devicestate: tasks for creating recovery systems at + runtime + - release-tools/changelog.py: implement script to update all the + changelog files + - tests: change machine type used for nested testsPrices: + - cmd/snap: include locale when linting description being lower case + - o/servicestate: add RemoveSnapFromQuota + - interfaces/serial-port: add Qualcomm serial port devices to + allowed list + - packaging: merge 2.50.1 changelog back + - interfaces/builtin: introduce raw-input interface + - tests: remove tests.cleanup prepare from nested test + - cmd/snap-update-ns: fix linter errors + - asserts: fix errors reported by linter + - o/hookstate/ctlcmd: allow system-mode for non-root + - overlord/devicestate: comment why explicit system mode check is + needed in ensuring tried recovery systems (#10275) + - overlord/devicesate: observe snap writes when creating recovery + systems + - packaging/ubuntu-16.04/changelog: add placeholder for 2.50.1 + - tests: moving to tests directories snaps built locally - part 1 + - seed/seedwriter: fail early when system seed directory exists + - o/snapstate: autorefresh phase1 for refresh-control + - c/snap: more precise message for ErrorKindSystemRestart op != + reboot + - tests: simplify the tests.cleanup tool + - boot: helpers for manipulating current and good recovery systems + list + - o/hookstate, o/snapstate: print revision, version, channel with + snapctl --pending + - overlord: unit test tweaks, use well known snap IDs, setup snap + declarations for most common snaps + - tests/nested/manual: add test for install-device + snapctl reboot + - o/servicestate: restart slices + services on modifications + - tests: update mount-ns test to support changes in the distro + - interfaces: fix linter issues + - overlord: mock logger in managers unit tests + - tests: adding support for fedora-34 + - tests: adding support for debian 10 on gce + - boot: reseal given keys when the respective boot chain has changed + - secboot: switch encryption key size to 32 byte (thanks to Chris) + - interfaces/dbus: allow claiming 'well-known' D-Bus names with a + wildcard suffix + - spread: bump delta reference version + - interfaces: builtin: update permitted paths to be compatible with + UC20 + - overlord: fix errors reported by linter + - tests: remove old fedora systems from tests + - tests: update spread url + - interfaces/camera: allow devices in /sys/devices/platform/**/usb* + - interfaces/udisks2: Allow access to the login manager via dbus + - cmd/snap: exit normally if "snap changes" has no changes + (LP #1823974) + - tests: more fixes for spread suite on openSUSE + - tests: fix tests expecting cgroup v1/hybrid on openSUSE Tumbleweed + - daemon: fix linter errors + - spread: add Fedora 34, leave a TODO about dropping Fedora 32 + - interfaces: fix linter errors + - tests: use op.paths tools instead of dirs.sh helper - part 2 + - client: Fix linter errors + - cmd/snap: Fix errors reported by linter + - cmd/snap-repair: fix linter issues + - cmd/snap-bootstrap: Fix linter errors + - tests: update permission denied message for test-snapd-event on + ubuntu 2104 + - cmd/snap: small tweaks based on previous reviews + - snap/snaptest: helper that mocks both the squashfs file and a snap + directory + - overlord/devicestate: tweak comment about creating recovery + systems, formatting tweaks + - overlord/devicestate: move devicemgr base suite helpers closer to + test suite struct + - overlord/devicestate: keep track of tried recovery system + - seed/seedwriter: clarify in the diagram when SetInfo is called + - overlord/devicestate: add helper for creating recovery systems at + runtime + - snap-seccomp: update syscalls.go list + - boot,image: support image.Customizations.BootFlags + - overlord: support snapctl --halt|--poweroff in gadget install- + device + - features,servicestate: add experimental.quota-groups flag + - o/servicestate: address comments from previous PR + - tests: basic spread test for snap quota commands + - tests: moving the snaps which are not locally built to the store + directory + - image,c/snap: implement prepare-image --customize + - daemon: implement REST API for quota groups (create / list / get) + - cmd/snap, client: snap quotas command + - o/devicestate,o/hookstate/ctlcmd: introduce SystemModeInfo methods + and snapctl system-mode + - o/servicestate/quota_control.go: introduce (very) basic group + manipulation methods + - cmd/snap, client: snap remove-quota command + - wrappers, quota: implement quota groups slice generation + - snap/quotas: followups from previous PR + - cmd/snap: introduce 'snap quota' command + - o/configstate/configcore/picfg.go: use ubuntu-seed config.txt in + uc20 run mode + - o/servicestate: test has internal ordering issues, consider both + cases + - o/servicestate/quotas: add functions for getting and setting + quotas in state + - tests: new buckets for snapd-spread project on gce + - spread.yaml: update the gce project to start using snapd-spread + - quota: new package for managing resource groups + - many: bind and check keys against models when using FDE hooks v2 + - many: move responsibilities down seboot -> kernel/fde and boot -> + secboot + - packaging: add placeholder changelog + - o/configstate/configcore/vitality: fix RequireMountedSnapdSnap + bug + - overlord: properly mock usr-lib-snapd tests to mimic an Ubuntu + Core system + - many: hide EncryptionKey size and refactors for fde hook v2 next + steps + - tests: adding debug info for create user tests + - o/hookstate: add "refresh" command to snapctl (hidden, not + complete yet) + - systemd: wait for zfs mounts (LP #1922293) + - testutil: support referencing files in FileEquals checker + - many: refactor to kernel/fde and allow `fde-setup initial-setup` + to return json + - o/snapstate: store refresh-candidates in the state + - o/snapstate: helper for creating gate-auto-refresh hooks + - bootloader/bootloadertest: provide interface implementation as + mixins, provide a mock for recovery-aware-trusted-asses bootloader + - tests/lib/nested: do not compress images, return early when + restored from pristine image + - boot: split out a helper for making recovery system bootable + - tests: update os.query check to match new bullseye codename used + on sid images + - o/snapstate: helper for getting snaps affected by refresh, define + new hook + - wrappers: support in EnsureSnapServices a callback to observe + changes (#10176) + - gadget: multi line support in gadget's cmdline file + - daemon: test that requesting restart from (early) Ensure works + - tests: use op.paths tools instead of dirs.sh helper - part 1 + - tests: add new command to snaps-state to get current core, kernel + and gadget + - boot, gadget: move opening the snap container into the gadget + helper + - tests, overlord: extend unit tests, extend spread tests to cover + full command line support + - interfaces/builtin: introduce dsp interface + - boot, bootloader, bootloader/assets: support for full command line + override from gadget + - overlord/devicestate, overlord/snapstate: add task for updating + kernel command lines from gadget + - o/snapstate: remove unused DeviceCtx argument of + ensureInstallPreconditions + - tests/lib/nested: proper status return for tpm/secure boot checks + - cmd/snap, boot: add snapd_full_cmdline_args to dumped boot vars + - wrappers/services.go: refactor helper lambda function to separate + function + - boot/flags.go: add HostUbuntuDataForMode + - boot: handle updating of components that contribute to kernel + command line + - tests: add 20.04 to systems for nested/core + - daemon: add new accessChecker implementations + - boot, overlord/devicestate: consider gadget command lines when + updating boot config + - tests: fix prepare-image-grub-core18 for arm devices + - tests: fix gadget-kernel-refs-update-pc test on arm and when + $TRUST_TEST_KEY is false + - tests: enable help test for all the systems + - boot: set extra command line arguments when preparing run mode + - boot: load bits of kernel command line from gadget snaps + - tests: update layout for tests - part 2 + - tests: update layout for tests - part 1 + - tests: remove the snap profiler from the test suite + - boot: drop gadget snap yaml which is already defined elsewhere in + the tests + - boot: set extra kernel command line arguments when making a + recovery system bootable + - boot: pass gadget path to command line helpers, load gadget from + seed + - tests: new os.paths tool + - daemon: make ucrednetGet() return a *ucrednet structure + - boot: derive boot variables for kernel command lines + - cmd/snap-bootstrap/initramfs-mounts: fix boot-flags location from + initramfs + +* Wed May 19 2021 Ian Johnson +- New upstream release 2.50.1 + - interfaces: update permitted /lib/.. paths to be compatible with + UC20 + - interfaces: builtin: update permitted paths to be compatible with + UC20 + - interfaces/greengrass-support: delete white spaces at the end of + lines + - snap-seccomp: update syscalls.go list + - many: backport kernel command line for 2.50 + - interfaces/dbus: allow claiming 'well-known' D-Bus names with a + wildcard suffix + - interfaces/camera: allow devices in /sys/devices/platform/**/usb* + - interfaces/builtin: introduce dsp interface + +* Sat Apr 24 2021 Michael Vogt +- New upstream release 2.50 + - overlord: properly mock usr-lib-snapd tests to mimic an Ubuntu + Core system + - o/configstate/configcore/vitality: fix RequireMountedSnapdSnap bug + - o/servicestate/servicemgr.go: add ensure loop for snap service + units + - wrappers/services.go: introduce EnsureSnapServices() + - snapstate: add "kernel-assets" to featureSet + - systemd: wait for zfs mounts + - overlord: make servicestate responsible to compute + SnapServiceOptions + - boot,tests: move where we write boot-flags one level up + - o/configstate: don't pass --root=/ when + masking/unmasking/enabling/disabling services + - cmd/snap-bootstrap/initramfs-mounts: write active boot-flags to + /run + - gadget: be more flexible with kernel content resolving + - boot, cmd/snap: include extra cmdline args in debug boot-vars + output + - boot: support read/writing boot-flags from userspace/initramfs + - interfaces/pwm: add PWM interface + - tests/lib/prepare-restore.sh: clean out snapd changes and snaps + before purging + - systemd: enrich UnitStatus returned by systemd.Status() with + Installed flag + - tests: updated restore phase of spread tests - part 1 + - gadget: add support for kernel command line provided by the gadget + - tests: Using GO111MODULE: "off" in spread.yaml + - features: add gate-auto-refresh-hook feature flag + - spread: ignore linux kernel upgrade in early stages for arch + preparation + - tests: use snaps-state commands and remove them from the snaps + helper + - o/configstate: fix panic with a sequence of config unset ops over + same path + - api: provide meaningful error message on connect/disconnect for + non-installed snap + - interfaces/u2f-devices: add HyperFIDO Pro + - tests: add simple sanity check for systemctl show + --property=UnitFileState for unknown service + - tests: use tests.session tool on interfaces-desktop-document- + portal test + - wrappers: install D-Bus service activation files for snapd session + tools on core + - many: add x-gvfs-hide option to mount units + - interfaces/builtin/gpio_test.go: actually test the generated gpio + apparmor + - spread: tentative workaround for arch failure caused by libc + upgrade and cgroups v2 + - tests: add spread test for snap validate against store assertions + - tests: remove snaps which are not used in any test + - ci: set the accept-existing-contributors parameter for the cla- + check action + - daemon: introduce apiBaseSuite.(json|sync|async|error)Req (and + some apiBaseSuite cosmetics) + - o/devicestate/devicemgr: register install-device hook, run if + present in install + - o/configstate/configcore: simple refactors in preparation for new + function + - tests: unifying the core20 nested suite with the core nested suite + - tests: uboot-unpacked-assets updated to reflect the real path used + to find the kernel + - daemon: switch api_test.go to daemon_test and various other + cleanups + - o/configstate/configcore/picfg.go: add hdmi_cvt support + - interfaces/apparmor: followup cleanups, comments and tweaks + - boot: cmd/snap-bootstrap: handle a candidate recovery system v2 + - overlord/snapstate: skip catalog refresh when snappy testing is + enabled + - overlord/snapstate, overlord/ifacestate: move late security + profile removal to ifacestate + - snap-seccomp: fix seccomp test on ppc64el + - interfaces, interfaces/apparmor, overlord/snapstate: late removal + of snap-confine apparmor profiles + - cmd/snap-bootstrap/initramfs-mounts: move time forward using + assertion times + - tests: reset the system while preparing the test suite + - tests: fix snap-advise-command check for 429 + - gadget: policy for gadget/kernel refreshes + - o/configstate: deal with no longer valid refresh.timer=managed + - interfaces/udisks2: allow locking /run/mount/utab for udisks 2.8.4 + - cla-check: Use has-signed-canonical-cla GitHub Action + - tests: validation sets spread test + - tests: simplify the reset.sh logic by removing not needed command + - overlord/snapstate: make sure that snapd current symlink is not + removed during refresh + - tests/core/fsck-on-boot: unmount /run/mnt/snapd directly on uc20 + - tests/lib/fde-setup-hook: also verify that fde-reveal-key key data + is base64 + - o/devicestate: split off ensuring next boot goes to run mode into + new task + - tests: fix cgroup-tracking test + - boot: export helper for clearing tried system state, add tests + - cmd/snap: use less aggressive client timeouts in unit tests + - daemon: fix signing key validity timestamp in unit tests + - o/{device,hook}state: encode fde-setup-request key as base64 + string + - packaging: drop dh-systemd from build-depends on ubuntu-16.04+ + - cmd/snap/pack: unhide the compression option + - boot: extend set try recovery system unit tests + - cmd/snap-bootstrap: refactor handling of ubuntu-save, do not use + secboot's implicit fallback + - o/configstate/configcore: add hdmi_timings to pi-config + - snapstate: reduce reRefreshRetryTimeout to 1/2 second + - interfaces/tee: add TEE/OPTEE interface + - o/snapstate: update validation sets assertions with auto-refresh + - vendor: update go-tpm2/secboot to latest version + - seed: ReadSystemEssentialAndBetterEarliestTime + - tests: replace while commands with the retry tool + - interfaces/builtin: update unit tests to use proper distro's + libexecdir + - tests: run the reset.sh helper and check test invariants while the + test is restored + - daemon: switch preexisting daemon_test tests to apiBaseSuite and + .req + - boot, o/devicestate: split makeBootable20 into two parts + - interfaces/docker-support: add autobind unix rules to docker- + support + - interfaces/apparmor: allow reading + /proc/sys/kernel/random/entropy_avail + - tests: use retry tool instead a loops + - tests/main/uc20-create-partitions: fix tests cleanup + - asserts: mode where Database only assumes cur time >= earliest + time + - daemon: validation sets/api tests cleanup + - tests: improve tests self documentation for nested test suite + - api: local assertion fallback when it's not in the store + - api: validation sets monitor mode + - tests: use fs-state tool in interfaces tests + - daemon: move out /v2/login|logout and errToResponse tests from + api_test.go + - boot: helper for inspecting the outcome of a recovery system try + - o/configstate, o/snapshotstate: fix handling of nil snap config on + snapshot restore + - tests: update documentation and checks for interfaces tests + - snap-seccomp: add new `close_range` syscall + - boot: revert #10009 + - gadget: remove `device-tree{,-origin}` from gadget tests + - boot: simplify systems test setup + - image: write resolved-content from snap prepare-image + - boot: reseal the run key for all recovery systems, but recovery + keys only for the good ones + - interfaces/builtin/network-setup-{control,observe}: allow using + netplan directly + - tests: improve sections prepare and restore - part 1 + - tests: update details on task.yaml files + - tests: revert os.query usage in spread.yaml + - boot: export bootAssetsMap as AssetsMap + - tests/lib/prepare: fix repacking of the UC20 kernel snap for with + ubuntu-core-initramfs 40 + - client: protect against reading too much data from stdin + - tests: improve tests documentation - part 2 + - boot: helper for setting up a try recover system + - tests: improve tests documentation - part 1 + - tests/unit/go: use tests.session wrapper for running tests as a + user + - tests: improvements for snap-seccomp-syscalls + - gadget: simplify filterUpdate (thanks to Maciej) + - tests/lib/prepare.sh: use /etc/group and friends from the core20 + snap + - tests: fix tumbleweed spread tests part 2 + - tests: use new commands of os.query tool on tests + - o/snapshotstate: create snapshots directory on import + - tests/main/lxd/prep-snapd-in-lxd.sh: dump contents of sources.list + - packaging: drop 99-snapd.conf via dpkg-maintscript-helper + - osutil: add SetTime() w/ 32-bit and 64-bit implementations + - interfaces/wayland: rm Xwayland Xauth file access from wayland + slot + - packaging/ubuntu-16.04/rules: turn modules off explicitly + - gadget,devicestate: perform kernel asset update for $kernel: style + refs + - cmd/recovery: small fix for `snap recovery` tab output + - bootloader/lkenv: add recovery systems related variables + - tests: fix new tumbleweed image + - boot: fix typo, should be systems + - o/devicestate: test that users.create.automatic is configured + early + - asserts: use Fetcher in AddSequenceToUpdate + - daemon,o/c/configcore: introduce users.create.automatic + - client, o/servicestate: expose enabled state of user daemons + - boot: helper for checking and marking tried recovery system status + from initramfs + - asserts: pool changes for validation-sets (#9930) + - daemon: move the last api_foo_test.go to daemon_test + - asserts: include the assertion timestamp in error message when + outside of signing key validity range + - ovelord/snapshotstate: keep a few of the last line tar prints + before failing + - gadget/many: rm, delay sector size + structure size checks to + runtime + - cmd/snap-bootstrap/triggerwatch: fix returning wrong errors + - interfaces: add allegro-vcu and media-control interfaces + - interfaces: opengl: add Xilinx zocl bits + - mkversion: check that version from changelog is set before + overriding the output version + - many: fix new ineffassign warnings + - .github/workflows/labeler.yaml: try work-around to not sync + labels + - cmd/snap, boot: add debug set-boot-vars + - interfaces: allow reading the Xauthority file KDE Plasma writes + for Wayland sessions + - tests/main/snap-repair: test running repair assertion w/ fakestore + - tests: disable lxd tests for 21.04 until the lxd images are + published for the system + - tests/regression/lp-1910456: cleanup the /snap symlink when done + - daemon: move single snap querying and ops to api_snaps.go + - tests: fix for preseed and dbus tests on 21.04 + - overlord/snapshotstate: include the last message printed by tar in + the error + - interfaces/system-observe: Allow reading /proc/zoneinfo + - interfaces: remove apparmor downgrade feature + - snap: fix unit tests on Go 1.16 + - spread: disable Go modules support in environment + - tests: use new path to find kernel.img in uc20 for arm devices + - tests: find files before using cat command when checking broadcom- + asic-control interface + - boot: introduce good recovery systems, provide compatibility + handling + - overlord: add manager gadget refresh test + - tests/lib/fakestore: support repair assertions too + - github: temporarily disable action labeler due to issues with + labels being removed + - o/devicestate,many: introduce DeviceManager.preloadGadget for + EarlyConfig + - tests: enable ubuntu 21.04 for spread tests + - snap: provide a useful error message if gdbserver is not installed + - data/selinux: allow system dbus to watch /var/lib/snapd/dbus-1 + - tests/lib/prepare.sh: split reflash.sh into two parts + - packaging/opensuse: sync with openSUSE packaging + - packaging: disable Go modules in snapd.mk + - snap: add deprecation noticed to "snap run --gdb" + - daemon: add API for checking and installing available theme snaps + - tests: using labeler action to add automatically a label to run + nested tests + - gadget: improve error handling around resolving content sources + - asserts: repeat the authority cross-check in CheckSignature as + well + - interfaces/seccomp/template.go: allow copy_file_range + - o/snapstate/check_snap.go: add support for many subversions in + assumes snapdX.. + - daemon: move postSnap and inst.dispatch tests to api_snaps_test.go + - wrappers: use proper paths for mocked mount units in tests + - snap: rename gdbserver option to `snap run --gdbserver` + - store: support validation sets with fetch-assertions action + - snap-confine.apparmor.in: support tmp and log dirs on Yocto/Poky + - packaging/fedora: sync with downstream packaging in Fedora + - many: add Delegate=true to generated systemd units for special + interfaces (master) + - boot: use a common helper for mocking boot assets in cache + - api: validate snaps against validation set assert from the store + - wrappers: don't generate an [Install] section for timer or dbus + activated services + - tests/nested/core20/boot-config-update: skip when snapd was not + built with test features + - o/configstate,o/devicestate: introduce devicestate.EarlyConfig + implemented by configstate.EarlyConfig + - cmd/snap-bootstrap/initramfs-mounts: fix typo in func name + - interfaces/builtin: mock distribution in fontconfig cache unit + tests + - tests/lib/prepare.sh: add another console= to the reflash magic + grub entry + - overlord/servicestate: expose dbus activators of a service + - desktop/notification: test against a real session bus and + notification server implementation + - cmd/snap-bootstrap/initramfs-mounts: write realistic modeenv for + recover+install + - HACKING.md: explain how to run UC20 spread tests with QEMU + - asserts: introduce AtSequence + - overlord/devicestate: task for updating boot configs, spread test + - gadget: fix documentation/typos + - gadget: cleanup MountedFilesystem{Writer,Updater} + - gadget: use ResolvedSource in MountedFilesystemWriter + - snap/info.go: add doc-comment for SortServices + - interfaces: add an optional mount-host-font-cache plug attribute + to the desktop interface + - osutil: skip TestReadBuildGo inside sbuild + - o/hookstate/ctlcmd: add optional --pid and --apparmor-label + arguments to "snapctl is-connected" + - data/env/snapd: use quoting in case PATH contains spaces + - boot: do not observe successful boot assets if not in run mode + - tests: fix umount for snapd snap on fsck-on-boot testumount: + /run/mnt/ubuntu-seed/systems/*/snaps/snapd_*.snap: no mount + - misc: little tweaks + - snap/info.go: ignore unknown daemons in SortSnapServices + - devicestate: keep log from install-mode on installed system + - seed: add LoadEssentialMeta to seed16 and allow all of its + implementations to be called multiple times + - cmd/snap-preseed: initialize snap.SanitizePlugsSlots for gadget in + seeds + - tests/core/uc20-recovery: move recover mode helpers to generic + testslib script + - interfaces/fwupd: allow any distros to access fw files via fwupd + - store: method for fetching validation set assertion + - store: switch to v2/assertions api + - gadget: add new ResolvedContent and populate from LayoutVolume() + - spread: use full format when listing processes + - osutil/many: make all test pkgs osutil_test instead of "osutil" + - tests/unit/go: drop unused environment variables, skip coverage + - OpenGL interface: Support more Tegra libs + - gadget,overlord: pass kernelRoot to install.Run() + - tests: run unit tests in Focal instead of Xenial + - interfaces/browser-support: allow sched_setaffinity with browser- + sandbox: true + - daemon: move query /snaps/ tests to api_snaps_test.go + - cmd/snap-repair/runner.go: add SNAP_SYSTEM_MODE to env of repair + runner + - systemd/systemd.go: support journald JSON messages with arrays for + values + - cmd: make string/error code more robust against errno leaking + - github, run-checks: do not collect coverage data on subsequent + test runs + - boot: boot config update & reseal + - o/snapshotstate: handle conflicts between snapshot forget, export + and import + - osutil/stat.go: add RegularFileExists + - cmd/snapd-generator: don't create mount overrides for snap-try + snaps inside lxc + - gadget/gadget.go: rename ubuntu-* to system-* in doc-comment + - tests: use 6 spread workers for centos8 + - bootloader/assets: support injecting bootloader assets in testing + builds of snapd + - gadget: enable multi-volume uc20 gadgets in + LaidOutSystemVolumeFromGadget; rename too + - overlord/devicestate, sysconfig: do nothing when cloud-init is not + present + - cmd/snap-repair: filter repair assertions based on bases + modes + - snap-confine: make host /etc/ssl available for snaps on classic + +* Fri Mar 26 2021 Michael Vogt +- New upstream release 2.49.2 + - interfaces/tee: add TEE/OPTEE interface + - o/configstate/configcore: add hdmi_timings to pi-config + - interfaces/udisks2: allow locking /run/mount/utab for udisks 2.8.4 + - snap-seccomp: fix seccomp test on ppc64el + - interfaces{,/apparmor}, overlord/snapstate: + late removal of snap-confine apparmor profiles + - overlord/snapstate, wrappers: add dependency on usr-lib- + snapd.mount for services on core with snapd snap + - o/configstate: deal with no longer valid refresh.timer=managed + - overlord/snapstate: make sure that snapd current symlink is not + removed during refresh + - packaging: drop dh-systemd from build-depends on ubuntu-16.04+ + - o/{device,hook}state: encode fde-setup-request key as base64 + - snapstate: reduce reRefreshRetryTimeout to 1/2 second + - tests/main/uc20-create-partitions: fix tests cleanup + - o/configstate, o/snapshotstate: fix handling of nil snap config on + snapshot restore + - snap-seccomp: add new `close_range` syscall + +* Mon Mar 08 2021 Michael Vogt +- New upstream release 2.49.1 + - tests: turn modules off explicitly in spread go unti test + - o/snapshotstate: create snapshots directory on import + - cmd/snap-bootstrap/triggerwatch: fix returning wrong errors + - interfaces: add allegro-vcu and media-control interfaces + - interfaces: opengl: add Xilinx zocl bits + - many: fix new ineffassign warnings + - interfaces/seccomp/template.go: allow copy_file_range + - interfaces: allow reading the Xauthority file KDE Plasma writes + for Wayland sessions + - data/selinux: allow system dbus to watch + /var/lib/snapd/dbus-1 + - Remove apparmor downgrade feature + - Support tmp and log dirs on Yocto/Poky + +* Wed Feb 10 2021 Michael Vogt +- New upstream release 2.49 + - many: add Delegate=true to generated systemd units for special + interfaces + - cmd/snap-bootstrap: rename ModeenvFromModel to + EphemeralModeenvForModel + - cmd/snap-bootstrap/initramfs-mounts: write realistic modeenv for + recover+install + - osutil: skip TestReadBuildGo inside sbuild + - tests: fix umount for snapd snap on fsck-on-boot test + - snap/info_test.go: add unit test cases for bug + - tests/main/services-after-before: add regression spread test + - snap/info.go: ignore unknown daemons in SortSnapServices + - cmd/snap-preseed: initialize snap.SanitizePlugsSlots for gadget in + seeds + - OpenGL interface: Support more Tegra libs + - interfaces/browser-support: allow sched_setaffinity with browser- + sandbox: true + - cmd: make string/error code more robust against errno leaking + - o/snapshotstate: handle conflicts between snapshot forget, export + and import + - cmd/snapd-generator: don't create mount overrides for snap-try + snaps inside lxc + - tests: update test pkg for fedora and centos + - gadget: pass sector size in to mkfs family of functions, use to + select block sz + - o/snapshotstate: fix returning of snap names when duplicated + snapshot is detected + - tests/main/snap-network-errors: skip flushing dns cache on + centos-7 + - interfaces/builtin: Allow DBus property access on + org.freedesktop.Notifications + - cgroup-support.c: fix link to CGROUP DELEGATION + - osutil: update go-udev package + - packaging: fix arch-indep build on debian-sid + - {,sec}boot: pass "key-name" to the FDE hooks + - asserts: sort by revision with Sort interface + - gadget: add gadget.ResolveContentPaths() + - cmd/snap-repair: save base snap and mode in device info; other + misc cleanups + - tests: cleanup the run-checks script + - asserts: snapasserts method to validate installed snaps against + validation sets + - tests: normalize test tools - part 1 + - snapshotstate: detect duplicated snapshot imports + - interfaces/builtin: fix unit test expecting snap-device-helper at + /usr/lib/snapd + - tests: apply workaround done for snap-advise-command to apt-hooks + test + - tests: skip main part of snap-advise test if 429 error is + encountered + - many: clarify gadget role-usage consistency checks for UC16/18 vs + UC20 + - sandbox/cgroup, tess/main: fix unit tests on v2 system, disable + broken tests on sid + - interfaces/builtin: more drive by fixes, import ordering, removing + dead code + - tests: skip interfaces-openvswitch spread test on debian sid + - interfaces/apparmor: drive by comment fix + - cmd/libsnap-confine-private/cleanup-funcs-test.c: rm g_autofree + usage + - cmd/libsnap-confine-private: make unit tests execute happily in a + container + - interfaces, wrappers: misc comment fixes, etc. + - asserts/repair.go: add "bases" and "modes" support to the repair + assertion + - interfaces/opengl: allow RPi MMAL video decoding + - snap: skip help output tests for go-flags v1.4.0 + - gadget: add validation for "$kernel:ref" style content + - packaging/deb, tests/main/lxd-postrm-purge: fix purge inside + containers + - spdx: update to SPDX license list version: 3.11 2020-11-25 + - tests: improve hotplug test setup on classic + - tests: update check to verify is the current system is arm + - tests: use os-query tool to check debian, trusty and tumbleweed + - daemon: start moving implementation to api_snaps.go + - tests/main/snap-validate-basic: disable test on Fedora due to go- + flags panics + - tests: fix library path used for tests.pkgs + - tests/main/cohorts: replace yq with a Python snippet + - run-checks: update to match new argument syntax of ineffassign + - tests: use apiBaseSuite for snapshots tests, fix import endpoint + path + - many: separate consistency/content validation into + gadget.Validate|Content + - o/{device,snap}state: enable devmode snaps with dangerous model + assertions + secboot: add test for when systemd-run does not honor + RuntimeMaxSec + - secboot: add workaround for snapcore/core-initrd issue #13 + - devicestate: log checkEncryption errors via logger.Noticef + - o/daemon: validation sets api and basic spread test + - gadget: move BuildPartitionList to install and make it unexported + - tests: add nested spread end-to-end test for fde-hooks + - devicestate: implement checkFDEFeatures() + - boot: tweak resealing with fde-setup hooks + - tests: add os query commands for subsystems and architectures + - o/snapshotstate: don't set auto flag in the snapshot file + - tests: use os.query tool instead of comparing the system var + - testutil: use the original environment when calling shellcheck + - sysconfig/cloudinit.go: add "manual_cache_clean: true" to cloud- + init restrict file + - gadget,o/devicestate,tests: drop EffectiveFilesystemLabel and + instead set the implicit labels when loading the yaml + - secboot: add new LockSealedKeys() that uses either TPM/fde-reveal- + key + - gadget/quantity: introduce Offset, start using it for offset + related fields in the gadget + - gadget: use "sealed-keys" to determine what method to use for + reseal + - tests/main/fake-netplan-apply: disable test on xenial for now + - daemon: start splitting snaps op tests out of api_test.go + - testutil: make DBusTest use a custom bus configuration file + - tests: replace pkgdb.sh (library) with tests.pkgs (program) + - gadget: prepare gadget kernel refs (0/N) + - interfaces/builtin/docker-support: allow /run/containerd/s/... + - cmd/snap-preseed: reset run inhibit locks on --reset. + - boot: add sealKeyToModeenvUsingFdeSetupHook() + - daemon: reorg snap.go and split out sections and icons support + from api.go + - sandbox/seccomp: use snap-seccomp's stdout for getting version + info + - daemon: split find support to its own api_*.go files and move some + helpers + - tests: move snapstate config defaults tests to a separate file. + - bootloader/{lk,lkenv}: followups from #9695 + - daemon: actually move APIBaseSuite to daemon_test.apiBaseSuite + - gadget,o/devicestate: set implicit values for schema and role + directly instead of relying on Effective* accessors + - daemon: split aliases support to its own api_*.go files + - gadget: start separating rule/convention validation from basic + soundness + - cmd/snap-update-ns: add better unit test for overname sorting + - secboot: use `fde-reveal-key` if available to unseal key + - tests: fix lp-1899664 test when snapd_x1 is not installed in the + system + - tests: fix the scenario when the "$SRC".orig file does not exist + - cmd/snap-update-ns: fix sorting of overname mount entries wrt + other entries + - devicestate: add runFDESetupHook() helper + - bootloader/lk: add support for UC20 lk bootloader with V2 lkenv + structs + - daemon: split unsupported buy implementation to its own api_*.go + files + - tests: download timeout spread test + - gadget,o/devicestate: hybrid 18->20 ready volume setups should be + valid + - o/devicestate: save model with serial in the device save db + - bootloader: add check for prepare-image time and more tests + validating options + - interfaces/builtin/log_observe.go: allow controlling apparmor + audit levels + - hookstate: refactor around EphemeralRunHook + - cmd/snap: implement 'snap validate' command + - secboot,devicestate: add scaffoling for "fde-reveal-key" support + - boot: observe successful command line update, provide a default + - tests: New queries for the os tools + - bootloader/lkenv: specify backup file as arg to NewEnv(), use "" + as path+"bak" + - osutil/disks: add FindMatchingPartitionUUIDWithPartLabel to Disk + iface + - daemon: split out snapctl support and snap configuration support + to their own api_*.go files + - snapshotstate: improve handling of multiple errors + - tests: sign new nested-18|20* models to allow for generic serials + - bootloader: remove installableBootloader interface and methods + - seed: cleanup/drop some no longer valid TODOS, clarify some other + points + - boot: set kernel command line in modeenv during install + - many: rename disks.FindMatching... to FindMatching...WithFsLabel + and err type + - cmd/snap: suppress a case of spurious stdout logging from tests + - hookstate: add new HookManager.EphemeralRunHook() + - daemon: move some more api tests from daemon to daemon_test + - daemon: split apps and logs endpoints to api_apps.go and tests + - interfaces/utf: Add Ledger to U2F devices + - seed/seedwriter: consider modes when checking for deps + availability + - o/devicestate,daemon: fix reboot system action to not require a + system label + - cmd/snap-repair,store: increase initial retry time intervals, + stalling TODOs + - daemon: split interfacesCmd to api_interfaces.go + - github: run nested suite when commit is pushed to release branch + - client: reduce again the /v2/system-info timeout + - tests: reset fakestore unit status + - update-pot: fix typo in plural keyword spec + - tests: remove workarounds that add "ubuntu-save" if missing + - tests: add unit test for auto-refresh with validate-snap failure + - osutil: add helper for getting the kernel command line + - tests/main/uc20-create-partitions: verify ubuntu-save encryption + keys, tweak not MATCH + - boot: add kernel command lines to the modeenv file + - spread: bump delta ref, tweak repacking to make smaller delta + archives + - bootloader/lkenv: add v2 struct + support using it + - snapshotstate: add cleanup of abandonded snapshot imports + - tests: fix uc20-create-parition-* tests for updated gadget + - daemon: split out /v2/interfaces tests to api_interfaces_test.go + - hookstate: implement snapctl fde-setup-{request,result} + - wrappers, o/devicestate: remove EnableSnapServices + - tests: enable nested on 20.10 + - daemon: simplify test helpers Get|PostReq into Req + - daemon: move general api to api_general*.go + - devicestate: make checkEncryption fde-setup hook aware + - client/snapctl, store: fix typos + - tests/main/lxd/prep-snapd-in-lxd.sh: wait for valid apt files + before doing apt ops + - cmd/snap-bootstrap: update model cross-check considerations + - client,snapctl: add naive support for "stdin" + - many: add new "install-mode: disable" option + - osutil/disks: allow building on mac os + - data/selinux: update the policy to allow operations on non-tmpfs + /tmp + - boot: add helper for generating candidate kernel lines for + recovery system + - wrappers: generate D-Bus service activation files + - bootloader/many: rm ConfigFile, add Present for indicating + presence of bloader + - osutil/disks: allow mocking DiskFromDeviceName + - daemon: start cleaning up api tests + - packaging/arch: sync with AUR packaging + - bootloader: indicate when boot config was updated + - tests: Fix snap-debug-bootvars test to make it work on arm devices + and core18 + - tests/nested/manual/core20-save: verify handling of ubuntu-save + with different system variants + - snap: use the boot-base for kernel hooks + - devicestate: support "storage-safety" defaults during install + - bootloader/lkenv: mv v1 to separate file, + include/lk/snappy_boot_v1.h: little fixups + - interfaces/fpga: add fpga interface + - store: download timeout + - vendor: update secboot repo to avoid including secboot.test binary + - osutil: add KernelCommandLineKeyValue + - gadget/gadget.go: allow system-recovery-{image,select} as roles in + gadget.yaml + - devicestate: implement boot.HasFDESetupHook + - osutil/disks: add DiskFromName to get a disk using a udev name + - usersession/agent: have session agent connect to the D-Bus session + bus + - o/servicestate: preserve order of services on snap restart + - o/servicestate: unlock state before calling wrappers in + doServiceControl + - spread: disable unattended-upgrades on ubuntu + - tests: testing new fedora 33 image + - tests: fix fsck on boot on arm devices + - tests: skip boot state test on arm devices + - tests: updated the systems to run prepare-image-grub test + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - tests: unmount /boot/efi in fsck-on-boot test + - strutil/shlex,osutil/udev/netlink: minimally import go-check + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - tests/many: enable some uc20 tests, delete old unneeded tests or + TODOs + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - tests: migrate test from boot.sh helper to boot-state tool + - asserts: implement "storage-safety" in uc20 model assertion + - bootloader: use ForGadget when installing boot config + - spread: UC20 no longer needs 2GB of mem + - cmd/snap-confine: implement snap-device-helper internally + - bootloader/grub: replace old reference to Managed...Blr... with + Trusted...Blr... + - cmd/snap-bootstrap: add readme for snap-bootstrap + real state + diagram + - interfaces: fix greengrass attr namingThe flavor attribute names + are now as follows: + - tests/lib/nested: poke the API to get the snap revisions + - tests: compare options of mount units created by snapd and snapd- + generator + - o/snapstate,servicestate: use service-control task for service + actions + - sandbox: track applications unconditionally + - interfaces/greengrass-support: add additional "process" flavor for + 1.11 update + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + +* Thu Dec 15 2020 Michael Vogt +- New upstream release 2.48.2 + - tests: sign new nested-18|20* models to allow for generic serials + - secboot: add extra paranoia when waiting for that fde-reveal-key + - tests: backport netplan workarounds from #9785 + - secboot: add workaround for snapcore/core-initrd issue #13 + - devicestate: log checkEncryption errors via logger.Noticef + - tests: add nested spread end-to-end test for fde-hooks + - devicestate: implement checkFDEFeatures() + - boot: tweak resealing with fde-setup hooks + - sysconfig/cloudinit.go: add "manual_cache_clean: true" to cloud- + init restrict file + - secboot: add new LockSealedKeys() that uses either TPM or + fde-reveal-key + - gadget: use "sealed-keys" to determine what method to use for + reseal + - boot: add sealKeyToModeenvUsingFdeSetupHook() + - secboot: use `fde-reveal-key` if available to unseal key + - cmd/snap-update-ns: fix sorting of overname mount entries wrt + other entries + - o/devicestate: save model with serial in the device save db + - devicestate: add runFDESetupHook() helper + - secboot,devicestate: add scaffoling for "fde-reveal-key" support + - hookstate: add new HookManager.EphemeralRunHook() + - update-pot: fix typo in plural keyword spec + - store,cmd/snap-repair: increase initial expontential time + intervals + - o/devicestate,daemon: fix reboot system action to not require a + system label + - github: run nested suite when commit is pushed to release branch + - tests: reset fakestore unit status + - tests: fix uc20-create-parition-* tests for updated gadget + - hookstate: implement snapctl fde-setup-{request,result} + - devicestate: make checkEncryption fde-setup hook aware + - client,snapctl: add naive support for "stdin" + - devicestate: support "storage-safety" defaults during install + - snap: use the boot-base for kernel hooks + - vendor: update secboot repo to avoid including secboot.test binary + +* Thu Dec 03 2020 Michael Vogt +- New upstream release 2.48.1 + - gadget: disable ubuntu-boot role validation check + +* Thu Nov 19 2020 Michael Vogt +- New upstream release 2.48 + - osutil: add KernelCommandLineKeyValue + - devicestate: implement boot.HasFDESetupHook + - boot/makebootable.go: set snapd_recovery_mode=install at image- + build time + - bootloader: use ForGadget when installing boot config + - interfaces/raw_usb: allow read access to /proc/tty/drivers + - boot: add scaffolding for "fde-setup" hook support for sealing + - tests: fix basic20 test on arm devices + - seed: make a shared seed system label validation helper + - snap: add new "fde-setup" hooktype + - cmd/snap-bootstrap, secboot, tests: misc cleanups, add spread test + - secboot,cmd/snap-bootstrap: fix degraded mode cases with better + device handling + - boot,dirs,c/snap-bootstrap: avoid InstallHost* at the cost of some + messiness + - tests/nested/manual/refresh-revert-fundamentals: temporarily + disable secure boot + - snap-bootstrap,secboot: call BlockPCRProtectionPolicies in all + boot modes + - many: address degraded recover mode feedback, cleanups + - tests: Use systemd-run on tests part2 + - tests: set the opensuse tumbleweed system as manual in spread.yaml + - secboot: call BlockPCRProtectionPolicies even if the TPM is + disabled + - vendor: update to current secboot + - cmd/snap-bootstrap,o/devicestate: use a secret to pair data and + save + - spread.yaml: increase number of workers on 20.10 + - snap: add new `snap recovery --show-keys` option + - tests: minor test tweaks suggested in the review of 9607 + - snapd-generator: set standard snapfuse options when generating + units for containers + - tests: enable lxd test on ubuntu-core-20 and 16.04-32 + - interfaces: share /tmp/.X11-unix/ from host or provider + - tests: enable main lxd test on 20.10 + - cmd/s-b/initramfs-mounts: refactor recover mode to implement + degraded mode + - gadget/install: add progress logging + - packaging: keep secboot/encrypt_dummy.go in debian + - interfaces/udev: use distro specific path to snap-device-helper + - o/devistate: fix chaining of tasks related to regular snaps when + preseeding + - gadget, overlord/devicestate: validate that system supports + encrypted data before install + - interfaces/fwupd: enforce the confined fwupd to align Ubuntu Core + ESP layout + - many: add /v2/system-recovery-keys API and client + - secboot, many: return UnlockMethod from Unlock* methods for future + usage + - many: mv keys to ubuntu-boot, move model file, rename keyring + prefix for secboot + - tests: using systemd-run instead of manually create a systemd unit + - part 1 + - secboot, cmd/snap-bootstrap: enable or disable activation with + recovery key + - secboot: refactor Unlock...IfEncrypted to take keyfile + check + disks first + - secboot: add LockTPMSealedKeys() to lock access to keys + independently + - gadget: correct sfdisk arguments + - bootloader/assets/grub: adjust fwsetup menuentry label + - tests: new boot state tool + - spread: use the official image for Ubuntu 20.10, no longer an + unstable system + - tests/lib/nested: enable snapd logging to console for core18 + - osutil/disks: re-implement partition searching for disk w/ non- + adjacent parts + - tests: using the nested-state tool in nested tests + - many: seal a fallback object to the recovery boot chain + - gadget, gadget/install: move helpers to install package, refactor + unit tests + - dirs: add "gentoo" to altDirDistros + - update-pot: include file locations in translation template, and + extract strings from desktop files + - gadget/many: drop usage of gpt attr 59 for indicating creation of + partitions + - gadget/quantity: tweak test name + - snap: fix failing unittest for quantity.FormatDuration() + - gadget/quantity: introduce a new package that captures quantities + - o/devicestate,a/sysdb: make a backup of the device serial to save + - tests: fix rare interaction of tests.session and specific tests + - features: enable classic-preserves-xdg-runtime-dir + - tests/nested/core20/save: check the bind mount and size bump + - o/devicetate,dirs: keep device keys in ubuntu-save/save for UC20 + - tests: rename hasHooks to hasInterfaceHooks in the ifacestate + tests + - o/devicestate: unit test tweaks + - boot: store the TPM{PolicyAuthKey,LockoutAuth}File in ubuntu-save + - testutil, cmd/snap/version: fix misc little errors + - overlord/devicestate: bind mount ubuntu-save under + /var/lib/snapd/save on startup + - gadget/internal: tune ext4 setting for smaller filesystems + - tests/nested/core20/save: a test that verifies ubuntu-save is + present and set up + - tests: update google sru backend to support groovy + - o/ifacestate: handle interface hooks when preseeding + - tests: re-enable the apt hooks test + - interfaces,snap: use correct type: {os,snapd} for test data + - secboot: set metadata and keyslots sizes when formatting LUKS2 + volumes + - tests: improve uc20-create-partitions-reinstall test + - client, daemon, cmd/snap: cleanups from #9489 + more unit tests + - cmd/snap-bootstrap: mount ubuntu-save during boot if present + - secboot: fix doc comment on helper for unlocking volume with key + - tests: add spread test for refreshing from an old snapd and core18 + - o/snapstate: generate snapd snap wrappers again after restart on + refresh + - secboot: version bump, unlock volume with key + - tests/snap-advise-command: re-enable test + - cmd/snap, snapmgr, tests: cleanups after #9418 + - interfaces: deny connected x11 plugs access to ICE + - daemon,client: write and read a maintenance.json file for when + snapd is shut down + - many: update to secboot v1 (part 1) + - osutil/disks/mockdisk: panic if same mountpoint shows up again + with diff opts + - tests/nested/core20/gadget,kernel-reseal: add sanity checks to the + reseal tests + - many: implement snap routine console-conf-start for synchronizing + auto-refreshes + - dirs, boot: add ubuntu-save directories and related locations + - usersession: fix typo in test name + - overlord/snapstate: refactor ihibitRefresh + - overlord/snapstate: stop warning about inhibited refreshes + - cmd/snap: do not hardcode snapshot age value + - overlord,usersession: initial notifications of pending refreshes + - tests: add a unit test for UpdateMany where a single snap fails + - o/snapstate/catalogrefresh.go: don't refresh catalog in install + mode uc20 + - tests: also check snapst.Current in undo-unlink tests + - tests: new nested tool + - o/snapstate: implement undo handler for unlink-snap + - tests: clean systems.sh helper and migrate last set of tests + - tests: moving the lib section from systems.sh helper to os.query + tool + - tests/uc20-create-partitions: don't check for grub.cfg + - packaging: make sure that static binaries are indeed static, fix + openSUSE + - many: have install return encryption keys for data and save, + improve tests + - overlord: add link participant for linkage transitions + - tests: lxd smoke test + - tests: add tests for fsck; cmd/s-b/initramfs-mounts: fsck ubuntu- + seed too + - tests: moving main suite from systems.sh to os.query tool + - tests: moving the core test suite from systems.sh to os.query tool + - cmd/snap-confine: mask host's apparmor config + - o/snapstate: move setting updated SnapState after error paths + - tests: add value to INSTANCE_KEY/regular + - spread, tests: tweaks for openSUSE + - cmd/snap-confine: update path to snap-device-helper in AppArmor + profile + - tests: new os.query tool + - overlord/snapshotstate/backend: specify tar format for snapshots + - tests/nested/manual/minimal-smoke: use 384MB of RAM for nested + UC20 + - client,daemon,snap: auto-import does not error on managed devices + - interfaces: PTP hardware clock interface + - tests: use tests.backup tool + - many: verify that unit tests work with nosecboot tag and without + secboot package + - wrappers: do not error out on read-only /etc/dbus-1/session.d + filesystem on core18 + - snapshots: import of a snapshot set + - tests: more output for sbuild test + - o/snapstate: re-order remove tasks for individual snap revisions + to remove current last + - boot: skip some unit tests when running as root + - o/assertstate: introduce + ValidationTrackingKey/ValidationSetTracking and basic methods + - many: allow ignoring running apps for specific request + - tests: allow the searching test to fail under load + - overlord/snapstate: inhibit startup while unlinked + - seed/seedwriter/writer.go: check DevModeConfinement for dangerous + features + - tests/main/sudo-env: snap bin is available on Fedora + - boot, overlord/devicestate: list trusted and managed assets + upfront + - gadget, gadget/install: support for ubuntu-save, create one during + install if needed + - spread-shellcheck: temporary workaround for deadlock, drop + unnecessary test + - snap: support different exit-code in the snap command + - logger: use strutil.KernelCommandLineSplit in + debugEnabledOnKernelCmdline + - logger: fix snapd.debug=1 parsing + - overlord: increase refresh postpone limit to 14 days + - spread-shellcheck: use single thread pool executor + - gadget/install,secboot: add debug messages + - spread-shellcheck: speed up spread-shellcheck even more + - spread-shellcheck: process paths from arguments in parallel + - tests: tweak error from tests.cleanup + - spread: remove workaround for openSUSE go issue + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - tests: new tests.backup tool + - tests: add tests.cleanup pop sub-command + - tests: migration of the main suite to snaps-state tool part 6 + - tests: fix journal-state test + - cmd/snap-bootstrap/initramfs-mounts: split off new helper for misc + recover files + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - boot, gadget, bootloader: observer preserves managed bootloader + configs + - tests/nested/manual: add uc20 grade signed cloud-init test + - o/snapstate/autorefresh.go: eliminate race when launching + autorefresh + - daemon,snapshotstate: do not return "size" from Import() + - daemon: limit reading from snapshot import to Content-Length + - many: set/expect Content-Length header when importing snapshots + - github: switch from ::set-env command to environment file + - tests: migration of the main suite to snaps-state tool part 5 + - client: cleanup the Client.raw* and Client.do* method families + - tests: moving main suite to snaps-state tool part 4 + - client,daemon,snap: use constant for snapshot content-type + - many: fix typos and repeated "the" + - secboot: fix tpm connection leak when it's not enabled + - many: scaffolding for snapshots import API + - run-checks: run spread-shellcheck too + - interfaces: update network-manager interface to allow + ObjectManager access from unconfined clients + - tests: move core and regression suites to snaps-state tool + - tests: moving interfaces tests to snaps-state tool + - gadget: preserve files when indicated by content change observer + - tests: moving smoke test suite and some tests from main suite to + snaps-state tool + - o/snapshotstate: pass set id to backend.Open, update tests + - asserts/snapasserts: introduce ValidationSets + - o/snapshotstate: improve allocation of new set IDs + - boot: look at the gadget for run mode bootloader when making the + system bootable + - cmd/snap: allow snap help vs --all to diverge purposefully + - usersession/userd: separate bus name ownership from defining + interfaces + - o/snapshotstate: set snapshot set id from its filename + - o/snapstate: move remove-related tests to snapstate_remove_test.go + - desktop/notification: switch ExpireTimeout to time.Duration + - desktop/notification: add unit tests + - snap: snap help output refresh + - tests/nested/manual/preseed: include a system-usernames snap when + preseeding + - tests: fix sudo-env test + - tests: fix nested core20 shellcheck bug + - tests/lib: move to new directory when restoring PWD, cleanup + unpacked unpacked snap directories + - desktop/notification: add bindings for FDO notifications + - dbustest: fix stale comment references + - many: move ManagedAssetsBootloader into TrustedAssetsBootloader, + drop former + - snap-repair: add uc20 support + - tests: print all the serial logs for the nested test + - o/snapstate/check_snap_test.go: mock osutil.Find{U,G}id to avoid + bug in test + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - osutil/group.go: treat all non-nil errs from user.Lookup{Group,} + as Unknown* + - asserts: deserialize grouping only once in Pool.AddBatch if needed + - gadget: allow content observer to have opinions about a change + - tests: new snaps-state command - part1 + - o/assertstate: support refreshing any number of snap-declarations + - boot: use test helpers + - tests/core/snap-debug-bootvars: also check snap_mode + - many/apparmor: adjust rules for reading profile/ execing new + profiles for new kernel + - tests/core/snap-debug-bootvars: spread test for snap debug boot- + vars + - tests/lib/nested.sh: more little tweaks + - tests/nested/manual/grade-signed-above-testkeys-boot: enable kvm + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - overlord: explicitly set refresh-app-awareness in tests + - kernel: remove "edition" from kernel.yaml and add "update" + - spread: drop vendor from the packed project archive + - boot: fix debug bootloader variables dump on UC20 systems + - wrappers, systemd: allow empty root dir and conditionally do not + pass --root to systemctl + - tests/nested/manual: add test for grades above signed booting with + testkeys + - tests/nested: misc robustness fixes + - o/assertstate,asserts: use bulk refresh to refresh snap- + declarations + - tests/lib/prepare.sh: stop patching the uc20 initrd since it has + been updated now + - tests/nested/manual/refresh-revert-fundamentals: re-enable test + - update-pot: ignore .go files inside .git when running xgettext-go + - tests: disable part of the lxd test completely on 16.04. + - o/snapshotstate: tweak comment regarding snapshot filename + - o/snapstate: improve snapshot iteration + - bootloader: lk cleanups + - tests: update to support nested kvm without reboots on UC20 + - tests/nested/manual/preseed: disable system-key check for 20.04 + image + - spread.yaml: add ubuntu-20.10-64 to qemu + - store: handle v2 error when fetching assertions + - gadget: resolve device mapper devices for fallback device lookup + - tests/nested/cloud-init-many: simplify tests and unify + helpers/seed inputs + - tests: copy /usr/lib/snapd/info to correct directory + - check-pr-title.py * : allow "*" in the first part of the title + - many: typos and small test tweak + - tests/main/lxd: disable cgroup combination for 16.04 that is + failing a lot + - tests: make nested signing helpers less confusing + - tests: misc nested changes + - tests/nested/manual/refresh-revert-fundamentals: disable + temporarily + - tests/lib/cla_check: default to Python 3, tweaks, formatting + - tests/lib/cl_check.py: use python3 compatible code + +* Thu Oct 08 2020 Michael Vogt +- New upstream release 2.47.1 + - o/configstate: create /etc/sysctl.d when applying early config + defaults + - cmd/snap-bootstrap/initramfs-mounts: also copy /etc/machine-id for + same IP addr + - packaging/{ubuntu,debian}: add liblzo2-dev as a dependency for + building snapd + - cmd/snap: allow snap help vs --all to diverge purposefully + - snap: snap help output refresh + +* Tue Sep 29 2020 Michael Vogt +- New upstream release 2.47 + - tests: fix nested core20 shellcheck bug + - many/apparmor: adjust rule for reading apparmor profile for new + kernel + - snap-repair: add uc20 support + - cmd/snap/auto-import: stop importing system user assertions from + initramfs mnts + - cmd/s-b/initramfs-mounts: use ConfigureTargetSystem for install, + recover modes + - gadget: resolve device mapper devices for fallback device lookup + - secboot: add boot manager profile to pcr protection profile + - sysconfig,o/devicestate: mv DisableNoCloud to + DisableAfterLocalDatasourcesRun + - tests: make gadget-reseal more robust + - tests: skip nested images pre-configuration by default + - tests: fix for basic20 test running on external backend and rpi + - tests: improve kernel reseal test + - boot: adjust comments, naming, log success around reseal + - tests/nested, fakestore: changes necessary to run nested uc20 + signed/secured tests + - tests: add nested core20 gadget reseal test + - boot/modeenv: track unknown keys in Read and put back into modeenv + during Write + - interfaces/process-control: add sched_setattr to seccomp + - boot: with unasserted kernels reseal if there's a hint modeenv + changed + - client: bump the default request timeout to 120s + - configcore: do not error in console-conf.disable for install mode + - boot: streamline bootstate20.go reseal and tests changes + - boot: reseal when changing kernel + - cmd/snap/model: specify grade in the model command output + - tests: simplify + repack_snapd_snap_with_deb_content_and_run_mode_first_boot_tweaks + - test: improve logging in nested tests + - nested: add support to telnet to serial port in nested VM + - secboot: use the snapcore/secboot native recovery key type + - tests/lib/nested.sh: use more focused cloud-init config for uc20 + - tests/lib/nested.sh: wait for the tpm socket to exist + - spread.yaml, tests/nested: misc changes + - tests: add more checks to disk space awareness spread test + - tests: disk space awareness spread test + - boot: make MockUC20Device use a model and MockDevice more + realistic + - boot,many: reseal only when meaningful and necessary + - tests/nested/core20/kernel-failover: add test for failed refresh + of uc20 kernel + - tests: fix nested to work with qemu and kvm + - boot: reseal when updating boot assets + - tests: fix snap-routime-portal-info test + - boot: verify boot chain file in seal and reseal tests + - tests: use full path to test-snapd-refresh.version binary + - boot: store boot chains during install, helper for checking + whether reseal is needed + - boot: add call to reseal an existing key + - boot: consider boot chains with unrevisioned kernels incomparable + - overlord: assorted typos and miscellaneous changes + - boot: group SealKeyModelParams by model, improve testing + - secboot: adjust parameters to buildPCRProtectionProfile + - strutil: add SortedListsUniqueMergefrom the doc comment: + - snap/naming: upgrade TODO to TODO:UC20 + - secboot: add call to reseal an existing key + - boot: in seal.go adjust error message and function names + - o/snapstate: check available disk space in RemoveMany + - boot: build bootchains data for sealing + - tests: remove "set -e" from function only shell libs + - o/snapstate: disk space check on UpdateMany + - o/snapstate: disk space check with snap update + - snap: implement new `snap reboot` command + - boot: do not reorder boot assets when generating predictable boot + chains and other small tweaks + - tests: some fixes and improvements for nested execution + - tests/core/uc20-recovery: fix check for at least specific calls to + mock-shutdown + - boot: be consistent using bootloader.Role* consts instead of + strings + - boot: helper for generating secboot load chains from a given boot + asset sequence + - boot: tweak boot chains to support a list of kernel command lines, + keep track of model and kernel boot file + - boot,secboot: switch to expose and use snapcore/secboot load event + trees + - tests: use `nested_exec` in core{20,}-early-config test + - devicestate: enable cloud-init on uc20 for grade signed and + secured + - boot: add "rootdir" to baseBootenvSuite and use in tests + - tests/lib/cla_check.py: don't allow users.noreply.github.com + commits to pass CLA + - boot: represent boot chains, helpers for marshalling and + equivalence checks + - boot: mark successful with boot assets + - client, api: handle insufficient space error + - o/snapstate: disk space check with single snap install + - configcore: "service.console-conf.disable" is gadget defaults only + - packaging/opensuse: fix for /usr/libexec on TW, do not hardcode + AppArmor profile path + - tests: skip udp protocol in nfs-support test on ubuntu-20.10 + - packaging/debian-sid: tweak code preparing _build tree + - many: move seal code from gadget/install to boot + - tests: remove workaround for cups on ubuntu-20.10 + - client: implement RebootToSystem + - many: seed.Model panics now if called before LoadAssertions + - daemon: add /v2/systems "reboot" action API + - github: run tests also on push to release branches + - interfaces/bluez: let slot access audio streams + - seed,c/snap-bootstrap: simplify snap-bootstrap seed reading with + new seed.ReadSystemEssential + - interfaces: allow snap-update-ns to read /proc/cmdline + - tests: new organization for nested tests + - o/snapstate, features: add feature flags for disk space awareness + - tests: workaround for cups issue on 20.10 where default printer is + not configured. + - interfaces: update cups-control and add cups for providing snaps + - boot: keep track of the original asset when observing updates + - tests: simplify and fix tests for disk space checks on snap remove + - sysconfig/cloudinit.go: add AllowCloudInit and use GadgetDir for + cloud.conf + - tests/main: mv core specific tests to core suite + - tests/lib/nested.sh: reset the TPM when we create the uc20 vm + - devicestate: rename "mockLogger" to "logbuf" + - many: introduce ContentChange for tracking gadget content in + observers + - many: fix partion vs partition typo + - bootloader: retrieve boot chains from bootloader + - devicestate: add tests around logging in RequestSystemAction + - boot: handle canceled update + - bootloader: tweak doc comments (thanks Samuele) + - seed/seedwriter: test local asserted snaps with UC20 grade signed + - sysconfig/cloudinit.go: add DisableNoCloud to + CloudInitRestrictOptions + - many: use BootFile type in load sequences + - boot,bootloader: clarifications after the changes to introduce + bootloader.Options.Role + - boot,bootloader,gadget: apply new bootloader.Options.Role + - o/snapstate, features: add feature flag for disk space check on + remove + - testutil: add checkers for symbolic link target + - many: refactor tpm seal parameter setting + - boot/bootstate20: reboot to rollback to previous kernel + - boot: add unit test helpers + - boot: observe update & rollback of trusted assets + - interfaces/utf: Add MIRKey to u2f devices + - o/devicestate/devicestate_cloudinit_test.go: test cleanup for uc20 + cloud-init tests + - many: check that users of BaseTest don't forget to consume + cleanups + - tests/nested/core20/tpm: verify trusted boot assets tracking + - github: run macOS job with Go 1.14 + - many: misc doc-comment changes and typo fixes + - o/snapstate: disk space check with InstallMany + - many: cloud-init cleanups from previous PR's + - tests: running tests on opensuse leap 15.2 + - run-checks: check for dirty build tree too + - vendor: run ./get-deps.sh to update the secboot hash + - tests: update listing test for "-dirty" versions + - overlord/devicestate: do not release the state lock when updating + gadget assets + - secboot: read kernel efi image from snap file + - snap: add size to the random access file return interface + - daemon: correctly parse Content-Type HTTP header. + - tests: account for apt-get on core18 + - cmd/snap-bootstrap/initramfs-mounts: compute string outside of + loop + - mkversion.sh: simple hack to include dirty in version if the tree + is dirty + - cgroup,snap: track hooks on system bus only + - interfaces/systemd: compare dereferenced Service + - run-checks: only check files in git for misspelling + - osutil: add a package doc comment (via doc.go) + - boot: complain about reused asset name during initial install + - snapstate: installSize helper that calculates total size of snaps + and their prerequisites + - snapshots: export of snapshots + - boot/initramfs_test.go: reset boot vars on the bootloader for each + iteration + +* Fri Sep 04 2020 Michael Vogt +- New upstream release 2.46.1 + - interfaces: allow snap-update-ns to read + /proc/cmdline + - github: run macOS job with Go 1.14 + - o/snapstate, features: add feature flag for disk space check on + remove + - tests: account for apt-get on core18 + - mkversion.sh: include dirty in version if the tree + is dirty + - interfaces/systemd: compare dereferenced Service + - vendor.json: update mysterious secboot SHA again + +* Tue Aug 25 2020 Michael Vogt +- New upstream release 2.46 + - logger: add support for setting snapd.debug=1 on kernel cmdline + - o/snapstate: check disk space before creating automatic snapshot + on remove + - boot, o/devicestate: observe existing recovery bootloader trusted + boot assets + - many: use transient scope for tracking apps and hooks + - features: add HiddenSnapFolder feature flag + - tests/lib/nested.sh: fix partition typo, unmount the image on uc20 + too + - runinhibit: open the lock file in read-only mode in IsLocked + - cmd/s-b/initramfs-mounts: make recover -> run mode transition + automatic + - tests: update spread test for unknown plug/slot with snapctl is- + connected + - osutil: add OpenExistingLockForReading + - kernel: add kernel.Validate() + - interfaces: add vcio interface + - interfaces/{docker,kubernetes}-support: load overlay and support + systemd cgroup driver + - tests/lib/nested.sh: use more robust code for finding what loop + dev we mounted + - cmd/snap-update-ns: detach all bind-mounted file + - snap/snapenv: set SNAP_REAL_HOME + - packaging: umount /snap on purge in containers + - interfaces: misc policy updates xlvi + - secboot,cmd/snap-bootstrap: cross-check partitions before + unlocking, mounting + - boot: copy boot assets cache to new root + - gadget,kernel: add new kernel.{Info,Asset} struct and helpers + - o/hookstate/ctlcmd: make is-connected check whether the plug or + slot exists + - tests: find -ignore_readdir_race when scanning cgroups + - interfaces/many: deny arbitrary desktop files and misc from + /usr/share + - tests: use "set -ex" in prep-snapd-in-lxd.sh + - tests: re-enable udisks test on debian-sid + - cmd/snapd-generator: use PATH fallback if PATH is not set + - tests: disable udisks2 test on arch linux + - github: use latest/stable go, not latest/edge + - tests: remove support for ubuntu 19.10 from spread tests + - tests: fix lxd test wrongly tracking 'latest' + - secboot: document exported functions + - cmd: compile snap gdbserver shim correctly + - many: correctly calculate the desktop file prefix everywhere + - interfaces: add kernel-crypto-api interface + - corecfg: add "system.timezone" setting to the system settings + - cmd/snapd-generator: generate drop-in to use fuse in container + - cmd/snap-bootstrap/initramfs-mounts: tweak names, add comments + from previous PR + - interfaces/many: miscellaneous updates for strict microk8s + - secboot,cmd/snap-bootstrap: don't import boot package from secboot + - cmd/snap-bootstrap/initramfs-mounts: call systemd-mount instead of + the-tool + - tests: work around broken update of systemd-networkd + - tests/main/install-fontconfig-cache-gen: enhance test by + verifying, add fonts to test + - o/devicestate: wrap asset update observer error + - boot: refactor such that bootStateUpdate20 mainly carries Modeenv + - mkversion.sh: disallow changelog versions that have git in it, if + we also have git version + - interfaces/many: miscellaneous updates for strict microk8s + - snap: fix repeated "cannot list recovery system" and add test + - boot: track trusted assets during initial install, assets cache + - vendor: update secboot to fix key data validation + - tests: unmount FUSE file-systems from XDG runtime dir + - overlord/devicestate: workaround non-nil interface with nil struct + - sandbox/cgroup: remove temporary workaround for multiple cgroup + writers + - sandbox/cgroup: detect dangling v2 cgroup + - bootloader: add helper for creating a bootloader based on gadget + - tests: support different images on nested execution + - many: reorg cmd/snapinfo.go into snap and new client/clientutil + - packaging/arch: use external linker when building statically + - tests: cope with ghost cgroupv2 + - tests: fix issues related to restarting systemd-logind.service + - boot, o/devicestate: TrustedAssetUpdateObserver stubs, hook up to + gadget updates + - vendor: update github.com/kr/pretty to fix diffs of values with + pointer cycles + - boot: move bootloaderKernelState20 impls to separate file + - .github/workflows: move snap building to test.yaml as separate + cached job + - tests/nested/manual/minimal-smoke: run core smoke tests in a VM + meeting minimal requirements + - osutil: add CommitAs to atomic file + - gadget: introduce content update observer + - bootloader: introduce TrustedAssetsBootloader, implement for grub + - o/snapshotstate: helpers for calculating disk space needed for an + automatic snapshot + - gadget/install: retrieve command lines from bootloader + - boot/bootstate20: unify commit method impls, rm + bootState20MarkSuccessful + - tests: add system information and image information when debug + info is displayed + - tests/main/cgroup-tracking: try to collect some information about + cgroups + - boot: introduce current_boot_assets and + current_recovery_boot_assets to modeenv + - tests: fix for timing issues on journal-state test + - many: remove usage and creation of hijacked pid cgroup + - tests: port regression-home-snap-root-owned to tests.session + - tests: run as hightest via tests.session + - github: run CLA checks on self-hosted workers + - github: remove Ubuntu 19.10 from actions workflow + - tests: remove End-Of-Life opensuse/fedora releases + - tests: remove End-Of-Life releases from spread.yaml + - tests: fix debug section of appstream-id test + - interfaces: check !b.preseed earlier + - tests: work around bug in systemd/debian + - boot: add deepEqual, Copy helpers for Modeenv to simplify + bootstate20 refactor + - cmd: add new "snap recovery" command + - interfaces/systemd: use emulation mode when preseeding + - interfaces/kmod: don't load kernel modules in kmod backend when + preseeding + - interfaces/udev: do not reload udevadm rules when preseeding + - cmd/snap-preseed: use snapd from the deb if newer than from seeds + - boot: fancy marshaller for modeenv values + - gadget, osutil: use atomic file copy, adjust tests + - overlord: use new tracking cgroup for refresh app awareness + - github: do not skip gofmt with Go 1.9/1.10 + - many: introduce content write observer, install mode glue, initial + seal stubs + - daemon,many: switch to use client.ErrorKind and drop the local + errorKind... + - tests: new parameters for nested execution + - client: move all error kinds into errors.go and add doc strings + - cmd/snap: display the error in snap debug seeding if seeding is in + error + - cmd/snap/debug/seeding: use unicode for proper yaml + - tests/cmd/snap-bootstrap/initramfs-mounts: add test case for empty + recovery_mode + - osutil/disks: add mock disk and tests for happy path of mock disks + - tests: refresh/revert snapd in uc20 + - osutil/disks: use a dedicated error to indicate a fs label wasn't + found + - interfaces/system-key: in WriteSystemKey during tests, don't call + ParserFeatures + - boot: add current recovery systems to modeenv + - bootloader: extend managed assets bootloader interface to compose + a candidate command line + - interfaces: make the unmarshal test match more the comment + - daemon/api: use pointers to time.Time for debug seeding aspect + - o/ifacestate: update security profiles in connect undo handler + - interfaces: add uinput interface + - cmd/snap-bootstrap/initramfs-mounts: add doSystemdMount + unit + tests + - o/devicestate: save seeding/preseeding times for use with debug + seeding api + - cmd/snap/debug: add "snap debug seeding" command for preseeding + debugging + - tests/main/selinux-clean: workaround SELinux denials triggered by + linger setup on Centos8 + - bootloader: compose command line with mode and extra arguments + - cmd/snap, daemon: detect and bail purge on multi-snap + - o/ifacestate: fix bug in snapsWithSecurityProfiles + - interfaces/builtin/multipass: replace U+00A0 no-break space with + simple space + - bootloader/assets: generate bootloader assets from files + - many/tests/preseed: reset the preseeded images before preseeding + them + - tests: drop accidental accents from e + - secboot: improve key sealing tests + - tests: replace _wait_for_file_change with retry + - tests: new fs-state which replaces the files.sh helper + - sysconfig/cloudinit_test.go: add test for initramfs case, rm "/" + from path + - cmd/snap: track started apps and hooks + - tests/main/interfaces-pulseaudio: disable start limit checking for + pulseaudio service + - api: seeding debug api + - .github/workflows/snap-build.yaml: build the snapd snap via GH + Actions too + - tests: moving journalctl.sh to a new journal-state tool + - tests/nested/manual: add spread tests for cloud-init vuln + - bootloader/assets: helpers for registering per-edition snippets, + register snippets for grub + - data,packaging,wrappers: extend D-Bus service activation search + path + - spread: add opensuse 15.2 and tumbleweed for qemu + - overlord,o/devicestate: restrict cloud-init on Ubuntu Core + - sysconfig/cloudinit: add RestrictCloudInit + - cmd/snap-preseed: check that target path exists and is a directory + on --reset + - tests: check for pids correctly + - gadget,gadget/install: refactor partition table update + - sysconfig/cloudinit: add CloudInitStatus func + CloudInitState + type + - interface/fwupd: add more policies for making fwupd upstream + strict + - tests: new to-one-line tool which replaces the strings.sh helper + - interfaces: new helpers to get and compare system key, for use + with seeding debug api + - osutil, many: add helper for checking whether the process is a go + test binary + - cmd/snap-seccomp/syscalls: add faccessat2 + - tests: adjust xdg-open after launcher changes + - tests: new core config helper + - usersession/userd: do not modify XDG_DATA_DIRS when calling xdg- + open + - cmd/snap-preseed: handle relative chroot path + - snapshotstate: move sizer to osutil.Sizer() + - tests/cmd/snap-bootstrap/initramfs-mounts: rm duplicated env ref + kernel tests + - gadget/install,secboot: use snapcore/secboot luks2 api + - boot/initramfs_test.go: add Commentf to more Assert()'s + - tests/lib: account for changes in arch package file name extension + - bootloader/bootloadertest: fix comment typo + - bootloader: add helper for getting recovery system environment + variables + - tests: preinstall shellcheck and run tests on focal + - strutil: add a helper for parsing kernel command line + - osutil: add CheckFreeSpace helper + - secboot: update tpm connection error handling + - packaging, cmd/snap-mgmt, tests: remove modules files on purge + - tests: add tests.cleanup helper + - packaging: add "ca-certificates" to build-depends + - tests: more checks in core20 early config spread test + - tests: fix some snapstate tests to use pointers for + snapmgrTestSuite + - boot: better naming of helpers for obtaining kernel command line + - many: use more specific check for unit test mocking + - systemd/escape: fix issues with "" and "\t" handling + - asserts: small improvements and corrections for sequence-forming + assertions' support + - boot, bootloader: query kernel command line of run mod and + recovery mode systems + - snap/validate.go: disallow snap layouts with new top-level + directories + - tests: allow to add a new label to run nested tests as part of PR + validation + - tests/core/gadget-update-pc: port to UC20 + - tests: improve nested tests flexibility + - asserts: integer headers: disallow prefix zeros and make parsing + more uniform + - asserts: implement Database.FindSequence + - asserts: introduce SequenceMemberAfter in the asserts backstores + - spread.yaml: remove tests/lib/tools from PATH + - overlord: refuse to install snaps whose activatable D-Bus services + conflict with installed snaps + - tests: shorten lxd-state undo-mount-changes + - snap-confine: don't die if a device from sysfs path cannot be + found by udev + - tests: fix argument handling of apt-state + - tests: rename lxd-tool to lxd-state + - tests: rename user-tool to user-state, fix --help + - interfaces: add gconf interface + - sandbox/cgroup: avoid parsing security tags twice + - tests: rename version-tool to version-compare + - cmd/snap-update-ns: handle anomalies better + - tests: fix call to apt.Package.mark_install(auto_inst=True) + - tests: rename mountinfo-tool to mountinfo.query + - tests: rename memory-tool to memory-observe-do + - tests: rename invariant-tool to tests.invariant + - tests: rename apt-tool to apt-state + - many: managed boot config during run mode setup + - asserts: introduce the concept of sequence-forming assertion types + - tests: tweak comments/output in uc20-recovery test + - tests/lib/pkgdb: do not use quiet when purging debs + - interfaces/apparmor: allow snap-specific /run/lock + - interfaces: add system-source-code for access to /usr/src + - sandbox/cgroup: extend SnapNameFromPid with tracking cgroup data + - gadget/install: move udev trigger to gadget/install + - many: make nested spread tests more reliable + - tests/core/uc20-recovery: apply hack to get gopath in recover mode + w/ external backend + - tests: enable tests on uc20 which now work with the real model + assertion + - tests: enable system-snap-refresh test on uc20 + - gadget, bootloader: preserve managed boot assets during gadget + updates + - tests: fix leaked dbus-daemon in selinux-clean + - tests: add servicestate.Control tests + - tests: fix "restart.service" + - wrappers: helper for enabling services - extract and move enabling + of services into a helper + - tests: new test to validate refresh and revert of kernel and + gadget on uc20 + - tests/lib/prepare-restore: collect debug info when prepare purge + fails + - bootloader: allow managed bootloader to update its boot config + - tests: Remove unity test from nightly test suite + - o/devicestate: set mark-seeded to done in the task itself + - tests: add spread test for disconnect undo caused by failing + disconnect hook + - sandbox/cgroup: allow discovering PIDs of given snap + - osutil/disks: support IsDecryptedDevice for mountpoints which are + dm devices + - osutil: detect autofs mounted in /home + - spread.yaml: allow amazon-linux-2-64 qemu with + ec2-user/ec2-user + - usersession: support additional zoom URL schemes + - overlord: mock timings.DurationThreshold in TestNewWithGoodState + - sandbox/cgroup: add tracking helpers + - tests: detect stray dbus-daemon + - overlord: refuse to install snaps providing user daemons on Ubuntu + 14.04 + - many: move encryption and installer from snap-boostrap to gadget + - o/ifacestate: fix connect undo handler + - interfaces: optimize rules of multiple connected iio/i2c/spi plugs + - bootloader: introduce managed bootloader, implement for grub + - tests: fix incorrect check in smoke/remove test + - asserts,seed: split handling of essential/not essential model + snaps + - gadget: fix typo in mounted filesystem updater + - gadget: do only one mount point lookup in mounted fs updater + - tests/core/snap-auto-mount: try to make the test more robust + - tests: adding ubuntu-20.04 to google-sru backend + - o/servicestate: add updateSnapstateServices helper + - bootloader: pull recovery grub config from internal assets + - tests/lib/tools: apply linger workaround when needed + - overlord/snapstate: graceful handling of denied "managed" refresh + schedule + - snapstate: fix autorefresh from classic->strict + - overlord/configstate: add system.kernel.printk.console-loglevel + option + - tests: fix assertion disk handling for nested UC systems + - snapstate: use testutil.HostScaledTimeout() in snapstate tests + - tests: extra worker for google-nested backend to avoid timeout + error on uc20 + - snapdtool: helper to check whether the current binary is reexeced + from a snap + - tests: mock servicestate in api tests to avoid systemctl checks + - many: rename back snap.Info.GetType to Type + - tests/lib/cla_check: expect explicit commit range + - osutil/disks: refactor diskFromMountPointImpl a bit + - o/snapstate: service-control task handler + - osutil: add disks pkg for associating mountpoints with + disks/partitions + - gadget,cmd/snap-bootstrap: move partitioning to gadget + - seed: fix LoadEssentialMeta when gadget is not loaded + - cmd/snap: Debian does not allow $SNAP_MOUNT_DIR/bin in sudo + secure_path + - asserts: introduce new assertion validation-set + - asserts,daemon: add support for "serials" field in system-user + assertion + - data/sudo: drop a failed sudo secure_path workaround + - gadget: mv encodeLabel to osutil/disks.EncodeHexBlkIDFormat + - boot, snap-bootstrap: move initramfs-mounts logic to boot pkg + - spread.yaml: update secure boot attribute name + - interfaces/block_devices: add NVMe subsystem devices, support + multipath paths + - tests: use the "jq" snap from the edge channel + - tests: simplify the tpm test by removing the test-snapd-mokutil + snap + - boot/bootstate16.go: clean snap_try_* vars when not in Trying + status too + - tests/main/sudo-env: check snap path under sudo + - tests/main/lxd: add test for snaps inside nested lxd containers + not working + - asserts/internal: expand errors about invalid serialized grouping + labels + - usersession/userd: add msteams url support + - tests/lib/prepare.sh: adjust comment about sgdisk + - tests: fix how gadget pc is detected when the snap does not exist + and ls fails + - tests: move a few more tests to snapstate_update_test.go + - tests/main: add spread test for running svc from install hook + - tests/lib/prepare: increase the size of the uc16/uc18 partitions + - tests/special-home-can-run-classic-snaps: re-enable + - workflow: test PR title as part of the static checks again + - tests/main/xdg-open-compat: backup and restore original xdg-open + - tests: move update-related tests to snapstate_update_test.go + - cmd,many: move Version and bits related to snapd tools to + snapdtool, merge cmdutil + - tests/prepare-restore.sh: reset-failed systemd-journald before + restarting + - interfaces: misc small interface updates + - spread: use find rather than recursive ls, skip mounted snaps + - tests/lib/prepare-restore.sh: if we failed to purge snapd deb, ls + /var/lib/snapd + - tests: enable snap-auto-mount test on core20 + - cmd/snap: do not show $PATH warning when executing under sudo on a + known distro + - asserts/internal: add some iteration benchmarks + - sandbox/cgroup: improve pid parsing code + - snap: add new `snap run --experimental-gdbserver` option + - asserts/internal: limit Grouping size switching to a bitset + representationWe don't always use the bit-set representation + because: + - snap: add an activates-on property to apps for D-Bus activation + - dirs: delete unused Cloud var, fix typo + - sysconfig/cloudinit: make callers of DisableCloudInit use + WritableDefaultsDir + - tests: fix classic ubuntu core transition auth + - tests: fail in setup_reflash_magic() if there is snapd state left + - tests: port interfaces-many-core-provided to tests.session + - tests: wait after creating partitions with sfdisk + - bootloader: introduce bootloarder assets, import grub.cfg with an + edition marker + - riscv64: bump timeouts + - gadget: drop dead code, hide exports that are not used externally + - tests: port 2 uc20 part1 + - tests: fix bug waiting for snap command to be ready + - tests: move try-related tests to snapstate_try_test.go + - tests: add debug for 20.04 prepare failure + - travis.yml: removed, all our checks run in GH actions now + - tests: clean up up the use of configcoreSuite in the configcore + tests + - sandbox/cgroup: remove redundant pathOfProcPidCgroup + - sandbox/cgroup: add tests for ParsePids + - tests: fix the basic20 test for uc20 on external backend + - tests: use configcoreSuite in journalSuite and remove some + duplicated code + - tests: move a few more tests to snapstate_install_test + - tests: assorted small patches + - dbusutil/dbustest: separate license from package + - interfaces/builtin/time-control: allow POSIX clock API + - usersession/userd: add "slack" to the white list of URL schemes + handled by xdg-open + - tests: check that host settings like hostname are settable on core + - tests: port xdg-settings test to tests.session + - tests: port snap-handle-link test to tests.session + - arch: add riscv64 + - tests: core20 early defaults spread test + - tests: move install tests from snapstate_test.go to + snapstate_install_test.go + - github: port macOS sanity checks from travis + - data/selinux: allow checking /var/cache/app-info + - o/devicestate: core20 early config from gadget defaults + - tests: autoremove after removing lxd in preseed-lxd test + - secboot,cmd/snap-bootstrap: add tpm sealing support to secboot + - sandbox/cgroup: move FreezerCgroupDir from dirs.go + - tests: update the file used to detect the boot path on uc20 + - spread.yaml: show /var/lib/snapd in debug + - cmd/snap-bootstrap/initramfs-mounts: also copy systemd clock + + netplan files + - snap/naming: add helpers to parse app and hook security tags + - tests: modernize retry tool + - tests: fix and trim debug section in xdg-open-portal + - tests: modernize and use snapd.tool + - vendor: update to latest github.com/snapcore/bolt for riscv64 + - cmd/snap-confine: add support for libc6-lse + - interfaces: miscellaneous policy updates xlv + - interfaces/system-packages-doc: fix typo in variable names + - tests: port interfaces-calendar-service to tests.session + - tests: install/run the lzo test snap too + - snap: (small) refactor of `snap download` code for + testing/extending + - data: fix shellcheck warnings in snapd.sh.in + - packaging: disable buildmode=pie for riscv64 + - tests: install test-snapd-rsync snap from edge channel + - tests: modernize tests.session and port everything using it + - tests: add ubuntu 20.10 to spread tests + - cmd/snap/remove: mention snap restore/automatic snapshots + - dbusutil: move all D-Bus helpers and D-Bus test helpers + - wrappers: pass 'disable' flag to StopServices wrapper + - osutil: enable riscv64 build + - snap/naming: add ParseSecurityTag and friends + - tests: port document-portal-activation to session-tool + - bootloader: rename test helpers to reflect we are mocking EFI boot + locations + - tests: disable test of nfs v3 with udp proto on debian-sid + - tests: plan to improve the naming and uniformity of utilities + - tests: move *-tool tests to their own suite + - snap-bootstrap: remove sealed key file on reinstall + - bootloader/ubootenv: don't panic with an empty uboot env + - systemd: rename actualFsTypeAndMountOptions to + hostFsTypeAndMountOptions + - daemon: fix filtering of service-control changes for snap.app + - tests: spread test for preseeding in lxd container + - tests: fix broken snapd.session agent.socket + - wrappers: add RestartServices function and ReloadOrRestart to + systemd + - o/cmdstate: handle ignore flag on exec-command tasks + - gadget: make ext4 filesystems with or without metadata checksum + - tests: update statx test to run on all LTS releases + - configcore: show better error when disabling services + - interfaces: add hugepages-control + - interfaces-ssh-keys: Support reading /etc/ssh/ssh_config.d/ + - tests: run ubuntu-20.04-* tests on all ubuntu-2* releases + - tests: skip interfaces-openvswitch for centos 8 in nightly suite + - tests: reload systemd --user for root, if present + - tests: reload systemd after editing /etc/fstab + - tests: add missing dependencies needed for sbuild test on debian + - tests: reload systemd after removing pulseaudio + - image, tests: core18 early config. + - interfaces: add system-packages-doc interface + - cmd/snap-preseed, systemd: fix handling of fuse.squashfuse when + preseeding + - interfaces/fwupd: allow bind mount to /boot on core + - tests: improve oom-vitality tests + - tests: add fedora 32 to spread.yaml + - config: apply vitality-hint immediately when the config changes + - tests: port snap-routine-portal-info to session-tool + - configcore: add "service.console-conf.disable" config option + - tests: port xdg-open to session-tool + - tests: port xdg-open-compat to session-tool + - tests: port interfaces-desktop-* to session-tool + - spread.yaml: apply yaml formatter/linter + - tests: port interfaces-wayland to session-tool + - o/devicestate: refactor current system handling + - snap-mgmt: perform cleanup of user services + - snap/snapfile,squashfs: followups from 8729 + - boot, many: require mode in modeenv + - data/selinux: update policy to allow forked processes to call + getpw*() + - tests: log stderr from dbus-monitor + - packaging: build cmd/snap and cmd/snap-bootstrap with nomanagers + tag + - snap/squashfs: also symlink snap Install with uc20 seed snap dir + layout + - interfaces/builtin/desktop: do not mount fonts cache on distros + with quirks + - data/selinux: allow snapd to remove/create the its socket + - testutil/exec.go: set PATH after running shellcheck + - tests: silence stderr from dbus-monitor + - snap,many: mv Open to snapfile pkg to support add'l options to + Container methods + - devicestate, sysconfig: revert support for cloud.cfg.d/ in the + gadget + - github: remove workaround for bug 133 in actions/cache + - tests: remove dbus.sh + - cmd/snap-preseed: improve mountpoint checks of the preseeded + chroot + - spread.yaml: add ps aux to debug section + - github: run all spread systems in a single go with cached results + - test: session-tool cli tweaks + - asserts: rest of the Pool API + - tests: port interfaces-network-status-classic to session-tool + - packaging: remove obsolete 16.10,17.04 symlinks + - tests: setup portals before starting user session + - o/devicestate: typo fix + - interfaces/serial-port: add NXP SC16IS7xx (ttySCX) to allowed + devices + - cmd/snap/model: support store, system-user-authority keys in + --verbose + - o/devicestate: raise conflict when requesting system action while + seeding + - tests: detect signs of crashed snap-confine + - tests: sign kernel and gadget to run nested tests using current + snapd code + - tests: remove gnome-online-accounts we install + - tests: fix the issue where all the tests were executed on secboot + system + - tests: port interfaces-accounts-service to session-tool + - interfaces/network-control: bring /var/lib/dhcp from host + - image,cmd/snap,tests: add support for store-wide cohort keys + - configcore: add nomanagers buildtag for conditional build + - tests: port interfaces-password-manager-service to session-tool + - o/devicestate: cleanup system actions supported by recover mode + - snap-bootstrap: remove create-partitions and update tests + - tests: fix nested tests + - packaging/arch: update PKGBUILD to match one in AUR + - tests: port interfaces-location-control to session-tool + - tests: port interfaces-contacts-service to session-tool + - state: log task errors in the journal too + - o/devicestate: change how current system is reported for different + modes + - devicestate: do not report "ErrNoState" for seeded up + - tests: add a note about broken test sequence + - tests: port interfaces-autopilot-introspection to session-tool + - tests: port interfaces-dbus to session-tool + - packaging: update sid packaging to match 16.04+ + - tests: enable degraded test on uc20 + - c/snaplock/runinhibit: add run inhibition operations + - tests: detect and report root-owned files in /home + - tests: reload root's systemd --user after snapd tests + - tests: test registration with serial-authority: [generic] + - cmd/snap-bootstrap/initramfs-mounts: copy auth.json and macaroon- + key in recover + - tests/mount-ns: stop binfmt_misc mount unit + - cmd/snap-bootstrap/initramfs-mounts: use booted kernel partition + uuid if available + - daemon, tests: indicate system mode, test switching to recovery + and back to run + - interfaces/desktop: silence more /var/lib/snapd/desktop/icons + denials + - tests/mount-ns: update to reflect new UEFI boot mode + - usersession,tests: clean ups for userd/settings.go and move + xdgopenproxy under usersession + - tests: disable mount-ns test + - tests: test user belongs to systemd-journald, on core20 + - tests: run core/snap-set-core-config on uc20 too + - tests: remove generated session-agent units + - sysconfig: use new _writable_defaults dir to create cloud config + - cmd/snap-bootstrap/initramfs-mounts: cosmetic changes in prep for + future work + - asserts: make clearer that with label we mean a serialized label + - cmd/snap-bootstrap: tweak recovery trigger log messages + - asserts: introduce PoolTo + - userd: allow setting default-url-scheme-handler + - secboot: append uuid to ubuntu-data when decrypting + - o/configcore: pass extra options to FileSystemOnlyApply + - tests: add dbus-user-session to bionic and reorder package names + - boot, bootloader: adjust comments, expand tests + - tests: improve debugging of user session agent tests + - packaging: add the inhibit directory + - many: add core.resiliance.vitality-hint config setting + - tests: test adjustments and fixes for recently published images + - cmd/snap: coldplug auto-import assertions from all removable + devices + - secboot,cmd/snap-bootstrap: move initramfs-mounts tpm access to + secboot + - tests: not fail when boot dir cannot be determined + - tests: new directory used to store the cloud images on gce + - tests: inject snapd from edge into seeds of the image in manual + preseed test + - usersession/agent,wrappers: fix races between Shutdown and Serve + - tests: add dependency needed for next upgrade of bionic + - tests: new test user is used for external backend + - cmd/snap: fix the order of positional parameters in help output + - tests: don't create root-owned things in ~test + - tests/lib/prepare.sh: delete patching of the initrd + - cmd/snap-bootstrap/initramfs-mounts: add sudoers to dirs to copy + as well + - progress: tweak multibyte label unit test data + - o/devicestate,cmd/snap-bootstrap: seal to recover mode cmdline + - gadget: fix fallback device lookup for 'mbr' type structures + - configcore: only reload journald if systemd is new enough + - cmd/snap-boostrap, boot: use /run/mnt/data instead of ubuntu-data + - wrappers: allow user mode systemd daemons + - progress: fix progress bar with multibyte duration units + - tests: fix raciness in pulseaudio test + - asserts/internal: introduce Grouping and Groupings + - tests: remove user.sh + - tests: pair of follow-ups from earlier reviews + - overlord/snapstate: warn of refresh/postpone events + - configcore,tests: use daemon-reexec to apply watchdog config + - c/snap-bootstrap: check mount states via initramfsMountStates + - store: implement DownloadAssertions + - tests: run smoke test with different bases + - tests: port user-mounts test to session-tool + - store: handle error-list in fetch-assertions results + - tests: port interfaces-audio-playback-record to session-tool + - data/completion: add `snap` command completion for zsh + - tests/degraded: ignore failure in systemd-vconsole-setup.service + - image: stub implementation of image.Prepare for darwin + - tests: session-tool --restore -u stops user-$UID.slice + - o/ifacestate/handlers.go: fix typo + - tests: port pulseaudio test to session-tool + - tests: port user-session-env to session-tool + - tests: work around journald bug in core16 + - tests: add debug to core-persistent-journal test + - tests: port selinux-clean to session-tool + - tests: port portals test to session-tool, fix portal tests on sid + - tests: adding option --no-install-recommends option also when + install all the deps + - tests: add session-tool --has-systemd-and-dbus + - packaging/debian-sid: add gcc-multilib to build deps + - osutil: expand FileLock to support shared locks and more + - packaging: stop depending on python-docutils + - store,asserts,many: support the new action fetch-assertions + - tests: port snap-session-agent-* to session-tool + - packaging/fedora: disable FIPS compliant crypto for static + binaries + - tests: fix for preseeding failures + +* Tue Jul 28 2020 Samuele Pedroni +- New upstream release, LP: #1875071 + - o/ifacestate: fix bug in snapsWithSecurityProfiles + - tests/main/selinux-clean: workaround SELinux denials triggered by + linger setup on Centos8 + +* Mon Jul 27 2020 Zygmunt Krynicki +- New upstream release, LP: #1875071 + - many: backport _writable_defaults dir changes + - tests: fix incorrect check in smoke/remove test + - cmd/snap-bootstrap,seed: backport of uc20 PRs + - tests: avoid exit when nested type var is not defined + - cmd/snap-preseed: backport fixes + - interfaces: optimize rules of multiple connected iio/i2c/spi plugs + - many: cherry-picks for 2.45, gh-action, test fixes + - tests/lib: account for changes in arch package file name extension + - postrm, snap-mgmt: cleanup modules and other cherry-picks + - snap-confine: don't die if a device from sysfs path cannot be + found by udev + - data/selinux: update policy to allow forked processes to call + getpw*() + - tests/main/interfaces-time-control: exercise setting time via date + - interfaces/builtin/time-control: allow POSIX clock API + - usersession/userd: add "slack" to the white list of URL schemes + handled by xdg-open + +* Fri Jul 10 2020 Michael Vogt +- New upstream release 2.45.2 + - SECURITY UPDATE: sandbox escape vulnerability on snapctl xdg-open + implementation + - usersession/userd/launcher.go: remove XDG_DATA_DIRS environment + variable modification when calling the system xdg-open. Patch + thanks to James Henstridge + - packaging/ubuntu-16.04/snapd.postinst: ensure "snap userd" is + restarted. Patch thanks to Michael Vogt + - CVE-2020-11934 + - SECURITY UPDATE: arbitrary code execution vulnerability on core + devices with access to physical removable media + - devicestate: Disable/restrict cloud-init after seeding. + - CVE-2020-11933 + +* Fri Jun 05 2020 Michael Vogt +- New upstream release 2.45.1 + - data/selinux: allow checking /var/cache/app-info + - cmd/snap-confine: add support for libc6-lse + - interfaces: miscellaneous policy updates xlv + - snap-bootstrap: remove sealed key file on reinstall + - interfaces-ssh-keys: Support reading /etc/ssh/ssh_config.d/ + - gadget: make ext4 filesystems with or without metadata checksum + - interfaces/fwupd: allow bind mount to /boot on core + - tests: cherry-pick test fixes from master + - snap/squashfs: also symlink snap Install with uc20 seed snap dir + layout + - interfaces/serial-port: add NXP SC16IS7xx (ttySCX) to allowed + devices + - snap,many: mv Open to snapfile pkg to support add'l options to + Container methods + - interfaces/builtin/desktop: do not mount fonts cache on distros + with quirks + - devicestate, sysconfig: revert support for cloud.cfg.d/ in the + gadget + - data/completion, packaging: cherry-pick zsh completion + - state: log task errors in the journal too + - devicestate: do not report "ErrNoState" for seeded up + - interfaces/desktop: silence more /var/lib/snapd/desktop/icons + denials + - packaging/fedora: disable FIPS compliant crypto for static + binaries + - packaging: stop depending on python-docutils + +* Tue May 12 2020 Michael Vogt +- New upstream release 2.45 + - o/devicestate: support doing system action reboots from recover + mode + - vendor: update to latest secboot + - tests: not fail when boot dir cannot be determined + - configcore: only reload journald if systemd is new enough + - cmd/snap-bootstrap/initramfs-mounts: append uuid to ubuntu-data + when decrypting + - tests/lib/prepare.sh: delete patching of the initrd + - cmd/snap: coldplug auto-import assertions from all removable + devices + - cmd/snap: fix the order of positional parameters in help output + - c/snap-bootstrap: port mount state mocking to the new style on + master + - cmd/snap-bootstrap/initramfs-mounts: add sudoers to dirs to copy + as well + - o/devicestate,cmd/snap-bootstrap: seal to recover mode cmdline, + unlock in recover mode initramfs + - progress: tweak multibyte label unit test data + - gadget: fix fallback device lookup for 'mbr' type structures + - progress: fix progress bar with multibyte duration units + - many: use /run/mnt/data over /run/mnt/ubuntu-data for uc20 + - many: put the sealed keys in a directory on seed for tidiness + - cmd/snap-bootstrap: measure epoch and model before unlocking + encrypted data + - o/configstate: core config handler for persistent journal + - bootloader/uboot: use secondary ubootenv file boot.sel for uc20 + - packaging: add "$TAGS" to dh_auto_test for debian packaging + - tests: ensure $cache_dir is actually available + - secboot,cmd/snap-bootstrap: add model to pcr protection profile + - devicestate: do not use snap-boostrap in devicestate to install + - tests: fix a typo in nested.sh helper + - devicestate: add support for cloud.cfg.d config from the gadget + - cmd/snap-bootstrap: cleanups, naming tweaks + - testutil: add NewDBusTestConn + - snap-bootstrap: lock access to sealed keys + - overlord/devicestate: preserve the current model inside ubuntu- + boot + - interfaces/apparmor: use differently templated policy for non-core + bases + - seccomp: add get_tls, io_pg* and *time64/*64 variants for existing + syscalls + - cmd/snap-bootstrap/initramfs-mounts: mount ubuntu-seed first, + other misc changes + - o/snapstate: tweak "waiting for restart" message + - boot: store model model and grade information in modeenv + - interfaces/firewall-control: allow -legacy and -nft for core20 + - boot: enable makeBootable20RunMode for EnvRefExtractedKernel + bootloaders + - boot/bootstate20: add EnvRefExtractedKernelBootloader bootstate20 + implementation + - daemon: fix error message from `snap remove-user foo` on classic + - overlord: have a variant of Mock that can take a state.State + - tests: 16.04 and 18.04 now have mediating pulseaudio (again) + - seed: clearer errors for missing essential snapd or core snap + - cmd/snap-bootstrap/initramfs-mounts: support + EnvRefExtractedKernelBootloader's + - gadget, cmd/snap-bootstrap: MBR schema support + - image: improve/adjust DownloadSnap doc comment + - asserts: introduce ModelGrade.Code + - tests: ignore user-12345 slice and service + - image,seed/seedwriter: support redirect channel aka default + tracks + - bootloader: use binary.Read/Write + - tests: uc20 nested suite part II + - tests/boot: refactor to make it easier for new + bootloaderKernelState20 impl + - interfaces/openvswitch: support use of ovs-appctl + - snap-bootstrap: copy auth data from real ubuntu-data in recovery + mode + - snap-bootstrap: seal and unseal encryption key using tpm + - tests: disable special-home-can-run-classic-snaps due to jenkins + repo issue + - packaging: fix build on Centos8 to support BUILDTAGS + - boot/bootstate20: small changes to bootloaderKernelState20 + - cmd/snap: Implement a "snap routine file-access" command + - spread.yaml: switch back to latest/candidate for lxd snap + - boot/bootstate20: re-factor kernel methods to use new interface + for state + - spread.yaml,tests/many: use global env var for lxd channel + - boot/bootstate20: fix bug in try-kernel cleanup + - config: add system.store-certs.[a-zA-Z0-9] support + - secboot: key sealing also depends on secure boot enabled + - httputil: fix client timeout retry tests + - cmd/snap-update-ns: handle EBUSY when unlinking files + - cmd/snap/debug/boot-vars: add opts for setting dir and/or uc20 + vars + - secboot: add tpm support helpers + - tests/lib/assertions/developer1-pi-uc20.model: use 20/edge for + kernel and gadget + - cmd/snap-bootstrap: switch to a 64-byte key for unlocking + - tests: preserve size for centos images on spread.yaml + - github: partition the github action workflows + - run-checks: use consistent "Checking ..." style messages + - bootloader: add efi pkg for reading efi variables + - data/systemd: do not run snapd.system-shutdown if finalrd is + available + - overlord: update tests to work with latest go + - cmd/snap: do not hide debug boot-vars on core + - cmd/snap-bootstrap: no error when not input devices are found + - snap-bootstrap: fix partition numbering in create-partitions + - httputil/client_test.go: add two TLS version tests + - tests: ignore user@12345.service hierarchy + - bootloader, gadget, cmd/snap-bootstrap: misc cosmetic things + - tests: rewrite timeserver-control test + - tests: fix racy pulseaudio tests + - many: fix loading apparmor profiles on Ubuntu 20.04 with ZFS + - tests: update snap-preseed --reset logic to accommodate for 2.44 + change + - cmd/snap: don't wait for system key when stopping + - sandbox/cgroup: avoid making arrays we don't use + - osutil: mock proc/self/mountinfo properly everywhere + - selinux: export MockIsEnforcing; systemd: use in tests + - tests: add 32 bit machine to GH actions + - tests/session-tool: kill cron session, if any + - asserts: it should be possible to omit many snap-ids if allowed, + fix + - boot: cleanup more things, simplify code + - github: skip spread jobs when corresponding label is set + - dirs: don't depend on osutil anymore, mv apparmor vars to apparmor + pkg + - tests/session-tool: add session-tool --dump + - github: allow cached debian downloads to restore + - tests/session-tool: session ordering is non-deterministic + - tests: enable unit tests on debian-sid again + - github: move spread to self-hosted workers + - secboot: import secboot on ubuntu, provide dummy on !ubuntu + - overlord/devicestate: support for recover and run modes + - snap/naming: add validator for snap security tag + - interfaces: add case for rootWritableOverlay + NFS + - tests/main/uc20-create-partitions: tweaks, renames, switch to + 20.04 + - github: port CLA check to Github Actions + - interfaces/many: miscellaneous policy updates xliv + - configcore,tests: fix setting watchdog options on UC18/20 + - tests/session-tool: collect information about services on startup + - tests/main/uc20-snap-recovery: unbreak, rename to uc20-create- + partitions + - state: add state.CopyState() helper + - tests/session-tool: stop anacron.service in prepare + - interfaces: don't use the owner modifier for files shared via + document portal + - systemd: move the doc comments to the interface so they are + visible + - cmd/snap-recovery-chooser: tweaks + - interfaces/docker-support: add overlayfs file access + - packaging: use debian/not-installed to ignore snap-preseed + - travis.yml: disable unit tests on travis + - store: start splitting store.go and store_test.go into subtopic + files + - tests/session-tool: stop cron/anacron from meddling + - github: disable fail-fast as spread cannot be interrupted + - github: move static checks and spread over + - tests: skip "/etc/machine-id" in "writablepaths" test + - snap-bootstrap: store encrypted partition recovery key + - httputil: increase testRetryStrategy max timelimit to 5s + - tests/session-tool: kill leaking closing session + - interfaces: allow raw access to USB printers + - tests/session-tool: reset failed session-tool units + - httputil: increase httpclient timeout in + TestRetryRequestTimeoutHandling + - usersession: extend timerange in TestExitOnIdle + - client: increase timeout in client tests to 100ms + - many: disentagle release and snapdenv from sandbox/* + - boot: simplify modeenv mocking to always write a modeenv + - snap-bootstrap: expand data partition on install + - o/configstate: add backlight option for core config + - cmd/snap-recovery-chooser: add recovery chooser + - features: enable robust mount ns updates + - snap: improve TestWaitRecovers test + - sandbox/cgroup: add ProcessPathInTrackingCgroup + - interfaces/policy: fix comment in recent new test + - tests: make session tool way more robust + - interfaces/seccomp: allow passing an address to setgroups + - o/configcore: introduce core config handlers (3/N) + - interfaces: updates to login-session-observe, network-manager and + modem-manager interfaces + - interfaces/policy/policy_test.go: add more tests'allow- + installation: false' and we grant based on interface attributes + - packaging: detect/disable broken seed in the postinst + - cmd/snap-confine/mount-support-nvidia.c: add libnvoptix as nvidia + library + - tests: remove google-tpm backend from spread.yaml + - tests: install dependencies with apt using --no-install-recommends + - usersession/userd: add zoommtg url support + - snap-bootstrap: fix disk layout sanity check + - snap: add `snap debug state --is-seeded` helper + - devicestate: generate warning if seeding fails + - config, features: move and rename config.GetFeatureFlag helper to + features.Flag + - boot, overlord/devicestate, daemon: implement requesting boot + into a given recovery system + - xdgopenproxy: forward requests to the desktop portal + - many: support immediate reboot + - store: search v2 tweaks + - tests: fix cross build tests when installing dependencies + - daemon: make POST /v2/systems/