diff options
99 files changed, 2387 insertions, 212 deletions
diff --git a/HACKING.md b/HACKING.md index c543bca666..db913017d8 100644 --- a/HACKING.md +++ b/HACKING.md @@ -101,9 +101,12 @@ 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`, which uses Ubuntu -16.04 as the base for building and Ubuntu 16.04 is in Extended Security -Maintenance (ESM), and as such only is buildable using snapcraft's 4.x channel. +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: @@ -129,31 +132,40 @@ 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 channel and using remote-build, where the -LP job created for the remote-build will attempt to use the 5.x channel instead -of the 4.x channel. This being tracked at -https://warthogs.atlassian.net/browse/CRAFT-568. To work-around this until the -bug is properly fixed, you can hack the snapcraft snap by applying this patch to -the snapcraft 4.8.3 git tag: https://pastebin.ubuntu.com/p/ZvrzghB32p/ and -rebuilding the snapcraft snap using snapcraft itself, then installing the -snapcraft snap that was built. This will force all remote-builds to use 4.x, so -obviously the patch is not suitable for general consumption but is a temporary -work-around until the bug is fixed properly in snapcraft upstream. - -``` -git clone -b 4.8.3 --single-branch --depth 1 https://github.com/snapcore/snapcraft.git -cd snapcraft -wget --quiet https://gist.githubusercontent.com/anonymouse64/8fc6e81dac06ed033636132b4d9215f9/raw/ea3128904d419de071d035f0c6b15b74ccfac4fa/snapcraft.patch -git apply --ignore-whitespace snapcraft.patch -snapcraft -snap install snapcraft_*.snap +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 ``` -Now you can use remote-build with snapcraft on the snapd tree: +And to go back to building the snapd snap locally, just revert the channel back +to 4.x: ``` -snapcraft remote-build --build-on=armhf +snap refresh snapcraft --channel=4.x/stable ``` @@ -166,10 +178,10 @@ complex in that it assumes it is built inside Launchpad with the 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, so 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. +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. 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: diff --git a/asserts/asserts.go b/asserts/asserts.go index 574e49a1a1..2b78457df7 100644 --- a/asserts/asserts.go +++ b/asserts/asserts.go @@ -150,7 +150,8 @@ func init() { // 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 --git a/asserts/database.go b/asserts/database.go index cebf084678..59488ceed6 100644 --- a/asserts/database.go +++ b/asserts/database.go @@ -102,6 +102,8 @@ type KeypairManager interface { 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 --git a/asserts/export_test.go b/asserts/export_test.go index e83e59c5b6..eae2bf8246 100644 --- a/asserts/export_test.go +++ b/asserts/export_test.go @@ -271,6 +271,13 @@ func MockRunGPG(mock func(prev GPGRunner, input []byte, args ...string) ([]byte, } } +func GPGBatchYes() (restore func()) { + gpgBatchYes = true + return func() { + gpgBatchYes = false + } +} + // Headers helpers to test var ( ParseHeaders = parseHeaders diff --git a/asserts/extkeypairmgr.go b/asserts/extkeypairmgr.go index 011da0cbd7..a84fde3444 100644 --- a/asserts/extkeypairmgr.go +++ b/asserts/extkeypairmgr.go @@ -204,7 +204,11 @@ func (em *ExternalKeypairManager) Put(privKey PrivateKey) error { 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 --git a/asserts/extkeypairmgr_test.go b/asserts/extkeypairmgr_test.go index c8e734b50f..f534dbf21a 100644 --- a/asserts/extkeypairmgr_test.go +++ b/asserts/extkeypairmgr_test.go @@ -300,11 +300,21 @@ func (s *extKeypairMgrSuite) TestListError(c *C) { 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 --git a/asserts/fsentryutils.go b/asserts/fsentryutils.go index ca057d8c76..858405a459 100644 --- a/asserts/fsentryutils.go +++ b/asserts/fsentryutils.go @@ -68,3 +68,8 @@ func readEntry(top string, subpath ...string) ([]byte, error) { 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 --git a/asserts/fskeypairmgr.go b/asserts/fskeypairmgr.go index 5a58ae1716..9fc81427c1 100644 --- a/asserts/fskeypairmgr.go +++ b/asserts/fskeypairmgr.go @@ -90,3 +90,17 @@ func (fskm *filesystemKeypairManager) Get(keyID string) (PrivateKey, error) { } 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 --git a/asserts/fskeypairmgr_test.go b/asserts/fskeypairmgr_test.go index 422ccddeda..0391b99443 100644 --- a/asserts/fskeypairmgr_test.go +++ b/asserts/fskeypairmgr_test.go @@ -63,3 +63,33 @@ func (fsbss *fsKeypairMgrSuite) TestOpenWorldWritableFail(c *C) { 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 --git a/asserts/gpgkeypairmgr.go b/asserts/gpgkeypairmgr.go index c6d0cafd8f..1f5044b696 100644 --- a/asserts/gpgkeypairmgr.go +++ b/asserts/gpgkeypairmgr.go @@ -31,6 +31,7 @@ import ( "golang.org/x/crypto/openpgp/packet" "github.com/snapcore/snapd/osutil" + "github.com/snapcore/snapd/strutil" ) func ensureGPGHomeDirectory() (string, error) { @@ -73,6 +74,8 @@ func findGPGCommand() (string, error) { return path, err } +var gpgBatchYes = false + func runGPGImpl(input []byte, args ...string) ([]byte, error) { homedir, err := ensureGPGHomeDirectory() if err != nil { @@ -92,6 +95,9 @@ func runGPGImpl(input []byte, args ...string) ([]byte, error) { } 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 @@ func (gkm *GPGKeypairManager) Put(privKey PrivateKey) error { 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 @@ func (gkm *GPGKeypairManager) Get(keyID string) (PrivateKey, error) { 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 @@ func (gkm *GPGKeypairManager) sign(fingerprint string, content []byte) (*packet. 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 @@ func (gkm *GPGKeypairManager) Export(name string) ([]byte, error) { 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 --git a/asserts/gpgkeypairmgr_test.go b/asserts/gpgkeypairmgr_test.go index 6885214bc3..709982c537 100644 --- a/asserts/gpgkeypairmgr_test.go +++ b/asserts/gpgkeypairmgr_test.go @@ -338,3 +338,20 @@ func (gkms *gpgKeypairMgrSuite) TestList(c *C) { 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 --git a/asserts/ifacedecls.go b/asserts/ifacedecls.go index 1f6385856a..1bd650335d 100644 --- a/asserts/ifacedecls.go +++ b/asserts/ifacedecls.go @@ -44,6 +44,8 @@ const ( 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 compileAltAttrMatcher(cc compileContext, l []interface{}) (attrMatcher, err } 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) feature(flabel string) bool { } 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 --git a/asserts/ifacedecls_test.go b/asserts/ifacedecls_test.go index 3ee9c90a96..470d772c42 100644 --- a/asserts/ifacedecls_test.go +++ b/asserts/ifacedecls_test.go @@ -283,6 +283,99 @@ bar: 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 --git a/asserts/memkeypairmgr.go b/asserts/memkeypairmgr.go index 68293a25d6..75555f81f5 100644 --- a/asserts/memkeypairmgr.go +++ b/asserts/memkeypairmgr.go @@ -57,3 +57,15 @@ func (mkm *memoryKeypairManager) Get(keyID string) (PrivateKey, error) { } 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 --git a/asserts/memkeypairmgr_test.go b/asserts/memkeypairmgr_test.go index a99018ff43..c812787cd3 100644 --- a/asserts/memkeypairmgr_test.go +++ b/asserts/memkeypairmgr_test.go @@ -71,3 +71,22 @@ func (mkms *memKeypairMgtSuite) TestGetNotFound(c *C) { 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 --git a/asserts/snap_asserts.go b/asserts/snap_asserts.go index b85c52472c..c941fe1278 100644 --- a/asserts/snap_asserts.go +++ b/asserts/snap_asserts.go @@ -175,6 +175,9 @@ func snapDeclarationFormatAnalyze(headers map[string]interface{}, body []byte) ( if rule.feature(nameConstraintsFeature) { setFormatNum(4) } + if rule.feature(altAttrMatcherFeature) { + setFormatNum(5) + } }) if err != nil { return 0, err @@ -194,6 +197,9 @@ func snapDeclarationFormatAnalyze(headers map[string]interface{}, body []byte) ( if rule.feature(nameConstraintsFeature) { setFormatNum(4) } + if rule.feature(altAttrMatcherFeature) { + setFormatNum(5) + } }) if err != nil { return 0, err diff --git a/asserts/snap_asserts_test.go b/asserts/snap_asserts_test.go index f86e700b8e..9a56080ed6 100644 --- a/asserts/snap_asserts_test.go +++ b/asserts/snap_asserts_test.go @@ -546,6 +546,24 @@ func (sds *snapDeclSuite) TestSuggestedFormat(c *C) { 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 --git a/cmd/libsnap-confine-private/cleanup-funcs-test.c b/cmd/libsnap-confine-private/cleanup-funcs-test.c index 203193e477..509df3ee8a 100644 --- a/cmd/libsnap-confine-private/cleanup-funcs-test.c +++ b/cmd/libsnap-confine-private/cleanup-funcs-test.c @@ -142,6 +142,27 @@ static void test_cleanup_close(void) 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 @@ static void __attribute__((constructor)) init(void) 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 --git a/cmd/libsnap-confine-private/cleanup-funcs.c b/cmd/libsnap-confine-private/cleanup-funcs.c index 369235cbcc..d96a2ba0f3 100644 --- a/cmd/libsnap-confine-private/cleanup-funcs.c +++ b/cmd/libsnap-confine-private/cleanup-funcs.c @@ -28,6 +28,14 @@ void sc_cleanup_string(char **ptr) } } +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 --git a/cmd/libsnap-confine-private/cleanup-funcs.h b/cmd/libsnap-confine-private/cleanup-funcs.h index b1fee959c5..43ef1515c9 100644 --- a/cmd/libsnap-confine-private/cleanup-funcs.h +++ b/cmd/libsnap-confine-private/cleanup-funcs.h @@ -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 --git a/cmd/snap-confine/mount-support-nvidia.c b/cmd/snap-confine/mount-support-nvidia.c index 2968e1f21a..75f7265f1b 100644 --- a/cmd/snap-confine/mount-support-nvidia.c +++ b/cmd/snap-confine/mount-support-nvidia.c @@ -81,19 +81,10 @@ static const size_t egl_vendor_globs_len = // 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 char *nvidia_globs[] = { 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 @@ static void sc_mkdir_and_mount_and_glob_files(const char *rootfs_dir, // // 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 @@ static void sc_mount_nvidia_driver_biarch(const char *rootfs_dir) // 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 @@ static int sc_mount_nvidia_is_driver_in_dir(const char *dir) 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 @@ static void sc_mount_nvidia_driver_multiarch(const char *rootfs_dir) 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 @@ static void sc_mount_nvidia_driver_multiarch(const char *rootfs_dir) 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 @@ static void sc_mount_egl(const char *rootfs_dir) 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 @@ void sc_mount_nvidia_driver(const char *rootfs_dir) 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 --git a/cmd/snap-confine/mount-support-nvidia.h b/cmd/snap-confine/mount-support-nvidia.h index 56ec893f6c..9835fb4266 100644 --- a/cmd/snap-confine/mount-support-nvidia.h +++ b/cmd/snap-confine/mount-support-nvidia.h @@ -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 --git a/cmd/snap-confine/mount-support.c b/cmd/snap-confine/mount-support.c index 44dea9d955..d5331b2eeb 100644 --- a/cmd/snap-confine/mount-support.c +++ b/cmd/snap-confine/mount-support.c @@ -494,7 +494,7 @@ static void sc_bootstrap_mount_namespace(const struct sc_mount_config *config) // 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 --git a/cmd/snap-preseed/preseed_linux.go b/cmd/snap-preseed/preseed_linux.go index 6c6e819445..bfb201819f 100644 --- a/cmd/snap-preseed/preseed_linux.go +++ b/cmd/snap-preseed/preseed_linux.go @@ -158,16 +158,16 @@ type targetSnapdInfo struct { // 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 --git a/cmd/snap/cmd_delete_key.go b/cmd/snap/cmd_delete_key.go index 517f6b936c..f4ed9b644d 100644 --- a/cmd/snap/cmd_delete_key.go +++ b/cmd/snap/cmd_delete_key.go @@ -62,7 +62,7 @@ func (x *cmdDeleteKey) Execute(args []string) error { 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 --git a/cmd/snap/keymgr.go b/cmd/snap/keymgr.go index 52dcb3fe08..20dac99173 100644 --- a/cmd/snap/keymgr.go +++ b/cmd/snap/keymgr.go @@ -36,7 +36,7 @@ type KeypairManager interface { 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 --git a/daemon/api.go b/daemon/api.go index 7a8f972890..0e39ddfb3d 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -142,7 +142,8 @@ var ( snapstateRevertToRevision = snapstate.RevertToRevision snapstateSwitch = snapstate.Switch - assertstateRefreshSnapAssertions = assertstate.RefreshSnapAssertions + assertstateRefreshSnapAssertions = assertstate.RefreshSnapAssertions + assertstateRestoreValidationSetsTracking = assertstate.RestoreValidationSetsTracking ) func ensureStateSoonImpl(st *state.State) { diff --git a/daemon/api_model.go b/daemon/api_model.go index 73cba335b6..609db73b0b 100644 --- a/daemon/api_model.go +++ b/daemon/api_model.go @@ -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 @@ import ( 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 @@ func getSerial(c *Command, r *http.Request, _ *auth.UserState) Response { 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 --git a/daemon/api_model_test.go b/daemon/api_model_test.go index 134f98ed6e..0cf24e68d2 100644 --- a/daemon/api_model_test.go +++ b/daemon/api_model_test.go @@ -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 @@ package daemon_test import ( "bytes" "encoding/json" + "errors" "io" "net/http" "net/http/httptest" @@ -376,3 +377,63 @@ func (s *modelSuite) TestGetModelJSONHasSerialAssertion(c *check.C) { 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 --git a/daemon/api_snaps.go b/daemon/api_snaps.go index 0089d14ae8..2e25a12541 100644 --- a/daemon/api_snaps.go +++ b/daemon/api_snaps.go @@ -607,6 +607,11 @@ func snapUpdateMany(inst *snapInstruction, st *state.State) (*snapInstructionRes // 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 --git a/daemon/api_snaps_test.go b/daemon/api_snaps_test.go index a9c12444b9..9c4131befa 100644 --- a/daemon/api_snaps_test.go +++ b/daemon/api_snaps_test.go @@ -596,6 +596,35 @@ func (s *snapsSuite) TestRefreshAllNoChanges(c *check.C) { 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 diff --git a/daemon/export_api_model_test.go b/daemon/export_api_model_test.go index e7cebe3a4a..66fca72c38 100644 --- a/daemon/export_api_model_test.go +++ b/daemon/export_api_model_test.go @@ -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 @@ package daemon import ( "github.com/snapcore/snapd/asserts" + "github.com/snapcore/snapd/overlord/devicestate" "github.com/snapcore/snapd/overlord/state" ) @@ -32,6 +33,14 @@ func MockDevicestateRemodel(mock func(*state.State, *asserts.Model) (*state.Chan } } +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 --git a/daemon/export_api_snaps_test.go b/daemon/export_api_snaps_test.go index 61173ecf87..1a4322d90d 100644 --- a/daemon/export_api_snaps_test.go +++ b/daemon/export_api_snaps_test.go @@ -21,6 +21,7 @@ package daemon import ( "github.com/snapcore/snapd/overlord/snapstate" + "github.com/snapcore/snapd/overlord/state" "github.com/snapcore/snapd/snap" ) @@ -31,3 +32,11 @@ func MakeAboutSnap(info *snap.Info, snapst *snapstate.SnapState) aboutSnap { var ( MapLocal = mapLocal ) + +func MockAssertstateRestoreValidationSetsTracking(f func(*state.State) error) (restore func()) { + old := assertstateRestoreValidationSetsTracking + assertstateRestoreValidationSetsTracking = f + return func() { + assertstateRestoreValidationSetsTracking = old + } +} diff --git a/data/env/snapd.fish.in b/data/env/snapd.fish.in index ca7c8358ad..f2a6591249 100644 --- a/data/env/snapd.fish.in +++ b/data/env/snapd.fish.in @@ -1,8 +1,6 @@ # Expand $PATH to include the directory where snappy applications go. set -u snap_bin_path "@SNAP_MOUNT_DIR@/bin" -if ! contains $snap_bin_path $PATH - set PATH $PATH $snap_bin_path -end +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 diff --git a/mkversion.sh b/mkversion.sh index 2fe1edf51a..7d6d108d21 100755 --- a/mkversion.sh +++ b/mkversion.sh @@ -130,4 +130,5 @@ EOF cat <<EOF > "$PKG_BUILDDIR/data/info" VERSION=$v +SNAPD_APPARMOR_REEXEC=0 EOF diff --git a/overlord/assertstate/assertstate.go b/overlord/assertstate/assertstate.go index bfd85cf612..d0b63a0687 100644 --- a/overlord/assertstate/assertstate.go +++ b/overlord/assertstate/assertstate.go @@ -344,6 +344,10 @@ func delayedCrossMgrInit() { 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 diff --git a/overlord/assertstate/validation_set_tracking.go b/overlord/assertstate/validation_set_tracking.go index 5a85d26b45..c037158960 100644 --- a/overlord/assertstate/validation_set_tracking.go +++ b/overlord/assertstate/validation_set_tracking.go @@ -261,3 +261,19 @@ func ValidationSetsHistory(st *state.State) ([]map[string]*ValidationSetTracking } 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 --git a/overlord/assertstate/validation_set_tracking_test.go b/overlord/assertstate/validation_set_tracking_test.go index 718ed79a13..11facaa4d4 100644 --- a/overlord/assertstate/validation_set_tracking_test.go +++ b/overlord/assertstate/validation_set_tracking_test.go @@ -409,3 +409,56 @@ func (s *validationSetTrackingSuite) TestAddToValidationSetsHistoryRemovesOldEnt }, }) } + +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 --git a/overlord/configstate/configcore/handlers.go b/overlord/configstate/configcore/handlers.go index ab6580f072..b773fd125b 100644 --- a/overlord/configstate/configcore/handlers.go +++ b/overlord/configstate/configcore/handlers.go @@ -97,8 +97,9 @@ func init() { // 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) sysconfig.ApplyFilesystemOnlyDefaultsImpl = filesystemOnlyApply } diff --git a/overlord/configstate/configcore/hostname.go b/overlord/configstate/configcore/hostname.go index b26f51c37f..cf99922dc9 100644 --- a/overlord/configstate/configcore/hostname.go +++ b/overlord/configstate/configcore/hostname.go @@ -38,24 +38,23 @@ func init() { 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 validHostnameRegexp = regexp.MustCompile(`^[a-z0-9][a-z0-9-]{0,62}(\.[a-z0-9-]{1,63})*$`).MatchString +// 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 -func validateHostnameSettings(tr config.ConfGetter) error { - hostname, err := coreCfg(tr, "system.hostname") - if err != nil { - return err - } - if hostname == "" { - return nil - } +// Note that HOST_NAME_MAX is 64 on Linux, but DNS allows domain names +// up to 255 characters +const HOST_NAME_MAX = 64 - validHostname := len(hostname) <= 253 && validHostnameRegexp(hostname) +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 } @@ -83,6 +82,10 @@ func handleHostnameConfiguration(_ sysconfig.Device, tr config.ConfGetter, opts 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 @@ -105,7 +108,18 @@ func getHostnameFromSystemHelper(key string) (interface{}, error) { } 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 --git a/overlord/configstate/configcore/hostname_test.go b/overlord/configstate/configcore/hostname_test.go index 8a1f338052..8a656b0017 100644 --- a/overlord/configstate/configcore/hostname_test.go +++ b/overlord/configstate/configcore/hostname_test.go @@ -46,40 +46,44 @@ func (s *hostnameSuite) SetUpTest(c *C) { 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", "foo..bar", + "-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() +func (s *hostnameSuite) TestConfigureHostnameFsOnlyHappy(c *C) { + tmpdir := c.MkDir() - mockedHostname := testutil.MockCommand(c, "hostname", "echo bar") - defer mockedHostname.Restore() - - filler := strings.Repeat("x", 63) + filler := strings.Repeat("x", 16) validHostnames := []string{ "a", "foo", @@ -91,11 +95,35 @@ func (s *hostnameSuite) TestConfigureHostnameIntegration(c *C) { "localhost.localdomain", "foo.-bar.com", "can-end-with-a-dash-", - // 3*63 + 61 + 3 dots = 253 - strings.Join([]string{filler, filler, filler, strings.Repeat("x", 61)}, "."), + // 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 _, hostname := range validHostnames { + 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 hostnames { err := configcore.Run(coreDev, &mockConf{ state: s.state, conf: map[string]interface{}{ @@ -103,39 +131,82 @@ func (s *hostnameSuite) TestConfigureHostnameIntegration(c *C) { }, }) 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 --git a/overlord/devicestate/devicemgr.go b/overlord/devicestate/devicemgr.go index e4823a60fd..de97e10139 100644 --- a/overlord/devicestate/devicemgr.go +++ b/overlord/devicestate/devicemgr.go @@ -1401,16 +1401,28 @@ func (m *DeviceManager) Unregister(opts *UnregisterOptions) error { return err } } + oldKeyID := device.KeyID device.Serial = "" device.KeyID = "" device.SessionMacaroon = "" if err := m.setDevice(device); err != nil { return err } - // TODO: delete device keypair + // 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 nil + return err } // device returns current device state. diff --git a/overlord/devicestate/devicestate_serial_test.go b/overlord/devicestate/devicestate_serial_test.go index 5cae183515..125289789a 100644 --- a/overlord/devicestate/devicestate_serial_test.go +++ b/overlord/devicestate/devicestate_serial_test.go @@ -2108,6 +2108,9 @@ func (s *deviceMgrSerialSuite) testFullDeviceUnregisterReregisterClassicGeneric( 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") diff --git a/overlord/managers_test.go b/overlord/managers_test.go index 7222fb5f81..5ae037ba23 100644 --- a/overlord/managers_test.go +++ b/overlord/managers_test.go @@ -6198,6 +6198,13 @@ type: gadget base: core20 ` +const pcGadget22SnapYaml = ` +version: 1.0 +name: pc +type: gadget +base: core22 +` + const oldPcGadgetSnapYaml = ` version: 1.0 name: pc @@ -6211,12 +6218,25 @@ 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 @@ -6269,11 +6289,15 @@ var ( 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", } @@ -6294,6 +6318,10 @@ var ( "core20-rev-33": { {"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 @@ -7546,6 +7574,263 @@ func (s *mgrsSuite) TestCheckRefreshFailureWithConcurrentRemoveOfConnectedSnap(c 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) + + dumpTasks(c, "after kernel install", chg.Tasks()) + // gadget update has been not been applied yet + c.Check(updater.updateCalls, Equals, 0) + + 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) + + s.o.DeviceManager().ResetToPostBootState() + st.Unlock() + err = s.o.DeviceManager().Ensure() + st.Lock() + c.Assert(err, IsNil) + + // 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) { bloader := boottest.MockUC16Bootenv(bootloadertest.Mock("mock", c.MkDir())) bootloader.Force(bloader) diff --git a/overlord/snapstate/export_test.go b/overlord/snapstate/export_test.go index c2b309c739..d68604f0ee 100644 --- a/overlord/snapstate/export_test.go +++ b/overlord/snapstate/export_test.go @@ -212,6 +212,8 @@ func MockAsyncPendingRefreshNotification(fn func(context.Context, *userclient.Cl var ( RefreshedSnaps = refreshedSnaps ReRefreshFilter = reRefreshFilter + + MaybeRestoreValidationSetsAndRevertSnaps = maybeRestoreValidationSetsAndRevertSnaps ) type UpdateFilter = updateFilter @@ -366,3 +368,27 @@ func MockSnapsToRefresh(f func(gatingTask *state.Task) ([]*refreshCandidate, err 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 --git a/overlord/snapstate/handlers.go b/overlord/snapstate/handlers.go index 3e206e0910..8ee57e77a1 100644 --- a/overlord/snapstate/handlers.go +++ b/overlord/snapstate/handlers.go @@ -34,6 +34,7 @@ import ( "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" @@ -3149,13 +3150,14 @@ func changeReadyUpToTask(task *state.Task) bool { } // 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() @@ -3197,15 +3199,16 @@ func refreshedSnaps(reTask *state.Task) []string { 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 @@ -3241,14 +3244,50 @@ func (m *SnapManager) doCheckReRefresh(t *state.Task, tomb *tomb.Tomb) error { 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 @@ -3317,6 +3356,86 @@ func (m *SnapManager) doConditionalAutoRefresh(t *state.Task, tomb *tomb.Tomb) e 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) { diff --git a/overlord/snapstate/handlers_rerefresh_test.go b/overlord/snapstate/handlers_rerefresh_test.go index e35f2d8253..05f714181d 100644 --- a/overlord/snapstate/handlers_rerefresh_test.go +++ b/overlord/snapstate/handlers_rerefresh_test.go @@ -28,9 +28,12 @@ import ( . "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 @@ func (s *reRefreshSuite) TestDoCheckReRefreshAddsNewTasks(c *C) { // 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 @@ func (s *reRefreshSuite) TestFilterReturnsFalseIfEpochEqualZero(c *C) { 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 --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index 7276af01bf..00867c9ed2 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -2069,6 +2069,10 @@ func infoForUpdate(st *state.State, snapst *SnapState, name string, opts *Revisi // 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. diff --git a/overlord/snapstate/snapstate_update_test.go b/overlord/snapstate/snapstate_update_test.go index e8bdd74dde..113cf222d4 100644 --- a/overlord/snapstate/snapstate_update_test.go +++ b/overlord/snapstate/snapstate_update_test.go @@ -37,6 +37,7 @@ import ( "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" @@ -6844,6 +6845,141 @@ func (s *snapmgrTestSuite) TestUpdatePrerequisiteWithSameDeviceContext(c *C) { }) } +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() diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index 4afc3b7a8e..3b3c2d3b76 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -11,7 +11,7 @@ pkgdesc="Service and tools for management of snap packages." depends=('squashfs-tools' 'libseccomp' 'libsystemd' 'apparmor') optdepends=('bash-completion: bash completion support' 'xdg-desktop-portal: desktop integration') -pkgver=2.53.2 +pkgver=2.53.4 pkgrel=1 arch=('x86_64' 'i686' 'armv7h' 'aarch64') url="https://github.com/snapcore/snapd" diff --git a/packaging/debian-sid/changelog b/packaging/debian-sid/changelog index fc1fdefee0..78069d0610 100644 --- a/packaging/debian-sid/changelog +++ b/packaging/debian-sid/changelog @@ -1,3 +1,38 @@ +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 <ian.johnson@canonical.com> 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 <ian.johnson@canonical.com> Thu, 02 Dec 2021 11:42:15 -0600 + snapd (2.53.2-1) unstable; urgency=medium * New upstream release, LP: #1946127 diff --git a/packaging/fedora/snapd.spec b/packaging/fedora/snapd.spec index be2e8db87c..41f3d02e06 100644 --- a/packaging/fedora/snapd.spec +++ b/packaging/fedora/snapd.spec @@ -102,7 +102,7 @@ %endif Name: snapd -Version: 2.53.2 +Version: 2.53.4 Release: 0%{?dist} Summary: A transactional software package manager License: GPLv3 @@ -989,6 +989,35 @@ fi %changelog +* Thu Dec 02 2021 Ian Johnson <ian.johnson@canonical.com> +- 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 <ian.johnson@canonical.com> +- 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 <ian.johnson@canonical.com> - New upstream release 2.53.2 - interfaces/builtin/block_devices: allow blkid to print block diff --git a/packaging/opensuse/snapd.changes b/packaging/opensuse/snapd.changes index ddff24a8cf..fa7d3f742e 100644 --- a/packaging/opensuse/snapd.changes +++ b/packaging/opensuse/snapd.changes @@ -1,4 +1,14 @@ ------------------------------------------------------------------- +Thu Dec 02 23:16:48 UTC 2021 - ian.johnson@canonical.com + +- Update to upstream release 2.53.4 + +------------------------------------------------------------------- +Thu Dec 02 17:42:15 UTC 2021 - ian.johnson@canonical.com + +- Update to upstream release 2.53.3 + +------------------------------------------------------------------- Mon Nov 15 22:09:09 UTC 2021 - ian.johnson@canonical.com - Update to upstream release 2.53.2 diff --git a/packaging/opensuse/snapd.spec b/packaging/opensuse/snapd.spec index 61b2a5af3f..5af9d461bd 100644 --- a/packaging/opensuse/snapd.spec +++ b/packaging/opensuse/snapd.spec @@ -81,7 +81,7 @@ Name: snapd -Version: 2.53.2 +Version: 2.53.4 Release: 0 Summary: Tools enabling systems to work with .snap files License: GPL-3.0 diff --git a/packaging/ubuntu-14.04/changelog b/packaging/ubuntu-14.04/changelog index e6c705b37b..8548350707 100644 --- a/packaging/ubuntu-14.04/changelog +++ b/packaging/ubuntu-14.04/changelog @@ -1,3 +1,38 @@ +snapd (2.53.4~14.04) trusty; 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 <ian.johnson@canonical.com> Thu, 02 Dec 2021 17:16:48 -0600 + +snapd (2.53.3~14.04) trusty; 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 <ian.johnson@canonical.com> Thu, 02 Dec 2021 11:42:15 -0600 + snapd (2.53.2~14.04) trusty; urgency=medium * New upstream release, LP: #1946127 diff --git a/packaging/ubuntu-16.04/changelog b/packaging/ubuntu-16.04/changelog index 924a7f1d66..6e88b17e56 100644 --- a/packaging/ubuntu-16.04/changelog +++ b/packaging/ubuntu-16.04/changelog @@ -1,3 +1,38 @@ +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 <ian.johnson@canonical.com> 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 <ian.johnson@canonical.com> Thu, 02 Dec 2021 11:42:15 -0600 + snapd (2.53.2) xenial; urgency=medium * New upstream release, LP: #1946127 diff --git a/snap/naming/wellknown.go b/snap/naming/wellknown.go index 27ef40a22f..1040bcc57d 100644 --- a/snap/naming/wellknown.go +++ b/snap/naming/wellknown.go @@ -29,6 +29,7 @@ var ( "snapd": "PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4", "core18": "CSO04Jhav2yK0uz97cr0ipQRyqg0qQL6", "core20": "DLqre5XGLbDqg9jPtiAhRRjDuPVa5X1q", + "core22": "amcUKQILKXHHTlmSa7NMdnXSx02dNeeT", } stagingWellKnownSnapIDs = map[string]string{ diff --git a/snapdtool/info_file.go b/snapdtool/info_file.go index 2780334501..93c5e0730d 100644 --- a/snapdtool/info_file.go +++ b/snapdtool/info_file.go @@ -20,34 +20,55 @@ package snapdtool import ( - "bytes" + "bufio" "fmt" - "io/ioutil" + "os" + "path/filepath" + "strings" ) -// SnapdVersionFromInfoFile returns snapd version read for the -// given info" file, pointed by infoPath. -// The format of the "info" file is a single line with "VERSION=..." -// in it. The file is produced by mkversion.sh and normally installed -// along snapd binary in /usr/lib/snapd. -func SnapdVersionFromInfoFile(infoPath string) (string, error) { - content, err := ioutil.ReadFile(infoPath) +// SnapdVersionFromInfoFile returns the snapd version read from the info file in +// the given dir, as well as any other key/value pairs/flags in the file. +// The format of the "info" file are lines with "KEY=VALUE" with the typical key +// being just VERSION. The file is produced by mkversion.sh and normally +// installed along snapd binary in /usr/lib/snapd. +// Other typical keys in this file include SNAPD_APPARMOR_REEXEC, which +// indicates whether or not the snapd-apparmor binary installed via the +// traditional linux package of snapd supports re-exec into the version in the +// snapd or core snaps. +func SnapdVersionFromInfoFile(dir string) (version string, flags map[string]string, err error) { + infoPath := filepath.Join(dir, "info") + f, err := os.Open(infoPath) if err != nil { - return "", fmt.Errorf("cannot open snapd info file %q: %s", infoPath, err) + return "", nil, fmt.Errorf("cannot open snapd info file %q: %s", infoPath, err) } + defer f.Close() - if !bytes.HasPrefix(content, []byte("VERSION=")) { - idx := bytes.Index(content, []byte("\nVERSION=")) - if idx < 0 { - return "", fmt.Errorf("cannot find snapd version information in %q", content) + flags = map[string]string{} + + scanner := bufio.NewScanner(f) + for scanner.Scan() { + line := scanner.Text() + if strings.HasPrefix(line, "VERSION=") { + version = strings.TrimPrefix(line, "VERSION=") + } else { + keyVal := strings.SplitN(line, "=", 2) + if len(keyVal) != 2 { + // potentially malformed line, just skip it + continue + } + + flags[keyVal[0]] = keyVal[1] } - content = content[idx+1:] } - content = content[8:] - idx := bytes.IndexByte(content, '\n') - if idx > -1 { - content = content[:idx] + + if err := scanner.Err(); err != nil { + return "", nil, fmt.Errorf("error reading snapd info file %q: %v", infoPath, err) + } + + if version == "" { + return "", nil, fmt.Errorf("cannot find snapd version information in file %q", infoPath) } - return string(content), nil + return version, flags, nil } diff --git a/snapdtool/info_file_test.go b/snapdtool/info_file_test.go index 1ce9a5dd23..0f311f79c8 100644 --- a/snapdtool/info_file_test.go +++ b/snapdtool/info_file_test.go @@ -20,6 +20,7 @@ package snapdtool_test import ( + "fmt" "io/ioutil" "path/filepath" @@ -33,8 +34,8 @@ type infoFileSuite struct{} var _ = Suite(&infoFileSuite{}) func (s *infoFileSuite) TestNoVersionFile(c *C) { - _, err := snapdtool.SnapdVersionFromInfoFile("/non-existing-file") - c.Assert(err, ErrorMatches, `cannot open snapd info file "/non-existing-file":.*`) + _, _, err := snapdtool.SnapdVersionFromInfoFile("/non-existing-dir") + c.Assert(err, ErrorMatches, `cannot open snapd info file "/non-existing-dir/info":.*`) } func (s *infoFileSuite) TestNoVersionData(c *C) { @@ -42,8 +43,8 @@ func (s *infoFileSuite) TestNoVersionData(c *C) { infoFile := filepath.Join(top, "info") c.Assert(ioutil.WriteFile(infoFile, []byte("foo"), 0644), IsNil) - _, err := snapdtool.SnapdVersionFromInfoFile(infoFile) - c.Assert(err, ErrorMatches, `cannot find snapd version information in "foo"`) + _, _, err := snapdtool.SnapdVersionFromInfoFile(top) + c.Assert(err, ErrorMatches, fmt.Sprintf(`cannot find snapd version information in file %q`, infoFile)) } func (s *infoFileSuite) TestVersionHappy(c *C) { @@ -51,7 +52,19 @@ func (s *infoFileSuite) TestVersionHappy(c *C) { infoFile := filepath.Join(top, "info") c.Assert(ioutil.WriteFile(infoFile, []byte("VERSION=1.2.3"), 0644), IsNil) - ver, err := snapdtool.SnapdVersionFromInfoFile(infoFile) + ver, flags, err := snapdtool.SnapdVersionFromInfoFile(top) c.Assert(err, IsNil) c.Check(ver, Equals, "1.2.3") + c.Assert(flags, HasLen, 0) +} + +func (s *infoFileSuite) TestInfoVersionFlags(c *C) { + top := c.MkDir() + infoFile := filepath.Join(top, "info") + c.Assert(ioutil.WriteFile(infoFile, []byte("VERSION=1.2.3\nFOO=BAR"), 0644), IsNil) + + ver, flags, err := snapdtool.SnapdVersionFromInfoFile(top) + c.Assert(err, IsNil) + c.Check(ver, Equals, "1.2.3") + c.Assert(flags, DeepEquals, map[string]string{"FOO": "BAR"}) } diff --git a/snapdtool/tool_linux.go b/snapdtool/tool_linux.go index 986b106bdd..5e8e554d7f 100644 --- a/snapdtool/tool_linux.go +++ b/snapdtool/tool_linux.go @@ -76,8 +76,8 @@ func distroSupportsReExec() bool { // Ensure we do not use older version of snapd, look for info file and ignore // version of core that do not yet have it. func coreSupportsReExec(coreOrSnapdPath string) bool { - infoPath := filepath.Join(coreOrSnapdPath, filepath.Join(dirs.CoreLibExecDir, "info")) - ver, err := SnapdVersionFromInfoFile(infoPath) + infoDir := filepath.Join(coreOrSnapdPath, filepath.Join(dirs.CoreLibExecDir)) + ver, _, err := SnapdVersionFromInfoFile(infoDir) if err != nil { logger.Noticef("%v", err) return false diff --git a/spread.yaml b/spread.yaml index daf110af91..33026f3117 100644 --- a/spread.yaml +++ b/spread.yaml @@ -785,7 +785,7 @@ suites: _/hosts: _ _/hosts_n_dirs: _ # twisted fails in travis (but not regular spread). - # _/twisted: _ + #_/twisted: _ _/func: _ _/funkyfunc: _ _/funcarg: _ diff --git a/tests/completion/data/twisted/this is a file with spaces in it.doc b/tests/completion/data/twisted/this is a file with spaces in it.doc deleted file mode 100644 index e69de29bb2..0000000000 --- a/tests/completion/data/twisted/this is a file with spaces in it.doc +++ /dev/null diff --git a/tests/completion/data/twisted/this isn't.innit b/tests/completion/data/twisted/this isn't.innit deleted file mode 100644 index e69de29bb2..0000000000 --- a/tests/completion/data/twisted/this isn't.innit +++ /dev/null diff --git a/tests/completion/data/twisted/twisted.tar b/tests/completion/data/twisted/twisted.tar Binary files differnew file mode 100644 index 0000000000..62081e2926 --- /dev/null +++ b/tests/completion/data/twisted/twisted.tar diff --git a/tests/completion/twisted.sh b/tests/completion/twisted.sh index 5d52812ce1..4f738f16b9 100644 --- a/tests/completion/twisted.sh +++ b/tests/completion/twisted.sh @@ -1 +1,2 @@ cd "$SPREAD_PATH/$SPREAD_SUITE/data/twisted" +tar -xvf $SPREAD_PATH/$SPREAD_SUITE/data/twisted/twisted.tar diff --git a/tests/core/kernel-and-base-single-reboot/task.yaml b/tests/core/kernel-and-base-single-reboot/task.yaml new file mode 100644 index 0000000000..da653f130a --- /dev/null +++ b/tests/core/kernel-and-base-single-reboot/task.yaml @@ -0,0 +1,145 @@ +summary: Exercises a simultaneous kernel and base refresh with a single reboot + +# TODO make the test work with ubuntu-core-20 +systems: [ubuntu-core-18-*] + +environment: + BLOB_DIR: $(pwd)/fake-store-blobdir + +prepare: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + snap ack "$TESTSLIB/assertions/testrootorg-store.account-key" + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + + setup_fake_store "$BLOB_DIR" + + core_snap=core20 + if os.query is-core18; then + core_snap=core18 + fi + readlink /snap/pc-kernel/current > pc-kernel.rev + readlink "/snap/$core_snap/current" > core.rev + +restore: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + teardown_fake_store "$BLOB_DIR" + +execute: | + if [ "$TRUST_TEST_KEYS" = "false" ]; then + echo "This test needs test keys to be trusted" + exit + fi + + #shellcheck source=tests/lib/store.sh + . "$TESTSLIB"/store.sh + + core_snap=core20 + if os.query is-core18; then + core_snap=core18 + fi + + if [ "$SPREAD_REBOOT" = 0 ]; then + init_fake_refreshes "$BLOB_DIR" pc-kernel + init_fake_refreshes "$BLOB_DIR" "$core_snap" + + # taken from transition_to_recover_mode() + cp /bin/systemctl /tmp/orig-systemctl + mount -o bind "$TESTSLIB/mock-shutdown" /bin/systemctl + tests.cleanup defer umount /bin/systemctl + + snap refresh --no-wait "$core_snap" pc-kernel > refresh-change-id + test -n "$(cat refresh-change-id)" + change_id="$(cat refresh-change-id)" + # wait until we observe reboots + # shellcheck disable=SC2016 + retry -n 100 --wait 5 sh -c 'test "$(wc -l < /tmp/mock-shutdown.calls)" -gt "1"' + # stop snapd now to avoid snapd waiting for too long and deciding to + # error out assuming a rollback across reboot + systemctl stop snapd.service snapd.socket + + # both link snaps should be done now, snapd was stopped, so we cannot + # use 'snap change' and we need to inspect the state directly (even if + # snapd was up, it would not respond to API requests as it would be busy + # retrying auto-connect) + snap debug state --change "$change_id" /var/lib/snapd/state.json > tasks.state + # both link snaps are done + MATCH ' Done\s+.*Make snap "pc-kernel" .* available' < tasks.state + MATCH " Done\s+.*Make snap \"$core_snap\" .* available" < tasks.state + # auto-connect of the base is in doing and waiting for reboot + MATCH " Doing\s+.*Automatically connect eligible plugs and slots of snap \"$core_snap\"" < tasks.state + # auto-connect of the kernel is still queued + MATCH ' Do\s+.*Automatically connect eligible plugs and slots of snap "pc-kernel"' < tasks.state + + if os.query is-core18; then + snap debug boot-vars > boot-vars.dump + MATCH 'snap_mode=try' < boot-vars.dump + MATCH 'snap_try_core=core18_.*.snap' < boot-vars.dump + MATCH 'snap_try_kernel=pc-kernel_.*.snap' < boot-vars.dump + elif os.query is-core20; then + stat /boot/grub/kernel.efi | MATCH 'pc_kernel.*.snap/kernel.efi' + stat -L /boot/grub/kernel.efi + stat /boot/grub/try-kernel.efi | MATCH 'pc_kernel.*.snap/kernel.efi' + stat -L /boot/grub/try-kernel.efi + else + echo "unsupported Ubuntu Core system" + exit 1 + fi + + # restore shutdown so that spread can reboot the host + tests.cleanup pop + + REBOOT + elif [ "$SPREAD_REBOOT" = 1 ]; then + change_id="$(cat refresh-change-id)" + # XXX: is this sufficiently robust? + snap watch "$change_id" || true + snap changes | MATCH "$change_id\s+(Done|Error)" + # we expect re-refresh to fail since the tests uses a fake store + snap change "$change_id" > tasks.done + MATCH '^Error .* Handling re-refresh' < tasks.done + # no other errors + grep -v 'Handling re-refresh' < tasks.done | NOMATCH '^Error' + # nothing was undone + grep -v 'Handling re-refresh' < tasks.done | NOMATCH '^Undone' + # we did not even try to hijack shutdown (/bin/systemctl) because that + # could race with snapd (if that wanted to call it), so just check that + # the system is in a stable state once we have already determined that + # the change is complete + # XXX systemctl exits with non-0 when in degraded state + (systemctl is-system-running || true) | MATCH '(running|degraded)' + + # fake refreshes generate revision numbers that are n+1 + expecting_kernel="$(($(cat pc-kernel.rev) + 1))" + expecting_core="$(($(cat core.rev) + 1))" + + # verify that current points to new revisions + test "$(readlink /snap/pc-kernel/current)" = "$expecting_kernel" + test "$(readlink /snap/$core_snap/current)" = "$expecting_core" + + # now we need to revert both snaps for restore to behave properly, start + # with the kernel + snap revert pc-kernel --revision "$(cat pc-kernel.rev)" + REBOOT + elif [ "$SPREAD_REBOOT" = 2 ]; then + snap watch --last=revert\? + # now the base + snap revert "$core_snap" --revision "$(cat core.rev)" + REBOOT + elif [ "$SPREAD_REBOOT" = 3 ]; then + snap watch --last=revert\? + # we're done, verify current symlinks to the right revisions + test "$(readlink /snap/pc-kernel/current)" = "$(cat pc-kernel.rev)" + test "$(readlink /snap/$core_snap/current)" = "$(cat core.rev)" + else + echo "unexpected reboot" + exit 1 + fi diff --git a/tests/core/snap-set-core-config/task.yaml b/tests/core/snap-set-core-config/task.yaml index a829a11515..1efc6822ef 100644 --- a/tests/core/snap-set-core-config/task.yaml +++ b/tests/core/snap-set-core-config/task.yaml @@ -18,6 +18,8 @@ prepare: | # create a flag to indicate the ryslog service is fake touch rsyslog.fake fi + # hostname is modified during the tests + hostnamectl status --static > hostname restore: | if [ -f rsyslog.fake ]; then @@ -29,6 +31,8 @@ restore: | systemctl start rsyslog.service fi rm -f /etc/systemd/login.conf.d/00-snap-core.conf + # restore hostname + hostnamectl set-hostname "$(cat hostname)" execute: | echo "Check that service disable works" @@ -144,3 +148,5 @@ execute: | tests.cleanup defer hostnamectl set-hostname "$(hostname)" snap set system system.hostname=foo hostname | MATCH foo + snap set system system.hostname=F00 + hostnamectl status | MATCH F00 diff --git a/tests/lib/assertions/valid-for-testing-pc-22-from-20.json b/tests/lib/assertions/valid-for-testing-pc-22-from-20.json new file mode 100644 index 0000000000..0196109290 --- /dev/null +++ b/tests/lib/assertions/valid-for-testing-pc-22-from-20.json @@ -0,0 +1,39 @@ +{ + "type": "model", + "authority-id": "FpA2tgNE8bUrUKSOhE6SOpsMdlcEJkDL", + "series": "16", + "brand-id": "FpA2tgNE8bUrUKSOhE6SOpsMdlcEJkDL", + "model": "my-model", + "architecture": "amd64", + "timestamp": "2021-06-01T12:18:12+00:00", + "grade": "dangerous", + "base": "core22", + "serial-authority": ["generic"], + "snaps": [ + { + "default-channel": "22/edge", + "id": "UqFziVZDHLSyO3TqSWgNBoAdHbLI4dAH", + "name": "pc", + "type": "gadget" + }, + { + "default-channel": "22/edge", + "id": "pYVQrBcKmBa0mZ4CCN7ExT6jH8rY1hza", + "name": "pc-kernel", + "type": "kernel" + }, + { + "default-channel": "latest/edge", + "id": "amcUKQILKXHHTlmSa7NMdnXSx02dNeeT", + "name": "core22", + "type": "base" + }, + { + "default-channel": "latest/edge", + "id": "PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4", + "name": "snapd", + "type": "snapd" + } + ], + "revision": "1" +} diff --git a/tests/lib/assertions/valid-for-testing-pc-22-from-20.model b/tests/lib/assertions/valid-for-testing-pc-22-from-20.model new file mode 100644 index 0000000000..a388ec0b3f --- /dev/null +++ b/tests/lib/assertions/valid-for-testing-pc-22-from-20.model @@ -0,0 +1,45 @@ +type: model +authority-id: FpA2tgNE8bUrUKSOhE6SOpsMdlcEJkDL +revision: 1 +series: 16 +brand-id: FpA2tgNE8bUrUKSOhE6SOpsMdlcEJkDL +model: my-model +architecture: amd64 +base: core22 +grade: dangerous +serial-authority: + - generic +snaps: + - + default-channel: 22/edge + id: UqFziVZDHLSyO3TqSWgNBoAdHbLI4dAH + name: pc + type: gadget + - + default-channel: 22/edge + id: pYVQrBcKmBa0mZ4CCN7ExT6jH8rY1hza + name: pc-kernel + type: kernel + - + default-channel: latest/edge + id: amcUKQILKXHHTlmSa7NMdnXSx02dNeeT + name: core22 + type: base + - + default-channel: latest/edge + id: PMrrV4ml8uWuEUDBT8dSGnKUYbevVhc4 + name: snapd + type: snapd +timestamp: 2021-06-01T12:18:12+00:00 +sign-key-sha3-384: u2EnPKDpHm8Or1VWwjs3duNosoBpFS4Da-c6vRQHPKwdllDOlQ02_JFGrxZDe03C + +AcLBcwQAAQoAHRYhBKg05U8V+aOkBzV5W4jJZcdh7V/PBQJhpyz9AAoJEIjJZcdh7V/PxPsP/18v +KytmhfTWofGCsCq1DJ2kf7aEz+jQ3pFP1I+BiwGpjR/5yGdz+vGfSrgkbpkoC7+ZBH3tQRNlDF+X +3PRUpNkVDkip2Nylqd+GjMO3buscQ8/HQX4vDWnHM6TEaUfqJqKsbAH3kmN6pMn5TY610ONXbsv0 +iQb7xdMq6BW3R66QicNardjWMN2J1wBFKxQ7mHWxH4F4Z034sYR/HGCH6hlfcBZzVx5PK9In0icH +BOF1xlaHFrkm5zgG1EcsPUss8G5tBcWI9B0Q9+OqJbKBUVEQd7y7oVClPs+9MUSLWOHkr4plQ1Vb +mmP6JZDYJFJn+dxKoqWWwcmx11so7YfhF3/isDqM1xsd7rMn3ldIQvfYnROVi0fGLAysbOZ6qxjc +d7yiALPX5kT+XAFy1TB5cZjO32IkHNVLU7TH0N9OvyB7RBN7MPoPfi6+tOrAxLXnev3PB8tZ2lwu +x1nZ6W0zAP1foWIlGoFvDELsxIynqsC9g6FkzMoA/DxvTgSMZci8+xp3dh4aBmXdCAQ2XxElIlCe +BxSMem0nXXgV+a2VZJ/jMsAEj1th2PPvwElhmecU9NvZUqOAeW02171suLb+wCikZpcNzCZzBNZS +i8r39Jn9/7Lz+AukZrAkhZFupgHbVZMTpp2xz56vx4ygR3Ku/VTdnFpdlrn5VZaAuPGfx3dw diff --git a/tests/lib/fakestore/refresh/refresh.go b/tests/lib/fakestore/refresh/refresh.go index 0187883eb6..e6ee51053d 100644 --- a/tests/lib/fakestore/refresh/refresh.go +++ b/tests/lib/fakestore/refresh/refresh.go @@ -141,7 +141,13 @@ func makeFakeRefreshForSnap(snap, targetDir string, db *asserts.Database, f asse } // fake new version - err = exec.Command("sudo", "sed", "-i", `s/version:\(.*\)/version:\1+fake1/`, filepath.Join(fakeUpdateDir, "meta/snap.yaml")).Run() + err = exec.Command("sudo", "sed", "-i", + // version can be all numbers thus making it ambiguous and + // needing quoting, eg. version: '2021112', but since we're + // adding +fake1 suffix, the resulting value will clearly be a + // string, so have the regex strip quoting too + `s/version:[ ]\+['"]\?\([-.a-zA-Z0-9]\+\)['"]\?/version: \1+fake1/`, + filepath.Join(fakeUpdateDir, "meta/snap.yaml")).Run() if err != nil { return fmt.Errorf("changing fake snap version: %v", err) } diff --git a/tests/main/generic-unregister/task.yaml b/tests/main/generic-unregister/task.yaml new file mode 100644 index 0000000000..25913dfc62 --- /dev/null +++ b/tests/main/generic-unregister/task.yaml @@ -0,0 +1,62 @@ +summary: | + Ensure that unregistration API works. + +# ubuntu-14.04: curl does not have --unix-socket option +systems: [-ubuntu-core-*, -ubuntu-14.04-*] + +environment: + UNTIL_REBOOT/rereg: false + UNTIL_REBOOT/until_reboot: true + +prepare: | + systemctl stop snapd.service snapd.socket + cp /var/lib/snapd/state.json state.json.bak + mkdir key + cp /var/lib/snapd/device/private-keys-v1/* key + systemctl start snapd.service snapd.socket + +restore: | + systemctl stop snapd.service snapd.socket + rm -f /var/lib/snapd/device/private-keys-v1/* + cp key/* /var/lib/snapd/device/private-keys-v1/ + cp state.json.bak /var/lib/snapd/state.json + rm -f /run/snapd/noregister + systemctl start snapd.service snapd.socket + +execute: | + #shellcheck source=tests/lib/core-config.sh + . "$TESTSLIB"/core-config.sh + + wait_for_device_initialized_change + + snap model --assertion | MATCH "series: 16" + + if snap model --verbose | NOMATCH "brand-id:\s* generic" ; then + echo "Not a generic model. Skipping." + exit 0 + fi + + keyfile=(/var/lib/snapd/device/private-keys-v1/*) + test -f "${keyfile[0]}" + + curl --data '{"action":"forget","no-registration-until-reboot":'${UNTIL_REBOOT}'}' --unix-socket /run/snapd.socket http://localhost/v2/model/serial + + snap model --serial 2>&1|MATCH "error: device not registered yet" + not test -e "${keyfile[0]}" + + if [ "${UNTIL_REBOOT}" = "true" ] ; then + test -f /run/snapd/noregister + systemctl restart snapd.service + snap model --serial 2>&1|MATCH "error: device not registered yet" + else + not test -e /run/snapd/noregister + snap debug ensure-state-soon + retry --wait 2 -n 120 sh -c 'snap model --serial 2>&1|NOMATCH "error: device not registered yet"' + fi + + snap find pc + if [ "${UNTIL_REBOOT}" = "true" ] ; then + NOMATCH '"session-macaroon":"[^"]' < /var/lib/snapd/state.json + else + MATCH '"session-macaroon":"[^"]' < /var/lib/snapd/state.json + fi diff --git a/tests/main/interfaces-opengl-nvidia/gl-core16/bin/run b/tests/main/interfaces-opengl-nvidia/gl-core16/bin/run new file mode 100755 index 0000000000..f07e1ec43b --- /dev/null +++ b/tests/main/interfaces-opengl-nvidia/gl-core16/bin/run @@ -0,0 +1,3 @@ +#!/bin/sh +PS1='$ ' +exec "$@" diff --git a/tests/main/interfaces-opengl-nvidia/gl-core16/meta/snap.yaml b/tests/main/interfaces-opengl-nvidia/gl-core16/meta/snap.yaml new file mode 100644 index 0000000000..83851c75ef --- /dev/null +++ b/tests/main/interfaces-opengl-nvidia/gl-core16/meta/snap.yaml @@ -0,0 +1,9 @@ +name: gl-core16 +version: 1.0 +summary: Test snap that plugs opengl and uses the core base snap +confinement: strict + +apps: + gl-core16: + command: bin/run + plugs: [ opengl ] diff --git a/tests/main/interfaces-opengl-nvidia/gl-core20/bin/run b/tests/main/interfaces-opengl-nvidia/gl-core20/bin/run new file mode 100755 index 0000000000..f07e1ec43b --- /dev/null +++ b/tests/main/interfaces-opengl-nvidia/gl-core20/bin/run @@ -0,0 +1,3 @@ +#!/bin/sh +PS1='$ ' +exec "$@" diff --git a/tests/main/interfaces-opengl-nvidia/gl-core20/meta/snap.yaml b/tests/main/interfaces-opengl-nvidia/gl-core20/meta/snap.yaml new file mode 100644 index 0000000000..422f183b83 --- /dev/null +++ b/tests/main/interfaces-opengl-nvidia/gl-core20/meta/snap.yaml @@ -0,0 +1,10 @@ +name: gl-core20 +version: 1.0 +summary: Test snap that plugs opengl and uses the core20 base snap +confinement: strict +base: core20 + +apps: + gl-core20: + command: bin/run + plugs: [ opengl ] diff --git a/tests/main/interfaces-opengl-nvidia/task.yaml b/tests/main/interfaces-opengl-nvidia/task.yaml index 0767c587b4..bad3ed4e3a 100644 --- a/tests/main/interfaces-opengl-nvidia/task.yaml +++ b/tests/main/interfaces-opengl-nvidia/task.yaml @@ -1,6 +1,6 @@ summary: Ensure that basic opengl works with faked nvidia -systems: [ubuntu-14.04-*, ubuntu-16.04-*, ubuntu-18.04-*] +systems: [ubuntu-16.04-*, ubuntu-18.04-*, ubuntu-20.04-*] environment: NV_VERSION/stable: "123.456" @@ -18,7 +18,7 @@ prepare: | mkdir -p /usr/share/vulkan/icd.d echo "canary-vulkan" > /usr/share/vulkan/icd.d/nvidia_icd.json - if os.query is-bionic; then + if ! os.query is-xenial; then # mock GLVND EGL vendor file echo "Test GLVND EGL vendor files access" mkdir -p /usr/share/glvnd/egl_vendor.d @@ -26,7 +26,7 @@ prepare: | fi # mock nvidia libraries - if os.query is-bionic; then + if ! os.query is-xenial; then mkdir -p /usr/lib/"$(dpkg-architecture -qDEB_HOST_MULTIARCH)"/tls mkdir -p /usr/lib/"$(dpkg-architecture -qDEB_HOST_MULTIARCH)"/vdpau echo "canary-triplet" >> /usr/lib/"$(dpkg-architecture -qDEB_HOST_MULTIARCH)"/libGLX.so.0.0.1 @@ -69,7 +69,7 @@ restore: | umount -t tmpfs /sys/module rm -rf /usr/share/vulkan - if os.query is-bionic; then + if ! os.query is-xenial; then rm -rf /usr/share/glvnd/egl_vendor.d/10_nvidia.json rm -rf /usr/lib/"$(dpkg-architecture -qDEB_HOST_MULTIARCH)"/tls rm -rf /usr/lib/"$(dpkg-architecture -qDEB_HOST_MULTIARCH)"/vdpau @@ -90,35 +90,55 @@ restore: | rm -rf /usr/lib32/nvidia-123 execute: | - "$TESTSTOOLS"/snaps-state install-local test-snapd-policy-app-consumer + "$TESTSTOOLS"/snaps-state install-local gl-core16 echo "When the interface is connected" - snap connect test-snapd-policy-app-consumer:opengl core:opengl + snap connect gl-core16:opengl core:opengl echo "App can access nvidia library files" - expected="canary-legacy" - if os.query is-bionic; then - expected="canary-triplet" + expected="canary-triplet" + if os.query is-xenial; then + expected="canary-legacy" fi files="libGLX.so.0.0.1 libGLX_nvidia.so.0.0.1 libnvidia-glcore.so.$NV_VERSION tls/libnvidia-tls.so.$NV_VERSION libnvidia-tls.so.$NV_VERSION vdpau/libvdpau_nvidia.so.$NV_VERSION" for f in $files; do - snap run test-snapd-policy-app-consumer.opengl -c "cat /var/lib/snapd/lib/gl/$f" | MATCH "$expected" + gl-core16 cat "/var/lib/snapd/lib/gl/$f" | MATCH "$expected" done if os.query is-pc-amd64; then - expected32="canary-32-legacy" - if os.query is-bionic; then - expected32="canary-32-triplet" + expected32="canary-32-triplet" + if os.query is-xenial; then + expected32="canary-32-legacy" fi for f in $files; do - snap run test-snapd-policy-app-consumer.opengl -c "cat /var/lib/snapd/lib/gl32/$f" | MATCH "$expected32" + gl-core16 cat "/var/lib/snapd/lib/gl32/$f" | MATCH "$expected32" done fi echo "And vulkan ICD file" - snap run test-snapd-policy-app-consumer.opengl -c "cat /var/lib/snapd/lib/vulkan/icd.d/nvidia_icd.json" | MATCH canary-vulkan + gl-core16 cat /var/lib/snapd/lib/vulkan/icd.d/nvidia_icd.json | MATCH canary-vulkan - if os.query is-bionic; then + if ! os.query is-xenial; then echo "And GLVND EGL vendor file" - snap run test-snapd-policy-app-consumer.opengl -c "cat /var/lib/snapd/lib/glvnd/egl_vendor.d/10_nvidia.json" | MATCH canary-egl + gl-core16 cat /var/lib/snapd/lib/glvnd/egl_vendor.d/10_nvidia.json | MATCH canary-egl + fi + + # There is no core20 snap on i386, so the following tests will not + # function there. + if os.query is-pc-i386; then + exit 0 + fi + + echo "For host systems using glvnd, the glvnd libraries are not exposed to snaps using newer bases" + "$TESTSTOOLS"/snaps-state install-local gl-core20 + snap connect gl-core20:opengl core:opengl + + echo "While glvnd frontend libraries are not available, the backend nvidia drivers are" + if ! os.query is-xenial; then + not gl-core20 test -f /var/lib/snapd/lib/gl/libGLX.so.0.0.1 + gl-core20 cat /var/lib/snapd/lib/gl/libGLX_nvidia.so.0.0.1 | MATCH canary-triplet + if os.query is-pc-amd64; then + not gl-core20 cat /var/lib/snapd/lib/gl32/libGLX.so.0.0.1 + gl-core20 cat /var/lib/snapd/lib/gl32/libGLX_nvidia.so.0.0.1 | MATCH canary-32-triplet + fi fi diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar deleted file mode 120000 index 5c68478d09..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar +++ /dev/null @@ -1 +0,0 @@ -foo -> baz -> qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> baz b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> baz deleted file mode 120000 index 5a883c8397..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> baz +++ /dev/null @@ -1 +0,0 @@ -foo -> qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> baz -> qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> baz -> qux deleted file mode 120000 index 1910281566..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> baz -> qux +++ /dev/null @@ -1 +0,0 @@ -foo \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> qux deleted file mode 120000 index 365c3e79f6..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/bar -> qux +++ /dev/null @@ -1 +0,0 @@ -foo -> baz \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/baz b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/baz deleted file mode 120000 index 6ea8eef97f..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/baz +++ /dev/null @@ -1 +0,0 @@ -foo -> bar -> qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/baz -> qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/baz -> qux deleted file mode 120000 index 14b8163083..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/baz -> qux +++ /dev/null @@ -1 +0,0 @@ -foo -> bar \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo deleted file mode 120000 index 35bb28b82d..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo +++ /dev/null @@ -1 +0,0 @@ -bar -> baz -> qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar deleted file mode 120000 index ec193c70de..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar +++ /dev/null @@ -1 +0,0 @@ -baz -> qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar -> baz b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar -> baz deleted file mode 120000 index 78df5b06bd..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar -> baz +++ /dev/null @@ -1 +0,0 @@ -qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar -> qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar -> qux deleted file mode 120000 index 3f95386662..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> bar -> qux +++ /dev/null @@ -1 +0,0 @@ -baz \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> baz b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> baz deleted file mode 120000 index 21f05bdff4..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> baz +++ /dev/null @@ -1 +0,0 @@ -bar -> qux \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> baz -> qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> baz -> qux deleted file mode 120000 index ba0e162e1c..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> baz -> qux +++ /dev/null @@ -1 +0,0 @@ -bar \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> qux deleted file mode 120000 index 522e3d9299..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/foo -> qux +++ /dev/null @@ -1 +0,0 @@ -bar -> baz \ No newline at end of file diff --git a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/qux b/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/qux deleted file mode 120000 index af0f493f56..0000000000 --- a/tests/main/validate-container-failures/test-snapd-validate-container-failures/hell/qux +++ /dev/null @@ -1 +0,0 @@ -foo -> bar -> baz \ No newline at end of file diff --git a/tests/main/validate-container-happy/task.yaml b/tests/main/validate-container-happy/task.yaml new file mode 100644 index 0000000000..b33faa0112 --- /dev/null +++ b/tests/main/validate-container-happy/task.yaml @@ -0,0 +1,38 @@ +summary: check the symlinks following the right track + +environment: + SNAP: test-snapd-validate-container-happy + +prepare: | + +execute: | + + SNAP_MOUNT_DIR="$(os.paths snap-mount-dir)" + + # We shouldn't use relative symlinks in Github as they cannot be packed correctly. + # So here let's test whether we can still pack such symlinks within a snap and use if needed. + # First we "try" to unpack the snap structure and untar the symlinks + # Then we pack the snap with these symlinks and then install + # Finally we check to see if the symlinks actually support the intervined symlinks + + # Untar the symlinks + tar -xvf "$SNAP"/hell/hell.tar -C "$SNAP"/hell + + snap try "$SNAP" + # Check to see if the symlinks point to the right paths + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/bar | MATCH "foo -> baz -> qux" + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/baz | MATCH "foo -> bar -> qux" + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/foo | MATCH "bar -> baz -> qux" + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/qux | MATCH "foo -> bar -> baz" + snap remove "$SNAP" + + # Create a new snap structure that includes the unpacked symlinks + snap pack "$SNAP" + snap install --dangerous test-snapd-validate-container-happy_1.0_all.snap + tests.cleanup defer snap remove --purge test-snapd-validate-container-happy + + # Check to see if the symlinks retain their existing paths + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/bar | MATCH "foo -> baz -> qux" + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/baz | MATCH "foo -> bar -> qux" + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/foo | MATCH "bar -> baz -> qux" + readlink "$SNAP_MOUNT_DIR"/"$SNAP"/current/hell/qux | MATCH "foo -> bar -> baz" diff --git a/tests/completion/data/twisted/.just a hidden file b/tests/main/validate-container-happy/test-snapd-validate-container-happy/bin/validate-container index e69de29bb2..e69de29bb2 100644..100755 --- a/tests/completion/data/twisted/.just a hidden file +++ b/tests/main/validate-container-happy/test-snapd-validate-container-happy/bin/validate-container diff --git a/tests/main/validate-container-happy/test-snapd-validate-container-happy/hell/hell.tar b/tests/main/validate-container-happy/test-snapd-validate-container-happy/hell/hell.tar Binary files differnew file mode 100644 index 0000000000..17c8f0c271 --- /dev/null +++ b/tests/main/validate-container-happy/test-snapd-validate-container-happy/hell/hell.tar diff --git a/tests/main/validate-container-happy/test-snapd-validate-container-happy/meta/snap.yaml b/tests/main/validate-container-happy/test-snapd-validate-container-happy/meta/snap.yaml new file mode 100644 index 0000000000..3f660211c7 --- /dev/null +++ b/tests/main/validate-container-happy/test-snapd-validate-container-happy/meta/snap.yaml @@ -0,0 +1,5 @@ +name: test-snapd-validate-container-happy +version: 1.0 +apps: + validate-container: + command: bin/validate-container diff --git a/tests/nested/manual/core-early-config/defaults.yaml b/tests/nested/manual/core-early-config/defaults.yaml index 99fda715e8..85ab053bcd 100644 --- a/tests/nested/manual/core-early-config/defaults.yaml +++ b/tests/nested/manual/core-early-config/defaults.yaml @@ -7,4 +7,4 @@ defaults: disable: true system: timezone: Europe/Malta - hostname: foo + hostname: F00 diff --git a/tests/nested/manual/core-early-config/task.yaml b/tests/nested/manual/core-early-config/task.yaml index bfe6db58d3..021f99ed8f 100644 --- a/tests/nested/manual/core-early-config/task.yaml +++ b/tests/nested/manual/core-early-config/task.yaml @@ -57,4 +57,4 @@ execute: | tests.nested exec "cat /var/lib/console-conf/complete" | MATCH "console-conf has been disabled by the snapd system configuration" # hostname is set - tests.nested exec "cat /etc/hostname" | MATCH "foo" + tests.nested exec "cat /etc/hostname" | MATCH "F00" diff --git a/tests/nested/manual/core20-to-core22/task.yaml b/tests/nested/manual/core20-to-core22/task.yaml new file mode 100644 index 0000000000..5a66f65141 --- /dev/null +++ b/tests/nested/manual/core20-to-core22/task.yaml @@ -0,0 +1,78 @@ +summary: verify a UC20 to UC22 remodel + +# the test may be unstable as UC22 is effectively a work-in-progress thing, and +# the model in question uses latest/edge of core22 and 22/edge of pc and +# pc-kernel snaps + +systems: [ubuntu-20.04-64] + +environment: + NESTED_CUSTOM_MODEL: $TESTSLIB/assertions/valid-for-testing-pc-20.model + NESTED_IMAGE_ID: uc22-remodel-testing + # TODO: disable TPM for now and investigate why the system cannot be booted + # after remodel completes + NESTED_ENABLE_TPM: false + NESTED_ENABLE_SECURE_BOOT: false + NESTED_BUILD_SNAPD_FROM_CURRENT: false + +prepare: | + # we need pc/pc-kernel/core20 from the store, such that they get properly + # refreshed when doing a UC20 to UC22 remodel + "$TESTSTOOLS"/snaps-state repack_snapd_deb_into_snap snapd "$PWD/extra-snaps" + tests.nested build-image core + tests.nested create-vm core + +execute: | + # shellcheck source=tests/lib/nested.sh + . "$TESTSLIB/nested.sh" + boot_id="$( nested_get_boot_id )" + tests.nested exec snap model |MATCH 'model +my-model$' + # XXX: recovery system label is based on a date; we may end up with a + # different label if the remodel runs around midnight; the label will + # conflict with an existing system label + label_base=$(tests.nested exec "date '+%Y%m%d'") + label="${label_base}-1" + + echo "Remodel to UC22" + nested_copy "$TESTSLIB/assertions/valid-for-testing-pc-22-from-20.model" + REMOTE_CHG_ID="$(tests.nested exec sudo snap remodel --no-wait valid-for-testing-pc-22-from-20.model)" + test -n "$REMOTE_CHG_ID" + # very long retry wait for the change to be in stable state, once it's + # stable it does not mean that the change was successful yet + retry -n 100 --wait 5 sh -c "tests.nested exec sudo snap changes | grep -E '^${REMOTE_CHG_ID}\s+(Done|Undone|Error)'" + # check that now + tests.nested exec sudo snap changes | grep -E "^${REMOTE_CHG_ID}\s+Done" + + # we should have rebooted a couple of times (at least twice for the recovery + # system and the base), so boot-id should be different + current_boot_id="$( nested_get_boot_id )" + test "$boot_id" != "$current_boot_id" + + tests.nested exec sudo snap list pc | MATCH " 22/edge " + tests.nested exec sudo snap list pc-kernel | MATCH " 22/edge " + tests.nested exec sudo snap list core22 | MATCH "core22 " + + echo "Verify seed system with label $label" + tests.nested exec "sudo cat /run/mnt/ubuntu-seed/systems/${label}/model" > model-from-seed.model + MATCH core22 < model-from-seed.model + NOMATCH core20 < model-from-seed.model + + echo "Verify that UC22 recover system is usable" + boot_id="$( nested_get_boot_id )" + tests.nested exec sudo snap reboot --recover "${label}" | MATCH 'Reboot into ".*" "recover" mode' + nested_wait_for_reboot "${boot_id}" + # Verify we are in recover mode with the expected system label + tests.nested exec 'sudo cat /proc/cmdline' | MATCH "snapd_recovery_mode=recover snapd_recovery_system=${label} " + # we are in recover mode, so tools need to be set up again + nested_prepare_tools + + nested_wait_for_snap_command + # there should be no core20 since the seed is UC22 + tests.nested exec sudo snap list | NOMATCH core20 + + boot_id="$( nested_get_boot_id )" + echo "And back to run mode" + tests.nested exec "sudo snap wait system seed.loaded" + tests.nested exec sudo snap reboot --run | MATCH 'Reboot into "run" mode.' + nested_wait_for_reboot "${boot_id}" + tests.nested exec 'sudo cat /proc/cmdline' | MATCH "snapd_recovery_mode=run " diff --git a/usersession/client/client.go b/usersession/client/client.go index 602091f7ab..9554257fcc 100644 --- a/usersession/client/client.go +++ b/usersession/client/client.go @@ -52,6 +52,7 @@ func dialSessionAgent(network, address string) (net.Conn, error) { type Client struct { doer *http.Client + uids []int } func New() *Client { @@ -61,6 +62,14 @@ func New() *Client { } } +// NewForUids creates a Client that sends requests to a specific list of uids +// only. +func NewForUids(uids ...int) *Client { + cli := New() + cli.uids = append(cli.uids, uids...) + return cli +} + type Error struct { Kind string `json:"kind"` Value interface{} `json:"value"` @@ -132,11 +141,11 @@ func (client *Client) sendRequest(ctx context.Context, socket string, method, ur return response } -// doMany sends the given request to all active user sessions. Please be -// careful when using this method, because it is not aware of the physical user -// who triggered the request and blindly forwards it to all logged in users. -// Some of them might not have the right to see the request (let alone to -// respond to it). +// doMany sends the given request to all active user sessions or a subset of them +// defined by optional client.uids field. Please be careful when using this +// method, because it is not aware of the physical user who triggered the request +// and blindly forwards it to all logged in users. Some of them might not have +// the right to see the request (let alone to respond to it). func (client *Client) doMany(ctx context.Context, method, urlpath string, query url.Values, headers map[string]string, body []byte) ([]*response, error) { sockets, err := filepath.Glob(filepath.Join(dirs.XdgRuntimeDirGlob, "snapd-session-agent.socket")) if err != nil { @@ -147,7 +156,26 @@ func (client *Client) doMany(ctx context.Context, method, urlpath string, query mu sync.Mutex responses []*response ) + + var uids map[string]bool + if len(client.uids) > 0 { + uids = make(map[string]bool) + for _, uid := range client.uids { + uids[fmt.Sprintf("%d", uid)] = true + } + } + for _, socket := range sockets { + // filter out sockets based on uids + if len(uids) > 0 { + // XXX: alternatively we could Stat() the socket and + // and check Uid field of stat.Sys().(*syscall.Stat_t), but it's + // more annyoing to unit-test. + userPart := filepath.Base(filepath.Dir(socket)) + if !uids[userPart] { + continue + } + } wg.Add(1) go func(socket string) { defer wg.Done() diff --git a/usersession/client/client_test.go b/usersession/client/client_test.go index 1ee96531f7..299cf5c9b2 100644 --- a/usersession/client/client_test.go +++ b/usersession/client/client_test.go @@ -26,6 +26,7 @@ import ( "net/http" "os" "path/filepath" + "sync/atomic" "testing" "time" @@ -448,7 +449,9 @@ func (s *clientSuite) TestServicesStopFailure(c *C) { } func (s *clientSuite) TestPendingRefreshNotification(c *C) { + var n int32 s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt32(&n, 1) c.Assert(r.URL.Path, Equals, "/v1/notifications/pending-refresh") w.Header().Set("Content-Type", "application/json") w.WriteHeader(200) @@ -456,4 +459,20 @@ func (s *clientSuite) TestPendingRefreshNotification(c *C) { }) err := s.cli.PendingRefreshNotification(context.Background(), &client.PendingSnapRefreshInfo{}) c.Assert(err, IsNil) + c.Check(atomic.LoadInt32(&n), Equals, int32(2)) +} + +func (s *clientSuite) TestPendingRefreshNotificationOneClient(c *C) { + cli := client.NewForUids(1000) + var n int32 + s.handler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + atomic.AddInt32(&n, 1) + c.Assert(r.URL.Path, Equals, "/v1/notifications/pending-refresh") + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(200) + w.Write([]byte(`{"type": "sync"}`)) + }) + err := cli.PendingRefreshNotification(context.Background(), &client.PendingSnapRefreshInfo{}) + c.Assert(err, IsNil) + c.Check(atomic.LoadInt32(&n), Equals, int32(1)) } |
