diff options
| author | Pawel Stolowski <stolowski@gmail.com> | 2018-05-16 13:05:29 +0200 |
|---|---|---|
| committer | Pawel Stolowski <stolowski@gmail.com> | 2018-05-16 13:05:29 +0200 |
| commit | 99e81c0fb6ce2a5ffec3404bf24b0f9d3cbfc3b6 (patch) | |
| tree | 70edb0a874232a75b0c68a69b35f592a15e1c99f | |
| parent | 5aa8a71b2931f94aba285833a0a3121b65618acd (diff) | |
| parent | 761506d292271cb7c5c452adf21a505fa35e82ae (diff) | |
Merge branch 'master' into remove-stale-connectionsremove-stale-connections
102 files changed, 1715 insertions, 516 deletions
diff --git a/asserts/ifacedecls.go b/asserts/ifacedecls.go index ce8f77c6fc..d9ceb353d2 100644 --- a/asserts/ifacedecls.go +++ b/asserts/ifacedecls.go @@ -150,6 +150,14 @@ func (matcher mapAttrMatcher) feature(flabel string) bool { func (matcher mapAttrMatcher) match(apath string, v interface{}, ctx AttrMatchContext) error { switch x := v.(type) { + case Attrer: + // we get Atter from root-level Check (apath is "") + for k, matcher1 := range matcher { + v, _ := x.Lookup(k) + if err := matchEntry("", k, matcher1, v, ctx); err != nil { + return err + } + } case map[string]interface{}: // maps in attributes look like this for k, matcher1 := range matcher { if err := matchEntry(apath, k, matcher1, x[k], ctx); err != nil { @@ -340,9 +348,14 @@ var ( NeverMatchAttributes = &AttributeConstraints{matcher: fixedAttrMatcher{errors.New("not allowed")}} ) +// Attrer reflects part of the Attrer interface (see interfaces.Attrer). +type Attrer interface { + Lookup(path string) (interface{}, bool) +} + // Check checks whether attrs don't match the constraints. -func (c *AttributeConstraints) Check(attrs map[string]interface{}, ctx AttrMatchContext) error { - return c.matcher.match("", attrs, ctx) +func (c *AttributeConstraints) Check(attrer Attrer, ctx AttrMatchContext) error { + return c.matcher.match("", attrer, ctx) } // OnClassicConstraint specifies a constraint based whether the system is classic and optional specific distros' sets. diff --git a/asserts/ifacedecls_test.go b/asserts/ifacedecls_test.go index 405630a746..5006ba2d05 100644 --- a/asserts/ifacedecls_test.go +++ b/asserts/ifacedecls_test.go @@ -41,7 +41,14 @@ type attrConstraintsSuite struct { testutil.BaseTest } -func attrs(yml string) map[string]interface{} { +type attrerObject map[string]interface{} + +func (o attrerObject) Lookup(path string) (interface{}, bool) { + v, ok := o[path] + return v, ok +} + +func attrs(yml string) *attrerObject { var attrs map[string]interface{} err := yaml.Unmarshal([]byte(yml), &attrs) if err != nil { @@ -56,11 +63,17 @@ func attrs(yml string) map[string]interface{} { if err != nil { panic(err) } + + // NOTE: it's important to go through snap yaml here even though we're really interested in Attrs only, + // as InfoFromSnapYaml normalizes yaml values. info, err := snap.InfoFromSnapYaml(snapYaml) if err != nil { panic(err) } - return info.Plugs["plug"].Attrs + + var ao attrerObject + ao = info.Plugs["plug"].Attrs + return &ao } func (s *attrConstraintsSuite) SetUpTest(c *C) { @@ -81,24 +94,27 @@ func (s *attrConstraintsSuite) TestSimple(c *C) { cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) c.Assert(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug := attrerObject(map[string]interface{}{ "foo": "FOO", "bar": "BAR", "baz": "BAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "foo": "FOO", "bar": "BAZ", "baz": "BAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BAZ" does not match \^\(BAR\)\$`) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "foo": "FOO", "baz": "BAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `attribute "bar" has constraints but is unset`) } @@ -110,29 +126,34 @@ func (s *attrConstraintsSuite) TestSimpleAnchorsVsRegexpAlt(c *C) { cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].(map[string]interface{})) c.Assert(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug := attrerObject(map[string]interface{}{ "bar": "BAR", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "bar": "BARR", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BARR" does not match \^\(BAR|BAZ\)\$`) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "bar": "BBAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BAZZ" does not match \^\(BAR|BAZ\)\$`) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "bar": "BABAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BABAZ" does not match \^\(BAR|BAZ\)\$`) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "bar": "BARAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `attribute "bar" value "BARAZ" does not match \^\(BAR|BAZ\)\$`) } @@ -199,25 +220,28 @@ func (s *attrConstraintsSuite) TestAlternative(c *C) { cstrs, err := asserts.CompileAttributeConstraints(m["attrs"].([]interface{})) c.Assert(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug := attrerObject(map[string]interface{}{ "foo": "FOO", "bar": "BAR", "baz": "BAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "foo": "FOO", "bar": "BAZ", "baz": "BAZ", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug = attrerObject(map[string]interface{}{ "foo": "FOO", "bar": "BARR", "baz": "BAR", - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, ErrorMatches, `no alternative matches: attribute "bar" value "BARR" does not match \^\(BAR\)\$`) } @@ -274,10 +298,11 @@ bar: true `), nil) c.Check(err, IsNil) - err = cstrs.Check(map[string]interface{}{ + plug := attrerObject(map[string]interface{}{ "foo": int64(1), "bar": true, - }, nil) + }) + err = cstrs.Check(plug, nil) c.Check(err, IsNil) } @@ -461,12 +486,14 @@ func (s *attrConstraintsSuite) TestNeverMatchAttributeConstraints(c *C) { type plugSlotRulesSuite struct{} func checkAttrs(c *C, attrs *asserts.AttributeConstraints, witness, expected string) { - c.Check(attrs.Check(map[string]interface{}{ + plug := attrerObject(map[string]interface{}{ witness: "XYZ", - }, nil), ErrorMatches, fmt.Sprintf(`attribute "%s".*does not match.*`, witness)) - c.Check(attrs.Check(map[string]interface{}{ + }) + c.Check(attrs.Check(plug, nil), ErrorMatches, fmt.Sprintf(`attribute "%s".*does not match.*`, witness)) + plug = attrerObject(map[string]interface{}{ witness: expected, - }, nil), IsNil) + }) + c.Check(attrs.Check(plug, nil), IsNil) } func checkBoolPlugConnConstraints(c *C, cstrs []*asserts.PlugConnectionConstraints, always bool) { diff --git a/asserts/snap_asserts_test.go b/asserts/snap_asserts_test.go index 60457fd11b..78dc0a675e 100644 --- a/asserts/snap_asserts_test.go +++ b/asserts/snap_asserts_test.go @@ -49,6 +49,12 @@ type snapDeclSuite struct { tsLine string } +type emptyAttrerObject struct{} + +func (o emptyAttrerObject) Lookup(path string) (interface{}, bool) { + return nil, false +} + func (sds *snapDeclSuite) SetUpSuite(c *C) { sds.ts = time.Now().Truncate(time.Second).UTC() sds.tsLine = "timestamp: " + sds.ts.Format(time.RFC3339) + "\n" @@ -298,23 +304,27 @@ AXNpZw==` c.Assert(plugRule1.DenyInstallation, HasLen, 1) c.Check(plugRule1.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) c.Assert(plugRule1.AllowAutoConnection, HasLen, 1) - c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) - c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) + + plug := emptyAttrerObject{} + slot := emptyAttrerObject{} + + c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "b1".*`) c.Check(plugRule1.AllowAutoConnection[0].SlotSnapTypes, DeepEquals, []string{"app"}) c.Check(plugRule1.AllowAutoConnection[0].SlotPublisherIDs, DeepEquals, []string{"acme"}) c.Assert(plugRule1.DenyAutoConnection, HasLen, 1) - c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) - c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "b1".*`) plugRule2 := snapDecl.PlugRule("interface2") c.Assert(plugRule2, NotNil) c.Assert(plugRule2.AllowInstallation, HasLen, 1) c.Check(plugRule2.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) c.Assert(plugRule2.AllowConnection, HasLen, 1) - c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) - c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "b2".*`) c.Assert(plugRule2.DenyConnection, HasLen, 1) - c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) - c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "b2".*`) c.Check(plugRule2.DenyConnection[0].SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) slotRule3 := snapDecl.SlotRule("interface3") @@ -322,24 +332,24 @@ AXNpZw==` c.Assert(slotRule3.DenyInstallation, HasLen, 1) c.Check(slotRule3.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) c.Assert(slotRule3.AllowAutoConnection, HasLen, 1) - c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) - c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "d1".*`) c.Check(slotRule3.AllowAutoConnection[0].PlugSnapTypes, DeepEquals, []string{"app"}) c.Check(slotRule3.AllowAutoConnection[0].PlugPublisherIDs, DeepEquals, []string{"acme"}) c.Assert(slotRule3.DenyAutoConnection, HasLen, 1) - c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) - c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "d1".*`) slotRule4 := snapDecl.SlotRule("interface4") c.Assert(slotRule4, NotNil) c.Assert(slotRule4.AllowAutoConnection, HasLen, 1) - c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) - c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "d2".*`) c.Assert(slotRule4.DenyAutoConnection, HasLen, 1) - c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) - c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "d2".*`) c.Check(slotRule4.DenyConnection[0].PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) c.Assert(slotRule4.AllowInstallation, HasLen, 1) - c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "e1".*`) + c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "e1".*`) c.Check(slotRule4.AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"app"}) } @@ -1197,28 +1207,31 @@ AXNpZw==` c.Check(baseDecl.PlugRule("interfaceX"), IsNil) c.Check(baseDecl.SlotRule("interfaceX"), IsNil) + plug := emptyAttrerObject{} + slot := emptyAttrerObject{} + plugRule1 := baseDecl.PlugRule("interface1") c.Assert(plugRule1, NotNil) c.Assert(plugRule1.DenyInstallation, HasLen, 1) c.Check(plugRule1.DenyInstallation[0].PlugAttributes, Equals, asserts.NeverMatchAttributes) c.Assert(plugRule1.AllowAutoConnection, HasLen, 1) - c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) - c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.AllowAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.AllowAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "b1".*`) c.Check(plugRule1.AllowAutoConnection[0].SlotSnapTypes, DeepEquals, []string{"app"}) c.Check(plugRule1.AllowAutoConnection[0].SlotPublisherIDs, DeepEquals, []string{"acme"}) c.Assert(plugRule1.DenyAutoConnection, HasLen, 1) - c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "a1".*`) - c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "b1".*`) + c.Check(plugRule1.DenyAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "a1".*`) + c.Check(plugRule1.DenyAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "b1".*`) plugRule2 := baseDecl.PlugRule("interface2") c.Assert(plugRule2, NotNil) c.Assert(plugRule2.AllowInstallation, HasLen, 1) c.Check(plugRule2.AllowInstallation[0].PlugAttributes, Equals, asserts.AlwaysMatchAttributes) c.Assert(plugRule2.AllowConnection, HasLen, 1) - c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) - c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.AllowConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.AllowConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "b2".*`) c.Assert(plugRule2.DenyConnection, HasLen, 1) - c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "a2".*`) - c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "b2".*`) + c.Check(plugRule2.DenyConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "a2".*`) + c.Check(plugRule2.DenyConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "b2".*`) c.Check(plugRule2.DenyConnection[0].SlotSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) slotRule3 := baseDecl.SlotRule("interface3") @@ -1226,24 +1239,24 @@ AXNpZw==` c.Assert(slotRule3.DenyInstallation, HasLen, 1) c.Check(slotRule3.DenyInstallation[0].SlotAttributes, Equals, asserts.NeverMatchAttributes) c.Assert(slotRule3.AllowAutoConnection, HasLen, 1) - c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) - c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.AllowAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.AllowAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "d1".*`) c.Check(slotRule3.AllowAutoConnection[0].PlugSnapTypes, DeepEquals, []string{"app"}) c.Check(slotRule3.AllowAutoConnection[0].PlugPublisherIDs, DeepEquals, []string{"acme"}) c.Assert(slotRule3.DenyAutoConnection, HasLen, 1) - c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "c1".*`) - c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "d1".*`) + c.Check(slotRule3.DenyAutoConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "c1".*`) + c.Check(slotRule3.DenyAutoConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "d1".*`) slotRule4 := baseDecl.SlotRule("interface4") c.Assert(slotRule4, NotNil) c.Assert(slotRule4.AllowConnection, HasLen, 1) - c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) - c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.AllowConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.AllowConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "d2".*`) c.Assert(slotRule4.DenyConnection, HasLen, 1) - c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(nil, nil), ErrorMatches, `attribute "c2".*`) - c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "d2".*`) + c.Check(slotRule4.DenyConnection[0].PlugAttributes.Check(plug, nil), ErrorMatches, `attribute "c2".*`) + c.Check(slotRule4.DenyConnection[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "d2".*`) c.Check(slotRule4.DenyConnection[0].PlugSnapIDs, DeepEquals, []string{"snapidsnapidsnapidsnapidsnapid01", "snapidsnapidsnapidsnapidsnapid02"}) c.Assert(slotRule4.AllowInstallation, HasLen, 1) - c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(nil, nil), ErrorMatches, `attribute "e1".*`) + c.Check(slotRule4.AllowInstallation[0].SlotAttributes.Check(slot, nil), ErrorMatches, `attribute "e1".*`) c.Check(slotRule4.AllowInstallation[0].SlotSnapTypes, DeepEquals, []string{"app"}) } diff --git a/client/client.go b/client/client.go index c447a60a7c..02622bfa3f 100644 --- a/client/client.go +++ b/client/client.go @@ -436,8 +436,9 @@ type SysInfo struct { KernelVersion string `json:"kernel-version,omitempty"` - Refresh RefreshInfo `json:"refresh,omitempty"` - Confinement string `json:"confinement"` + Refresh RefreshInfo `json:"refresh,omitempty"` + Confinement string `json:"confinement"` + SandboxFeatures map[string][]string `json:"sandbox-features,omitempty"` } func (rsp *response) err() error { diff --git a/client/client_test.go b/client/client_test.go index 36337ad812..ee4e12a1fd 100644 --- a/client/client_test.go +++ b/client/client_test.go @@ -217,7 +217,8 @@ func (cs *clientSuite) TestClientSysInfo(c *C) { "os-release": {"id": "ubuntu", "version-id": "16.04"}, "on-classic": true, "build-id": "1234", - "confinement": "strict"}}` + "confinement": "strict", + "sandbox-features": {"backend": ["feature-1", "feature-2"]}}}` sysInfo, err := cs.cli.SysInfo() c.Check(err, IsNil) c.Check(sysInfo, DeepEquals, &client.SysInfo{ @@ -229,7 +230,10 @@ func (cs *clientSuite) TestClientSysInfo(c *C) { }, OnClassic: true, Confinement: "strict", - BuildID: "1234", + SandboxFeatures: map[string][]string{ + "backend": {"feature-1", "feature-2"}, + }, + BuildID: "1234", }) } diff --git a/cmd/snap-update-ns/main.go b/cmd/snap-update-ns/main.go index 49e046236e..5e5667bad7 100644 --- a/cmd/snap-update-ns/main.go +++ b/cmd/snap-update-ns/main.go @@ -239,10 +239,7 @@ func applyUserFstab(snapName string) error { } // Replace XDG_RUNTIME_DIR in mount profile - xdgRuntimeDir, ok := os.LookupEnv("XDG_RUNTIME_DIR") - if !ok { - return fmt.Errorf("XDG_RUNTIME_DIR is not set") - } + xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid()) for i := range desired.Entries { if strings.HasPrefix(desired.Entries[i].Name, "$XDG_RUNTIME_DIR/") { desired.Entries[i].Name = strings.Replace(desired.Entries[i].Name, "$XDG_RUNTIME_DIR", xdgRuntimeDir, 1) diff --git a/cmd/snap-update-ns/main_test.go b/cmd/snap-update-ns/main_test.go index 763efdf4bd..460b4739b3 100644 --- a/cmd/snap-update-ns/main_test.go +++ b/cmd/snap-update-ns/main_test.go @@ -317,9 +317,6 @@ func (s *mainSuite) TestApplyUserFstab(c *C) { dirs.SetRootDir(c.MkDir()) defer dirs.SetRootDir("/") - os.Setenv("XDG_RUNTIME_DIR", filepath.Join(c.MkDir(), "run/user/42")) - defer os.Unsetenv("XDG_RUNTIME_DIR") - var changes []update.Change restore := update.MockChangePerform(func(chg *update.Change, sec *update.Secure) ([]*update.Change, error) { changes = append(changes, *chg) @@ -339,8 +336,10 @@ func (s *mainSuite) TestApplyUserFstab(c *C) { err = update.ApplyUserFstab("foo") c.Assert(err, IsNil) + xdgRuntimeDir := fmt.Sprintf("%s/%d", dirs.XdgRuntimeDirBase, os.Getuid()) + c.Assert(changes, HasLen, 1) c.Assert(changes[0].Action, Equals, update.Mount) - c.Assert(changes[0].Entry.Name, Matches, `.*/run/user/42/doc/by-app/snap\.foo`) - c.Assert(changes[0].Entry.Dir, Matches, `.*/run/user/42/doc`) + c.Assert(changes[0].Entry.Name, Equals, xdgRuntimeDir+"/doc/by-app/snap.foo") + c.Assert(changes[0].Entry.Dir, Matches, xdgRuntimeDir+"/doc") } diff --git a/cmd/snap/cmd_can_manage_refreshes.go b/cmd/snap/cmd_can_manage_refreshes.go index a1295cce5d..4367e4f1de 100644 --- a/cmd/snap/cmd_can_manage_refreshes.go +++ b/cmd/snap/cmd_can_manage_refreshes.go @@ -33,7 +33,7 @@ func init() { "(internal) return if refresh.schedule=managed can be used", func() flags.Commander { return &cmdCanManageRefreshes{} - }) + }, nil, nil) cmd.hidden = true } diff --git a/cmd/snap/cmd_confinement.go b/cmd/snap/cmd_confinement.go index 64ec7e20a0..05ba11803d 100644 --- a/cmd/snap/cmd_confinement.go +++ b/cmd/snap/cmd_confinement.go @@ -36,7 +36,9 @@ partial or none) the system operates in. type cmdConfinement struct{} func init() { - addDebugCommand("confinement", shortConfinementHelp, longConfinementHelp, func() flags.Commander { return &cmdConfinement{} }) + addDebugCommand("confinement", shortConfinementHelp, longConfinementHelp, func() flags.Commander { + return &cmdConfinement{} + }, nil, nil) } func (cmd cmdConfinement) Execute(args []string) error { diff --git a/cmd/snap/cmd_ensure_state_soon.go b/cmd/snap/cmd_ensure_state_soon.go index cb44363b62..3f029ecb66 100644 --- a/cmd/snap/cmd_ensure_state_soon.go +++ b/cmd/snap/cmd_ensure_state_soon.go @@ -31,7 +31,7 @@ func init() { "(internal) trigger an ensure run in the state engine", func() flags.Commander { return &cmdEnsureStateSoon{} - }) + }, nil, nil) cmd.hidden = true } diff --git a/cmd/snap/cmd_get_base_declaration.go b/cmd/snap/cmd_get_base_declaration.go index c0795001af..9c96134927 100644 --- a/cmd/snap/cmd_get_base_declaration.go +++ b/cmd/snap/cmd_get_base_declaration.go @@ -33,7 +33,7 @@ func init() { "(internal) obtain the base declaration for all interfaces", func() flags.Commander { return &cmdGetBaseDeclaration{} - }) + }, nil, nil) } func (x *cmdGetBaseDeclaration) Execute(args []string) error { diff --git a/cmd/snap/cmd_sandbox_features.go b/cmd/snap/cmd_sandbox_features.go new file mode 100644 index 0000000000..8c9395bfb7 --- /dev/null +++ b/cmd/snap/cmd_sandbox_features.go @@ -0,0 +1,88 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package main + +import ( + "fmt" + "sort" + "strings" + + "github.com/jessevdk/go-flags" + + "github.com/snapcore/snapd/i18n" +) + +var shortSandboxFeaturesHelp = i18n.G("Print sandbox features available on the system") +var longSandboxFeaturesHelp = i18n.G(` +The sandbox command prints tags describing features of individual sandbox +components used by snapd on a given system. +`) + +type cmdSandboxFeatures struct { + Required []string `long:"required" arg-name:"<backend feature>"` +} + +func init() { + addDebugCommand("sandbox-features", shortSandboxFeaturesHelp, longSandboxFeaturesHelp, func() flags.Commander { + return &cmdSandboxFeatures{} + }, map[string]string{ + "required": i18n.G("Ensure that given backend:feature is available"), + }, nil) +} + +func (cmd cmdSandboxFeatures) Execute(args []string) error { + if len(args) > 0 { + return ErrExtraArgs + } + + cli := Client() + sysInfo, err := cli.SysInfo() + if err != nil { + return err + } + + sandboxFeatures := sysInfo.SandboxFeatures + + if len(cmd.Required) > 0 { + avail := make(map[string]bool) + for backend := range sandboxFeatures { + for _, feature := range sandboxFeatures[backend] { + avail[fmt.Sprintf("%s:%s", backend, feature)] = true + } + } + for _, required := range cmd.Required { + if !avail[required] { + return fmt.Errorf("sandbox feature not available: %q", required) + } + } + } else { + backends := make([]string, 0, len(sandboxFeatures)) + for backend := range sandboxFeatures { + backends = append(backends, backend) + } + sort.Strings(backends) + w := tabWriter() + defer w.Flush() + for _, backend := range backends { + fmt.Fprintf(w, "%s:\t%s\n", backend, strings.Join(sandboxFeatures[backend], " ")) + } + } + return nil +} diff --git a/cmd/snap/cmd_sandbox_features_test.go b/cmd/snap/cmd_sandbox_features_test.go new file mode 100644 index 0000000000..c53ca56d7e --- /dev/null +++ b/cmd/snap/cmd_sandbox_features_test.go @@ -0,0 +1,61 @@ +// -*- Mode: Go; indent-tabs-mode: t -*- + +/* + * Copyright (C) 2018 Canonical Ltd + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License version 3 as + * published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see <http://www.gnu.org/licenses/>. + * + */ + +package main_test + +import ( + "fmt" + "net/http" + + . "gopkg.in/check.v1" + + snap "github.com/snapcore/snapd/cmd/snap" +) + +func (s *SnapSuite) TestSandboxFeatures(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "sync", "result": {"sandbox-features": {"apparmor": ["a", "b", "c"], "selinux": ["1", "2", "3"]}}}`) + }) + _, err := snap.Parser().ParseArgs([]string{"debug", "sandbox-features"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, ""+ + "apparmor: a b c\n"+ + "selinux: 1 2 3\n") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestSandboxFeaturesRequired(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "sync", "result": {"sandbox-features": {"apparmor": ["a", "b", "c"], "selinux": ["1", "2", "3"]}}}`) + }) + _, err := snap.Parser().ParseArgs([]string{"debug", "sandbox-features", "--required=apparmor:a", "--required=selinux:2"}) + c.Assert(err, IsNil) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} + +func (s *SnapSuite) TestSandboxFeaturesRequiredButMissing(c *C) { + s.RedirectClientToTestServer(func(w http.ResponseWriter, r *http.Request) { + fmt.Fprintln(w, `{"type": "sync", "result": {"sandbox-features": {"apparmor": ["a", "b", "c"], "selinux": ["1", "2", "3"]}}}`) + }) + _, err := snap.Parser().ParseArgs([]string{"debug", "sandbox-features", "--required=magic:thing"}) + c.Assert(err, ErrorMatches, `sandbox feature not available: "magic:thing"`) + c.Assert(s.Stdout(), Equals, "") + c.Assert(s.Stderr(), Equals, "") +} diff --git a/cmd/snap/cmd_snap_op.go b/cmd/snap/cmd_snap_op.go index 269d027e13..2fa23f1a47 100644 --- a/cmd/snap/cmd_snap_op.go +++ b/cmd/snap/cmd_snap_op.go @@ -281,8 +281,8 @@ func showDone(names []string, op string) error { fmt.Fprintf(Stdout, "internal error: unknown op %q", op) } if snap.TrackingChannel != snap.Channel && snap.Channel != "" { - // TRANSLATORS: first %s is a snap name, following %s is a channel name - fmt.Fprintf(Stdout, i18n.G("Snap %s is no longer tracking %s.\n"), snap.Name, snap.TrackingChannel) + // TRANSLATORS: first %s is a channel name, following %s is a snap name, last %s is a channel name again. + fmt.Fprintf(Stdout, i18n.G("Channel %s for %s is closed; temporarily forwarding to %s.\n"), snap.TrackingChannel, snap.Name, snap.Channel) } } diff --git a/cmd/snap/cmd_snap_op_test.go b/cmd/snap/cmd_snap_op_test.go index ae7db521a6..2906d548b5 100644 --- a/cmd/snap/cmd_snap_op_test.go +++ b/cmd/snap/cmd_snap_op_test.go @@ -444,7 +444,7 @@ func (s *SnapOpSuite) TestRevertRunthrough(c *check.C) { c.Assert(rest, check.DeepEquals, []string{}) // tracking channel is "" in the test server c.Check(s.Stdout(), check.Equals, `foo reverted to 1.0 -Snap foo is no longer tracking . +Channel for foo is closed; temporarily forwarding to potato. `) c.Check(s.Stderr(), check.Equals, "") // ensure that the fake server api was actually hit diff --git a/cmd/snap/main.go b/cmd/snap/main.go index f556b86137..3babe24800 100644 --- a/cmd/snap/main.go +++ b/cmd/snap/main.go @@ -114,12 +114,14 @@ func addCommand(name, shortHelp, longHelp string, builder func() flags.Commander // addDebugCommand replaces parser.addCommand() in a way that is // compatible with re-constructing a pristine parser. It is meant for // adding debug commands. -func addDebugCommand(name, shortHelp, longHelp string, builder func() flags.Commander) *cmdInfo { +func addDebugCommand(name, shortHelp, longHelp string, builder func() flags.Commander, optDescs map[string]string, argDescs []argDesc) *cmdInfo { info := &cmdInfo{ name: name, shortHelp: shortHelp, longHelp: longHelp, builder: builder, + optDescs: optDescs, + argDescs: argDescs, } debugCommands = append(debugCommands, info) return info @@ -247,6 +249,39 @@ snaps on the system. Start with 'snap list' to see installed snaps.`) logger.Panicf("cannot add debug command %q: %v", c.name, err) } cmd.Hidden = c.hidden + opts := cmd.Options() + if c.optDescs != nil && len(opts) != len(c.optDescs) { + logger.Panicf("wrong number of option descriptions for %s: expected %d, got %d", c.name, len(opts), len(c.optDescs)) + } + for _, opt := range opts { + name := opt.LongName + if name == "" { + name = string(opt.ShortName) + } + desc, ok := c.optDescs[name] + if !(c.optDescs == nil || ok) { + logger.Panicf("%s missing description for %s", c.name, name) + } + lintDesc(c.name, name, desc, opt.Description) + if desc != "" { + opt.Description = desc + } + } + + args := cmd.Args() + if c.argDescs != nil && len(args) != len(c.argDescs) { + logger.Panicf("wrong number of argument descriptions for %s: expected %d, got %d", c.name, len(args), len(c.argDescs)) + } + for i, arg := range args { + name, desc := arg.Name, "" + if c.argDescs != nil { + name = c.argDescs[i].name + desc = c.argDescs[i].desc + } + lintArg(c.name, name, desc, arg.Description) + arg.Name = name + arg.Description = desc + } } return parser } diff --git a/daemon/api.go b/daemon/api.go index 2b47b3551c..97874fed7d 100644 --- a/daemon/api.go +++ b/daemon/api.go @@ -324,9 +324,39 @@ func sysInfo(c *Command, r *http.Request, user *auth.UserState) Response { m["confinement"] = "strict" } + // Convey richer information about features of available security backends. + if features := sandboxFeatures(c.d.overlord.InterfaceManager().Repository().Backends()); features != nil { + m["sandbox-features"] = features + } + return SyncResponse(m, nil) } +func sandboxFeatures(backends []interfaces.SecurityBackend) map[string][]string { + result := make(map[string][]string, len(backends)+1) + for _, backend := range backends { + features := backend.SandboxFeatures() + if len(features) > 0 { + sort.Strings(features) + result[string(backend.Name())] = features + } + } + + // Add information about supported confinement types as a fake backend + features := make([]string, 1, 3) + features[0] = "devmode" + if !release.ReleaseInfo.ForceDevMode() { + features = append(features, "strict") + } + if dirs.SupportsClassicConfinement() { + features = append(features, "classic") + } + sort.Strings(features) + result["confinement-options"] = features + + return result +} + // userResponseData contains the data releated to user creation/login/query type userResponseData struct { ID int `json:"id,omitempty"` @@ -1815,7 +1845,7 @@ type interfaceAction struct { Slots []slotJSON `json:"slots,omitempty"` } -func snapNamesFromConns(conns []interfaces.ConnRef) []string { +func snapNamesFromConns(conns []*interfaces.ConnRef) []string { m := make(map[string]bool) for _, conn := range conns { m[conn.PlugRef.Snap] = true @@ -1867,7 +1897,7 @@ func changeInterfaces(c *Command, r *http.Request, user *auth.UserState) Respons switch a.Action { case "connect": - var connRef interfaces.ConnRef + var connRef *interfaces.ConnRef repo := c.d.overlord.InterfaceManager().Repository() connRef, err = repo.ResolveConnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) if err == nil { @@ -1875,10 +1905,10 @@ func changeInterfaces(c *Command, r *http.Request, user *auth.UserState) Respons summary = fmt.Sprintf("Connect %s:%s to %s:%s", connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name) ts, err = ifacestate.Connect(st, connRef.PlugRef.Snap, connRef.PlugRef.Name, connRef.SlotRef.Snap, connRef.SlotRef.Name) tasksets = append(tasksets, ts) - affected = snapNamesFromConns([]interfaces.ConnRef{connRef}) + affected = snapNamesFromConns([]*interfaces.ConnRef{connRef}) } case "disconnect": - var conns []interfaces.ConnRef + var conns []*interfaces.ConnRef repo := c.d.overlord.InterfaceManager().Repository() summary = fmt.Sprintf("Disconnect %s:%s from %s:%s", a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) conns, err = repo.ResolveDisconnect(a.Plugs[0].Snap, a.Plugs[0].Name, a.Slots[0].Snap, a.Slots[0].Name) diff --git a/daemon/api_test.go b/daemon/api_test.go index b8dfd71d30..6e3e94f062 100644 --- a/daemon/api_test.go +++ b/daemon/api_test.go @@ -766,7 +766,8 @@ func (s *apiSuite) TestSysInfo(c *check.C) { // only the "timer" field "timer": "8:00~9:00/2", }, - "confinement": "partial", + "confinement": "partial", + "sandbox-features": map[string]interface{}{"confinement-options": []interface{}{"classic", "devmode"}}, } var rsp resp c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil) @@ -801,6 +802,13 @@ func (s *apiSuite) TestSysInfoLegacyRefresh(c *check.C) { tr.Commit() st.Unlock() + // add a test security backend + err := d.overlord.InterfaceManager().Repository().AddBackend(&ifacetest.TestSecurityBackend{ + BackendName: "apparmor", + SandboxFeaturesCallback: func() []string { return []string{"feature-1", "feature-2"} }, + }) + c.Assert(err, check.IsNil) + buildID, err := osutil.MyBuildID() c.Assert(err, check.IsNil) @@ -827,6 +835,10 @@ func (s *apiSuite) TestSysInfoLegacyRefresh(c *check.C) { "schedule": "00:00-9:00/12:00-13:00", }, "confinement": "partial", + "sandbox-features": map[string]interface{}{ + "apparmor": []interface{}{"feature-1", "feature-2"}, + "confinement-options": []interface{}{"classic", "devmode"}, // we know it's this because of the release.Mock... calls above + }, } var rsp resp c.Assert(json.Unmarshal(rec.Body.Bytes(), &rsp), check.IsNil) @@ -3510,7 +3522,7 @@ func (s *apiSuite) TestInterfaces(c *check.C) { s.mockSnap(c, producerYaml) repo := d.overlord.InterfaceManager().Repository() - connRef := interfaces.ConnRef{ + connRef := &interfaces.ConnRef{ PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, } @@ -3853,7 +3865,7 @@ func (s *apiSuite) testDisconnect(c *check.C, plugSnap, plugName, slotSnap, slot s.mockSnap(c, producerYaml) repo := d.overlord.InterfaceManager().Repository() - connRef := interfaces.ConnRef{ + connRef := &interfaces.ConnRef{ PlugRef: interfaces.PlugRef{Snap: "consumer", Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: "producer", Name: "slot"}, } diff --git a/data/systemd/snapd.core-fixup.sh b/data/systemd/snapd.core-fixup.sh index 3543bda0d1..795939a4d9 100755 --- a/data/systemd/snapd.core-fixup.sh +++ b/data/systemd/snapd.core-fixup.sh @@ -19,9 +19,13 @@ fi # the other has a "uboot.env" lfn name and a FSCK0000.000 FAT16 # name. The only known workaround is to remove all dupes and put # one file back in place. -if [ $(ls /boot/uboot | grep ^uboot.env$ | wc -l) -gt 1 ]; then +if [ "$(find /boot/uboot -name uboot.env | wc -l)" -gt 1 ]; then echo "Corrupted uboot.env file detected" - # ensure we have one uboot.env to go back to + # Ensure we have one uboot.env to go back to. Note that it does + # not matter which one we pick (we can't choose anyway, we get + # whatever the kernel gives us). The key part is that there is + # only a single one after this script finishes. The bootloader + # logic will recover in any case. cp -a /boot/uboot/uboot.env /boot/uboot/uboot.env.save # now delete all dupes while ls /boot/uboot/uboot.env 2>/dev/null; do diff --git a/interfaces/apparmor/backend.go b/interfaces/apparmor/backend.go index d72014509f..31c0e6d698 100644 --- a/interfaces/apparmor/backend.go +++ b/interfaces/apparmor/backend.go @@ -60,6 +60,7 @@ var ( procSelfExe = "/proc/self/exe" isHomeUsingNFS = osutil.IsHomeUsingNFS isRootWritableOverlay = osutil.IsRootWritableOverlay + kernelFeatures = release.AppArmorFeatures ) // Backend is responsible for maintaining apparmor profiles for snaps and parts of snapd. @@ -492,3 +493,15 @@ func unloadProfiles(profiles []string, cacheDir string) error { func (b *Backend) NewSpecification() interfaces.Specification { return &Specification{} } + +// SandboxFeatures returns the list of apparmor features supported by the kernel. +func (b *Backend) SandboxFeatures() []string { + features := kernelFeatures() + tags := make([]string, 0, len(features)) + for _, feature := range features { + // Prepend "kernel:" to apparmor kernel features to namespace them and + // allow us to introduce our own tags later. + tags = append(tags, "kernel:"+feature) + } + return tags +} diff --git a/interfaces/apparmor/backend_test.go b/interfaces/apparmor/backend_test.go index 8fcf49a2ad..fb0754abb7 100644 --- a/interfaces/apparmor/backend_test.go +++ b/interfaces/apparmor/backend_test.go @@ -1200,3 +1200,10 @@ func (s *backendSuite) TestProfileGlobs(c *C) { func (s *backendSuite) TestNsProfile(c *C) { c.Assert(apparmor.NsProfile("foo"), Equals, "snap-update-ns.foo") } + +func (s *backendSuite) TestSandboxFeatures(c *C) { + restore := apparmor.MockKernelFeatures(func() []string { return []string{"foo", "bar"} }) + defer restore() + + c.Assert(s.Backend.SandboxFeatures(), DeepEquals, []string{"kernel:foo", "kernel:bar"}) +} diff --git a/interfaces/apparmor/export_test.go b/interfaces/apparmor/export_test.go index c1ac85230f..791f56b4ff 100644 --- a/interfaces/apparmor/export_test.go +++ b/interfaces/apparmor/export_test.go @@ -88,3 +88,11 @@ func MockClassicTemplate(fakeTemplate string) (restore func()) { func SetSpecScope(spec *Specification, securityTags []string) (restore func()) { return spec.setScope(securityTags) } + +func MockKernelFeatures(f func() []string) (resture func()) { + old := kernelFeatures + kernelFeatures = f + return func() { + kernelFeatures = old + } +} diff --git a/interfaces/apparmor/template.go b/interfaces/apparmor/template.go index 577cb56eb3..3839cf0bb2 100644 --- a/interfaces/apparmor/template.go +++ b/interfaces/apparmor/template.go @@ -609,11 +609,7 @@ profile snap-update-ns.###SNAP_NAME### (attach_disconnected) { umount /snap/###SNAP_NAME###/*/**, # set up user mount namespace - # FIXME: this should be moved to the desktop interface when - # xdg-desktop-portal support is integrated mount options=(rslave) -> /, - mount options=(ro bind) /run/user/*/** -> /run/user/*/**, - mount options=(rw bind) /run/user/*/** -> /run/user/*/**, # Allow traversing from the root directory and several well-known places. # Specific directory permissions are added by snippets below. diff --git a/interfaces/backend.go b/interfaces/backend.go index f5d8cecb6f..e1cfee54c5 100644 --- a/interfaces/backend.go +++ b/interfaces/backend.go @@ -91,4 +91,7 @@ type SecurityBackend interface { // NewSpecification returns a new specification associated with this backend. NewSpecification() Specification + + // SandboxFeatures returns a list of tags that identify sandbox features. + SandboxFeatures() []string } diff --git a/interfaces/builtin/desktop.go b/interfaces/builtin/desktop.go index fc5833cc0e..de968e6f01 100644 --- a/interfaces/builtin/desktop.go +++ b/interfaces/builtin/desktop.go @@ -180,19 +180,20 @@ dbus (send) ## Allow access to xdg-document-portal file system. Access control is ## handled by bind mounting a snap-specific sub-tree to this location. -#owner /run/user/[0-9]*/doc/** rw, +owner /run/user/[0-9]*/doc/ r, +owner /run/user/[0-9]*/doc/** rw, # Allow access to xdg-desktop-portal and xdg-document-portal dbus (receive, send) bus=session interface=org.freedesktop.portal.* - path=/org/freedesktop/portal/{desktop,documents} + path=/org/freedesktop/portal/{desktop,documents}{,/**} peer=(label=unconfined), dbus (receive, send) bus=session interface=org.freedesktop.DBus.Properties - path=/org/freedesktop/portal/{desktop,documents} + path=/org/freedesktop/portal/{desktop,documents}{,/**} peer=(label=unconfined), ` @@ -230,11 +231,19 @@ func (iface *desktopInterface) fontconfigDirs() []string { func (iface *desktopInterface) AppArmorConnectedPlug(spec *apparmor.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { spec.AddSnippet(desktopConnectedPlugAppArmor) + // Allow mounting document portal + var buf bytes.Buffer + fmt.Fprintf(&buf, " # Mount the document portal\n") + fmt.Fprintf(&buf, " mount options=(bind) /run/user/[0-9]*/doc/by-app/snap.%s/ -> /run/user/[0-9]*/doc/,\n", plug.Snap().Name()) + fmt.Fprintf(&buf, " umount /run/user/[0-9]*/doc/,\n\n") + spec.AddUpdateNS(buf.String()) + if !release.OnClassic { // We only need the font mount rules on classic systems return nil } + // Allow mounting fonts for _, dir := range iface.fontconfigDirs() { var buf bytes.Buffer source := "/var/lib/snapd/hostfs" + dir @@ -250,8 +259,15 @@ func (iface *desktopInterface) AppArmorConnectedPlug(spec *apparmor.Specificatio } func (iface *desktopInterface) MountConnectedPlug(spec *mount.Specification, plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) error { + appId := "snap." + plug.Snap().Name() + spec.AddUserMountEntry(osutil.MountEntry{ + Name: "$XDG_RUNTIME_DIR/doc/by-app/" + appId, + Dir: "$XDG_RUNTIME_DIR/doc", + Options: []string{"bind", "rw", osutil.XSnapdIgnoreMissing()}, + }) + if !release.OnClassic { - // There is nothing to expose on an all-snaps system + // We only need the font mount rules on classic systems return nil } @@ -266,15 +282,6 @@ func (iface *desktopInterface) MountConnectedPlug(spec *mount.Specification, plu }) } - /* - appId := "snap.pkg." + plug.Snap.Name() - spec.AddUserMount(mount.Entry{ - Name: "$XDG_RUNTIME_DIR/doc/by-app/" + appId, - Dir: "$XDG_RUNTIME_DIR/doc", - Options: []string{"bind", "rw"}, - }) - */ - return nil } diff --git a/interfaces/builtin/desktop_test.go b/interfaces/builtin/desktop_test.go index fe556d7b71..0a24cd361d 100644 --- a/interfaces/builtin/desktop_test.go +++ b/interfaces/builtin/desktop_test.go @@ -109,9 +109,15 @@ func (s *DesktopInterfaceSuite) TestAppArmorSpec(c *C) { c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "/etc/gtk-3.0/settings.ini r,") c.Assert(spec.SnippetForTag("snap.consumer.app"), testutil.Contains, "# Allow access to xdg-desktop-portal and xdg-document-portal") - // No UpdateNS rules on an all-snaps system + // On an all-snaps system, the only UpdateNS rule is for the + // document portal. updateNS := spec.UpdateNS() - c.Assert(updateNS, HasLen, 0) + c.Assert(updateNS, HasLen, 1) + c.Check(updateNS[0], Equals, ` # Mount the document portal + mount options=(bind) /run/user/[0-9]*/doc/by-app/snap.consumer/ -> /run/user/[0-9]*/doc/, + umount /run/user/[0-9]*/doc/, + +`) // On a classic system, there are UpdateNS rules for the host // system font mounts @@ -120,10 +126,11 @@ func (s *DesktopInterfaceSuite) TestAppArmorSpec(c *C) { spec = &apparmor.Specification{} c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) updateNS = spec.UpdateNS() - c.Assert(updateNS, HasLen, 3) - c.Check(updateNS[0], testutil.Contains, "# Read-only access to /usr/share/fonts") - c.Check(updateNS[1], testutil.Contains, "# Read-only access to /usr/local/share/fonts") - c.Check(updateNS[2], testutil.Contains, "# Read-only access to /var/cache/fontconfig") + c.Assert(updateNS, HasLen, 4) + c.Check(updateNS[0], testutil.Contains, "# Mount the document portal") + c.Check(updateNS[1], testutil.Contains, "# Read-only access to /usr/share/fonts") + c.Check(updateNS[2], testutil.Contains, "# Read-only access to /usr/local/share/fonts") + c.Check(updateNS[3], testutil.Contains, "# Read-only access to /var/cache/fontconfig") // connected plug to core slot spec = &apparmor.Specification{} @@ -141,11 +148,17 @@ func (s *DesktopInterfaceSuite) TestMountSpec(c *C) { restore := release.MockOnClassic(false) defer restore() - // On all-snaps systems, no mount entries are added + // On all-snaps systems, the font related mount entries are missing spec := &mount.Specification{} c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) c.Check(spec.MountEntries(), HasLen, 0) + entries := spec.UserMountEntries() + c.Check(entries, HasLen, 1) + c.Check(entries[0].Name, Equals, "$XDG_RUNTIME_DIR/doc/by-app/snap.consumer") + c.Check(entries[0].Dir, Equals, "$XDG_RUNTIME_DIR/doc") + c.Check(entries[0].Options, DeepEquals, []string{"bind", "rw", "x-snapd.ignore-missing"}) + // On classic systems, a number of font related directories // are bind mounted from the host system if they exist. restore = release.MockOnClassic(true) @@ -153,7 +166,7 @@ func (s *DesktopInterfaceSuite) TestMountSpec(c *C) { spec = &mount.Specification{} c.Assert(spec.AddConnectedPlug(s.iface, s.plug, s.coreSlot), IsNil) - entries := spec.MountEntries() + entries = spec.MountEntries() c.Assert(entries, HasLen, 3) const hostfs = "/var/lib/snapd/hostfs" @@ -168,6 +181,10 @@ func (s *DesktopInterfaceSuite) TestMountSpec(c *C) { c.Check(entries[2].Name, Equals, hostfs+dirs.SystemFontconfigCacheDir) c.Check(entries[2].Dir, Equals, "/var/cache/fontconfig") c.Check(entries[2].Options, DeepEquals, []string{"bind", "ro"}) + + entries = spec.UserMountEntries() + c.Assert(entries, HasLen, 1) + c.Check(entries[0].Dir, Equals, "$XDG_RUNTIME_DIR/doc") } func (s *DesktopInterfaceSuite) TestStaticInfo(c *C) { diff --git a/interfaces/connection.go b/interfaces/connection.go index 292c79b63a..362cc7a389 100644 --- a/interfaces/connection.go +++ b/interfaces/connection.go @@ -22,6 +22,7 @@ package interfaces import ( "fmt" "reflect" + "strings" "github.com/snapcore/snapd/snap" ) @@ -49,29 +50,51 @@ type ConnectedSlot struct { // Attrer is an interface with Attr getter method common // to ConnectedSlot, ConnectedPlug, PlugInfo and SlotInfo types. type Attrer interface { - Attr(key string, val interface{}) error + // Attr returns attribute value for given path, or an error. Dotted paths are supported. + Attr(path string, value interface{}) error + // Lookup returns attribute value for given path, or false. Dotted paths are supported. + Lookup(path string) (value interface{}, ok bool) } -func getAttribute(snapName string, ifaceName string, staticAttrs map[string]interface{}, dynamicAttrs map[string]interface{}, key string, val interface{}) error { +func lookupAttr(staticAttrs map[string]interface{}, dynamicAttrs map[string]interface{}, path string) (interface{}, bool) { var v interface{} - var ok bool + comps := strings.FieldsFunc(path, func(r rune) bool { return r == '.' }) + if len(comps) == 0 { + return nil, false + } + if _, ok := dynamicAttrs[comps[0]]; ok { + v = dynamicAttrs + } else { + v = staticAttrs + } - v, ok = dynamicAttrs[key] - if !ok { - v, ok = staticAttrs[key] + for _, comp := range comps { + m, ok := v.(map[string]interface{}) + if !ok { + return nil, false + } + v, ok = m[comp] + if !ok { + return nil, false + } } + return v, true +} + +func getAttribute(snapName string, ifaceName string, staticAttrs map[string]interface{}, dynamicAttrs map[string]interface{}, path string, val interface{}) error { + v, ok := lookupAttr(staticAttrs, dynamicAttrs, path) if !ok { - return fmt.Errorf("snap %q does not have attribute %q for interface %q", snapName, key, ifaceName) + return fmt.Errorf("snap %q does not have attribute %q for interface %q", snapName, path, ifaceName) } rt := reflect.TypeOf(val) if rt.Kind() != reflect.Ptr || val == nil { - return fmt.Errorf("internal error: cannot get %q attribute of interface %q with non-pointer value", key, ifaceName) + return fmt.Errorf("internal error: cannot get %q attribute of interface %q with non-pointer value", path, ifaceName) } if reflect.TypeOf(v) != rt.Elem() { - return fmt.Errorf("snap %q has interface %q with invalid value type for %q attribute", snapName, ifaceName, key) + return fmt.Errorf("snap %q has interface %q with invalid value type for %q attribute", snapName, ifaceName, path) } rv := reflect.ValueOf(val) rv.Elem().Set(reflect.ValueOf(v)) @@ -148,6 +171,10 @@ func (plug *ConnectedPlug) Attr(key string, val interface{}) error { return getAttribute(plug.Snap().Name(), plug.Interface(), plug.staticAttrs, plug.dynamicAttrs, key, val) } +func (plug *ConnectedPlug) Lookup(path string) (interface{}, bool) { + return lookupAttr(plug.staticAttrs, plug.dynamicAttrs, path) +} + // SetAttr sets the given dynamic attribute. Error is returned if the key is already used by a static attribute. func (plug *ConnectedPlug) SetAttr(key string, value interface{}) error { if _, ok := plug.staticAttrs[key]; ok { @@ -217,6 +244,10 @@ func (slot *ConnectedSlot) Attr(key string, val interface{}) error { return getAttribute(slot.Snap().Name(), slot.Interface(), slot.staticAttrs, slot.dynamicAttrs, key, val) } +func (slot *ConnectedSlot) Lookup(path string) (interface{}, bool) { + return lookupAttr(slot.staticAttrs, slot.dynamicAttrs, path) +} + // SetAttr sets the given dynamic attribute. Error is returned if the key is already used by a static attribute. func (slot *ConnectedSlot) SetAttr(key string, value interface{}) error { if _, ok := slot.staticAttrs[key]; ok { diff --git a/interfaces/connection_test.go b/interfaces/connection_test.go index 10cc706a36..8fe98b4c58 100644 --- a/interfaces/connection_test.go +++ b/interfaces/connection_test.go @@ -47,6 +47,8 @@ plugs: plug: interface: interface attr: value + complex: + c: d `, nil) s.plug = consumer.Plugs["plug"] producer := snaptest.MockInfo(c, ` @@ -58,6 +60,8 @@ slots: slot: interface: interface attr: value + complex: + a: b `, nil) s.slot = producer.Slots["slot"] } @@ -82,7 +86,8 @@ func (s *connSuite) TestStaticSlotAttrs(c *C) { attrs := slot.StaticAttrs() c.Assert(attrs, DeepEquals, map[string]interface{}{ - "attr": "value", + "attr": "value", + "complex": map[string]interface{}{"a": "b"}, }) slot.StaticAttr("attr", &val) c.Assert(val, Equals, "value") @@ -114,7 +119,8 @@ func (s *connSuite) TestStaticPlugAttrs(c *C) { attrs := plug.StaticAttrs() c.Assert(attrs, DeepEquals, map[string]interface{}{ - "attr": "value", + "attr": "value", + "complex": map[string]interface{}{"c": "d"}, }) plug.StaticAttr("attr", &val) c.Assert(val, Equals, "value") @@ -156,6 +162,96 @@ func (s *connSuite) TestDynamicSlotAttrs(c *C) { c.Check(slot.Attr("number", intVal), ErrorMatches, `internal error: cannot get "number" attribute of interface "interface" with non-pointer value`) } +func (s *connSuite) TestDottedPathSlot(c *C) { + attrs := map[string]interface{}{ + "nested": map[string]interface{}{ + "foo": "bar", + }, + } + var strVal string + + slot := NewConnectedSlot(s.slot, attrs) + c.Assert(slot, NotNil) + + // static attribute complex.a + c.Assert(slot.Attr("complex.a", &strVal), IsNil) + c.Assert(strVal, Equals, "b") + + v, ok := slot.Lookup("complex.a") + c.Assert(ok, Equals, true) + c.Assert(v, Equals, "b") + + // dynamic attribute nested.foo + c.Assert(slot.Attr("nested.foo", &strVal), IsNil) + c.Assert(strVal, Equals, "bar") + + v, ok = slot.Lookup("nested.foo") + c.Assert(ok, Equals, true) + c.Assert(v, Equals, "bar") + + _, ok = slot.Lookup("..") + c.Assert(ok, Equals, false) +} + +func (s *connSuite) TestDottedPathPlug(c *C) { + attrs := map[string]interface{}{ + "a": "b", + "nested": map[string]interface{}{ + "foo": "bar", + }, + } + var strVal string + + plug := NewConnectedPlug(s.plug, attrs) + c.Assert(plug, NotNil) + + v, ok := plug.Lookup("a") + c.Assert(ok, Equals, true) + c.Assert(v, Equals, "b") + + // static attribute complex.c + c.Assert(plug.Attr("complex.c", &strVal), IsNil) + c.Assert(strVal, Equals, "d") + + v, ok = plug.Lookup("complex.c") + c.Assert(ok, Equals, true) + c.Assert(v, Equals, "d") + + // dynamic attribute nested.foo + c.Assert(plug.Attr("nested.foo", &strVal), IsNil) + c.Assert(strVal, Equals, "bar") + + v, ok = plug.Lookup("nested.foo") + c.Assert(ok, Equals, true) + c.Assert(v, Equals, "bar") + + _, ok = plug.Lookup("nested.x") + c.Assert(ok, Equals, false) + + _, ok = plug.Lookup("nested.foo.y") + c.Assert(ok, Equals, false) + + _, ok = plug.Lookup("..") + c.Assert(ok, Equals, false) +} + +func (s *connSuite) TestLookupFailure(c *C) { + attrs := map[string]interface{}{} + + slot := NewConnectedSlot(s.slot, attrs) + c.Assert(slot, NotNil) + plug := NewConnectedPlug(s.plug, attrs) + c.Assert(plug, NotNil) + + v, ok := slot.Lookup("a") + c.Assert(ok, Equals, false) + c.Assert(v, IsNil) + + v, ok = plug.Lookup("a") + c.Assert(ok, Equals, false) + c.Assert(v, IsNil) +} + func (s *connSuite) TestDynamicPlugAttrs(c *C) { attrs := map[string]interface{}{ "foo": "bar", diff --git a/interfaces/core.go b/interfaces/core.go index 48d38c1174..6dfe292090 100644 --- a/interfaces/core.go +++ b/interfaces/core.go @@ -133,22 +133,22 @@ func (conn *ConnRef) ID() string { } // ParseConnRef parses an ID string -func ParseConnRef(id string) (ConnRef, error) { +func ParseConnRef(id string) (*ConnRef, error) { var conn ConnRef parts := strings.SplitN(id, " ", 2) if len(parts) != 2 { - return conn, fmt.Errorf("malformed connection identifier: %q", id) + return nil, fmt.Errorf("malformed connection identifier: %q", id) } plugParts := strings.Split(parts[0], ":") slotParts := strings.Split(parts[1], ":") if len(plugParts) != 2 || len(slotParts) != 2 { - return conn, fmt.Errorf("malformed connection identifier: %q", id) + return nil, fmt.Errorf("malformed connection identifier: %q", id) } conn.PlugRef.Snap = plugParts[0] conn.PlugRef.Name = plugParts[1] conn.SlotRef.Snap = slotParts[0] conn.SlotRef.Name = slotParts[1] - return conn, nil + return &conn, nil } // Interface describes a group of interchangeable capabilities with common features. diff --git a/interfaces/core_test.go b/interfaces/core_test.go index 236a32b89a..94c4887d30 100644 --- a/interfaces/core_test.go +++ b/interfaces/core_test.go @@ -181,7 +181,7 @@ func (s *CoreSuite) TestConnRefID(c *C) { func (s *CoreSuite) TestParseConnRef(c *C) { ref, err := ParseConnRef("consumer:plug producer:slot") c.Assert(err, IsNil) - c.Check(ref, DeepEquals, ConnRef{ + c.Check(ref, DeepEquals, &ConnRef{ PlugRef: PlugRef{Snap: "consumer", Name: "plug"}, SlotRef: SlotRef{Snap: "producer", Name: "slot"}, }) diff --git a/interfaces/dbus/backend.go b/interfaces/dbus/backend.go index e7f0cc6af3..816fea100b 100644 --- a/interfaces/dbus/backend.go +++ b/interfaces/dbus/backend.go @@ -168,3 +168,8 @@ func addContent(securityTag string, snippet string, content map[string]*osutil.F func (b *Backend) NewSpecification() interfaces.Specification { return &Specification{} } + +// SandboxFeatures returns list of features supported by snapd for dbus communication. +func (b *Backend) SandboxFeatures() []string { + return []string{"mediated-bus-access"} +} diff --git a/interfaces/dbus/backend_test.go b/interfaces/dbus/backend_test.go index 41dbeb7bbf..40f6602593 100644 --- a/interfaces/dbus/backend_test.go +++ b/interfaces/dbus/backend_test.go @@ -274,3 +274,7 @@ func (s *backendSuite) TestAppBoundIfaces(c *C) { _, err = os.Stat(filepath.Join(dirs.SnapBusPolicyDir, "snap.samba.nmbd.conf")) c.Check(err, IsNil) } + +func (s *backendSuite) TestSandboxFeatures(c *C) { + c.Assert(s.Backend.SandboxFeatures(), DeepEquals, []string{"mediated-bus-access"}) +} diff --git a/interfaces/ifacetest/backend.go b/interfaces/ifacetest/backend.go index 910bc4f5d3..d4b8b09d62 100644 --- a/interfaces/ifacetest/backend.go +++ b/interfaces/ifacetest/backend.go @@ -35,6 +35,8 @@ type TestSecurityBackend struct { SetupCallback func(snapInfo *snap.Info, opts interfaces.ConfinementOptions, repo *interfaces.Repository) error // RemoveCallback is a callback that is optionally called in Remove RemoveCallback func(snapName string) error + // SandboxFeaturesCallback is a callback that is optionally called in SandboxFeatures + SandboxFeaturesCallback func() []string } // TestSetupCall stores details about calls to TestSecurityBackend.Setup @@ -76,3 +78,10 @@ func (b *TestSecurityBackend) Remove(snapName string) error { func (b *TestSecurityBackend) NewSpecification() interfaces.Specification { return &Specification{} } + +func (b *TestSecurityBackend) SandboxFeatures() []string { + if b.SandboxFeaturesCallback == nil { + return nil + } + return b.SandboxFeaturesCallback() +} diff --git a/interfaces/kmod/backend.go b/interfaces/kmod/backend.go index 7cc5a3295c..3896b561ff 100644 --- a/interfaces/kmod/backend.go +++ b/interfaces/kmod/backend.go @@ -131,3 +131,8 @@ func deriveContent(spec *Specification, snapInfo *snap.Info) (map[string]*osutil func (b *Backend) NewSpecification() interfaces.Specification { return &Specification{} } + +// SandboxFeatures returns the list of features supported by snapd for loading kernel modules. +func (b *Backend) SandboxFeatures() []string { + return []string{"mediated-modprobe"} +} diff --git a/interfaces/kmod/backend_test.go b/interfaces/kmod/backend_test.go index 0894ba2538..c89e79ef20 100644 --- a/interfaces/kmod/backend_test.go +++ b/interfaces/kmod/backend_test.go @@ -131,3 +131,7 @@ func (s *backendSuite) TestSecurityIsStable(c *C) { s.RemoveSnap(c, snapInfo) } } + +func (s *backendSuite) TestSandboxFeatures(c *C) { + c.Assert(s.Backend.SandboxFeatures(), DeepEquals, []string{"mediated-modprobe"}) +} diff --git a/interfaces/mount/backend.go b/interfaces/mount/backend.go index e66412be80..34cbccc211 100644 --- a/interfaces/mount/backend.go +++ b/interfaces/mount/backend.go @@ -121,3 +121,17 @@ func deriveContent(spec *Specification, snapInfo *snap.Info) map[string]*osutil. func (b *Backend) NewSpecification() interfaces.Specification { return &Specification{} } + +// SandboxFeatures returns the list of features supported by snapd for composing mount namespaces. +func (b *Backend) SandboxFeatures() []string { + return []string{ + "freezer-cgroup-v1", /* Snapd creates a freezer cgroup (v1) for each snap */ + "layouts-beta", /* Mount profiles take layout data into account (experimental) */ + "mount-namespace", /* Snapd creates a mount namespace for each snap */ + "per-snap-persistency", /* Per-snap profiles are persisted across invocations */ + "per-snap-profiles", /* Per-snap profiles allow changing mount namespace of a given snap */ + "per-snap-updates", /* Changes to per-snap mount profiles are applied instantly */ + "per-snap-user-profiles", /* Per-snap profiles allow changing mount namespace of a given snap for a given user */ + "stale-base-invalidation", /* Mount namespaces that go stale because base snap changes are automatically invalidated */ + } +} diff --git a/interfaces/mount/backend_test.go b/interfaces/mount/backend_test.go index 9358e39641..49e97164e0 100644 --- a/interfaces/mount/backend_test.go +++ b/interfaces/mount/backend_test.go @@ -169,3 +169,16 @@ func (s *backendSuite) TestSetupSetsupWithoutDir(c *C) { os.Remove(dirs.SnapMountPolicyDir) s.InstallSnap(c, interfaces.ConfinementOptions{}, mockSnapYaml, 0) } + +func (s *backendSuite) TestSandboxFeatures(c *C) { + c.Assert(s.Backend.SandboxFeatures(), DeepEquals, []string{ + "freezer-cgroup-v1", + "layouts-beta", + "mount-namespace", + "per-snap-persistency", + "per-snap-profiles", + "per-snap-updates", + "per-snap-user-profiles", + "stale-base-invalidation", + }) +} diff --git a/interfaces/policy/helpers.go b/interfaces/policy/helpers.go index 105e7085ea..7e78519225 100644 --- a/interfaces/policy/helpers.go +++ b/interfaces/policy/helpers.go @@ -81,10 +81,10 @@ func checkOnClassic(c *asserts.OnClassicConstraint) error { } func checkPlugConnectionConstraints1(connc *ConnectCandidate, cstrs *asserts.PlugConnectionConstraints) error { - if err := cstrs.PlugAttributes.Check(connc.plugAttrs(), connc); err != nil { + if err := cstrs.PlugAttributes.Check(connc.Plug, connc); err != nil { return err } - if err := cstrs.SlotAttributes.Check(connc.slotAttrs(), connc); err != nil { + if err := cstrs.SlotAttributes.Check(connc.Slot, connc); err != nil { return err } if err := checkSnapType(connc.slotSnapType(), cstrs.SlotSnapTypes); err != nil { @@ -121,10 +121,10 @@ func checkPlugConnectionConstraints(connc *ConnectCandidate, cstrs []*asserts.Pl } func checkSlotConnectionConstraints1(connc *ConnectCandidate, cstrs *asserts.SlotConnectionConstraints) error { - if err := cstrs.PlugAttributes.Check(connc.plugAttrs(), connc); err != nil { + if err := cstrs.PlugAttributes.Check(connc.Plug, connc); err != nil { return err } - if err := cstrs.SlotAttributes.Check(connc.slotAttrs(), connc); err != nil { + if err := cstrs.SlotAttributes.Check(connc.Slot, connc); err != nil { return err } if err := checkSnapType(connc.plugSnapType(), cstrs.PlugSnapTypes); err != nil { @@ -162,7 +162,7 @@ func checkSlotConnectionConstraints(connc *ConnectCandidate, cstrs []*asserts.Sl func checkSlotInstallationConstraints1(slot *snap.SlotInfo, cstrs *asserts.SlotInstallationConstraints) error { // TODO: allow evaluated attr constraints here too? - if err := cstrs.SlotAttributes.Check(slot.Attrs, nil); err != nil { + if err := cstrs.SlotAttributes.Check(slot, nil); err != nil { return err } if err := checkSnapType(slot.Snap.Type, cstrs.SlotSnapTypes); err != nil { @@ -191,7 +191,7 @@ func checkSlotInstallationConstraints(slot *snap.SlotInfo, cstrs []*asserts.Slot func checkPlugInstallationConstraints1(plug *snap.PlugInfo, cstrs *asserts.PlugInstallationConstraints) error { // TODO: allow evaluated attr constraints here too? - if err := cstrs.PlugAttributes.Check(plug.Attrs, nil); err != nil { + if err := cstrs.PlugAttributes.Check(plug, nil); err != nil { return err } if err := checkSnapType(plug.Snap.Type, cstrs.PlugSnapTypes); err != nil { diff --git a/interfaces/policy/helpers_test.go b/interfaces/policy/helpers_test.go index cfaf766782..2d5c08ced4 100644 --- a/interfaces/policy/helpers_test.go +++ b/interfaces/policy/helpers_test.go @@ -20,9 +20,11 @@ package policy_test import ( - . "gopkg.in/check.v1" - + "github.com/snapcore/snapd/interfaces" "github.com/snapcore/snapd/interfaces/policy" + "github.com/snapcore/snapd/snap/snaptest" + + . "gopkg.in/check.v1" ) type helpersSuite struct{} @@ -30,25 +32,51 @@ type helpersSuite struct{} var _ = Suite(&helpersSuite{}) func (s *helpersSuite) TestNestedGet(c *C) { - _, err := policy.NestedGet("slot", nil, "a") - c.Check(err, ErrorMatches, `slot attribute "a" not found`) + consumer := snaptest.MockInfo(c, ` +name: consumer +version: 0 +apps: + app: +plugs: + plug: + interface: interface +`, nil) + plugInfo := consumer.Plugs["plug"] + plug := interfaces.NewConnectedPlug(plugInfo, map[string]interface{}{ + "a": "123", + }) - _, err = policy.NestedGet("plug", map[string]interface{}{ + producer := snaptest.MockInfo(c, ` +name: producer +version: 0 +apps: + app: +slots: + slot: + interface: interface +`, nil) + slotInfo := producer.Slots["slot"] + slot := interfaces.NewConnectedSlot(slotInfo, map[string]interface{}{ "a": "123", - }, "a.b") + }) + + _, err := policy.NestedGet("slot", slot, "b") + c.Check(err, ErrorMatches, `slot attribute "b" not found`) + + _, err = policy.NestedGet("plug", plug, "a.b") c.Check(err, ErrorMatches, `plug attribute "a\.b" not found`) - v, err := policy.NestedGet("slot", map[string]interface{}{ - "a": "123", - }, "a") + v, err := policy.NestedGet("slot", slot, "a") c.Check(err, IsNil) c.Check(v, Equals, "123") - v, err = policy.NestedGet("slot", map[string]interface{}{ + slot = interfaces.NewConnectedSlot(slotInfo, map[string]interface{}{ "a": map[string]interface{}{ "b": []interface{}{"1", "2", "3"}, }, - }, "a.b") + }) + + v, err = policy.NestedGet("slot", slot, "a.b") c.Check(err, IsNil) c.Check(v, DeepEquals, []interface{}{"1", "2", "3"}) } diff --git a/interfaces/policy/policy.go b/interfaces/policy/policy.go index ec122c21d7..7d9c5b3052 100644 --- a/interfaces/policy/policy.go +++ b/interfaces/policy/policy.go @@ -24,11 +24,9 @@ package policy import ( "fmt" - "strings" "github.com/snapcore/snapd/asserts" "github.com/snapcore/snapd/interfaces" - "github.com/snapcore/snapd/logger" "github.com/snapcore/snapd/snap" ) @@ -125,68 +123,22 @@ type ConnectCandidate struct { SlotSnapDeclaration *asserts.SnapDeclaration BaseDeclaration *asserts.BaseDeclaration - - // Static + dynamic attributes, merged lazily by plugAttrs/slotAttrs below (FIXME: remove it) - mergedPlugAttrs map[string]interface{} - mergedSlotAttrs map[string]interface{} } -func mergedAttributes(staticAttrs, dynamicAttrs map[string]interface{}, errorContext string) map[string]interface{} { - merged := make(map[string]interface{}) - for k, v := range staticAttrs { - merged[k] = v - } - for k, v := range dynamicAttrs { - if _, ok := merged[k]; ok { - // Safeguard. This should never happen as it's prevented - // when attributes are populated at higher levels. - logger.Noticef("internal error: attempted to overwrite static attribute %q (%s)", k, errorContext) - continue - } - merged[k] = v - } - return merged -} - -func (connc *ConnectCandidate) plugAttrs() map[string]interface{} { - // FIXME: change policy code to use Attrer interface, remove merging. - if connc.mergedPlugAttrs == nil { - connc.mergedPlugAttrs = mergedAttributes(connc.Plug.StaticAttrs(), connc.Plug.DynamicAttrs(), fmt.Sprintf("plug %q of snap %q", connc.Plug.Name(), connc.Plug.Snap().Name())) - } - return connc.mergedPlugAttrs -} - -func (connc *ConnectCandidate) slotAttrs() map[string]interface{} { - // FIXME: change policy code to use Attrer interface, remove merging. - if connc.mergedSlotAttrs == nil { - connc.mergedSlotAttrs = mergedAttributes(connc.Slot.StaticAttrs(), connc.Slot.DynamicAttrs(), fmt.Sprintf("slot %q of snap %q", connc.Slot.Name(), connc.Slot.Snap().Name())) - } - return connc.mergedSlotAttrs -} - -func nestedGet(which string, attrs map[string]interface{}, path string) (interface{}, error) { - notFound := fmt.Errorf("%s attribute %q not found", which, path) - comps := strings.Split(path, ".") - var v interface{} = attrs - for _, comp := range comps { - m, ok := v.(map[string]interface{}) - if !ok { - return nil, notFound - } - v, ok = m[comp] - if !ok { - return nil, notFound - } +func nestedGet(which string, attrs interfaces.Attrer, path string) (interface{}, error) { + val, ok := attrs.Lookup(path) + if !ok { + return nil, fmt.Errorf("%s attribute %q not found", which, path) } - return v, nil + return val, nil } func (connc *ConnectCandidate) PlugAttr(arg string) (interface{}, error) { - return nestedGet("plug", connc.plugAttrs(), arg) + return nestedGet("plug", connc.Plug, arg) } func (connc *ConnectCandidate) SlotAttr(arg string) (interface{}, error) { - return nestedGet("slot", connc.slotAttrs(), arg) + return nestedGet("slot", connc.Slot, arg) } func (connc *ConnectCandidate) plugSnapType() snap.Type { diff --git a/interfaces/repo.go b/interfaces/repo.go index c4b97c3026..e4321573d5 100644 --- a/interfaces/repo.go +++ b/interfaces/repo.go @@ -397,22 +397,20 @@ func (r *Repository) RemoveSlot(snapName, slotName string) error { // ResolveConnect resolves potentially missing plug or slot names and returns a // fully populated connection reference. -func (r *Repository) ResolveConnect(plugSnapName, plugName, slotSnapName, slotName string) (ConnRef, error) { +func (r *Repository) ResolveConnect(plugSnapName, plugName, slotSnapName, slotName string) (*ConnRef, error) { r.m.Lock() defer r.m.Unlock() - ref := ConnRef{} - if plugSnapName == "" { - return ref, fmt.Errorf("cannot resolve connection, plug snap name is empty") + return nil, fmt.Errorf("cannot resolve connection, plug snap name is empty") } if plugName == "" { - return ref, fmt.Errorf("cannot resolve connection, plug name is empty") + return nil, fmt.Errorf("cannot resolve connection, plug name is empty") } // Ensure that such plug exists plug := r.plugs[plugSnapName][plugName] if plug == nil { - return ref, fmt.Errorf("snap %q has no plug named %q", plugSnapName, plugName) + return nil, fmt.Errorf("snap %q has no plug named %q", plugSnapName, plugName) } if slotSnapName == "" { @@ -425,7 +423,7 @@ func (r *Repository) ResolveConnect(plugSnapName, plugName, slotSnapName, slotNa default: // XXX: perhaps this should not be an error and instead it should // silently assume "core" now? - return ref, fmt.Errorf("cannot resolve connection, slot snap name is empty") + return nil, fmt.Errorf("cannot resolve connection, slot snap name is empty") } } if slotName == "" { @@ -439,27 +437,26 @@ func (r *Repository) ResolveConnect(plugSnapName, plugName, slotSnapName, slotNa } switch len(candidates) { case 0: - return ref, fmt.Errorf("snap %q has no %q interface slots", slotSnapName, plug.Interface) + return nil, fmt.Errorf("snap %q has no %q interface slots", slotSnapName, plug.Interface) case 1: slotName = candidates[0] default: sort.Strings(candidates) - return ref, fmt.Errorf("snap %q has multiple %q interface slots: %s", slotSnapName, plug.Interface, strings.Join(candidates, ", ")) + return nil, fmt.Errorf("snap %q has multiple %q interface slots: %s", slotSnapName, plug.Interface, strings.Join(candidates, ", ")) } } // Ensure that such slot exists slot := r.slots[slotSnapName][slotName] if slot == nil { - return ref, fmt.Errorf("snap %q has no slot named %q", slotSnapName, slotName) + return nil, fmt.Errorf("snap %q has no slot named %q", slotSnapName, slotName) } // Ensure that plug and slot are compatible if slot.Interface != plug.Interface { - return ref, fmt.Errorf("cannot connect %s:%s (%q interface) to %s:%s (%q interface)", + return nil, fmt.Errorf("cannot connect %s:%s (%q interface) to %s:%s (%q interface)", plugSnapName, plugName, plug.Interface, slotSnapName, slotName, slot.Interface) } - ref = *NewConnRef(plug, slot) - return ref, nil + return NewConnRef(plug, slot), nil } // ResolveDisconnect resolves potentially missing plug or slot names and @@ -477,7 +474,7 @@ func (r *Repository) ResolveConnect(plugSnapName, plugName, slotSnapName, slotNa // In both cases the snap name can be omitted to implicitly refer to the core // snap. If there's no core snap it is simply assumed to be called "core" to // provide consistent error messages. -func (r *Repository) ResolveDisconnect(plugSnapName, plugName, slotSnapName, slotName string) ([]ConnRef, error) { +func (r *Repository) ResolveDisconnect(plugSnapName, plugName, slotSnapName, slotName string) ([]*ConnRef, error) { r.m.Lock() defer r.m.Unlock() @@ -517,7 +514,7 @@ func (r *Repository) ResolveDisconnect(plugSnapName, plugName, slotSnapName, slo return nil, fmt.Errorf("cannot disconnect %s:%s from %s:%s, it is not connected", plugSnapName, plugName, slotSnapName, slotName) } - return []ConnRef{*NewConnRef(plug, slot)}, nil + return []*ConnRef{NewConnRef(plug, slot)}, nil // 2: <snap>:<plug or slot> (through 1st pair) // Return a list of connections involving specified plug or slot. case plugName != "" && slotName == "" && slotSnapName == "": @@ -554,7 +551,7 @@ type PolicyFunc func(*ConnectedPlug, *ConnectedSlot) (bool, error) // Connect establishes a connection between a plug and a slot. // The plug and the slot must have the same interface. // When connections are reloaded policyCheck is null (we don't check policy again). -func (r *Repository) Connect(ref ConnRef, plugDynamicAttrs, slotDynamicAttrs map[string]interface{}, policyCheck PolicyFunc) (*Connection, error) { +func (r *Repository) Connect(ref *ConnRef, plugDynamicAttrs, slotDynamicAttrs map[string]interface{}, policyCheck PolicyFunc) (*Connection, error) { r.m.Lock() defer r.m.Unlock() @@ -665,21 +662,21 @@ func (r *Repository) Disconnect(plugSnapName, plugName, slotSnapName, slotName s // Connected returns references for all connections that are currently // established with the provided plug or slot. -func (r *Repository) Connected(snapName, plugOrSlotName string) ([]ConnRef, error) { +func (r *Repository) Connected(snapName, plugOrSlotName string) ([]*ConnRef, error) { r.m.Lock() defer r.m.Unlock() return r.connected(snapName, plugOrSlotName) } -func (r *Repository) connected(snapName, plugOrSlotName string) ([]ConnRef, error) { +func (r *Repository) connected(snapName, plugOrSlotName string) ([]*ConnRef, error) { if snapName == "" { snapName, _ = r.guessCoreSnapName() if snapName == "" { return nil, fmt.Errorf("internal error: cannot obtain core snap name while computing connections") } } - var conns []ConnRef + var conns []*ConnRef if plugOrSlotName == "" { return nil, fmt.Errorf("plug or slot name is empty") } @@ -691,14 +688,14 @@ func (r *Repository) connected(snapName, plugOrSlotName string) ([]ConnRef, erro if plug, ok := r.plugs[snapName][plugOrSlotName]; ok { for slotInfo := range r.plugSlots[plug] { - connRef := *NewConnRef(plug, slotInfo) + connRef := NewConnRef(plug, slotInfo) conns = append(conns, connRef) } } if slot, ok := r.slots[snapName][plugOrSlotName]; ok { for plugInfo := range r.slotPlugs[slot] { - connRef := *NewConnRef(plugInfo, slot) + connRef := NewConnRef(plugInfo, slot) conns = append(conns, connRef) } } @@ -706,7 +703,7 @@ func (r *Repository) connected(snapName, plugOrSlotName string) ([]ConnRef, erro return conns, nil } -func (r *Repository) Connections(snapName string) ([]ConnRef, error) { +func (r *Repository) Connections(snapName string) ([]*ConnRef, error) { r.m.Lock() defer r.m.Unlock() @@ -717,16 +714,16 @@ func (r *Repository) Connections(snapName string) ([]ConnRef, error) { } } - var conns []ConnRef + var conns []*ConnRef for _, plugInfo := range r.plugs[snapName] { for slotInfo := range r.plugSlots[plugInfo] { - connRef := *NewConnRef(plugInfo, slotInfo) + connRef := NewConnRef(plugInfo, slotInfo) conns = append(conns, connRef) } } for _, slotInfo := range r.slots[snapName] { for plugInfo := range r.slotPlugs[slotInfo] { - connRef := *NewConnRef(plugInfo, slotInfo) + connRef := NewConnRef(plugInfo, slotInfo) conns = append(conns, connRef) } } @@ -747,7 +744,7 @@ func (r *Repository) guessCoreSnapName() (string, error) { } // DisconnectAll disconnects all provided connection references. -func (r *Repository) DisconnectAll(conns []ConnRef) { +func (r *Repository) DisconnectAll(conns []*ConnRef) { r.m.Lock() defer r.m.Unlock() diff --git a/interfaces/repo_test.go b/interfaces/repo_test.go index a47f3ff728..9e6a05e9b0 100644 --- a/interfaces/repo_test.go +++ b/interfaces/repo_test.go @@ -313,7 +313,7 @@ func (s *RepositorySuite) TestRemovePlugFailsWhenPlugIsConnected(c *C) { err = s.testRepo.AddSlot(s.slot) c.Assert(err, IsNil) connRef := NewConnRef(s.plug, s.slot) - _, err = s.testRepo.Connect(*connRef, nil, nil, nil) + _, err = s.testRepo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) // Removing a plug used by a slot returns an appropriate error err = s.testRepo.RemovePlug(s.plug.Snap.Name(), s.plug.Name) @@ -559,7 +559,7 @@ func (s *RepositorySuite) TestRemoveSlotFailsWhenSlotIsConnected(c *C) { err = s.testRepo.AddSlot(s.slot) c.Assert(err, IsNil) connRef := NewConnRef(s.plug, s.slot) - _, err = s.testRepo.Connect(*connRef, nil, nil, nil) + _, err = s.testRepo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) // Removing a slot occupied by a plug returns an appropriate error err = s.testRepo.RemoveSlot(s.slot.Snap.Name(), s.slot.Name) @@ -576,7 +576,7 @@ func (s *RepositorySuite) TestResolveConnectExplicit(c *C) { c.Assert(s.testRepo.AddPlug(s.plug), IsNil) conn, err := s.testRepo.ResolveConnect("consumer", "plug", "producer", "slot") c.Check(err, IsNil) - c.Check(conn, Equals, ConnRef{ + c.Check(conn, DeepEquals, &ConnRef{ PlugRef: PlugRef{Snap: "consumer", Name: "plug"}, SlotRef: SlotRef{Snap: "producer", Name: "slot"}, }) @@ -596,7 +596,7 @@ slots: c.Assert(s.testRepo.AddPlug(s.plug), IsNil) conn, err := s.testRepo.ResolveConnect("consumer", "plug", "", "slot") c.Check(err, IsNil) - c.Check(conn, Equals, ConnRef{ + c.Check(conn, DeepEquals, &ConnRef{ PlugRef: PlugRef{Snap: "consumer", Name: "plug"}, SlotRef: SlotRef{Snap: "core", Name: "slot"}, }) @@ -616,7 +616,7 @@ slots: c.Assert(s.testRepo.AddPlug(s.plug), IsNil) conn, err := s.testRepo.ResolveConnect("consumer", "plug", "", "slot") c.Check(err, IsNil) - c.Check(conn, Equals, ConnRef{ + c.Check(conn, DeepEquals, &ConnRef{ PlugRef: PlugRef{Snap: "consumer", Name: "plug"}, SlotRef: SlotRef{Snap: "ubuntu-core", Name: "slot"}, }) @@ -664,7 +664,7 @@ slots: c.Assert(s.testRepo.AddPlug(s.plug), IsNil) conn, err := s.testRepo.ResolveConnect("consumer", "plug", "", "") c.Check(err, ErrorMatches, `snap "core" has no "interface" interface slots`) - c.Check(conn, Equals, ConnRef{}) + c.Check(conn, IsNil) } // ResolveConnect detects ambiguities when slot snap name is empty @@ -684,28 +684,28 @@ slots: c.Assert(s.testRepo.AddPlug(s.plug), IsNil) conn, err := s.testRepo.ResolveConnect("consumer", "plug", "", "") c.Check(err, ErrorMatches, `snap "core" has multiple "interface" interface slots: slot-a, slot-b`) - c.Check(conn, Equals, ConnRef{}) + c.Check(conn, IsNil) } // Pug snap name cannot be empty func (s *RepositorySuite) TestResolveConnectEmptyPlugSnapName(c *C) { conn, err := s.testRepo.ResolveConnect("", "plug", "producer", "slot") c.Check(err, ErrorMatches, "cannot resolve connection, plug snap name is empty") - c.Check(conn, Equals, ConnRef{}) + c.Check(conn, IsNil) } // Plug name cannot be empty func (s *RepositorySuite) TestResolveConnectEmptyPlugName(c *C) { conn, err := s.testRepo.ResolveConnect("consumer", "", "producer", "slot") c.Check(err, ErrorMatches, "cannot resolve connection, plug name is empty") - c.Check(conn, Equals, ConnRef{}) + c.Check(conn, IsNil) } // Plug must exist func (s *RepositorySuite) TestResolveNoSuchPlug(c *C) { conn, err := s.testRepo.ResolveConnect("consumer", "plug", "consumer", "slot") c.Check(err, ErrorMatches, `snap "consumer" has no plug named "plug"`) - c.Check(conn, Equals, ConnRef{}) + c.Check(conn, IsNil) } // Slot snap name cannot be empty if there's no core snap around @@ -713,7 +713,7 @@ func (s *RepositorySuite) TestResolveConnectEmptySlotSnapName(c *C) { c.Assert(s.testRepo.AddPlug(s.plug), IsNil) conn, err := s.testRepo.ResolveConnect("consumer", "plug", "", "slot") c.Check(err, ErrorMatches, "cannot resolve connection, slot snap name is empty") - c.Check(conn, Equals, ConnRef{}) + c.Check(conn, IsNil) } // Slot name cannot be empty if there's no core snap around @@ -721,7 +721,7 @@ func (s *RepositorySuite) TestResolveConnectEmptySlotName(c *C) { c.Assert(s.testRepo.AddPlug(s.plug), IsNil) conn, err := s.testRepo.ResolveConnect("consumer", "plug", "producer", "") c.Check(err, ErrorMatches, `snap "producer" has no "interface" interface slots`) - c.Check(conn, Equals, ConnRef{}) + c.Check(conn, IsNil) } // Slot must exists @@ -729,7 +729,7 @@ func (s *RepositorySuite) TestResolveNoSuchSlot(c *C) { c.Assert(s.testRepo.AddPlug(s.plug), IsNil) conn, err := s.testRepo.ResolveConnect("consumer", "plug", "producer", "slot") c.Check(err, ErrorMatches, `snap "producer" has no slot named "slot"`) - c.Check(conn, Equals, ConnRef{}) + c.Check(conn, IsNil) } // Plug and slot must have matching types @@ -746,7 +746,7 @@ func (s *RepositorySuite) TestResolveIncompatibleTypes(c *C) { conn, err := s.testRepo.ResolveConnect("consumer", "plug", "producer", "slot") c.Check(err, ErrorMatches, `cannot connect consumer:plug \("other-interface" interface\) to producer:slot \("interface" interface\)`) - c.Check(conn, Equals, ConnRef{}) + c.Check(conn, IsNil) } // Tests for Repository.ResolveDisconnect() @@ -977,7 +977,7 @@ func (s *RepositorySuite) TestResolveDisconnectMatrixTypical(c *C) { c.Assert(s.testRepo.AddPlug(s.plug), IsNil) c.Assert(s.testRepo.AddSlot(s.slot), IsNil) connRef := NewConnRef(s.plug, s.slot) - _, err := s.testRepo.Connect(*connRef, nil, nil, nil) + _, err := s.testRepo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) scenarios := []struct { @@ -1047,7 +1047,7 @@ func (s *RepositorySuite) TestResolveDisconnectMatrixTypical(c *C) { c.Check(connRefList, HasLen, 0) } else { c.Check(err, IsNil) - c.Check(connRefList, DeepEquals, []ConnRef{*connRef}) + c.Check(connRefList, DeepEquals, []*ConnRef{connRef}) } } } @@ -1059,7 +1059,7 @@ func (s *RepositorySuite) TestConnectFailsWhenPlugDoesNotExist(c *C) { c.Assert(err, IsNil) // Connecting an unknown plug returns an appropriate error connRef := NewConnRef(s.plug, s.slot) - _, err = s.testRepo.Connect(*connRef, nil, nil, nil) + _, err = s.testRepo.Connect(connRef, nil, nil, nil) c.Assert(err, ErrorMatches, `cannot connect plug "plug" from snap "consumer": no such plug`) } @@ -1068,7 +1068,7 @@ func (s *RepositorySuite) TestConnectFailsWhenSlotDoesNotExist(c *C) { c.Assert(err, IsNil) // Connecting to an unknown slot returns an error connRef := NewConnRef(s.plug, s.slot) - _, err = s.testRepo.Connect(*connRef, nil, nil, nil) + _, err = s.testRepo.Connect(connRef, nil, nil, nil) c.Assert(err, ErrorMatches, `cannot connect slot "slot" from snap "producer": no such slot`) } @@ -1078,7 +1078,7 @@ func (s *RepositorySuite) TestConnectSucceedsWhenIdenticalConnectExists(c *C) { err = s.testRepo.AddSlot(s.slot) c.Assert(err, IsNil) connRef := NewConnRef(s.plug, s.slot) - conn, err := s.testRepo.Connect(*connRef, nil, nil, nil) + conn, err := s.testRepo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) c.Assert(conn, NotNil) c.Assert(conn.Plug, NotNil) @@ -1086,7 +1086,7 @@ func (s *RepositorySuite) TestConnectSucceedsWhenIdenticalConnectExists(c *C) { c.Assert(conn.Plug.Name(), Equals, "plug") c.Assert(conn.Slot.Name(), Equals, "slot") // Connecting exactly the same thing twice succeeds without an error but does nothing. - _, err = s.testRepo.Connect(*connRef, nil, nil, nil) + _, err = s.testRepo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) // Only one connection is actually present. c.Assert(s.testRepo.Interfaces(), DeepEquals, &Interfaces{ @@ -1111,7 +1111,7 @@ func (s *RepositorySuite) TestConnectFailsWhenSlotAndPlugAreIncompatible(c *C) { c.Assert(err, IsNil) // Connecting a plug to an incompatible slot fails with an appropriate error connRef := NewConnRef(s.plug, s.slot) - _, err = s.testRepo.Connect(*connRef, nil, nil, nil) + _, err = s.testRepo.Connect(connRef, nil, nil, nil) c.Assert(err, ErrorMatches, `cannot connect plug "consumer:plug" \(interface "other-interface"\) to "producer:slot" \(interface "interface"\)`) } @@ -1122,7 +1122,7 @@ func (s *RepositorySuite) TestConnectSucceeds(c *C) { c.Assert(err, IsNil) // Connecting a plug works okay connRef := NewConnRef(s.plug, s.slot) - _, err = s.testRepo.Connect(*connRef, nil, nil, nil) + _, err = s.testRepo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) } @@ -1166,9 +1166,9 @@ func (s *RepositorySuite) TestDisconnectFailsWhenNotConnected(c *C) { func (s *RepositorySuite) TestDisconnectSucceeds(c *C) { c.Assert(s.testRepo.AddPlug(s.plug), IsNil) c.Assert(s.testRepo.AddSlot(s.slot), IsNil) - _, err := s.testRepo.Connect(*NewConnRef(s.plug, s.slot), nil, nil, nil) + _, err := s.testRepo.Connect(NewConnRef(s.plug, s.slot), nil, nil, nil) c.Assert(err, IsNil) - _, err = s.testRepo.Connect(*NewConnRef(s.plug, s.slot), nil, nil, nil) + _, err = s.testRepo.Connect(NewConnRef(s.plug, s.slot), nil, nil, nil) c.Assert(err, IsNil) err = s.testRepo.Disconnect(s.plug.Snap.Name(), s.plug.Name, s.slot.Snap.Name(), s.slot.Name) c.Assert(err, IsNil) @@ -1204,16 +1204,16 @@ func (s *RepositorySuite) TestConnectedFailsWithoutPlugOrSlot(c *C) { func (s *RepositorySuite) TestConnectedFindsConnections(c *C) { c.Assert(s.testRepo.AddPlug(s.plug), IsNil) c.Assert(s.testRepo.AddSlot(s.slot), IsNil) - _, err := s.testRepo.Connect(*NewConnRef(s.plug, s.slot), nil, nil, nil) + _, err := s.testRepo.Connect(NewConnRef(s.plug, s.slot), nil, nil, nil) c.Assert(err, IsNil) conns, err := s.testRepo.Connected(s.plug.Snap.Name(), s.plug.Name) c.Assert(err, IsNil) - c.Check(conns, DeepEquals, []ConnRef{*NewConnRef(s.plug, s.slot)}) + c.Check(conns, DeepEquals, []*ConnRef{NewConnRef(s.plug, s.slot)}) conns, err = s.testRepo.Connected(s.slot.Snap.Name(), s.slot.Name) c.Assert(err, IsNil) - c.Check(conns, DeepEquals, []ConnRef{*NewConnRef(s.plug, s.slot)}) + c.Check(conns, DeepEquals, []*ConnRef{NewConnRef(s.plug, s.slot)}) } // Connected uses the core snap if snap name is empty @@ -1225,28 +1225,28 @@ func (s *RepositorySuite) TestConnectedFindsCoreSnap(c *C) { } c.Assert(s.testRepo.AddPlug(s.plug), IsNil) c.Assert(s.testRepo.AddSlot(slot), IsNil) - _, err := s.testRepo.Connect(*NewConnRef(s.plug, slot), nil, nil, nil) + _, err := s.testRepo.Connect(NewConnRef(s.plug, slot), nil, nil, nil) c.Assert(err, IsNil) conns, err := s.testRepo.Connected("", s.slot.Name) c.Assert(err, IsNil) - c.Check(conns, DeepEquals, []ConnRef{*NewConnRef(s.plug, slot)}) + c.Check(conns, DeepEquals, []*ConnRef{NewConnRef(s.plug, slot)}) } // Connected finds connections when asked from plug or from slot side func (s *RepositorySuite) TestConnections(c *C) { c.Assert(s.testRepo.AddPlug(s.plug), IsNil) c.Assert(s.testRepo.AddSlot(s.slot), IsNil) - _, err := s.testRepo.Connect(*NewConnRef(s.plug, s.slot), nil, nil, nil) + _, err := s.testRepo.Connect(NewConnRef(s.plug, s.slot), nil, nil, nil) c.Assert(err, IsNil) conns, err := s.testRepo.Connections(s.plug.Snap.Name()) c.Assert(err, IsNil) - c.Check(conns, DeepEquals, []ConnRef{*NewConnRef(s.plug, s.slot)}) + c.Check(conns, DeepEquals, []*ConnRef{NewConnRef(s.plug, s.slot)}) conns, err = s.testRepo.Connections(s.slot.Snap.Name()) c.Assert(err, IsNil) - c.Check(conns, DeepEquals, []ConnRef{*NewConnRef(s.plug, s.slot)}) + c.Check(conns, DeepEquals, []*ConnRef{NewConnRef(s.plug, s.slot)}) conns, err = s.testRepo.Connections("abc") c.Assert(err, IsNil) @@ -1258,10 +1258,10 @@ func (s *RepositorySuite) TestConnections(c *C) { func (s *RepositorySuite) TestDisconnectAll(c *C) { c.Assert(s.testRepo.AddPlug(s.plug), IsNil) c.Assert(s.testRepo.AddSlot(s.slot), IsNil) - _, err := s.testRepo.Connect(*NewConnRef(s.plug, s.slot), nil, nil, nil) + _, err := s.testRepo.Connect(NewConnRef(s.plug, s.slot), nil, nil, nil) c.Assert(err, IsNil) - conns := []ConnRef{*NewConnRef(s.plug, s.slot)} + conns := []*ConnRef{NewConnRef(s.plug, s.slot)} s.testRepo.DisconnectAll(conns) c.Assert(s.testRepo.Interfaces(), DeepEquals, &Interfaces{ Plugs: []*snap.PlugInfo{s.plug}, @@ -1278,7 +1278,7 @@ func (s *RepositorySuite) TestInterfacesSmokeTest(c *C) { c.Assert(err, IsNil) // After connecting the result is as expected connRef := NewConnRef(s.plug, s.slot) - _, err = s.testRepo.Connect(*connRef, nil, nil, nil) + _, err = s.testRepo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) ifaces := s.testRepo.Interfaces() c.Assert(ifaces, DeepEquals, &Interfaces{ @@ -1339,7 +1339,7 @@ func (s *RepositorySuite) TestSnapSpecification(c *C) { // Establish connection between plug and slot connRef := NewConnRef(s.plug, s.slot) - _, err = repo.Connect(*connRef, nil, nil, nil) + _, err = repo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) // Snaps should get static and connection-specific security now @@ -1377,7 +1377,7 @@ func (s *RepositorySuite) TestSnapSpecificationFailureWithConnectionSnippets(c * c.Assert(repo.AddPlug(s.plug), IsNil) c.Assert(repo.AddSlot(s.slot), IsNil) connRef := NewConnRef(s.plug, s.slot) - _, err := repo.Connect(*connRef, nil, nil, nil) + _, err := repo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) spec, err := repo.SnapSpecification(testSecurity, s.plug.Snap.Name()) @@ -1407,7 +1407,7 @@ func (s *RepositorySuite) TestSnapSpecificationFailureWithPermanentSnippets(c *C c.Assert(repo.AddPlug(s.plug), IsNil) c.Assert(repo.AddSlot(s.slot), IsNil) connRef := NewConnRef(s.plug, s.slot) - _, err := repo.Connect(*connRef, nil, nil, nil) + _, err := repo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) spec, err := repo.SnapSpecification(testSecurity, s.plug.Snap.Name()) @@ -1662,7 +1662,7 @@ func (s *AddRemoveSuite) TestRemoveSnapErrorsOnStillConnectedPlug(c *C) { c.Assert(err, IsNil) _, err = s.addSnap(c, testProducerYaml) c.Assert(err, IsNil) - connRef := ConnRef{PlugRef: PlugRef{Snap: "consumer", Name: "iface"}, SlotRef: SlotRef{Snap: "producer", Name: "iface"}} + connRef := &ConnRef{PlugRef: PlugRef{Snap: "consumer", Name: "iface"}, SlotRef: SlotRef{Snap: "producer", Name: "iface"}} _, err = s.repo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) err = s.repo.RemoveSnap("consumer") @@ -1674,7 +1674,7 @@ func (s *AddRemoveSuite) TestRemoveSnapErrorsOnStillConnectedSlot(c *C) { c.Assert(err, IsNil) _, err = s.addSnap(c, testProducerYaml) c.Assert(err, IsNil) - connRef := ConnRef{PlugRef: PlugRef{Snap: "consumer", Name: "iface"}, SlotRef: SlotRef{Snap: "producer", Name: "iface"}} + connRef := &ConnRef{PlugRef: PlugRef{Snap: "consumer", Name: "iface"}, SlotRef: SlotRef{Snap: "producer", Name: "iface"}} _, err = s.repo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) err = s.repo.RemoveSnap("producer") @@ -1735,7 +1735,7 @@ func (s *DisconnectSnapSuite) TestNotConnected(c *C) { } func (s *DisconnectSnapSuite) TestOutgoingConnection(c *C) { - connRef := ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "iface-a"}, SlotRef: SlotRef{Snap: "s2", Name: "iface-a"}} + connRef := &ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "iface-a"}, SlotRef: SlotRef{Snap: "s2", Name: "iface-a"}} _, err := s.repo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) // Disconnect s1 with which has an outgoing connection to s2 @@ -1746,7 +1746,7 @@ func (s *DisconnectSnapSuite) TestOutgoingConnection(c *C) { } func (s *DisconnectSnapSuite) TestIncomingConnection(c *C) { - connRef := ConnRef{PlugRef: PlugRef{Snap: "s2", Name: "iface-b"}, SlotRef: SlotRef{Snap: "s1", Name: "iface-b"}} + connRef := &ConnRef{PlugRef: PlugRef{Snap: "s2", Name: "iface-b"}, SlotRef: SlotRef{Snap: "s1", Name: "iface-b"}} _, err := s.repo.Connect(connRef, nil, nil, nil) c.Assert(err, IsNil) // Disconnect s1 with which has an incoming connection from s2 @@ -1759,10 +1759,10 @@ func (s *DisconnectSnapSuite) TestIncomingConnection(c *C) { func (s *DisconnectSnapSuite) TestCrossConnection(c *C) { // This test is symmetric wrt s1 <-> s2 connections for _, snapName := range []string{"s1", "s2"} { - connRef1 := ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "iface-a"}, SlotRef: SlotRef{Snap: "s2", Name: "iface-a"}} + connRef1 := &ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "iface-a"}, SlotRef: SlotRef{Snap: "s2", Name: "iface-a"}} _, err := s.repo.Connect(connRef1, nil, nil, nil) c.Assert(err, IsNil) - connRef2 := ConnRef{PlugRef: PlugRef{Snap: "s2", Name: "iface-b"}, SlotRef: SlotRef{Snap: "s1", Name: "iface-b"}} + connRef2 := &ConnRef{PlugRef: PlugRef{Snap: "s2", Name: "iface-b"}, SlotRef: SlotRef{Snap: "s1", Name: "iface-b"}} _, err = s.repo.Connect(connRef2, nil, nil, nil) c.Assert(err, IsNil) affected, err := s.repo.DisconnectSnap(snapName) @@ -1892,11 +1892,11 @@ slots: c.Assert(r.AddSnap(s3), IsNil) // Connect a few things for the tests below. - _, err := r.Connect(ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "i1"}, SlotRef: SlotRef{Snap: "s2", Name: "i1"}}, nil, nil, nil) + _, err := r.Connect(&ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "i1"}, SlotRef: SlotRef{Snap: "s2", Name: "i1"}}, nil, nil, nil) c.Assert(err, IsNil) - _, err = r.Connect(ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "i1"}, SlotRef: SlotRef{Snap: "s2", Name: "i1"}}, nil, nil, nil) + _, err = r.Connect(&ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "i1"}, SlotRef: SlotRef{Snap: "s2", Name: "i1"}}, nil, nil, nil) c.Assert(err, IsNil) - _, err = r.Connect(ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "i2"}, SlotRef: SlotRef{Snap: "s3", Name: "i2"}}, nil, nil, nil) + _, err = r.Connect(&ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "i2"}, SlotRef: SlotRef{Snap: "s3", Name: "i2"}}, nil, nil, nil) c.Assert(err, IsNil) // Without any names or options we get the summary of all the interfaces. @@ -1986,7 +1986,7 @@ func (s *RepositorySuite) TestBeforeConnectValidation(c *C) { slotDynAttrs := map[string]interface{}{"attr1": "val1"} policyCheck := func(plug *ConnectedPlug, slot *ConnectedSlot) (bool, error) { return true, nil } - conn, err := s.emptyRepo.Connect(ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "consumer"}, SlotRef: SlotRef{Snap: "s2", Name: "producer"}}, plugDynAttrs, slotDynAttrs, policyCheck) + conn, err := s.emptyRepo.Connect(&ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "consumer"}, SlotRef: SlotRef{Snap: "s2", Name: "producer"}}, plugDynAttrs, slotDynAttrs, policyCheck) c.Assert(err, IsNil) c.Assert(conn, NotNil) @@ -2021,7 +2021,7 @@ func (s *RepositorySuite) TestBeforeConnectValidationFailure(c *C) { policyCheck := func(plug *ConnectedPlug, slot *ConnectedSlot) (bool, error) { return true, nil } - conn, err := s.emptyRepo.Connect(ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "consumer"}, SlotRef: SlotRef{Snap: "s2", Name: "producer"}}, plugDynAttrs, slotDynAttrs, policyCheck) + conn, err := s.emptyRepo.Connect(&ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "consumer"}, SlotRef: SlotRef{Snap: "s2", Name: "producer"}}, plugDynAttrs, slotDynAttrs, policyCheck) c.Assert(err, NotNil) c.Assert(err, ErrorMatches, `cannot connect plug "consumer" of snap "s1": invalid plug`) c.Assert(conn, IsNil) @@ -2047,7 +2047,7 @@ func (s *RepositorySuite) TestBeforeConnectValidationPolicyCheckFailure(c *C) { return false, fmt.Errorf("policy check failed") } - conn, err := s.emptyRepo.Connect(ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "consumer"}, SlotRef: SlotRef{Snap: "s2", Name: "producer"}}, plugDynAttrs, slotDynAttrs, policyCheck) + conn, err := s.emptyRepo.Connect(&ConnRef{PlugRef: PlugRef{Snap: "s1", Name: "consumer"}, SlotRef: SlotRef{Snap: "s2", Name: "producer"}}, plugDynAttrs, slotDynAttrs, policyCheck) c.Assert(err, NotNil) c.Assert(err, ErrorMatches, `policy check failed`) c.Assert(conn, IsNil) diff --git a/interfaces/seccomp/backend.go b/interfaces/seccomp/backend.go index d72bd3a506..40de971199 100644 --- a/interfaces/seccomp/backend.go +++ b/interfaces/seccomp/backend.go @@ -48,7 +48,10 @@ import ( "github.com/snapcore/snapd/snap" ) -var osReadlink = os.Readlink +var ( + osReadlink = os.Readlink + kernelFeatures = release.SecCompActions +) func seccompToBpfPath() string { // FIXME: use cmd.InternalToolPath here once: @@ -193,3 +196,16 @@ func addContent(securityTag string, opts interfaces.ConfinementOptions, snippetF func (b *Backend) NewSpecification() interfaces.Specification { return &Specification{} } + +// SandboxFeatures returns the list of seccomp features supported by the kernel. +func (b *Backend) SandboxFeatures() []string { + features := kernelFeatures() + tags := make([]string, 0, len(features)+1) + for _, feature := range features { + // Prepend "kernel:" to apparmor kernel features to namespace them and + // allow us to introduce our own tags later. + tags = append(tags, "kernel:"+feature) + } + tags = append(tags, "bpf-argument-filtering") + return tags +} diff --git a/interfaces/seccomp/backend_test.go b/interfaces/seccomp/backend_test.go index 55fa5b3feb..a6613355c1 100644 --- a/interfaces/seccomp/backend_test.go +++ b/interfaces/seccomp/backend_test.go @@ -385,3 +385,10 @@ func (s *backendSuite) TestSystemKeyRetLogUnsupported(c *C) { c.Assert(profile+".src", Not(testutil.FileContains), "# complain mode logging unavailable\n") s.RemoveSnap(c, snapInfo) } + +func (s *backendSuite) TestSandboxFeatures(c *C) { + restore := seccomp.MockKernelFeatures(func() []string { return []string{"foo", "bar"} }) + defer restore() + + c.Assert(s.Backend.SandboxFeatures(), DeepEquals, []string{"kernel:foo", "kernel:bar", "bpf-argument-filtering"}) +} diff --git a/interfaces/seccomp/export_test.go b/interfaces/seccomp/export_test.go index c1e460c4c8..285c3f6220 100644 --- a/interfaces/seccomp/export_test.go +++ b/interfaces/seccomp/export_test.go @@ -36,3 +36,11 @@ func MockOsReadlink(f func(string) (string, error)) (restore func()) { osReadlink = realOsReadlink } } + +func MockKernelFeatures(f func() []string) (resture func()) { + old := kernelFeatures + kernelFeatures = f + return func() { + kernelFeatures = old + } +} diff --git a/interfaces/systemd/backend.go b/interfaces/systemd/backend.go index 34c0cb3a00..01c3b7c47f 100644 --- a/interfaces/systemd/backend.go +++ b/interfaces/systemd/backend.go @@ -126,6 +126,11 @@ func (b *Backend) NewSpecification() interfaces.Specification { return &Specification{} } +// SandboxFeatures returns nil +func (b *Backend) SandboxFeatures() []string { + return nil +} + // deriveContent computes .service files based on requests made to the specification. func deriveContent(spec *Specification, snapInfo *snap.Info) map[string]*osutil.FileState { services := spec.Services() diff --git a/interfaces/systemd/backend_test.go b/interfaces/systemd/backend_test.go index ba99aa2661..4d538c4778 100644 --- a/interfaces/systemd/backend_test.go +++ b/interfaces/systemd/backend_test.go @@ -153,3 +153,7 @@ func (s *backendSuite) TestSettingUpSecurityWithFewerServices(c *C) { {"systemctl", "daemon-reload"}, }) } + +func (s *backendSuite) TestSandboxFeatures(c *C) { + c.Assert(s.Backend.SandboxFeatures(), IsNil) +} diff --git a/interfaces/udev/backend.go b/interfaces/udev/backend.go index aac31b85d3..b90070b494 100644 --- a/interfaces/udev/backend.go +++ b/interfaces/udev/backend.go @@ -150,3 +150,11 @@ func (b *Backend) deriveContent(spec *Specification, snapInfo *snap.Info) (conte func (b *Backend) NewSpecification() interfaces.Specification { return &Specification{} } + +// SandboxFeatures returns the list of features supported by snapd for mediating access to kernel devices. +func (b *Backend) SandboxFeatures() []string { + return []string{ + "device-cgroup-v1", /* Snapd creates a device group (v1) for each snap */ + "tagging", /* Tagging dynamically associates new devices with specific snaps */ + } +} diff --git a/interfaces/udev/backend_test.go b/interfaces/udev/backend_test.go index 95138c5609..a0cf183736 100644 --- a/interfaces/udev/backend_test.go +++ b/interfaces/udev/backend_test.go @@ -413,3 +413,10 @@ func (s *backendSuite) TestUpdatingSnapWithoutSlotsToOneWithoutSlots(c *C) { s.RemoveSnap(c, snapInfo) } } + +func (s *backendSuite) TestSandboxFeatures(c *C) { + c.Assert(s.Backend.SandboxFeatures(), DeepEquals, []string{ + "device-cgroup-v1", + "tagging", + }) +} diff --git a/overlord/configstate/handler_test.go b/overlord/configstate/handler_test.go index 90c6bb639b..9fbd4da5ec 100644 --- a/overlord/configstate/handler_test.go +++ b/overlord/configstate/handler_test.go @@ -49,10 +49,29 @@ type configureHandlerSuite struct { var _ = Suite(&configureHandlerSuite{}) func (s *configureHandlerSuite) SetUpTest(c *C) { + dirs.SetRootDir(c.MkDir()) + s.state = state.New(nil) s.state.Lock() defer s.state.Unlock() + coreSnapYaml := `name: core +version: 1.0 +type: os +` + snaptest.MockSnap(c, coreSnapYaml, &snap.SideInfo{ + RealName: "core", + Revision: snap.R(1), + }) + snapstate.Set(s.state, "core", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "core", Revision: snap.R(1), SnapID: "core-snap-id"}, + }, + Current: snap.R(1), + SnapType: "os", + }) + s.restore = snap.MockSanitizePlugsSlots(func(snapInfo *snap.Info) {}) task := s.state.NewTask("test-task", "my test task") @@ -67,6 +86,7 @@ func (s *configureHandlerSuite) SetUpTest(c *C) { func (s *configureHandlerSuite) TearDownTest(c *C) { s.restore() + dirs.SetRootDir("/") } func (s *configureHandlerSuite) TestBeforeInitializesTransaction(c *C) { @@ -91,8 +111,6 @@ func (s *configureHandlerSuite) TestBeforeInitializesTransaction(c *C) { func (s *configureHandlerSuite) TestBeforeInitializesTransactionUseDefaults(c *C) { r := release.MockOnClassic(false) defer r() - dirs.SetRootDir(c.MkDir()) - defer dirs.SetRootDir("/") const mockGadgetSnapYaml = ` name: canonical-pc @@ -162,8 +180,6 @@ hooks: func (s *configureHandlerSuite) TestBeforeUseDefaultsMissingHook(c *C) { r := release.MockOnClassic(false) defer r() - dirs.SetRootDir(c.MkDir()) - defer dirs.SetRootDir("/") const mockGadgetSnapYaml = ` name: canonical-pc diff --git a/overlord/devicestate/devicestate_test.go b/overlord/devicestate/devicestate_test.go index a1e87fdefa..733fbb5093 100644 --- a/overlord/devicestate/devicestate_test.go +++ b/overlord/devicestate/devicestate_test.go @@ -2120,7 +2120,7 @@ func makeMockRepoWithConnectedSnaps(c *C, st *state.State, info11, core11 *snap. c.Assert(err, IsNil) err = repo.AddSnap(core11) c.Assert(err, IsNil) - _, err = repo.Connect(interfaces.ConnRef{ + _, err = repo.Connect(&interfaces.ConnRef{ PlugRef: interfaces.PlugRef{Snap: info11.Name(), Name: ifname}, SlotRef: interfaces.SlotRef{Snap: core11.Name(), Name: ifname}, }, nil, nil, nil) diff --git a/overlord/hookstate/ctlcmd/get_test.go b/overlord/hookstate/ctlcmd/get_test.go index 48945867e4..a64a263a14 100644 --- a/overlord/hookstate/ctlcmd/get_test.go +++ b/overlord/hookstate/ctlcmd/get_test.go @@ -200,6 +200,7 @@ func (s *getAttrSuite) SetUpTest(c *C) { attrsTask.Set("plug-dynamic", dynamicPlugAttrs) attrsTask.Set("slot-static", staticSlotAttrs) attrsTask.Set("slot-dynamic", dynamicSlotAttrs) + ch.AddTask(attrsTask) state.Unlock() diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go index 97d23d742d..dc7bb33968 100644 --- a/overlord/ifacestate/handlers.go +++ b/overlord/ifacestate/handlers.go @@ -388,7 +388,7 @@ func (m *InterfaceManager) doConnect(task *state.Task, _ *tomb.Tomb) error { return err } - connRef := interfaces.ConnRef{PlugRef: plugRef, SlotRef: slotRef} + connRef := &interfaces.ConnRef{PlugRef: plugRef, SlotRef: slotRef} var plugSnapst snapstate.SnapState if err := snapstate.Get(st, plugRef.Snap, &plugSnapst); err != nil { diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go index 85678bc833..4baa4f1f54 100644 --- a/overlord/ifacestate/ifacestate_test.go +++ b/overlord/ifacestate/ifacestate_test.go @@ -1950,7 +1950,7 @@ func (s *interfaceManagerSuite) TestSetupProfilesDevModeMultiple(c *C) { Interface: "test", }) c.Assert(err, IsNil) - connRef := interfaces.ConnRef{ + connRef := &interfaces.ConnRef{ PlugRef: interfaces.PlugRef{Snap: siP.Name(), Name: "plug"}, SlotRef: interfaces.SlotRef{Snap: siC.Name(), Name: "slot"}, } diff --git a/overlord/managers_test.go b/overlord/managers_test.go index dda907a3ba..54d69f005d 100644 --- a/overlord/managers_test.go +++ b/overlord/managers_test.go @@ -2075,7 +2075,7 @@ func (ms *mgrsSuite) testTwoInstalls(c *C, snapName1, snapYaml1, snapName2, snap cn, err := repo.Connected("snap1", "shared-data-plug") c.Assert(err, IsNil) c.Assert(cn, HasLen, 1) - c.Assert(cn, DeepEquals, []interfaces.ConnRef{{ + c.Assert(cn, DeepEquals, []*interfaces.ConnRef{{ PlugRef: interfaces.PlugRef{Snap: "snap1", Name: "shared-data-plug"}, SlotRef: interfaces.SlotRef{Snap: "snap2", Name: "shared-data-slot"}, }}) diff --git a/overlord/snapstate/handlers_prereq_test.go b/overlord/snapstate/handlers_prereq_test.go index a0fc5a9619..69097a6ad9 100644 --- a/overlord/snapstate/handlers_prereq_test.go +++ b/overlord/snapstate/handlers_prereq_test.go @@ -247,16 +247,18 @@ func (s *prereqSuite) TestDoPrereqRetryWhenBaseInFlight(c *C) { c.Check(t.Status(), Equals, state.DoingStatus) c.Check(tCore.Status(), Equals, state.DoneStatus) - s.state.Unlock() - // wait the prereq-retry-timeout - for i := 0; i < 10; i++ { + // wait, we will hit prereq-retry-timeout eventually + // (this can take a while on very slow machines) + for i := 0; i < 50; i++ { time.Sleep(10 * time.Millisecond) + s.state.Unlock() s.snapmgr.Ensure() s.snapmgr.Wait() + s.state.Lock() + if t.Status() == state.DoneStatus { + break + } } - s.state.Lock() - defer s.state.Unlock() - c.Check(t.Status(), Equals, state.DoneStatus) } diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go index ee8e505f05..8552d2609a 100644 --- a/overlord/snapstate/snapstate.go +++ b/overlord/snapstate/snapstate.go @@ -1844,7 +1844,9 @@ func CoreInfo(st *state.State) (*snap.Info, error) { return nil, fmt.Errorf("unexpected number of cores, got %d", len(res)) } -// ConfigDefaults returns the configuration defaults for the snap specified in the gadget. If gadget is absent or the snap has no snap-id it returns ErrNoState. +// ConfigDefaults returns the configuration defaults for the snap specified in +// the gadget. If gadget is absent or the snap has no snap-id it returns +// ErrNoState. func ConfigDefaults(st *state.State, snapName string) (map[string]interface{}, error) { gadget, err := GadgetInfo(st) if err != nil { @@ -1856,8 +1858,17 @@ func ConfigDefaults(st *state.State, snapName string) (map[string]interface{}, e return nil, err } + core, err := CoreInfo(st) + if err != nil { + return nil, err + } + isCoreDefaults := core.Name() == snapName + si := snapst.CurrentSideInfo() - if si.SnapID == "" { + // core snaps can be addressed even without a snap-id via the special + // "system" value in the config; first-boot always configures the core + // snap with UseConfigDefaults + if si.SnapID == "" && !isCoreDefaults { return nil, state.ErrNoState } @@ -1866,6 +1877,17 @@ func ConfigDefaults(st *state.State, snapName string) (map[string]interface{}, e return nil, err } + // we support setting core defaults via "system" + if isCoreDefaults { + if defaults, ok := gadgetInfo.Defaults["system"]; ok { + if _, ok := gadgetInfo.Defaults[si.SnapID]; ok && si.SnapID != "" { + logger.Noticef("core snap configuration defaults found under both 'system' key and core-snap-id, preferring 'system'") + } + + return defaults, nil + } + } + defaults, ok := gadgetInfo.Defaults[si.SnapID] if !ok { return nil, state.ErrNoState diff --git a/overlord/snapstate/snapstate_test.go b/overlord/snapstate/snapstate_test.go index 095b982f85..fcfee371a5 100644 --- a/overlord/snapstate/snapstate_test.go +++ b/overlord/snapstate/snapstate_test.go @@ -27,6 +27,7 @@ import ( "os" "path/filepath" "sort" + "strings" "testing" "time" @@ -7568,7 +7569,7 @@ volumes: bootloader: grub ` -func (s *snapmgrTestSuite) prepareGadget(c *C) { +func (s *snapmgrTestSuite) prepareGadget(c *C, extraGadgetYaml ...string) { gadgetSideInfo := &snap.SideInfo{RealName: "the-gadget", SnapID: "the-gadget-id", Revision: snap.R(1)} gadgetInfo := snaptest.MockSnap(c, ` name: the-gadget @@ -7576,7 +7577,8 @@ type: gadget version: 1.0 `, gadgetSideInfo) - err := ioutil.WriteFile(filepath.Join(gadgetInfo.MountDir(), "meta/gadget.yaml"), []byte(gadgetYaml), 0600) + gadgetYamlWhole := strings.Join(append([]string{gadgetYaml}, extraGadgetYaml...), "") + err := ioutil.WriteFile(filepath.Join(gadgetInfo.MountDir(), "meta/gadget.yaml"), []byte(gadgetYamlWhole), 0600) c.Assert(err, IsNil) snapstate.Set(s.state, "the-gadget", &snapstate.SnapState{ @@ -7607,6 +7609,7 @@ func (s *snapmgrTestSuite) TestConfigDefaults(c *C) { Current: snap.R(11), SnapType: "app", }) + makeInstalledMockCoreSnap(c) defls, err := snapstate.ConfigDefaults(s.state, "some-snap") c.Assert(err, IsNil) @@ -7624,6 +7627,65 @@ func (s *snapmgrTestSuite) TestConfigDefaults(c *C) { c.Assert(err, Equals, state.ErrNoState) } +func (s *snapmgrTestSuite) TestConfigDefaultsSystem(c *C) { + r := release.MockOnClassic(false) + defer r() + + // using MockSnapReadInfo, we want to read the bits on disk + snapstate.MockSnapReadInfo(snap.ReadInfo) + + s.state.Lock() + defer s.state.Unlock() + + s.prepareGadget(c, ` +defaults: + system: + foo: bar +`) + + makeInstalledMockCoreSnap(c) + + defls, err := snapstate.ConfigDefaults(s.state, "core") + c.Assert(err, IsNil) + c.Assert(defls, DeepEquals, map[string]interface{}{"foo": "bar"}) +} + +func (s *snapmgrTestSuite) TestConfigDefaultsSystemConflictsCoreSnapId(c *C) { + r := release.MockOnClassic(false) + defer r() + + // using MockSnapReadInfo, we want to read the bits on disk + snapstate.MockSnapReadInfo(snap.ReadInfo) + + s.state.Lock() + defer s.state.Unlock() + + s.prepareGadget(c, ` +defaults: + system: + foo: bar + the-core-snap: + foo: other-bar + other-key: other-key-default +`) + + snapstate.Set(s.state, "core", &snapstate.SnapState{ + Active: true, + Sequence: []*snap.SideInfo{ + {RealName: "core", SnapID: "the-core-snap", Revision: snap.R(1)}, + }, + Current: snap.R(1), + SnapType: "os", + }) + + makeInstalledMockCoreSnap(c) + + // 'system' key defaults take precedence over snap-id ones + defls, err := snapstate.ConfigDefaults(s.state, "core") + c.Assert(err, IsNil) + c.Assert(defls, DeepEquals, map[string]interface{}{"foo": "bar"}) +} + func (s *snapmgrTestSuite) TestGadgetDefaultsAreNormalizedForConfigHook(c *C) { var mockGadgetSnapYaml = ` name: canonical-pc diff --git a/packaging/arch/PKGBUILD b/packaging/arch/PKGBUILD index de5acb55cf..73071c032e 100644 --- a/packaging/arch/PKGBUILD +++ b/packaging/arch/PKGBUILD @@ -10,7 +10,7 @@ pkgname=snapd pkgdesc="Service and tools for management of snap packages." depends=('squashfs-tools' 'libseccomp' 'libsystemd') optdepends=('bash-completion: bash completion support') -pkgver=2.32.8.r734.gd79c34ba4 +pkgver=2.32.9.r734.gd79c34ba4 pkgrel=1 arch=('x86_64') url="https://github.com/snapcore/snapd" diff --git a/packaging/fedora/snapd.spec b/packaging/fedora/snapd.spec index 4cef21b7d9..ef50be4689 100644 --- a/packaging/fedora/snapd.spec +++ b/packaging/fedora/snapd.spec @@ -70,7 +70,7 @@ %endif Name: snapd -Version: 2.32.8 +Version: 2.32.9 Release: 0%{?dist} Summary: A transactional software package manager Group: System Environment/Base @@ -724,6 +724,11 @@ fi %changelog +* Tue May 16 2018 Michael Vogt <mvo@ubuntu.com> +- New upstream release 2.32.9 + - tests: run all spread tests inside GCE + - tests: build spread in the autopkgtests with a more recent go + * Fri May 11 2018 Michael Vogt <mvo@ubuntu.com> - New upstream release 2.32.8 - snapd.core-fixup.sh: add workaround for corrupted uboot.env diff --git a/packaging/opensuse-42.2/snapd.changes b/packaging/opensuse-42.2/snapd.changes index 05b802b18d..1414eb3b6d 100644 --- a/packaging/opensuse-42.2/snapd.changes +++ b/packaging/opensuse-42.2/snapd.changes @@ -1,4 +1,9 @@ ------------------------------------------------------------------- +Wed May 16 10:20:08 UTC 2018 - mvo@fastmail.fm + +- Update to upstream release 2.32.9 + +------------------------------------------------------------------- Fri May 11 14:36:16 UTC 2018 - mvo@fastmail.fm - Update to upstream release 2.32.8 diff --git a/packaging/opensuse-42.2/snapd.spec b/packaging/opensuse-42.2/snapd.spec index d6c965d6cc..cbe26efe7c 100644 --- a/packaging/opensuse-42.2/snapd.spec +++ b/packaging/opensuse-42.2/snapd.spec @@ -32,7 +32,7 @@ %define systemd_services_list snapd.socket snapd.service Name: snapd -Version: 2.32.8 +Version: 2.32.9 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 980ca94145..7366ee86cf 100644 --- a/packaging/ubuntu-14.04/changelog +++ b/packaging/ubuntu-14.04/changelog @@ -1,3 +1,11 @@ +snapd (2.32.9~14.04) trusty; urgency=medium + + * New upstream release, LP: #1767833 + - tests: run all spread tests inside GCE + - tests: build spread in the autopkgtests with a more recent go + + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 16 May 2018 10:20:08 +0200 + snapd (2.32.8~14.04) trusty; urgency=medium * New upstream release, LP: #1767833 diff --git a/packaging/ubuntu-14.04/control b/packaging/ubuntu-14.04/control index 14bba81273..859686f037 100644 --- a/packaging/ubuntu-14.04/control +++ b/packaging/ubuntu-14.04/control @@ -33,6 +33,8 @@ Build-Depends: autoconf, squashfs-tools, udev, xfslibs-dev +Recommends: gnupg1 | gnupg +Suggests: zenity | kdialog Standards-Version: 3.9.7 Homepage: https://github.com/snapcore/snapd Vcs-Browser: https://github.com/snapcore/snapd diff --git a/packaging/ubuntu-16.04/changelog b/packaging/ubuntu-16.04/changelog index 360a31d029..bfe07d0238 100644 --- a/packaging/ubuntu-16.04/changelog +++ b/packaging/ubuntu-16.04/changelog @@ -1,3 +1,11 @@ +snapd (2.32.9) xenial; urgency=medium + + * New upstream release, LP: #1767833 + - tests: run all spread tests inside GCE + - tests: build spread in the autopkgtests with a more recent go + + -- Michael Vogt <michael.vogt@ubuntu.com> Wed, 16 May 2018 10:20:08 +0200 + snapd (2.32.8) xenial; urgency=medium * New upstream release, LP: #1767833 diff --git a/packaging/ubuntu-16.04/control b/packaging/ubuntu-16.04/control index effa6a2e7a..e86a698506 100644 --- a/packaging/ubuntu-16.04/control +++ b/packaging/ubuntu-16.04/control @@ -71,6 +71,7 @@ Depends: adduser, Replaces: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9), snap-confine (<< 2.23), ubuntu-core-launcher (<< 2.22), snapd-xdg-open (<= 0.0.0) Breaks: ubuntu-snappy (<< 1.9), ubuntu-snappy-cli (<< 1.9), snap-confine (<< 2.23), ubuntu-core-launcher (<< 2.22), snapd-xdg-open (<= 0.0.0) Recommends: gnupg +Suggests: zenity | kdialog Conflicts: snap (<< 2013-11-29-1ubuntu1) Built-Using: ${Built-Using} ${misc:Built-Using} Description: Daemon and tooling that enable snap packages diff --git a/packaging/ubuntu-16.04/tests/integrationtests b/packaging/ubuntu-16.04/tests/integrationtests index 191ab592a3..f9065a25fe 100644 --- a/packaging/ubuntu-16.04/tests/integrationtests +++ b/packaging/ubuntu-16.04/tests/integrationtests @@ -36,11 +36,14 @@ elif apt -qq list snapd | grep -q -- -updates; then export SPREAD_CORE_CHANNEL=stable fi +# Spread will only buid with recent go +snap install --classic go + # and now run spread against localhost # shellcheck disable=SC1091 . /etc/os-release export GOPATH=/tmp/go -go get -u github.com/snapcore/spread/cmd/spread +/snap/bin/go get -u github.com/snapcore/spread/cmd/spread /tmp/go/bin/spread -v "autopkgtest:${ID}-${VERSION_ID}-$(dpkg --print-architecture)" # store journal info for inspectsion diff --git a/snap/info.go b/snap/info.go index f1ad1fc271..ca111cda91 100644 --- a/snap/info.go +++ b/snap/info.go @@ -464,27 +464,55 @@ type PlugInfo struct { Hooks map[string]*HookInfo } -func getAttribute(snapName string, ifaceName string, attrs map[string]interface{}, key string, val interface{}) error { - if v, ok := attrs[key]; ok { - rt := reflect.TypeOf(val) - if rt.Kind() != reflect.Ptr || val == nil { - return fmt.Errorf("internal error: cannot get %q attribute of interface %q with non-pointer value", key, ifaceName) +func lookupAttr(attrs map[string]interface{}, path string) (interface{}, bool) { + var v interface{} + comps := strings.FieldsFunc(path, func(r rune) bool { return r == '.' }) + if len(comps) == 0 { + return nil, false + } + v = attrs + for _, comp := range comps { + m, ok := v.(map[string]interface{}) + if !ok { + return nil, false } - - if reflect.TypeOf(v) != rt.Elem() { - return fmt.Errorf("snap %q has interface %q with invalid value type for %q attribute", snapName, ifaceName, key) + v, ok = m[comp] + if !ok { + return nil, false } - rv := reflect.ValueOf(val) - rv.Elem().Set(reflect.ValueOf(v)) - return nil } - return fmt.Errorf("snap %q does not have attribute %q for interface %q", snapName, key, ifaceName) + + return v, true +} + +func getAttribute(snapName string, ifaceName string, attrs map[string]interface{}, key string, val interface{}) error { + v, ok := lookupAttr(attrs, key) + if !ok { + return fmt.Errorf("snap %q does not have attribute %q for interface %q", snapName, key, ifaceName) + } + + rt := reflect.TypeOf(val) + if rt.Kind() != reflect.Ptr || val == nil { + return fmt.Errorf("internal error: cannot get %q attribute of interface %q with non-pointer value", key, ifaceName) + } + + if reflect.TypeOf(v) != rt.Elem() { + return fmt.Errorf("snap %q has interface %q with invalid value type for %q attribute", snapName, ifaceName, key) + } + rv := reflect.ValueOf(val) + rv.Elem().Set(reflect.ValueOf(v)) + + return nil } func (plug *PlugInfo) Attr(key string, val interface{}) error { return getAttribute(plug.Snap.Name(), plug.Interface, plug.Attrs, key, val) } +func (plug *PlugInfo) Lookup(key string) (interface{}, bool) { + return lookupAttr(plug.Attrs, key) +} + // SecurityTags returns security tags associated with a given plug. func (plug *PlugInfo) SecurityTags() []string { tags := make([]string, 0, len(plug.Apps)+len(plug.Hooks)) @@ -507,6 +535,10 @@ func (slot *SlotInfo) Attr(key string, val interface{}) error { return getAttribute(slot.Snap.Name(), slot.Interface, slot.Attrs, key, val) } +func (slot *SlotInfo) Lookup(key string) (interface{}, bool) { + return lookupAttr(slot.Attrs, key) +} + // SecurityTags returns security tags associated with a given slot. func (slot *SlotInfo) SecurityTags() []string { tags := make([]string, 0, len(slot.Apps)) diff --git a/snap/info_test.go b/snap/info_test.go index 20be43c82a..1cb686d8a5 100644 --- a/snap/info_test.go +++ b/snap/info_test.go @@ -934,6 +934,69 @@ func (s *infoSuite) TestSlotInfoAttr(c *C) { c.Check(slot.Attr("key", intVal), ErrorMatches, `internal error: cannot get "key" attribute of interface "interface" with non-pointer value`) } +func (s *infoSuite) TestDottedPathSlot(c *C) { + attrs := map[string]interface{}{ + "nested": map[string]interface{}{ + "foo": "bar", + }, + } + + slot := &snap.SlotInfo{Attrs: attrs} + c.Assert(slot, NotNil) + + v, ok := slot.Lookup("nested.foo") + c.Assert(ok, Equals, true) + c.Assert(v, Equals, "bar") + + v, ok = slot.Lookup("nested") + c.Assert(ok, Equals, true) + c.Assert(v, DeepEquals, map[string]interface{}{ + "foo": "bar", + }) + + _, ok = slot.Lookup("x") + c.Assert(ok, Equals, false) + + _, ok = slot.Lookup("..") + c.Assert(ok, Equals, false) + + _, ok = slot.Lookup("nested.foo.x") + c.Assert(ok, Equals, false) + + _, ok = slot.Lookup("nested.x") + c.Assert(ok, Equals, false) +} + +func (s *infoSuite) TestDottedPathPlug(c *C) { + attrs := map[string]interface{}{ + "nested": map[string]interface{}{ + "foo": "bar", + }, + } + + plug := &snap.PlugInfo{Attrs: attrs} + c.Assert(plug, NotNil) + + v, ok := plug.Lookup("nested") + c.Assert(ok, Equals, true) + c.Assert(v, DeepEquals, map[string]interface{}{ + "foo": "bar", + }) + + v, ok = plug.Lookup("nested.foo") + c.Assert(ok, Equals, true) + c.Assert(v, Equals, "bar") + + _, ok = plug.Lookup("x") + c.Assert(ok, Equals, false) + + _, ok = plug.Lookup("..") + c.Assert(ok, Equals, false) + + _, ok = plug.Lookup("nested.foo.x") + c.Assert(ok, Equals, false) +} + func (s *infoSuite) TestExpandSnapVariables(c *C) { dirs.SetRootDir("") info, err := snap.InfoFromSnapYaml([]byte(`name: foo`)) diff --git a/spread.yaml b/spread.yaml index d789805234..5cde2e1483 100644 --- a/spread.yaml +++ b/spread.yaml @@ -288,8 +288,11 @@ exclude: debug-each: | if [ "$SPREAD_DEBUG_EACH" = 1 ]; then + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + echo '# journal messages for snapd' - journalctl -u snapd + get_journalctl_log -u snapd echo '# apparmor denials ' dmesg --ctime | grep DENIED || true echo '# seccomp denials (kills) ' diff --git a/tests/lib/journalctl.sh b/tests/lib/journalctl.sh new file mode 100644 index 0000000000..ad54b4b215 --- /dev/null +++ b/tests/lib/journalctl.sh @@ -0,0 +1,28 @@ +#!/bin/sh + +JOURNALCTL_CURSOR_FILE="${SPREAD_PATH}"/journalctl_cursor + +get_last_journalctl_cursor(){ + journalctl --output=export -n1 | grep --binary-files=text -o '__CURSOR=.*' | sed -e 's/^__CURSOR=//' +} + +start_new_journalctl_log(){ + cursor=$(get_last_journalctl_cursor) + if [ -z "$cursor" ]; then + echo "Empty journalctl cursor, exiting..." + exit 1 + else + echo "$cursor" > "$JOURNALCTL_CURSOR_FILE" + fi +} + +get_journalctl_log(){ + cursor=$(cat "$JOURNALCTL_CURSOR_FILE") + get_journalctl_log_from_cursor "$cursor" "$@" +} + +get_journalctl_log_from_cursor(){ + cursor=$1 + shift + journalctl "$@" --cursor "$cursor" +} diff --git a/tests/lib/prepare-restore.sh b/tests/lib/prepare-restore.sh index 317596cc3a..82b7f8d4bc 100755 --- a/tests/lib/prepare-restore.sh +++ b/tests/lib/prepare-restore.sh @@ -28,6 +28,10 @@ set -o pipefail # shellcheck source=tests/lib/spread-funcs.sh . "$TESTSLIB/spread-funcs.sh" +# shellcheck source=tests/lib/journalctl.sh +. "$TESTSLIB/journalctl.sh" + + ### ### Utility functions reused below. ### @@ -369,34 +373,8 @@ prepare_project_each() { # We want to rotate the logs so that when inspecting or dumping them we # will just see logs since the test has started. - # Clear the systemd journal. Unfortunately the deputy-systemd on Ubuntu - # 14.04 does not know about --rotate or --vacuum-time so we need to remove - # the journal the hard way. - case "$SPREAD_SYSTEM" in - ubuntu-14.04-*) - # Force a log rotation with small size - sed -i.bak s/#SystemMaxUse=/SystemMaxUse=1K/g /etc/systemd/journald.conf - systemctl kill --kill-who=main --signal=SIGUSR2 systemd-journald.service - - # Restore the initial configuration and rotate logs - mv /etc/systemd/journald.conf.bak /etc/systemd/journald.conf - systemctl kill --kill-who=main --signal=SIGUSR2 systemd-journald.service - - # Remove rotated journal logs - systemctl stop systemd-journald.service - find /run/log/journal/ -name "*@*.journal" -delete - systemctl start systemd-journald.service - ;; - *) - # per journalctl's implementation, --rotate and --sync 'override' - # each other if used in a single command, with the one appearing - # later being effective - journalctl --sync - journalctl --rotate - sleep .1 - journalctl --vacuum-time=1ms - ;; - esac + # Reset systemd journal cursor. + start_new_journalctl_log # Clear the kernel ring buffer. dmesg -c > /dev/null diff --git a/tests/lib/store.sh b/tests/lib/store.sh index 84db342d44..178f88203d 100644 --- a/tests/lib/store.sh +++ b/tests/lib/store.sh @@ -5,6 +5,9 @@ STORE_CONFIG=/etc/systemd/system/snapd.service.d/store.conf # shellcheck source=tests/lib/systemd.sh . "$TESTSLIB/systemd.sh" +# shellcheck source=tests/lib/journalctl.sh +. "$TESTSLIB/journalctl.sh" + _configure_store_backends(){ systemctl stop snapd.service snapd.socket mkdir -p "$(dirname $STORE_CONFIG)" @@ -76,7 +79,7 @@ setup_fake_store(){ echo "fakestore service not started properly" netstat -ntlp | grep "127.0.0.1:11028" || true - journalctl -u fakestore || true + get_journalctl_log -u fakestore || true systemctl status fakestore || true exit 1 } diff --git a/tests/main/catalog-update/task.yaml b/tests/main/catalog-update/task.yaml index 590d1e7a28..425fbffa8a 100644 --- a/tests/main/catalog-update/task.yaml +++ b/tests/main/catalog-update/task.yaml @@ -1,17 +1,20 @@ summary: Ensure catalog update works execute: | + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + echo "Ensure that catalog refresh happens on startup" for _ in $(seq 60); do - if journalctl -u snapd | MATCH "Catalog refresh"; then + if get_journalctl_log -u snapd | MATCH "Catalog refresh"; then break fi sleep 1 done - journalctl -u snapd | MATCH "Catalog refresh" + get_journalctl_log -u snapd | MATCH "Catalog refresh" echo "Ensure that we don't log all catalog body data" - if journalctl -u snapd | MATCH "Tools for testing the snapd application"; then + if get_journalctl_log -u snapd | MATCH "Tools for testing the snapd application"; then echo "Catalog update is doing verbose http logging (it should not)." exit 1 fi diff --git a/tests/main/debug-sandbox/task.yaml b/tests/main/debug-sandbox/task.yaml new file mode 100644 index 0000000000..e0a84e2f42 --- /dev/null +++ b/tests/main/debug-sandbox/task.yaml @@ -0,0 +1,25 @@ +summary: Verify sandbox is correctly reported + +execute: | + case "$SPREAD_SYSTEM" in + ubuntu-*) + snap debug sandbox-features | MATCH "apparmor: .+" + ;; + fedora-*|debian-*|opensuse-*) + # Fedora because it uses SELinux + # Debian and openSUSE because partial apparmor is not enabled + snap debug sandbox-features | MATCH -v "apparmor: .+" + ;; + esac + snap debug sandbox-features | MATCH "dbus: .+" + snap debug sandbox-features | MATCH "kmod: .+" + snap debug sandbox-features | MATCH "mount: .+" + snap debug sandbox-features | MATCH "seccomp: .+" + snap debug sandbox-features | MATCH "udev: .+" + + # The command can be used as script helper + snap debug sandbox-features --required kmod:mediated-modprobe + ! snap debug sandbox-features --required magic:evil-bit + + # Multiple requirements may be listed + snap debug sandbox-features --required kmod:mediated-modprobe --required mount:stale-base-invalidation diff --git a/tests/main/enable-disable-units-gpio/task.yaml b/tests/main/enable-disable-units-gpio/task.yaml index 9de48cf966..ccf27fce37 100644 --- a/tests/main/enable-disable-units-gpio/task.yaml +++ b/tests/main/enable-disable-units-gpio/task.yaml @@ -69,6 +69,9 @@ restore: | rm -rf $GPIO_MOCK_DIR /etc/systemd/system/gpio-mock.service /home/test/gpio-mock.sh execute: | + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + if [ "$TRUST_TEST_KEYS" = "false" ]; then echo "This test needs test keys to be trusted" exit @@ -76,7 +79,7 @@ execute: | echo "Then the snap service units concerning the gpio device must be run before and after a reboot" expected="Unit snap.core.interface.gpio-100.service has finished starting up" - journalctl -xe --no-pager | MATCH "$expected" + get_journalctl_log -xe --no-pager | MATCH "$expected" if [ "$SPREAD_REBOOT" = "1" ]; then cat $GPIO_MOCK_DIR/export | MATCH "^100$" diff --git a/tests/main/install-cache/task.yaml b/tests/main/install-cache/task.yaml index fabece8398..3c80d4d986 100644 --- a/tests/main/install-cache/task.yaml +++ b/tests/main/install-cache/task.yaml @@ -1,13 +1,16 @@ summary: Check that download caching works execute: | + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + snap install test-snapd-tools snap remove test-snapd-tools snap install test-snapd-tools for i in $(seq 10); do - if journalctl -u snapd | MATCH "using cache for .*/test-snapd-tools.*\.snap"; then + if get_journalctl_log -u snapd | MATCH "using cache for .*/test-snapd-tools.*\.snap"; then break fi sleep 1 done - journalctl -u snapd | MATCH "using cache for .*/test-snapd-tools.*\.snap" + get_journalctl_log -u snapd | MATCH "using cache for .*/test-snapd-tools.*\.snap" diff --git a/tests/main/install-closed-channel/task.yaml b/tests/main/install-closed-channel/task.yaml new file mode 100644 index 0000000000..7b729ee422 --- /dev/null +++ b/tests/main/install-closed-channel/task.yaml @@ -0,0 +1,9 @@ +summary: Check that installing a snap from a closed channel DTRT + +details: | + When a snap is installed from a closed channel, we're supposed to + "forward" to the next available risk level. + +execute: | + snap install --beta test-snapd-tools | MATCH '^Channel beta for test-snapd-tools is closed; temporarily forwarding to stable\.$' + snap info test-snapd-tools | MATCH 'tracking:.*beta' diff --git a/tests/main/interfaces-cups-control/task.yaml b/tests/main/interfaces-cups-control/task.yaml index f52be8e98a..1f1c2a9a31 100644 --- a/tests/main/interfaces-cups-control/task.yaml +++ b/tests/main/interfaces-cups-control/task.yaml @@ -42,8 +42,11 @@ restore: | rm -rf $HOME/PDF $TEST_FILE print.error debug: | + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + systemctl status cups || true - journalctl -u cpus || true + get_journalctl_log -u cpus || true execute: | echo "Then the plug is disconnected by default" diff --git a/tests/main/interfaces-desktop-document-portal/task.yaml b/tests/main/interfaces-desktop-document-portal/task.yaml new file mode 100644 index 0000000000..b63e76a083 --- /dev/null +++ b/tests/main/interfaces-desktop-document-portal/task.yaml @@ -0,0 +1,60 @@ +summary: The document portal is mounted by snaps using the desktop interface +details: | + The document portal is a component of xdg-desktop-portal that + provides a way to share files with a confined application in a + controlled fashion. To provide proper security, a subtree of the + portal needs to be mounted over $XDG_RUNTIME_DIR/doc inside the + sandbox. +systems: [-ubuntu-core-*] +prepare: | + snap try "$TESTSLIB"/snaps/test-snapd-desktop + snap disconnect test-snapd-desktop:desktop +restore: | + rm -rf /run/user/12345/* + rm -f /tmp/check-doc-portal.sh +execute: | + #shellcheck source=tests/lib/dirs.sh + . $TESTSLIB/dirs.sh + + echo "Create XDG_RUNTIME_DIR for test user" + USER_RUNTIME_DIR=/run/user/12345 + mkdir -p $USER_RUNTIME_DIR || true + #shellcheck disable=SC2115 + rm -rf $USER_RUNTIME_DIR/* + chmod u=rwX,go= $USER_RUNTIME_DIR + chown test:test $USER_RUNTIME_DIR + + cat << EOF > /tmp/check-doc-portal.sh + set -eu + mkdir -p /run/user/12345/doc/by-app/snap.test-snapd-desktop + touch /run/user/12345/doc/is-unconfined + touch /run/user/12345/doc/by-app/snap.test-snapd-desktop/is-confined + test-snapd-desktop.check-dirs /run/user/12345/doc + EOF + + if [ "$(snap debug confinement)" = strict ]; then + echo "Without desktop interface connected" + if su -l -c "sh /tmp/check-doc-portal.sh" test 2> check.error; then + cat check.error >&2 + echo "Expected permission error when checking document portal dir" + exit 1 + fi + MATCH "Permission denied" < check.error + fi + + "$LIBEXECDIR"/snapd/snap-discard-ns test-snapd-desktop + + echo "With desktop connected, we see confined version" + snap connect test-snapd-desktop:desktop + su -l -c "sh /tmp/check-doc-portal.sh" test | MATCH is-confined + + "$LIBEXECDIR"/snapd/snap-discard-ns test-snapd-desktop + + echo "It is not an error if the document portal is missing" + rm -rf /run/user/12345/* + if su -l -c "test-snapd-desktop.check-dirs /run/user/12345/doc" test 2> check.error; then + cat check.error >&2 + echo "Expected not found error when checking document portal dir" + exit 1 + fi + MATCH "No such file or directory" < check.error diff --git a/tests/main/interfaces-many/task.yaml b/tests/main/interfaces-many/task.yaml index e8dc71cbfb..328e5ec374 100644 --- a/tests/main/interfaces-many/task.yaml +++ b/tests/main/interfaces-many/task.yaml @@ -4,8 +4,11 @@ summary: Ensure that commands run when their interfaces are connected priority: 100 debug: | + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + # get the full journal to see any out-of-memory errors - journalctl + get_journalctl_log details: | Install a test snap that plugs as many interfaces as is possible and diff --git a/tests/main/interfaces-snapd-control-with-manage/task.yaml b/tests/main/interfaces-snapd-control-with-manage/task.yaml index e4f0721b84..3046318eb2 100644 --- a/tests/main/interfaces-snapd-control-with-manage/task.yaml +++ b/tests/main/interfaces-snapd-control-with-manage/task.yaml @@ -54,6 +54,9 @@ debug: | jq .data.auth.device /var/lib/snapd/state.json execute: | + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + if [ "$TRUST_TEST_KEYS" = "false" ]; then echo "This test needs test keys to be trusted" exit @@ -72,7 +75,7 @@ execute: | echo "Then the core refresh.schedule can be set to 'managed'" snap set core refresh.schedule=managed - if journalctl -u snapd |grep 'cannot parse "managed"'; then + if get_journalctl_log -u snapd |grep 'cannot parse "managed"'; then echo "refresh.schedule=managed was not rejected as it should be" exit 1 fi diff --git a/tests/main/lxd/task.yaml b/tests/main/lxd/task.yaml index e5c61e3167..285e9ee445 100644 --- a/tests/main/lxd/task.yaml +++ b/tests/main/lxd/task.yaml @@ -23,8 +23,11 @@ restore: | lxd.lxc delete my-ubuntu debug: | + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + # debug output from lxd - journalctl -u snap.lxd.daemon.service + get_journalctl_log -u snap.lxd.daemon.service execute: | if [[ $(ls -1 "$GOHOME"/snapd_*.deb | wc -l || echo 0) -eq 0 ]]; then diff --git a/tests/main/refresh-delta-from-core/task.yaml b/tests/main/refresh-delta-from-core/task.yaml index c80c6eb073..28f7192bb5 100644 --- a/tests/main/refresh-delta-from-core/task.yaml +++ b/tests/main/refresh-delta-from-core/task.yaml @@ -21,7 +21,10 @@ restore: | fi execute: | + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + echo "When the snap is refreshed" snap refresh --beta $SNAP_NAME echo "Then deltas are successfully applied" - journalctl -u snapd | MATCH "Successfully applied delta" + get_journalctl_log -u snapd | MATCH "Successfully applied delta" diff --git a/tests/main/refresh-delta/task.yaml b/tests/main/refresh-delta/task.yaml index 7632a8339d..a48c9ab7b4 100644 --- a/tests/main/refresh-delta/task.yaml +++ b/tests/main/refresh-delta/task.yaml @@ -19,8 +19,11 @@ prepare: | snap install --edge $SNAP_NAME execute: | + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + echo "When the snap is refreshed" snap refresh --beta $SNAP_NAME echo "Then deltas are successfully applied" - journalctl -u snapd | MATCH "Successfully applied delta" + get_journalctl_log -u snapd | MATCH "Successfully applied delta" diff --git a/tests/main/refresh-undo/task.yaml b/tests/main/refresh-undo/task.yaml index 81d8009291..af8f156f14 100644 --- a/tests/main/refresh-undo/task.yaml +++ b/tests/main/refresh-undo/task.yaml @@ -19,7 +19,9 @@ prepare: | snap pack $TESTSLIB/snaps/$SNAP_NAME_BAD debug: | - journalctl -u snap.test-snapd-service.service.service + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB"/journalctl.sh + get_journalctl_log -u snap.test-snapd-service.service.service execute: | wait_for_service_status() { diff --git a/tests/main/snap-confine-from-core/task.yaml b/tests/main/snap-confine-from-core/task.yaml index cec9e75852..c96015d3e4 100644 --- a/tests/main/snap-confine-from-core/task.yaml +++ b/tests/main/snap-confine-from-core/task.yaml @@ -14,6 +14,9 @@ restore: | chmod 4755 /usr/lib/snapd/snap-confine execute: | + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + if [ "${SNAP_REEXEC:-}" = "0" ]; then echo "skipping test when SNAP_REEXEC is disabled" exit 0 @@ -21,7 +24,7 @@ execute: | echo "Ensure we re-exec by default" snap list - journalctl | MATCH "DEBUG: restarting into" + get_journalctl_log | MATCH "DEBUG: restarting into" echo "Ensure snap-confine from the core snap is run" # do not use "strace -f" for unknown reasons that hangs diff --git a/tests/main/snap-core-fixup/task.yaml b/tests/main/snap-core-fixup/task.yaml index 2e2264705b..62ce93668f 100644 --- a/tests/main/snap-core-fixup/task.yaml +++ b/tests/main/snap-core-fixup/task.yaml @@ -28,7 +28,7 @@ execute: | echo "Now test with the real corrupted image" unxz -c -d test.img.xz >test.img mount -t vfat test.img /boot/uboot - n=$(ls /boot/uboot | grep ^uboot.env$ | wc -l) + n=$(ls /boot/uboot | grep -c ^uboot.env$) if [ "$n" != "2" ]; then echo "Image not broken in the right way, expected two uboot.env files" ls /boot/uboot @@ -38,7 +38,7 @@ execute: | echo "Trigger cleanup" systemctl restart snapd.core-fixup.service - n=$(ls /boot/uboot | grep ^uboot.env$ | wc -l) + n=$(ls /boot/uboot | grep -c ^uboot.env$) if [ "$n" != "1" ]; then echo "Image not repaired" ls /boot/uboot diff --git a/tests/main/snap-service-refresh-mode/task.yaml b/tests/main/snap-service-refresh-mode/task.yaml index f71d1e6543..b742c08ba4 100644 --- a/tests/main/snap-service-refresh-mode/task.yaml +++ b/tests/main/snap-service-refresh-mode/task.yaml @@ -1,14 +1,18 @@ summary: "Check that refresh-modes works" -kill-timeout: 3m +kill-timeout: 5m debug: | grep -n '' *.pid || true systemctl status snap.test-snapd-service.test-snapd-endure-service || true execute: | + # shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB/snaps.sh" + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + echo "When the service snap is installed" - . $TESTSLIB/snaps.sh install_local test-snapd-service echo "We can see it running" @@ -25,7 +29,7 @@ execute: | echo "Once the snap is removed, the service is stopped" snap remove test-snapd-service - journalctl | MATCH "stop endure" + get_journalctl_log | MATCH "stop endure" restore: rm -f *.pid || true diff --git a/tests/main/snap-service-stop-mode-sigkill/task.yaml b/tests/main/snap-service-stop-mode-sigkill/task.yaml index 0d0bd4619c..37f6fdd097 100644 --- a/tests/main/snap-service-stop-mode-sigkill/task.yaml +++ b/tests/main/snap-service-stop-mode-sigkill/task.yaml @@ -1,6 +1,6 @@ summary: "Check that refresh-modes sigkill works" -kill-timeout: 3m +kill-timeout: 5m restore: | # remove to ensure all services are stopped diff --git a/tests/main/snap-service-stop-mode/task.yaml b/tests/main/snap-service-stop-mode/task.yaml index ab1f72043c..7d17de66ba 100644 --- a/tests/main/snap-service-stop-mode/task.yaml +++ b/tests/main/snap-service-stop-mode/task.yaml @@ -1,6 +1,6 @@ summary: "Check that stop-modes works" -kill-timeout: 3m +kill-timeout: 5m # journald in ubuntu-14.04 not reliable systems: [-ubuntu-14.04-*] @@ -12,8 +12,12 @@ debug: | done execute: | + # shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB/snaps.sh" + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + echo "When the service snap is installed" - . $TESTSLIB/snaps.sh install_local test-snapd-service echo "We can see it running" @@ -36,11 +40,11 @@ execute: | echo "and it got the right signal" echo "checking that the right signal was sent" sleep 1 - journalctl -u snap.test-snapd-service.test-snapd-${s}-service | MATCH "got ${s%%-all}" + get_journalctl_log -u snap.test-snapd-service.test-snapd-${s}-service | MATCH "got ${s%%-all}" done echo "Regular services are restarted normally" - journalctl -u snap.test-snapd-service.test-snapd-service | MATCH "stop service" + get_journalctl_log -u snap.test-snapd-service.test-snapd-service | MATCH "stop service" systemctl show -p MainPID snap.test-snapd-service.test-snapd-service > new-main.pid test -e new-main.pid && test -e old-main.pid test "$(cat new-main.pid)" != "$(cat old-main.pid)" @@ -48,7 +52,7 @@ execute: | echo "Once the snap is removed, all services are stopped" snap remove test-snapd-service for s in $stop_modes; do - journalctl | MATCH "stop ${s}" + get_journalctl_log | MATCH "stop ${s}" done restore: | diff --git a/tests/main/snap-userd-desktop-app-autostart/task.yaml b/tests/main/snap-userd-desktop-app-autostart/task.yaml index 8df99f3da3..42ff2b8414 100644 --- a/tests/main/snap-userd-desktop-app-autostart/task.yaml +++ b/tests/main/snap-userd-desktop-app-autostart/task.yaml @@ -1,8 +1,12 @@ summary: Check that snap userd can autostart user session applications execute: | + # shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB/snaps.sh" + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + echo "When the snap is installed" - . $TESTSLIB/snaps.sh install_local test-snapd-xdg-autostart # run the app directly, it will dump a *.desktop file @@ -14,10 +18,7 @@ execute: | echo "Applications can be automatically started by snap userd --autostart" test ! -e ~/snap/test-xdg-snap-autostart/current/foo-autostarted - if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then - # 14.04 journalctl does not have cursor support - cursor=$(journalctl --show-cursor -n 0 |grep cursor | cut -f3 -d' ') - fi + cursor=$(get_last_journalctl_cursor) snap userd --autostart > autostart.log 2>&1 # when app is autostarted it dumps a file at $SNAP_USER_DATA/foo-autostarted, @@ -28,14 +29,7 @@ execute: | done test -e ~/snap/test-snapd-xdg-autostart/current/foo-autostarted - if [[ "$SPREAD_SYSTEM" != ubuntu-14.04-* ]]; then - journalctl --flush - test "$(journalctl -t foo.desktop --after-cursor=$cursor | wc -l)" -eq 1 - else - # 14.04 journalctl does not have cursor, neither --identifier support, - # nor --flush - test "$(journalctl | grep foo.desktop | wc -l)" -eq 1 - fi + test "$(get_journalctl_log_from_cursor "$cursor" | grep foo.desktop | wc -l)" -gt 0 restore: | rm -f ~/snap/test-snapd-xdg-autostart/current/foo-autostarted diff --git a/tests/main/snapctl-services/task.yaml b/tests/main/snapctl-services/task.yaml index 3c0df175d4..259d81f3a6 100644 --- a/tests/main/snapctl-services/task.yaml +++ b/tests/main/snapctl-services/task.yaml @@ -33,8 +33,12 @@ execute: | done } + # shellcheck source=tests/lib/snaps.sh + . "$TESTSLIB/snaps.sh" + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + echo "When the service snap is installed" - . $TESTSLIB/snaps.sh install_local test-snapd-service echo "We can see it running" @@ -73,7 +77,7 @@ execute: | echo "Reinstalling the snap with configure hook calling snapctl restart works" snap set test-snapd-service command=restart install_local test-snapd-service - if journalctl|grep -q "error running snapctl"; then + if get_journalctl_log | MATCH "error running snapctl"; then echo "snapctl should not report errors" exit 1 fi diff --git a/tests/main/snapd-notify/task.yaml b/tests/main/snapd-notify/task.yaml index 46299aa496..3768b0acb4 100644 --- a/tests/main/snapd-notify/task.yaml +++ b/tests/main/snapd-notify/task.yaml @@ -5,9 +5,12 @@ summary: Ensure snapd notify feature is working backends: [-external] execute: | + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + for _ in $(seq 5); do if systemctl status snapd.service | MATCH "Active: active"; then - journalctl -u snapd | MATCH "activation done in" + get_journalctl_log -u snapd | MATCH "activation done in" exit fi sleep 1 diff --git a/tests/main/snapd-reexec/task.yaml b/tests/main/snapd-reexec/task.yaml index b1b80c5d9d..c525a37053 100644 --- a/tests/main/snapd-reexec/task.yaml +++ b/tests/main/snapd-reexec/task.yaml @@ -29,7 +29,10 @@ execute: | echo "Ensure we re-exec by default" /usr/bin/env SNAPD_DEBUG=1 snap list 2>&1 | MATCH "DEBUG: restarting into" - . $TESTSLIB/dirs.sh + # shellcheck source=tests/lib/dirs.sh + . "$TESTSLIB/dirs.sh" + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" echo "Ensure that we do not re-exec into older versions" systemctl stop snapd.service snapd.socket @@ -38,7 +41,7 @@ execute: | mount --bind /tmp/old-info $SNAP_MOUNT_DIR/core/current/usr/lib/snapd/info systemctl start snapd.service snapd.socket snap list - journalctl | MATCH "core snap \(at .*\) is older \(.*\) than distribution package" + get_journalctl_log | MATCH "core snap \(at .*\) is older \(.*\) than distribution package" echo "Revert back to normal" systemctl stop snapd.service snapd.socket diff --git a/tests/main/ubuntu-core-gadget-config-defaults/task.yaml b/tests/main/ubuntu-core-gadget-config-defaults/task.yaml index 4a3201fbf8..80fd1c6d21 100644 --- a/tests/main/ubuntu-core-gadget-config-defaults/task.yaml +++ b/tests/main/ubuntu-core-gadget-config-defaults/task.yaml @@ -27,6 +27,10 @@ prepare: | ${TEST_SNAP_ID}: a: A b: B + system: + service: + rsyslog: + disable: true EOF mksquashfs squashfs-root pc_x1.snap -comp xz -no-fragments rm -rf squashfs-root @@ -47,6 +51,11 @@ restore: | echo "This test needs test keys to be trusted" exit fi + echo "Undo the rsyslog disable" + systemctl unmask rsyslog.service || true + systemctl enable rsyslog.service || true + systemctl start rsyslog.service || true + . $TESTSLIB/systemd.sh systemctl stop snapd.service snapd.socket rm -rf /var/lib/snapd/assertions/* @@ -100,3 +109,8 @@ execute: | echo "The configuration defaults from the gadget where applied" snap get test-snapd-with-configure a|MATCH "^A$" snap get test-snapd-with-configure b|MATCH "^B$" + + echo "The configuration for core is applied" + snap get core service.rsyslog.disable|MATCH true + echo "And the service is masked" + systemctl status rsyslog.service|MATCH masked diff --git a/tests/main/user-mounts/check-user-mount.sh b/tests/main/user-mounts/check-user-mount.sh deleted file mode 100755 index 9281579014..0000000000 --- a/tests/main/user-mounts/check-user-mount.sh +++ /dev/null @@ -1,16 +0,0 @@ -#!/bin/sh - -set -eux - -export XDG_RUNTIME_DIR="$1" - -# Add some content to the runtime dir -rm -rf $XDG_RUNTIME_DIR/snap.test-snapd-sh -mkdir $XDG_RUNTIME_DIR/snap.test-snapd-sh -mkdir $XDG_RUNTIME_DIR/snap.test-snapd-sh/source -mkdir $XDG_RUNTIME_DIR/snap.test-snapd-sh/target -touch $XDG_RUNTIME_DIR/snap.test-snapd-sh/source/in-source -touch $XDG_RUNTIME_DIR/snap.test-snapd-sh/target/in-target - -# Check target directory from sandbox -test-snapd-sh -c 'ls $XDG_RUNTIME_DIR/target' diff --git a/tests/main/user-mounts/task.yaml b/tests/main/user-mounts/task.yaml index 190d9e994c..5b47a852ff 100644 --- a/tests/main/user-mounts/task.yaml +++ b/tests/main/user-mounts/task.yaml @@ -4,29 +4,45 @@ details: | be performed prior to running the application. These mounts are private to the invocation of the application, so can be used to manipulate how per-user data is presented. +# This test makes use of the user mount provided by the desktop +# interface. When we have an interface providing user mounts that is +# available on core, we can switch to that. +systems: [-ubuntu-core-*] prepare: | - . $TESTSLIB/snaps.sh - install_local test-snapd-sh - rm -f /var/lib/snapd/mount/snap.test-snapd-sh.user-fstab + snap try "$TESTSLIB"/snaps/test-snapd-desktop + snap disconnect test-snapd-desktop:desktop +restore: | + rm -rf /run/user/12345/* + rm -f /tmp/check-user-mount.sh execute: | + #shellcheck source=tests/lib/dirs.sh . $TESTSLIB/dirs.sh echo "Create XDG_RUNTIME_DIR for test user" USER_RUNTIME_DIR=/run/user/12345 mkdir -p $USER_RUNTIME_DIR || true + #shellcheck disable=SC2115 rm -rf $USER_RUNTIME_DIR/* chmod u=rwX,go= $USER_RUNTIME_DIR chown test:test $USER_RUNTIME_DIR - echo "Without user-fstab we see target" - su -l -c "$(pwd)/check-user-mount.sh $USER_RUNTIME_DIR" test | MATCH in-target + echo "Without desktop interface connected, there is no user-fstab file" + [ ! -f /var/lib/snapd/mount/snap.test-snapd-desktop.user-fstab ] - echo "Configuring user-fstab" - cat << \EOF > /var/lib/snapd/mount/snap.test-snapd-sh.user-fstab - $XDG_RUNTIME_DIR/source $XDG_RUNTIME_DIR/target none bind,rw 0 0 + echo "With desktop interface connected, it is created" + snap connect test-snapd-desktop:desktop + [ -f /var/lib/snapd/mount/snap.test-snapd-desktop.user-fstab ] + diff -u /var/lib/snapd/mount/snap.test-snapd-desktop.user-fstab - << \EOF + $XDG_RUNTIME_DIR/doc/by-app/snap.test-snapd-desktop $XDG_RUNTIME_DIR/doc none bind,rw,x-snapd.ignore-missing 0 0 EOF - $LIBEXECDIR/snapd/snap-discard-ns test-snapd-sh + "$LIBEXECDIR"/snapd/snap-discard-ns test-snapd-sh - echo "With user-fstab we see source" - su -l -c "$(pwd)/check-user-mount.sh $USER_RUNTIME_DIR" test | MATCH in-source + echo "The user-fstab file is used to prepare the confinement sandbox" + cat << \EOF > /tmp/check-user-mount.sh + mkdir -p /run/user/12345/doc/by-app/snap.test-snapd-desktop + touch /run/user/12345/doc/by-app/snap.test-snapd-desktop/in-source + touch /run/user/12345/doc/in-target + test-snapd-desktop.check-dirs /run/user/12345/doc + EOF + su -l -c "sh /tmp/check-user-mount.sh" test | MATCH in-source diff --git a/tests/main/validate-container-failures/task.yaml b/tests/main/validate-container-failures/task.yaml index a5a015be6f..ab1d0d7673 100644 --- a/tests/main/validate-container-failures/task.yaml +++ b/tests/main/validate-container-failures/task.yaml @@ -15,13 +15,16 @@ prepare: | chmod 0644 "$p/meta/hooks/what" execute: | + # shellcheck source=tests/lib/journalctl.sh + . "$TESTSLIB/journalctl.sh" + echo "Snap refuses to install" snap try $TESTSLIB/snaps/$SNAP 2>error.log && exit 1 || true echo "The error tells you to ask the dev" tr -s "\n " " " < error.log | MATCH 'contact developer' echo "And the journal counts the ways" - journalctl -u snapd > journal.log + get_journalctl_log -u snapd > journal.log MATCH '"comp.sh" should be world-readable' < journal.log MATCH '"bin/bar" should be executable' < journal.log MATCH '"bin/foo" should be world-readable and executable' < journal.log diff --git a/tests/unit/spread-shellcheck/must b/tests/unit/spread-shellcheck/must index e28e40de74..151c58fb14 100644 --- a/tests/unit/spread-shellcheck/must +++ b/tests/unit/spread-shellcheck/must @@ -21,3 +21,4 @@ tests/main/classic-ubuntu-core-transition-two-cores/task.yaml tests/main/classic-ubuntu-core-transition/task.yaml tests/main/completion/task.yaml tests/main/config-versions/task.yaml +tests/main/install-closed-channel/task.yaml diff --git a/testutil/lowlevel.go b/testutil/lowlevel.go index 0356d24a40..73e3fc85e7 100644 --- a/testutil/lowlevel.go +++ b/testutil/lowlevel.go @@ -158,6 +158,16 @@ func formatUnmountFlags(flags int) string { return strings.Join(fl, "|") } +// CallResultError describes a system call and the corresponding result or error. +// +// The field names stand for Call, Result and Error respectively. They are +// abbreviated due to the nature of their use (in large quantity). +type CallResultError struct { + C string + R interface{} + E error +} + // SyscallRecorder stores which system calls were invoked. // // The recorder supports a small set of features useful for testing: injecting @@ -165,7 +175,7 @@ func formatUnmountFlags(flags int) string { // verification of file descriptors. type SyscallRecorder struct { // History of all the system calls made. - calls []string + rcalls []CallResultError // Error function for a given system call. errors map[string]func() error // pre-arranged result of lstat, fstat and readdir calls. @@ -218,16 +228,35 @@ func (sys *SyscallRecorder) InsertFaultFunc(call string, fn func() error) { // Calls returns the sequence of mocked calls that have been made. func (sys *SyscallRecorder) Calls() []string { - return sys.calls + if len(sys.rcalls) == 0 { + return nil + } + calls := make([]string, 0, len(sys.rcalls)) + for _, rc := range sys.rcalls { + calls = append(calls, rc.C) + } + return calls } -// call remembers that a given call has occurred and returns a pre-arranged error, if any -func (sys *SyscallRecorder) call(call string) error { - sys.calls = append(sys.calls, call) - if fn := sys.errors[call]; fn != nil { - return fn() +// RCalls returns the sequence of mocked calls that have been made along with their results. +func (sys *SyscallRecorder) RCalls() []CallResultError { + return sys.rcalls +} + +// rcall remembers that a given call has occurred and returns a pre-arranged error or value, if any +func (sys *SyscallRecorder) rcall(call string, resultFn func(call string) (interface{}, error)) (val interface{}, err error) { + if errorFn := sys.errors[call]; errorFn != nil { + err = errorFn() } - return nil + if err == nil && resultFn != nil { + val, err = resultFn(call) + } + if err != nil { + sys.rcalls = append(sys.rcalls, CallResultError{C: call, E: err}) + } else { + sys.rcalls = append(sys.rcalls, CallResultError{C: call, R: val}) + } + return val, err } // allocFd assigns a file descriptor to a given operation. @@ -271,61 +300,75 @@ func (sys *SyscallRecorder) CheckForStrayDescriptors(c *check.C) { // Open is a fake implementation of syscall.Open func (sys *SyscallRecorder) Open(path string, flags int, mode uint32) (int, error) { call := fmt.Sprintf("open %q %s %#o", path, formatOpenFlags(flags), mode) - if err := sys.call(call); err != nil { + fd, err := sys.rcall(call, func(call string) (interface{}, error) { + return sys.allocFd(call), nil + }) + if err != nil { return -1, err } - return sys.allocFd(call), nil + return fd.(int), nil } // Openat is a fake implementation of syscall.Openat func (sys *SyscallRecorder) Openat(dirfd int, path string, flags int, mode uint32) (int, error) { call := fmt.Sprintf("openat %d %q %s %#o", dirfd, path, formatOpenFlags(flags), mode) - if _, ok := sys.fds[dirfd]; !ok { - sys.calls = append(sys.calls, call) - return -1, fmt.Errorf("attempting to openat with an invalid file descriptor %d", dirfd) - } - if err := sys.call(call); err != nil { + fd, err := sys.rcall(call, func(call string) (interface{}, error) { + if _, ok := sys.fds[dirfd]; !ok { + return -1, fmt.Errorf("attempting to openat with an invalid file descriptor %d", dirfd) + } + return sys.allocFd(call), nil + }) + if err != nil { return -1, err } - return sys.allocFd(call), nil + return fd.(int), nil } // Close is a fake implementation of syscall.Close func (sys *SyscallRecorder) Close(fd int) error { - if err := sys.call(fmt.Sprintf("close %d", fd)); err != nil { - return err - } - return sys.freeFd(fd) + call := fmt.Sprintf("close %d", fd) + _, err := sys.rcall(call, func(call string) (interface{}, error) { + return nil, sys.freeFd(fd) + }) + return err } // Fchown is a fake implementation of syscall.Fchown func (sys *SyscallRecorder) Fchown(fd int, uid sys.UserID, gid sys.GroupID) error { call := fmt.Sprintf("fchown %d %d %d", fd, uid, gid) - if _, ok := sys.fds[fd]; !ok { - sys.calls = append(sys.calls, call) - return fmt.Errorf("attempting to fchown an invalid file descriptor %d", fd) - } - return sys.call(call) + _, err := sys.rcall(call, func(call string) (interface{}, error) { + if _, ok := sys.fds[fd]; !ok { + return nil, fmt.Errorf("attempting to fchown an invalid file descriptor %d", fd) + } + return nil, nil + }) + return err } // Mkdirat is a fake implementation of syscall.Mkdirat func (sys *SyscallRecorder) Mkdirat(dirfd int, path string, mode uint32) error { call := fmt.Sprintf("mkdirat %d %q %#o", dirfd, path, mode) - if _, ok := sys.fds[dirfd]; !ok { - sys.calls = append(sys.calls, call) - return fmt.Errorf("attempting to mkdirat with an invalid file descriptor %d", dirfd) - } - return sys.call(call) + _, err := sys.rcall(call, func(call string) (interface{}, error) { + if _, ok := sys.fds[dirfd]; !ok { + return nil, fmt.Errorf("attempting to mkdirat with an invalid file descriptor %d", dirfd) + } + return nil, nil + }) + return err } // Mount is a fake implementation of syscall.Mount -func (sys *SyscallRecorder) Mount(source string, target string, fstype string, flags uintptr, data string) (err error) { - return sys.call(fmt.Sprintf("mount %q %q %q %s %q", source, target, fstype, formatMountFlags(int(flags)), data)) +func (sys *SyscallRecorder) Mount(source string, target string, fstype string, flags uintptr, data string) error { + call := fmt.Sprintf("mount %q %q %q %s %q", source, target, fstype, formatMountFlags(int(flags)), data) + _, err := sys.rcall(call, nil) + return err } // Unmount is a fake implementation of syscall.Unmount -func (sys *SyscallRecorder) Unmount(target string, flags int) (err error) { - return sys.call(fmt.Sprintf("unmount %q %s", target, formatUnmountFlags(flags))) +func (sys *SyscallRecorder) Unmount(target string, flags int) error { + call := fmt.Sprintf("unmount %q %s", target, formatUnmountFlags(flags)) + _, err := sys.rcall(call, nil) + return err } // InsertOsLstatResult makes given subsequent call to OsLstat return the specified fake file info. @@ -348,27 +391,32 @@ func (sys *SyscallRecorder) InsertSysLstatResult(call string, sb syscall.Stat_t) func (sys *SyscallRecorder) OsLstat(name string) (os.FileInfo, error) { // NOTE the syscall.Lstat uses a different signature `lstat %q <ptr>`. call := fmt.Sprintf("lstat %q", name) - if err := sys.call(call); err != nil { + val, err := sys.rcall(call, func(call string) (interface{}, error) { + if fi, ok := sys.osLstats[call]; ok { + return fi, nil + } + panic(fmt.Sprintf("one of InsertOsLstatResult() or InsertFault() for %s must be used", call)) + }) + if err != nil { return nil, err } - if fi, ok := sys.osLstats[call]; ok { - return fi, nil - } - panic(fmt.Sprintf("one of InsertOsLstatResult() or InsertFault() for %s must be used", call)) + return val.(os.FileInfo), err } // SysLstat is a fake implementation of syscall.Lstat func (sys *SyscallRecorder) SysLstat(name string, sb *syscall.Stat_t) error { // NOTE the os.Lstat uses a different signature `lstat %q`. call := fmt.Sprintf("lstat %q <ptr>", name) - if err := sys.call(call); err != nil { - return err - } - if ssb, ok := sys.sysLstats[call]; ok { - *sb = ssb - return nil + val, err := sys.rcall(call, func(call string) (interface{}, error) { + if buf, ok := sys.sysLstats[call]; ok { + return buf, nil + } + panic(fmt.Sprintf("one of InsertSysLstatResult() or InsertFault() for %s must be used", call)) + }) + if err == nil && sb != nil { + *sb = val.(syscall.Stat_t) } - panic(fmt.Sprintf("one of InsertSysLstatResult() or InsertFault() for %s must be used", call)) + return err } // InsertFstatResult makes given subsequent call fstat return the specified stat buffer. @@ -382,18 +430,19 @@ func (sys *SyscallRecorder) InsertFstatResult(call string, buf syscall.Stat_t) { // Fstat is a fake implementation of syscall.Fstat func (sys *SyscallRecorder) Fstat(fd int, buf *syscall.Stat_t) error { call := fmt.Sprintf("fstat %d <ptr>", fd) - if _, ok := sys.fds[fd]; !ok { - sys.calls = append(sys.calls, call) - return fmt.Errorf("attempting to fstat with an invalid file descriptor %d", fd) - } - if err := sys.call(call); err != nil { - return err - } - if b, ok := sys.fstats[call]; ok { - *buf = b - return nil + val, err := sys.rcall(call, func(call string) (interface{}, error) { + if _, ok := sys.fds[fd]; !ok { + return nil, fmt.Errorf("attempting to fstat with an invalid file descriptor %d", fd) + } + if buf, ok := sys.fstats[call]; ok { + return buf, nil + } + panic(fmt.Sprintf("one of InsertFstatResult() or InsertFault() for %s must be used", call)) + }) + if err == nil && buf != nil { + *buf = val.(syscall.Stat_t) } - panic(fmt.Sprintf("one of InsertFstatResult() or InsertFault() for %s must be used", call)) + return err } // InsertReadDirResult makes given subsequent call readdir return the specified fake file infos. @@ -407,29 +456,35 @@ func (sys *SyscallRecorder) InsertReadDirResult(call string, infos []os.FileInfo // ReadDir is a fake implementation of os.ReadDir func (sys *SyscallRecorder) ReadDir(dirname string) ([]os.FileInfo, error) { call := fmt.Sprintf("readdir %q", dirname) - if err := sys.call(call); err != nil { - return nil, err - } - if fi, ok := sys.readdirs[call]; ok { - return fi, nil + val, err := sys.rcall(call, func(call string) (interface{}, error) { + if fi, ok := sys.readdirs[call]; ok { + return fi, nil + } + panic(fmt.Sprintf("one of InsertReadDirResult() or InsertFault() for %s must be used", call)) + }) + if err == nil { + return val.([]os.FileInfo), nil } - panic(fmt.Sprintf("one of InsertReadDirResult() or InsertFault() for %s must be used", call)) + return nil, err } // Symlink is a fake implementation of syscall.Symlink func (sys *SyscallRecorder) Symlink(oldname, newname string) error { call := fmt.Sprintf("symlink %q -> %q", newname, oldname) - return sys.call(call) + _, err := sys.rcall(call, nil) + return err } // Symlinkat is a fake implementation of osutil.Symlinkat (syscall.Symlinkat is not exposed) func (sys *SyscallRecorder) Symlinkat(oldname string, dirfd int, newname string) error { call := fmt.Sprintf("symlinkat %q %d %q", oldname, dirfd, newname) - if _, ok := sys.fds[dirfd]; !ok { - sys.calls = append(sys.calls, call) - return fmt.Errorf("attempting to symlinkat with an invalid file descriptor %d", dirfd) - } - return sys.call(call) + _, err := sys.rcall(call, func(call string) (interface{}, error) { + if _, ok := sys.fds[dirfd]; !ok { + return nil, fmt.Errorf("attempting to symlinkat with an invalid file descriptor %d", dirfd) + } + return nil, nil + }) + return err } // InsertReadlinkatResult makes given subsequent call to readlinkat return the specified oldname. @@ -443,32 +498,37 @@ func (sys *SyscallRecorder) InsertReadlinkatResult(call, oldname string) { // Readlinkat is a fake implementation of osutil.Readlinkat (syscall.Readlinkat is not exposed) func (sys *SyscallRecorder) Readlinkat(dirfd int, path string, buf []byte) (int, error) { call := fmt.Sprintf("readlinkat %d %q <ptr>", dirfd, path) - if _, ok := sys.fds[dirfd]; !ok { - sys.calls = append(sys.calls, call) - return 0, fmt.Errorf("attempting to readlinkat with an invalid file descriptor %d", dirfd) - } - if err := sys.call(call); err != nil { - return 0, err - } - if oldname, ok := sys.readlinkats[call]; ok { - n := copy(buf, oldname) + val, err := sys.rcall(call, func(call string) (interface{}, error) { + if _, ok := sys.fds[dirfd]; !ok { + return nil, fmt.Errorf("attempting to readlinkat with an invalid file descriptor %d", dirfd) + } + if oldname, ok := sys.readlinkats[call]; ok { + return oldname, nil + } + panic(fmt.Sprintf("one of InsertReadlinkatResult() or InsertFault() for %s must be used", call)) + }) + if err == nil { + n := copy(buf, val.(string)) return n, nil } - panic(fmt.Sprintf("one of InsertReadlinkatResult() or InsertFault() for %s must be used", call)) + return 0, err } // Remove is a fake implementation of os.Remove func (sys *SyscallRecorder) Remove(name string) error { call := fmt.Sprintf("remove %q", name) - return sys.call(call) + _, err := sys.rcall(call, nil) + return err } // Fchdir is a fake implementation of syscall.Fchdir func (sys *SyscallRecorder) Fchdir(fd int) error { call := fmt.Sprintf("fchdir %d", fd) - if _, ok := sys.fds[fd]; !ok { - sys.calls = append(sys.calls, call) - return fmt.Errorf("attempting to fchdir with an invalid file descriptor %d", fd) - } - return sys.call(call) + _, err := sys.rcall(call, func(call string) (interface{}, error) { + if _, ok := sys.fds[fd]; !ok { + return nil, fmt.Errorf("attempting to fchdir with an invalid file descriptor %d", fd) + } + return nil, nil + }) + return err } diff --git a/testutil/lowlevel_test.go b/testutil/lowlevel_test.go index e7db54d505..abe2aa84ce 100644 --- a/testutil/lowlevel_test.go +++ b/testutil/lowlevel_test.go @@ -65,6 +65,9 @@ func (s *lowLevelSuite) TestOpenSuccess(c *C) { c.Assert(s.sys.Calls(), DeepEquals, []string{ `open "/some/path" O_NOFOLLOW|O_CLOEXEC|O_RDWR|O_CREAT|O_EXCL 0`, // -> 3 }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/some/path" O_NOFOLLOW|O_CLOEXEC|O_RDWR|O_CREAT|O_EXCL 0`, R: 3}, + }) } func (s *lowLevelSuite) TestOpenFailure(c *C) { @@ -73,6 +76,12 @@ func (s *lowLevelSuite) TestOpenFailure(c *C) { fd, err := s.sys.Open("/some/path", 0, 0) c.Assert(err, ErrorMatches, "no such file or directory") c.Assert(fd, Equals, -1) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/some/path" 0 0`, // -> ENOENT + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/some/path" 0 0`, E: syscall.ENOENT}, + }) } func (s *lowLevelSuite) TestOpenVariableFailure(c *C) { @@ -90,6 +99,16 @@ func (s *lowLevelSuite) TestOpenVariableFailure(c *C) { fd, err = s.sys.Open("/some/path", syscall.O_RDWR, 0) c.Assert(err, IsNil) c.Assert(fd, Equals, 3) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/some/path" O_RDWR 0`, // -> ENOENT + `open "/some/path" O_RDWR 0`, // -> EPERM + `open "/some/path" O_RDWR 0`, // -> 3 + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/some/path" O_RDWR 0`, E: syscall.ENOENT}, + {C: `open "/some/path" O_RDWR 0`, E: syscall.EPERM}, + {C: `open "/some/path" O_RDWR 0`, R: 3}, + }) } func (s *lowLevelSuite) TestOpenCustomFailure(c *C) { @@ -112,6 +131,18 @@ func (s *lowLevelSuite) TestOpenCustomFailure(c *C) { fd, err := s.sys.Open("/some/path", syscall.O_RDWR, 0) c.Assert(err, IsNil) c.Assert(fd, Equals, 3) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/some/path" O_RDWR 0`, // -> 3 more + `open "/some/path" O_RDWR 0`, // -> 2 more + `open "/some/path" O_RDWR 0`, // -> 1 more + `open "/some/path" O_RDWR 0`, // -> 3 + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/some/path" O_RDWR 0`, E: fmt.Errorf("3 more")}, + {C: `open "/some/path" O_RDWR 0`, E: fmt.Errorf("2 more")}, + {C: `open "/some/path" O_RDWR 0`, E: fmt.Errorf("1 more")}, + {C: `open "/some/path" O_RDWR 0`, R: 3}, + }) } func (s *lowLevelSuite) TestUnclosedFile(c *C) { @@ -123,6 +154,9 @@ func (s *lowLevelSuite) TestUnclosedFile(c *C) { c.Assert(s.sys.Calls(), DeepEquals, []string{ `open "/some/path" O_RDWR 0`, // -> 3 }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/some/path" O_RDWR 0`, R: 3}, + }) c.Assert(s.sys.StrayDescriptorsError(), ErrorMatches, `unclosed file descriptor 3 \(open "/some/path" O_RDWR 0\)`) } @@ -132,6 +166,9 @@ func (s *lowLevelSuite) TestUnopenedFile(c *C) { err := s.sys.Close(7) c.Assert(err, ErrorMatches, "attempting to close a closed file descriptor 7") c.Assert(s.sys.Calls(), DeepEquals, []string{`close 7`}) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `close 7`, E: fmt.Errorf("attempting to close a closed file descriptor 7")}, + }) } func (s *lowLevelSuite) TestCloseSuccess(c *C) { @@ -144,6 +181,10 @@ func (s *lowLevelSuite) TestCloseSuccess(c *C) { `open "/some/path" O_RDWR 0`, // -> 3 `close 3`, }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/some/path" O_RDWR 0`, R: 3}, + {C: `close 3`}, + }) c.Assert(s.sys.StrayDescriptorsError(), IsNil) } @@ -155,6 +196,9 @@ func (s *lowLevelSuite) TestCloseFailure(c *C) { c.Assert(s.sys.Calls(), DeepEquals, []string{ `close 3`, }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `close 3`, E: syscall.ENOSYS}, + }) } func (s *lowLevelSuite) TestOpenatSuccess(c *C) { @@ -170,6 +214,12 @@ func (s *lowLevelSuite) TestOpenatSuccess(c *C) { `close 4`, `close 3`, }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/" O_DIRECTORY 0`, R: 3}, + {C: `openat 3 "foo" O_DIRECTORY 0`, R: 4}, + {C: `close 4`}, + {C: `close 3`}, + }) } func (s *lowLevelSuite) TestOpenatFailure(c *C) { @@ -185,6 +235,11 @@ func (s *lowLevelSuite) TestOpenatFailure(c *C) { `openat 3 "foo" O_DIRECTORY 0`, // -> ENOENT `close 3`, }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/" O_DIRECTORY 0`, R: 3}, + {C: `openat 3 "foo" O_DIRECTORY 0`, E: syscall.ENOENT}, + {C: `close 3`}, + }) } func (s *lowLevelSuite) TestOpenatBadFd(c *C) { @@ -194,6 +249,9 @@ func (s *lowLevelSuite) TestOpenatBadFd(c *C) { c.Assert(s.sys.Calls(), DeepEquals, []string{ `openat 3 "foo" O_DIRECTORY 0`, // -> error }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `openat 3 "foo" O_DIRECTORY 0`, E: fmt.Errorf("attempting to openat with an invalid file descriptor 3")}, + }) } func (s *lowLevelSuite) TestFchownSuccess(c *C) { @@ -207,6 +265,11 @@ func (s *lowLevelSuite) TestFchownSuccess(c *C) { `fchown 3 0 0`, `close 3`, }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/" O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`}, + {C: `close 3`}, + }) } func (s *lowLevelSuite) TestFchownFailure(c *C) { @@ -221,6 +284,11 @@ func (s *lowLevelSuite) TestFchownFailure(c *C) { `fchown 3 0 0`, // -> EPERM `close 3`, }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/" O_DIRECTORY 0`, R: 3}, + {C: `fchown 3 0 0`, E: syscall.EPERM}, + {C: `close 3`}, + }) } func (s *lowLevelSuite) TestFchownBadFd(c *C) { @@ -229,6 +297,9 @@ func (s *lowLevelSuite) TestFchownBadFd(c *C) { c.Assert(s.sys.Calls(), DeepEquals, []string{ `fchown 3 0 0`, }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `fchown 3 0 0`, E: fmt.Errorf("attempting to fchown an invalid file descriptor 3")}, + }) } func (s *lowLevelSuite) TestMkdiratSuccess(c *C) { @@ -242,6 +313,11 @@ func (s *lowLevelSuite) TestMkdiratSuccess(c *C) { `mkdirat 3 "foo" 0755`, `close 3`, }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/" O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "foo" 0755`}, + {C: `close 3`}, + }) } func (s *lowLevelSuite) TestMkdiratFailure(c *C) { @@ -256,12 +332,22 @@ func (s *lowLevelSuite) TestMkdiratFailure(c *C) { `mkdirat 3 "foo" 0755`, // -> EPERM `close 3`, }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/" O_DIRECTORY 0`, R: 3}, + {C: `mkdirat 3 "foo" 0755`, E: syscall.EPERM}, + {C: `close 3`}, + }) } func (s *lowLevelSuite) TestMkdiratBadFd(c *C) { err := s.sys.Mkdirat(3, "foo", 0755) c.Assert(err, ErrorMatches, "attempting to mkdirat with an invalid file descriptor 3") - c.Assert(s.sys.Calls(), DeepEquals, []string{`mkdirat 3 "foo" 0755`}) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `mkdirat 3 "foo" 0755`, + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `mkdirat 3 "foo" 0755`, E: fmt.Errorf("attempting to mkdirat with an invalid file descriptor 3")}, + }) } func (s *lowLevelSuite) TestMountSuccess(c *C) { @@ -270,6 +356,9 @@ func (s *lowLevelSuite) TestMountSuccess(c *C) { c.Assert(s.sys.Calls(), DeepEquals, []string{ `mount "source" "target" "fstype" MS_BIND|MS_REC|MS_RDONLY ""`, }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `mount "source" "target" "fstype" MS_BIND|MS_REC|MS_RDONLY ""`}, + }) } func (s *lowLevelSuite) TestMountFailure(c *C) { @@ -279,12 +368,18 @@ func (s *lowLevelSuite) TestMountFailure(c *C) { c.Assert(s.sys.Calls(), DeepEquals, []string{ `mount "source" "target" "fstype" 0 ""`, }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `mount "source" "target" "fstype" 0 ""`, E: syscall.EPERM}, + }) } func (s *lowLevelSuite) TestUnmountSuccess(c *C) { err := s.sys.Unmount("target", testutil.UmountNoFollow|syscall.MNT_DETACH) c.Assert(err, IsNil) c.Assert(s.sys.Calls(), DeepEquals, []string{`unmount "target" UMOUNT_NOFOLLOW|MNT_DETACH`}) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `unmount "target" UMOUNT_NOFOLLOW|MNT_DETACH`}, + }) } func (s *lowLevelSuite) TestUnmountFailure(c *C) { @@ -292,6 +387,9 @@ func (s *lowLevelSuite) TestUnmountFailure(c *C) { err := s.sys.Unmount("target", 0) c.Assert(err, ErrorMatches, "operation not permitted") c.Assert(s.sys.Calls(), DeepEquals, []string{`unmount "target" 0`}) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `unmount "target" 0`, E: syscall.EPERM}, + }) } func (s *lowLevelSuite) TestOsLstat(c *C) { @@ -306,6 +404,10 @@ func (s *lowLevelSuite) TestOsLstatSuccess(c *C) { fi, err := s.sys.OsLstat("/foo") c.Assert(err, IsNil) c.Assert(fi, DeepEquals, testutil.FileInfoFile) + c.Assert(s.sys.Calls(), DeepEquals, []string{`lstat "/foo"`}) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `lstat "/foo"`, R: testutil.FileInfoFile}, + }) } func (s *lowLevelSuite) TestOsLstatFailure(c *C) { @@ -315,6 +417,10 @@ func (s *lowLevelSuite) TestOsLstatFailure(c *C) { fi, err := s.sys.OsLstat("/foo") c.Assert(err, ErrorMatches, "no such file or directory") c.Assert(fi, IsNil) + c.Assert(s.sys.Calls(), DeepEquals, []string{`lstat "/foo"`}) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `lstat "/foo"`, E: syscall.ENOENT}, + }) } func (s *lowLevelSuite) TestSysLstat(c *C) { @@ -331,6 +437,12 @@ func (s *lowLevelSuite) TestSysLstatSuccess(c *C) { err := s.sys.SysLstat("/foo", &buf) c.Assert(err, IsNil) c.Assert(buf, DeepEquals, syscall.Stat_t{Uid: 123}) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `lstat "/foo" <ptr>`, + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `lstat "/foo" <ptr>`, R: syscall.Stat_t{Uid: 123}}, + }) } func (s *lowLevelSuite) TestSysLstatFailure(c *C) { @@ -341,6 +453,12 @@ func (s *lowLevelSuite) TestSysLstatFailure(c *C) { err := s.sys.SysLstat("/foo", &buf) c.Assert(err, ErrorMatches, "no such file or directory") c.Assert(buf, DeepEquals, syscall.Stat_t{}) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `lstat "/foo" <ptr>`, // -> ENOENT + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `lstat "/foo" <ptr>`, E: syscall.ENOENT}, + }) } func (s *lowLevelSuite) TestFstat(c *C) { @@ -355,7 +473,12 @@ func (s *lowLevelSuite) TestFstatBadFd(c *C) { var buf syscall.Stat_t err := s.sys.Fstat(3, &buf) c.Assert(err, ErrorMatches, "attempting to fstat with an invalid file descriptor 3") - c.Assert(s.sys.Calls(), DeepEquals, []string{`fstat 3 <ptr>`}) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `fstat 3 <ptr>`, + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `fstat 3 <ptr>`, E: fmt.Errorf("attempting to fstat with an invalid file descriptor 3")}, + }) } func (s *lowLevelSuite) TestFstatSuccess(c *C) { @@ -366,6 +489,14 @@ func (s *lowLevelSuite) TestFstatSuccess(c *C) { err = s.sys.Fstat(fd, &buf) c.Assert(err, IsNil) c.Assert(buf, Equals, syscall.Stat_t{Dev: 0xC0FE}) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/foo" 0 0`, // -> 3 + `fstat 3 <ptr>`, + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/foo" 0 0`, R: 3}, + {C: `fstat 3 <ptr>`, R: syscall.Stat_t{Dev: 0xC0FE}}, + }) } func (s *lowLevelSuite) TestFstatFailure(c *C) { @@ -376,6 +507,14 @@ func (s *lowLevelSuite) TestFstatFailure(c *C) { err = s.sys.Fstat(fd, &buf) c.Assert(err, ErrorMatches, "operation not permitted") c.Assert(buf, Equals, syscall.Stat_t{}) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `open "/foo" 0 0`, // -> 3 + `fstat 3 <ptr>`, // -> EPERM + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/foo" 0 0`, R: 3}, + {C: `fstat 3 <ptr>`, E: syscall.EPERM}, + }) } func (s *lowLevelSuite) TestReadDir(c *C) { @@ -384,13 +523,20 @@ func (s *lowLevelSuite) TestReadDir(c *C) { } func (s *lowLevelSuite) TestReadDirSuccess(c *C) { - s.sys.InsertReadDirResult(`readdir "/foo"`, []os.FileInfo{ + files := []os.FileInfo{ testutil.FakeFileInfo("file", 0644), testutil.FakeFileInfo("dir", 0755|os.ModeDir), - }) + } + s.sys.InsertReadDirResult(`readdir "/foo"`, files) files, err := s.sys.ReadDir("/foo") c.Assert(err, IsNil) c.Assert(files, HasLen, 2) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `readdir "/foo"`, + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `readdir "/foo"`, R: files}, + }) } func (s *lowLevelSuite) TestReadDirFailure(c *C) { @@ -398,25 +544,46 @@ func (s *lowLevelSuite) TestReadDirFailure(c *C) { files, err := s.sys.ReadDir("/foo") c.Assert(err, ErrorMatches, "no such file or directory") c.Assert(files, IsNil) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `readdir "/foo"`, // -> ENOENT + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `readdir "/foo"`, E: syscall.ENOENT}, + }) } func (s *lowLevelSuite) TestSymlinkSuccess(c *C) { err := s.sys.Symlink("oldname", "newname") c.Assert(err, IsNil) - c.Assert(s.sys.Calls(), DeepEquals, []string{`symlink "newname" -> "oldname"`}) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `symlink "newname" -> "oldname"`, + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `symlink "newname" -> "oldname"`}, + }) } func (s *lowLevelSuite) TestSymlinkFailure(c *C) { s.sys.InsertFault(`symlink "newname" -> "oldname"`, syscall.EPERM) err := s.sys.Symlink("oldname", "newname") c.Assert(err, ErrorMatches, "operation not permitted") - c.Assert(s.sys.Calls(), DeepEquals, []string{`symlink "newname" -> "oldname"`}) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `symlink "newname" -> "oldname"`, // -> EPERM + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `symlink "newname" -> "oldname"`, E: syscall.EPERM}, + }) } func (s *lowLevelSuite) TestRemoveSuccess(c *C) { err := s.sys.Remove("file") c.Assert(err, IsNil) - c.Assert(s.sys.Calls(), DeepEquals, []string{`remove "file"`}) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `remove "file"`, + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `remove "file"`}, + }) } func (s *lowLevelSuite) TestRemoveFailure(c *C) { @@ -424,12 +591,23 @@ func (s *lowLevelSuite) TestRemoveFailure(c *C) { err := s.sys.Remove("file") c.Assert(err, ErrorMatches, "operation not permitted") c.Assert(s.sys.Calls(), DeepEquals, []string{`remove "file"`}) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `remove "file"`, // -> EPERM + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `remove "file"`, E: syscall.EPERM}, + }) } func (s *lowLevelSuite) TestSymlinkatBadFd(c *C) { err := s.sys.Symlinkat("/old", 3, "new") c.Assert(err, ErrorMatches, "attempting to symlinkat with an invalid file descriptor 3") - c.Assert(s.sys.Calls(), DeepEquals, []string{`symlinkat "/old" 3 "new"`}) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `symlinkat "/old" 3 "new"`, + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `symlinkat "/old" 3 "new"`, E: fmt.Errorf("attempting to symlinkat with an invalid file descriptor 3")}, + }) } func (s *lowLevelSuite) TestSymlinkatSuccess(c *C) { @@ -441,6 +619,10 @@ func (s *lowLevelSuite) TestSymlinkatSuccess(c *C) { `open "/foo" 0 0`, `symlinkat "/old" 3 "new"`, }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/foo" 0 0`, R: 3}, + {C: `symlinkat "/old" 3 "new"`}, + }) } func (s *lowLevelSuite) TestSymlinkatFailure(c *C) { @@ -450,9 +632,13 @@ func (s *lowLevelSuite) TestSymlinkatFailure(c *C) { err = s.sys.Symlinkat("/old", fd, "new") c.Assert(err, ErrorMatches, "operation not permitted") c.Assert(s.sys.Calls(), DeepEquals, []string{ - `open "/foo" 0 0`, + `open "/foo" 0 0`, // -> 3 `symlinkat "/old" 3 "new"`, }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `open "/foo" 0 0`, R: 3}, + {C: `symlinkat "/old" 3 "new"`, E: syscall.EPERM}, + }) } func (s *lowLevelSuite) TestReadlinkat(c *C) { @@ -468,7 +654,12 @@ func (s *lowLevelSuite) TestReadlinkatBadFd(c *C) { n, err := s.sys.Readlinkat(3, "new", buf) c.Assert(err, ErrorMatches, "attempting to readlinkat with an invalid file descriptor 3") c.Assert(n, Equals, 0) - c.Assert(s.sys.Calls(), DeepEquals, []string{`readlinkat 3 "new" <ptr>`}) + c.Assert(s.sys.Calls(), DeepEquals, []string{ + `readlinkat 3 "new" <ptr>`, + }) + c.Assert(s.sys.RCalls(), DeepEquals, []testutil.CallResultError{ + {C: `readlinkat 3 "new" <ptr>`, E: fmt.Errorf("attempting to readlinkat with an invalid file descriptor 3")}, + }) } func (s *lowLevelSuite) TestReadlinkatSuccess(c *C) { |
