summaryrefslogtreecommitdiff
diff options
authorPawel Stolowski <stolowski@gmail.com>2019-11-07 10:34:20 +0100
committerPawel Stolowski <stolowski@gmail.com>2019-11-07 10:34:20 +0100
commitb407e74241ee41b94166b7357857de89946e28b9 (patch)
tree09ca88033a73b8728dc613c3578aeabc1a70625d
parent75fcfbb6326d21e065a1cca559d9a54aa359e8de (diff)
parent780f4a86b268d4c7232afb8411b569992c9a3a3e (diff)
Merge branch 'master' into remove-autoconnectionsremove-autoconnections
-rw-r--r--asserts/ifacedecls.go29
-rw-r--r--asserts/ifacedecls_test.go59
-rw-r--r--interfaces/policy/basedeclaration_test.go76
-rw-r--r--interfaces/policy/helpers.go29
-rw-r--r--interfaces/policy/policy.go52
-rw-r--r--interfaces/policy/policy_test.go110
-rw-r--r--interfaces/repo.go23
-rw-r--r--interfaces/repo_test.go104
-rw-r--r--overlord/devicestate/devicestate.go59
-rw-r--r--overlord/devicestate/devicestate_remodel_test.go59
-rw-r--r--overlord/ifacestate/handlers.go5
-rw-r--r--overlord/ifacestate/helpers.go37
-rw-r--r--overlord/ifacestate/ifacestate_test.go192
-rw-r--r--overlord/managers_test.go430
-rw-r--r--overlord/snapstate/snapstate.go49
-rw-r--r--seed/seed20.go12
-rw-r--r--seed/seed20_test.go364
-rw-r--r--seed/seedtest/seedtest.go19
-rw-r--r--seed/seedwriter/writer_test.go37
-rw-r--r--spread.yaml1
-rw-r--r--tests/lib/assertions/developer1-pc-18-new-base.model23
-rwxr-xr-xtests/lib/state.sh5
-rw-r--r--tests/main/remodel-base/task.yaml122
23 files changed, 1715 insertions, 181 deletions
diff --git a/asserts/ifacedecls.go b/asserts/ifacedecls.go
index d2394ad8d2..aa36bf1260 100644
--- a/asserts/ifacedecls.go
+++ b/asserts/ifacedecls.go
@@ -361,10 +361,11 @@ func (c *AttributeConstraints) Check(attrer Attrer, ctx AttrMatchContext) error
}
// SideArityConstraint specifies a constraint for the overall arity of
-// the opposite connected set of slots for a plug, respectively plugs
-// for a slot.
-// It is used to express parsed slots-per-plug, respectively plugs-per-slot
+// the set of connected slots for a given plug or the set of
+// connected plugs for a given slot.
+// It is used to express parsed slots-per-plug and plugs-per-slot
// constraints.
+// See https://forum.snapcraft.io/t/plug-slot-declaration-rules-greedy-plugs/12438
type SideArityConstraint struct {
// N can be:
// =>1
@@ -386,7 +387,7 @@ func compileSideArityConstraint(context *subruleContext, which string, v interfa
return a, fmt.Errorf("%s cannot specify a %s constraint, they apply only to allow-*connection", context, which)
}
x, ok := v.(string)
- if !ok {
+ if !ok || len(x) == 0 {
return a, fmt.Errorf("%s in %s must be an integer >=1 or *", which, context)
}
if x == "*" {
@@ -412,7 +413,7 @@ func normalizeSideArityConstraints(context *subruleContext, c sideArityConstrain
return
}
any := SideArityConstraint{N: -1}
- // normalized plugs-per-slots is always *
+ // normalized plugs-per-slot is always *
c.setPlugsPerSlot(any)
slotsPerPlug := c.slotsPerPlug()
if context.autoConnection() {
@@ -646,10 +647,21 @@ type constraintsDef struct {
invert bool
}
+// subruleContext carries queryable context information about one the
+// {allow,deny}-* subrules that end up compiled as
+// Plug|Slot*Constraints. The information includes the parent rule,
+// the introductory subrule key ({allow,deny}-*) and which alternative
+// it corresponds to if any.
+// The information is useful for constraints compilation now that we
+// have constraints with different behavior depending on the kind of
+// subrule that hosts them (e.g. slots-per-plug, plugs-per-slot).
type subruleContext struct {
- rule string
+ // rule is the parent rule context description
+ rule string
+ // subrule is the subrule key
subrule string
- alt int
+ // alt is which alternative this is (if > 0)
+ alt int
}
func (c *subruleContext) String() string {
@@ -660,14 +672,17 @@ func (c *subruleContext) String() string {
return subctxt
}
+// allow returns whether the subrule is an allow-* subrule.
func (c *subruleContext) allow() bool {
return strings.HasPrefix(c.subrule, "allow-")
}
+// installation returns whether the subrule is an *-installation subrule.
func (c *subruleContext) installation() bool {
return strings.HasSuffix(c.subrule, "-installation")
}
+// autoConnection returns whether the subrule is an *-auto-connection subrule.
func (c *subruleContext) autoConnection() bool {
return strings.HasSuffix(c.subrule, "-auto-connection")
}
diff --git a/asserts/ifacedecls_test.go b/asserts/ifacedecls_test.go
index 6d1c571d86..105c27af35 100644
--- a/asserts/ifacedecls_test.go
+++ b/asserts/ifacedecls_test.go
@@ -497,6 +497,11 @@ func checkAttrs(c *C, attrs *asserts.AttributeConstraints, witness, expected str
c.Check(attrs.Check(plug, nil), IsNil)
}
+var (
+ sideArityAny = asserts.SideArityConstraint{N: -1}
+ sideArityOne = asserts.SideArityConstraint{N: 1}
+)
+
func checkBoolPlugConnConstraints(c *C, subrule string, cstrs []*asserts.PlugConnectionConstraints, always bool) {
expected := asserts.NeverMatchAttributes
if always {
@@ -511,13 +516,11 @@ func checkBoolPlugConnConstraints(c *C, subrule string, cstrs []*asserts.PlugCon
c.Check(cstrs1.SlotsPerPlug, Equals, undef)
c.Check(cstrs1.PlugsPerSlot, Equals, undef)
} else {
- any := asserts.SideArityConstraint{N: -1}
- one := asserts.SideArityConstraint{N: 1}
- c.Check(cstrs1.PlugsPerSlot, Equals, any)
+ c.Check(cstrs1.PlugsPerSlot, Equals, sideArityAny)
if strings.HasSuffix(subrule, "-auto-connection") {
- c.Check(cstrs1.SlotsPerPlug, Equals, one)
+ c.Check(cstrs1.SlotsPerPlug, Equals, sideArityOne)
} else {
- c.Check(cstrs1.SlotsPerPlug, Equals, any)
+ c.Check(cstrs1.SlotsPerPlug, Equals, sideArityAny)
}
}
c.Check(cstrs1.SlotSnapIDs, HasLen, 0)
@@ -539,13 +542,11 @@ func checkBoolSlotConnConstraints(c *C, subrule string, cstrs []*asserts.SlotCon
c.Check(cstrs1.SlotsPerPlug, Equals, undef)
c.Check(cstrs1.PlugsPerSlot, Equals, undef)
} else {
- any := asserts.SideArityConstraint{N: -1}
- one := asserts.SideArityConstraint{N: 1}
- c.Check(cstrs1.PlugsPerSlot, Equals, any)
+ c.Check(cstrs1.PlugsPerSlot, Equals, sideArityAny)
if strings.HasSuffix(subrule, "-auto-connection") {
- c.Check(cstrs1.SlotsPerPlug, Equals, one)
+ c.Check(cstrs1.SlotsPerPlug, Equals, sideArityOne)
} else {
- c.Check(cstrs1.SlotsPerPlug, Equals, any)
+ c.Check(cstrs1.SlotsPerPlug, Equals, sideArityAny)
}
}
c.Check(cstrs1.PlugSnapIDs, HasLen, 0)
@@ -1000,7 +1001,9 @@ func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsSideArityCo
c.Check(rule.AllowConnection[0].SlotsPerPlug.Any(), Equals, true)
c.Check(rule.AllowConnection[0].PlugsPerSlot.Any(), Equals, true)
- // allow-connection => *
+ // test that the arity constraints get normalized away to any
+ // under allow-connection
+ // see https://forum.snapcraft.io/t/plug-slot-declaration-rules-greedy-plugs/12438
allowConnTests := []string{
`iface:
allow-connection:
@@ -1027,23 +1030,26 @@ func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsSideArityCo
c.Check(rule.AllowConnection[0].PlugsPerSlot.Any(), Equals, true)
}
- // allow-auto-connection => *
+ // test that under allow-auto-connection:
+ // slots-per-plug can be * (any) or otherwise gets normalized to 1
+ // plugs-per-slot gets normalized to any (*)
+ // see https://forum.snapcraft.io/t/plug-slot-declaration-rules-greedy-plugs/12438
allowAutoConnTests := []struct {
rule string
- slotsPerPlug int
+ slotsPerPlug asserts.SideArityConstraint
}{
{`iface:
allow-auto-connection:
slots-per-plug: 1
- plugs-per-slot: 2`, 1},
+ plugs-per-slot: 2`, sideArityOne},
{`iface:
allow-auto-connection:
slots-per-plug: *
- plugs-per-slot: 1`, -1},
+ plugs-per-slot: 1`, sideArityAny},
{`iface:
allow-auto-connection:
slots-per-plug: 2
- plugs-per-slot: *`, 1},
+ plugs-per-slot: *`, sideArityOne},
}
for _, t := range allowAutoConnTests {
@@ -1053,7 +1059,7 @@ func (s *plugSlotRulesSuite) TestCompilePlugRuleConnectionConstraintsSideArityCo
rule, err = asserts.CompilePlugRule("iface", m["iface"].(map[string]interface{}))
c.Assert(err, IsNil)
- c.Check(rule.AllowAutoConnection[0].SlotsPerPlug, Equals, asserts.SideArityConstraint{N: t.slotsPerPlug})
+ c.Check(rule.AllowAutoConnection[0].SlotsPerPlug, Equals, t.slotsPerPlug)
c.Check(rule.AllowAutoConnection[0].PlugsPerSlot.Any(), Equals, true)
}
}
@@ -1706,7 +1712,9 @@ func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsSideArityCo
c.Check(rule.AllowConnection[0].SlotsPerPlug.Any(), Equals, true)
c.Check(rule.AllowConnection[0].PlugsPerSlot.Any(), Equals, true)
- // allow-connection => *
+ // test that the arity constraints get normalized away to any
+ // under allow-connection
+ // see https://forum.snapcraft.io/t/plug-slot-declaration-rules-greedy-plugs/12438
allowConnTests := []string{
`iface:
allow-connection:
@@ -1733,23 +1741,26 @@ func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsSideArityCo
c.Check(rule.AllowConnection[0].PlugsPerSlot.Any(), Equals, true)
}
- // allow-auto-connection => *
+ // test that under allow-auto-connection:
+ // slots-per-plug can be * (any) or otherwise gets normalized to 1
+ // plugs-per-slot gets normalized to any (*)
+ // see https://forum.snapcraft.io/t/plug-slot-declaration-rules-greedy-plugs/12438
allowAutoConnTests := []struct {
rule string
- slotsPerPlug int
+ slotsPerPlug asserts.SideArityConstraint
}{
{`iface:
allow-auto-connection:
slots-per-plug: 1
- plugs-per-slot: 2`, 1},
+ plugs-per-slot: 2`, sideArityOne},
{`iface:
allow-auto-connection:
slots-per-plug: *
- plugs-per-slot: 1`, -1},
+ plugs-per-slot: 1`, sideArityAny},
{`iface:
allow-auto-connection:
slots-per-plug: 2
- plugs-per-slot: *`, 1},
+ plugs-per-slot: *`, sideArityOne},
}
for _, t := range allowAutoConnTests {
@@ -1759,7 +1770,7 @@ func (s *plugSlotRulesSuite) TestCompileSlotRuleConnectionConstraintsSideArityCo
rule, err = asserts.CompileSlotRule("iface", m["iface"].(map[string]interface{}))
c.Assert(err, IsNil)
- c.Check(rule.AllowAutoConnection[0].SlotsPerPlug, Equals, asserts.SideArityConstraint{N: t.slotsPerPlug})
+ c.Check(rule.AllowAutoConnection[0].SlotsPerPlug, Equals, t.slotsPerPlug)
c.Check(rule.AllowAutoConnection[0].PlugsPerSlot.Any(), Equals, true)
}
}
diff --git a/interfaces/policy/basedeclaration_test.go b/interfaces/policy/basedeclaration_test.go
index 3df856ae59..04740d77c7 100644
--- a/interfaces/policy/basedeclaration_test.go
+++ b/interfaces/policy/basedeclaration_test.go
@@ -183,9 +183,10 @@ func (s *baseDeclSuite) TestAutoConnection(c *C) {
// check base declaration
cand := s.connectCand(c, iface.Name(), "", "")
- err := cand.CheckAutoConnect()
+ arity, err := cand.CheckAutoConnect()
if expected {
c.Check(err, IsNil, comm)
+ c.Check(arity.SlotsPerPlugAny(), Equals, false)
} else {
c.Check(err, NotNil, comm)
}
@@ -216,11 +217,12 @@ func (s *baseDeclSuite) TestInterimAutoConnectionHome(c *C) {
restore := release.MockOnClassic(true)
defer restore()
cand := s.connectCand(c, "home", "", "")
- err := cand.CheckAutoConnect()
+ arity, err := cand.CheckAutoConnect()
c.Check(err, IsNil)
+ c.Check(arity.SlotsPerPlugAny(), Equals, false)
release.OnClassic = false
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, ErrorMatches, `auto-connection denied by slot rule of interface \"home\"`)
}
@@ -237,14 +239,14 @@ plugs:
err := cand.Check()
c.Check(err, NotNil)
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, NotNil)
release.OnClassic = false
err = cand.Check()
c.Check(err, NotNil)
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, NotNil)
}
@@ -261,21 +263,22 @@ plugs:
c.Check(err, IsNil)
// Same as TestInterimAutoConnectionHome()
- err = cand.CheckAutoConnect()
+ arity, err := cand.CheckAutoConnect()
c.Check(err, IsNil)
+ c.Check(arity.SlotsPerPlugAny(), Equals, false)
release.OnClassic = false
err = cand.Check()
c.Check(err, IsNil)
// Same as TestInterimAutoConnectionHome()
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, NotNil)
}
func (s *baseDeclSuite) TestAutoConnectionSnapdControl(c *C) {
cand := s.connectCand(c, "snapd-control", "", "")
- err := cand.CheckAutoConnect()
+ _, err := cand.CheckAutoConnect()
c.Check(err, NotNil)
c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"snapd-control\"")
@@ -287,15 +290,16 @@ plugs:
lxdDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
cand.PlugSnapDeclaration = lxdDecl
- err = cand.CheckAutoConnect()
+ arity, err := cand.CheckAutoConnect()
c.Check(err, IsNil)
+ c.Check(arity.SlotsPerPlugAny(), Equals, false)
}
func (s *baseDeclSuite) TestAutoConnectionContent(c *C) {
// random snaps cannot connect with content
// (Sanitize* will now also block this)
cand := s.connectCand(c, "content", "", "")
- err := cand.CheckAutoConnect()
+ _, err := cand.CheckAutoConnect()
c.Check(err, NotNil)
slotDecl1 := s.mockSnapDecl(c, "slot-snap", "slot-snap-id", "pub1", "")
@@ -320,13 +324,14 @@ plugs:
`)
cand.SlotSnapDeclaration = slotDecl1
cand.PlugSnapDeclaration = plugDecl1
- err = cand.CheckAutoConnect()
+ arity, err := cand.CheckAutoConnect()
c.Check(err, IsNil)
+ c.Check(arity.SlotsPerPlugAny(), Equals, false)
// different publisher, same content
cand.SlotSnapDeclaration = slotDecl1
cand.PlugSnapDeclaration = plugDecl2
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, NotNil)
// same publisher, different content
@@ -346,14 +351,14 @@ plugs:
`)
cand.SlotSnapDeclaration = slotDecl1
cand.PlugSnapDeclaration = plugDecl1
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, NotNil)
}
func (s *baseDeclSuite) TestAutoConnectionLxdSupportOverride(c *C) {
// by default, don't auto-connect
cand := s.connectCand(c, "lxd-support", "", "")
- err := cand.CheckAutoConnect()
+ _, err := cand.CheckAutoConnect()
c.Check(err, NotNil)
plugsSlots := `
@@ -364,7 +369,7 @@ plugs:
lxdDecl := s.mockSnapDecl(c, "lxd", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
cand.PlugSnapDeclaration = lxdDecl
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, IsNil)
}
@@ -378,14 +383,14 @@ plugs:
lxdDecl := s.mockSnapDecl(c, "notlxd", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
cand.PlugSnapDeclaration = lxdDecl
- err := cand.CheckAutoConnect()
+ _, err := cand.CheckAutoConnect()
c.Check(err, NotNil)
c.Assert(err, ErrorMatches, "auto-connection not allowed by plug rule of interface \"lxd-support\" for \"notlxd\" snap")
}
func (s *baseDeclSuite) TestAutoConnectionKernelModuleControlOverride(c *C) {
cand := s.connectCand(c, "kernel-module-control", "", "")
- err := cand.CheckAutoConnect()
+ _, err := cand.CheckAutoConnect()
c.Check(err, NotNil)
c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"kernel-module-control\"")
@@ -397,13 +402,13 @@ plugs:
snapDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
cand.PlugSnapDeclaration = snapDecl
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, IsNil)
}
func (s *baseDeclSuite) TestAutoConnectionDockerSupportOverride(c *C) {
cand := s.connectCand(c, "docker-support", "", "")
- err := cand.CheckAutoConnect()
+ _, err := cand.CheckAutoConnect()
c.Check(err, NotNil)
c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"docker-support\"")
@@ -415,13 +420,13 @@ plugs:
snapDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
cand.PlugSnapDeclaration = snapDecl
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, IsNil)
}
func (s *baseDeclSuite) TestAutoConnectionClassicSupportOverride(c *C) {
cand := s.connectCand(c, "classic-support", "", "")
- err := cand.CheckAutoConnect()
+ _, err := cand.CheckAutoConnect()
c.Check(err, NotNil)
c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"classic-support\"")
@@ -433,13 +438,13 @@ plugs:
snapDecl := s.mockSnapDecl(c, "classic", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
cand.PlugSnapDeclaration = snapDecl
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, IsNil)
}
func (s *baseDeclSuite) TestAutoConnectionKubernetesSupportOverride(c *C) {
cand := s.connectCand(c, "kubernetes-support", "", "")
- err := cand.CheckAutoConnect()
+ _, err := cand.CheckAutoConnect()
c.Check(err, NotNil)
c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"kubernetes-support\"")
@@ -451,13 +456,13 @@ plugs:
snapDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
cand.PlugSnapDeclaration = snapDecl
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, IsNil)
}
func (s *baseDeclSuite) TestAutoConnectionGreengrassSupportOverride(c *C) {
cand := s.connectCand(c, "greengrass-support", "", "")
- err := cand.CheckAutoConnect()
+ _, err := cand.CheckAutoConnect()
c.Check(err, NotNil)
c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"greengrass-support\"")
@@ -469,13 +474,13 @@ plugs:
snapDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
cand.PlugSnapDeclaration = snapDecl
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, IsNil)
}
func (s *baseDeclSuite) TestAutoConnectionMultipassSupportOverride(c *C) {
cand := s.connectCand(c, "multipass-support", "", "")
- err := cand.CheckAutoConnect()
+ _, err := cand.CheckAutoConnect()
c.Check(err, NotNil)
c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"multipass-support\"")
@@ -487,13 +492,13 @@ plugs:
snapDecl := s.mockSnapDecl(c, "multipass-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
cand.PlugSnapDeclaration = snapDecl
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, IsNil)
}
func (s *baseDeclSuite) TestAutoConnectionBlockDevicesOverride(c *C) {
cand := s.connectCand(c, "block-devices", "", "")
- err := cand.CheckAutoConnect()
+ _, err := cand.CheckAutoConnect()
c.Check(err, NotNil)
c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"block-devices\"")
@@ -505,13 +510,13 @@ plugs:
snapDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
cand.PlugSnapDeclaration = snapDecl
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, IsNil)
}
func (s *baseDeclSuite) TestAutoConnectionPackagekitControlOverride(c *C) {
cand := s.connectCand(c, "packagekit-control", "", "")
- err := cand.CheckAutoConnect()
+ _, err := cand.CheckAutoConnect()
c.Check(err, NotNil)
c.Assert(err, ErrorMatches, "auto-connection denied by plug rule of interface \"packagekit-control\"")
@@ -523,7 +528,7 @@ plugs:
snapDecl := s.mockSnapDecl(c, "some-snap", "J60k4JY0HppjwOjW8dZdYc8obXKxujRu", "canonical", plugsSlots)
cand.PlugSnapDeclaration = snapDecl
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, IsNil)
}
@@ -560,8 +565,9 @@ plugs:
cand := s.connectCand(c, iface.Name(), "", "")
cand.PlugSnapDeclaration = snapDecl
- err := cand.CheckAutoConnect()
+ arity, err := cand.CheckAutoConnect()
c.Check(err, IsNil)
+ c.Check(arity.SlotsPerPlugAny(), Equals, false)
}
}
@@ -945,7 +951,7 @@ plugs:
err := cand.Check()
c.Check(err, NotNil)
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, NotNil)
}
@@ -992,7 +998,7 @@ plugs:
cand := s.connectCand(c, "optical-drive", "", plugYaml)
err := cand.Check()
c.Check(err, checker)
- err = cand.CheckAutoConnect()
+ _, err = cand.CheckAutoConnect()
c.Check(err, checker)
}
diff --git a/interfaces/policy/helpers.go b/interfaces/policy/helpers.go
index f8a18ced8a..3ddd09b286 100644
--- a/interfaces/policy/helpers.go
+++ b/interfaces/policy/helpers.go
@@ -152,19 +152,19 @@ func checkPlugConnectionConstraints1(connc *ConnectCandidate, constraints *asser
return nil
}
-func checkPlugConnectionAltConstraints(connc *ConnectCandidate, altConstraints []*asserts.PlugConnectionConstraints) error {
+func checkPlugConnectionAltConstraints(connc *ConnectCandidate, altConstraints []*asserts.PlugConnectionConstraints) (*asserts.PlugConnectionConstraints, error) {
var firstErr error
// OR of constraints
for _, constraints := range altConstraints {
err := checkPlugConnectionConstraints1(connc, constraints)
if err == nil {
- return nil
+ return constraints, nil
}
if firstErr == nil {
firstErr = err
}
}
- return firstErr
+ return nil, firstErr
}
func checkSlotConnectionConstraints1(connc *ConnectCandidate, constraints *asserts.SlotConnectionConstraints) error {
@@ -195,19 +195,19 @@ func checkSlotConnectionConstraints1(connc *ConnectCandidate, constraints *asser
return nil
}
-func checkSlotConnectionAltConstraints(connc *ConnectCandidate, altConstraints []*asserts.SlotConnectionConstraints) error {
+func checkSlotConnectionAltConstraints(connc *ConnectCandidate, altConstraints []*asserts.SlotConnectionConstraints) (*asserts.SlotConnectionConstraints, error) {
var firstErr error
// OR of constraints
for _, constraints := range altConstraints {
err := checkSlotConnectionConstraints1(connc, constraints)
if err == nil {
- return nil
+ return constraints, nil
}
if firstErr == nil {
firstErr = err
}
}
- return firstErr
+ return nil, firstErr
}
func checkSnapTypeSlotInstallationConstraints1(ic *InstallCandidateMinimalCheck, slot *snap.SlotInfo, constraints *asserts.SlotInstallationConstraints) error {
@@ -303,3 +303,20 @@ func checkPlugInstallationAltConstraints(ic *InstallCandidate, plug *snap.PlugIn
}
return firstErr
}
+
+// sideArity carries relevant arity constraints for successful
+// allow-auto-connection rules. It implements policy.SideArity.
+// ATM only slots-per-plug might have an interesting non-default
+// value.
+// See: https://forum.snapcraft.io/t/plug-slot-declaration-rules-greedy-plugs/12438
+type sideArity struct {
+ slotsPerPlug asserts.SideArityConstraint
+}
+
+func (a sideArity) SlotsPerPlugOne() bool {
+ return a.slotsPerPlug.N == 1
+}
+
+func (a sideArity) SlotsPerPlugAny() bool {
+ return a.slotsPerPlug.Any()
+}
diff --git a/interfaces/policy/policy.go b/interfaces/policy/policy.go
index d546770fdf..9e2f157f41 100644
--- a/interfaces/policy/policy.go
+++ b/interfaces/policy/policy.go
@@ -176,7 +176,7 @@ func (connc *ConnectCandidate) slotPublisherID() string {
return "" // never a valid publisher-id
}
-func (connc *ConnectCandidate) checkPlugRule(kind string, rule *asserts.PlugRule, snapRule bool) error {
+func (connc *ConnectCandidate) checkPlugRule(kind string, rule *asserts.PlugRule, snapRule bool) (interfaces.SideArity, error) {
context := ""
if snapRule {
context = fmt.Sprintf(" for %q snap", connc.PlugSnapDeclaration.SnapName())
@@ -187,16 +187,18 @@ func (connc *ConnectCandidate) checkPlugRule(kind string, rule *asserts.PlugRule
denyConst = rule.DenyAutoConnection
allowConst = rule.AllowAutoConnection
}
- if checkPlugConnectionAltConstraints(connc, denyConst) == nil {
- return fmt.Errorf("%s denied by plug rule of interface %q%s", kind, connc.Plug.Interface(), context)
+ if _, err := checkPlugConnectionAltConstraints(connc, denyConst); err == nil {
+ return nil, fmt.Errorf("%s denied by plug rule of interface %q%s", kind, connc.Plug.Interface(), context)
}
- if checkPlugConnectionAltConstraints(connc, allowConst) != nil {
- return fmt.Errorf("%s not allowed by plug rule of interface %q%s", kind, connc.Plug.Interface(), context)
+
+ allowedConstraints, err := checkPlugConnectionAltConstraints(connc, allowConst)
+ if err != nil {
+ return nil, fmt.Errorf("%s not allowed by plug rule of interface %q%s", kind, connc.Plug.Interface(), context)
}
- return nil
+ return sideArity{allowedConstraints.SlotsPerPlug}, nil
}
-func (connc *ConnectCandidate) checkSlotRule(kind string, rule *asserts.SlotRule, snapRule bool) error {
+func (connc *ConnectCandidate) checkSlotRule(kind string, rule *asserts.SlotRule, snapRule bool) (interfaces.SideArity, error) {
context := ""
if snapRule {
context = fmt.Sprintf(" for %q snap", connc.SlotSnapDeclaration.SnapName())
@@ -207,25 +209,27 @@ func (connc *ConnectCandidate) checkSlotRule(kind string, rule *asserts.SlotRule
denyConst = rule.DenyAutoConnection
allowConst = rule.AllowAutoConnection
}
- if checkSlotConnectionAltConstraints(connc, denyConst) == nil {
- return fmt.Errorf("%s denied by slot rule of interface %q%s", kind, connc.Plug.Interface(), context)
+ if _, err := checkSlotConnectionAltConstraints(connc, denyConst); err == nil {
+ return nil, fmt.Errorf("%s denied by slot rule of interface %q%s", kind, connc.Plug.Interface(), context)
}
- if checkSlotConnectionAltConstraints(connc, allowConst) != nil {
- return fmt.Errorf("%s not allowed by slot rule of interface %q%s", kind, connc.Plug.Interface(), context)
+
+ allowedConstraints, err := checkSlotConnectionAltConstraints(connc, allowConst)
+ if err != nil {
+ return nil, fmt.Errorf("%s not allowed by slot rule of interface %q%s", kind, connc.Plug.Interface(), context)
}
- return nil
+ return sideArity{allowedConstraints.SlotsPerPlug}, nil
}
-func (connc *ConnectCandidate) check(kind string) error {
+func (connc *ConnectCandidate) check(kind string) (interfaces.SideArity, error) {
baseDecl := connc.BaseDeclaration
if baseDecl == nil {
- return fmt.Errorf("internal error: improperly initialized ConnectCandidate")
+ return nil, fmt.Errorf("internal error: improperly initialized ConnectCandidate")
}
iface := connc.Plug.Interface()
if connc.Slot.Interface() != iface {
- return fmt.Errorf("cannot connect mismatched plug interface %q to slot interface %q", iface, connc.Slot.Interface())
+ return nil, fmt.Errorf("cannot connect mismatched plug interface %q to slot interface %q", iface, connc.Slot.Interface())
}
if plugDecl := connc.PlugSnapDeclaration; plugDecl != nil {
@@ -244,17 +248,27 @@ func (connc *ConnectCandidate) check(kind string) error {
if rule := baseDecl.SlotRule(iface); rule != nil {
return connc.checkSlotRule(kind, rule, false)
}
- return nil
+ return nil, nil
}
// Check checks whether the connection is allowed.
func (connc *ConnectCandidate) Check() error {
- return connc.check("connection")
+ _, err := connc.check("connection")
+ return err
}
// CheckAutoConnect checks whether the connection is allowed to auto-connect.
-func (connc *ConnectCandidate) CheckAutoConnect() error {
- return connc.check("auto-connection")
+func (connc *ConnectCandidate) CheckAutoConnect() (interfaces.SideArity, error) {
+ arity, err := connc.check("auto-connection")
+ if err != nil {
+ return nil, err
+ }
+ if arity == nil {
+ // shouldn't happen but be safe, the callers should be able
+ // to assume arity to be non nil
+ arity = sideArity{asserts.SideArityConstraint{N: 1}}
+ }
+ return arity, nil
}
// InstallCandidateMinimalCheck represents a candidate snap installed with --dangerous flag that should pass minimum checks
diff --git a/interfaces/policy/policy_test.go b/interfaces/policy/policy_test.go
index 6b1ee984b1..3e189855d4 100644
--- a/interfaces/policy/policy_test.go
+++ b/interfaces/policy/policy_test.go
@@ -298,6 +298,20 @@ slots:
- debian
install-slot-device-scope:
allow-installation: false
+ slots-arity-default:
+ allow-auto-connection: true
+ slots-arity-slot-any:
+ deny-auto-connection: true
+ slots-arity-plug-any:
+ deny-auto-connection: true
+ slots-arity-slot-any-plug-one:
+ deny-auto-connection: true
+ slots-arity-slot-any-plug-two:
+ deny-auto-connection: true
+ slots-arity-slot-any-plug-default:
+ deny-auto-connection: true
+ slots-arity-slot-one-plug-any:
+ deny-auto-connection: true
timestamp: 2016-09-30T12:00:00Z
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
@@ -477,6 +491,14 @@ plugs:
plug-on-classic-true:
plug-on-classic-distros:
plug-on-classic-false:
+
+ slots-arity-default:
+ slots-arity-slot-any:
+ slots-arity-plug-any:
+ slots-arity-slot-any-plug-one:
+ slots-arity-slot-any-plug-two:
+ slots-arity-slot-any-plug-default:
+ slots-arity-slot-one-plug-any:
`, nil)
s.slotSnap = snaptest.MockInfo(c, `
@@ -642,6 +664,14 @@ slots:
plug-on-classic-true:
plug-on-classic-distros:
plug-on-classic-false:
+
+ slots-arity-default:
+ slots-arity-slot-any:
+ slots-arity-plug-any:
+ slots-arity-slot-any-plug-one:
+ slots-arity-slot-any-plug-two:
+ slots-arity-slot-any-plug-default:
+ slots-arity-slot-one-plug-any:
`, nil)
a, err = asserts.Decode([]byte(`type: snap-declaration
@@ -703,6 +733,20 @@ plugs:
on-model:
- my-brand/my-model1
- my-brand-subbrand/my-model2
+ slots-arity-plug-any:
+ allow-auto-connection:
+ slots-per-plug: *
+ slots-arity-slot-any-plug-one:
+ allow-auto-connection:
+ slots-per-plug: 1
+ slots-arity-slot-any-plug-two:
+ allow-auto-connection:
+ slots-per-plug: 2
+ slots-arity-slot-any-plug-default:
+ allow-auto-connection: true
+ slots-arity-slot-one-plug-any:
+ allow-auto-connection:
+ slots-per-plug: *
timestamp: 2016-09-30T12:00:00Z
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
@@ -767,6 +811,21 @@ slots:
on-model:
- my-brand/my-model1
- my-brand-subbrand/my-model2
+ slots-arity-slot-any:
+ allow-auto-connection:
+ slots-per-plug: *
+ slots-arity-slot-any-plug-one:
+ allow-auto-connection:
+ slots-per-plug: *
+ slots-arity-slot-any-plug-two:
+ allow-auto-connection:
+ slots-per-plug: *
+ slots-arity-slot-any-plug-default:
+ allow-auto-connection:
+ slots-per-plug: *
+ slots-arity-slot-one-plug-any:
+ allow-auto-connection:
+ slots-per-plug: 1
timestamp: 2016-09-30T12:00:00Z
sign-key-sha3-384: Jv8_JiHiIzJVcO9M55pPdqSDWUvuhfDIBJUS-3VW7F_idjix7Ffn5qMxB21ZQuij
@@ -814,7 +873,9 @@ func (s *policySuite) TestBaselineDefaultIsAllow(c *C) {
}
c.Check(cand.Check(), IsNil)
- c.Check(cand.CheckAutoConnect(), IsNil)
+ arity, err := cand.CheckAutoConnect()
+ c.Check(err, IsNil)
+ c.Check(arity.SlotsPerPlugAny(), Equals, false)
}
func (s *policySuite) TestInterfaceMismatch(c *C) {
@@ -896,9 +957,10 @@ func (s *policySuite) TestBaseDeclAllowDenyAutoConnection(c *C) {
BaseDeclaration: s.baseDecl,
}
- err := cand.CheckAutoConnect()
+ arity, err := cand.CheckAutoConnect()
if t.expected == "" {
c.Check(err, IsNil)
+ c.Check(arity.SlotsPerPlugAny(), Equals, false)
} else {
c.Check(err, ErrorMatches, t.expected)
}
@@ -968,9 +1030,10 @@ func (s *policySuite) TestSnapDeclAllowDenyAutoConnection(c *C) {
BaseDeclaration: s.baseDecl,
}
- err := cand.CheckAutoConnect()
+ arity, err := cand.CheckAutoConnect()
if t.expected == "" {
c.Check(err, IsNil)
+ c.Check(arity.SlotsPerPlugAny(), Equals, false)
} else {
c.Check(err, ErrorMatches, t.expected)
}
@@ -1866,9 +1929,10 @@ func (s *policySuite) TestPlugDeviceScopeCheckAutoConnection(c *C) {
Model: t.model,
}
- err := cand.CheckAutoConnect()
+ arity, err := cand.CheckAutoConnect()
if t.err == "" {
c.Check(err, IsNil)
+ c.Check(arity.SlotsPerPlugAny(), Equals, false)
} else {
c.Check(err, ErrorMatches, t.err)
}
@@ -1900,9 +1964,10 @@ func (s *policySuite) TestPlugDeviceScopeFriendlyStoreCheckAutoConnection(c *C)
Model: t.model,
Store: t.store,
}
- err := cand.CheckAutoConnect()
+ arity, err := cand.CheckAutoConnect()
if t.err == "" {
c.Check(err, IsNil)
+ c.Check(arity.SlotsPerPlugAny(), Equals, false)
} else {
c.Check(err, ErrorMatches, t.err)
}
@@ -1942,9 +2007,10 @@ func (s *policySuite) TestSlotDeviceScopeCheckAutoConnection(c *C) {
Model: t.model,
}
- err := cand.CheckAutoConnect()
+ arity, err := cand.CheckAutoConnect()
if t.err == "" {
c.Check(err, IsNil)
+ c.Check(arity.SlotsPerPlugAny(), Equals, false)
} else {
c.Check(err, ErrorMatches, t.err)
}
@@ -1976,9 +2042,10 @@ func (s *policySuite) TestSlotDeviceScopeFriendlyStoreCheckAutoConnection(c *C)
Model: t.model,
Store: t.store,
}
- err := cand.CheckAutoConnect()
+ arity, err := cand.CheckAutoConnect()
if t.err == "" {
c.Check(err, IsNil)
+ c.Check(arity.SlotsPerPlugAny(), Equals, false)
} else {
c.Check(err, ErrorMatches, t.err)
}
@@ -2220,3 +2287,32 @@ func (s *policySuite) TestPlugDollarSlotDynamicAttrConnection(c *C) {
}
c.Check(cand.Check(), IsNil)
}
+
+func (s *policySuite) TestSlotsArityAutoConnection(c *C) {
+ tests := []struct {
+ iface string
+ any bool
+ }{
+ {"slots-arity-default", false},
+ {"slots-arity-slot-any", true},
+ {"slots-arity-plug-any", true},
+ {"slots-arity-slot-any-plug-one", false},
+ {"slots-arity-slot-any-plug-two", false},
+ {"slots-arity-slot-any-plug-default", false},
+ {"slots-arity-slot-one-plug-any", true},
+ }
+
+ for _, t := range tests {
+ cand := policy.ConnectCandidate{
+ Plug: interfaces.NewConnectedPlug(s.plugSnap.Plugs[t.iface], nil, nil),
+ Slot: interfaces.NewConnectedSlot(s.slotSnap.Slots[t.iface], nil, nil),
+ PlugSnapDeclaration: s.plugDecl,
+ SlotSnapDeclaration: s.slotDecl,
+
+ BaseDeclaration: s.baseDecl,
+ }
+ arity, err := cand.CheckAutoConnect()
+ c.Assert(err, IsNil)
+ c.Check(arity.SlotsPerPlugAny(), Equals, t.any)
+ }
+}
diff --git a/interfaces/repo.go b/interfaces/repo.go
index 232c732a59..bdc38be9d0 100644
--- a/interfaces/repo.go
+++ b/interfaces/repo.go
@@ -1110,18 +1110,28 @@ func (r *Repository) DisconnectSnap(snapName string) ([]string, error) {
return result, nil
}
+// SideArity conveys the arity constraints for an allowed auto-connection.
+// ATM only slots-per-plug might have an interesting non-default
+// value.
+// See: https://forum.snapcraft.io/t/plug-slot-declaration-rules-greedy-plugs/12438
+type SideArity interface {
+ SlotsPerPlugAny() bool
+ // TODO: consider PlugsPerSlot*
+}
+
// AutoConnectCandidateSlots finds and returns viable auto-connection candidates
// for a given plug.
-func (r *Repository) AutoConnectCandidateSlots(plugSnapName, plugName string, policyCheck func(*ConnectedPlug, *ConnectedSlot) (bool, error)) []*snap.SlotInfo {
+func (r *Repository) AutoConnectCandidateSlots(plugSnapName, plugName string, policyCheck func(*ConnectedPlug, *ConnectedSlot) (bool, SideArity, error)) ([]*snap.SlotInfo, []SideArity) {
r.m.Lock()
defer r.m.Unlock()
plugInfo := r.plugs[plugSnapName][plugName]
if plugInfo == nil {
- return nil
+ return nil, nil
}
var candidates []*snap.SlotInfo
+ var arities []SideArity
for _, slotsForSnap := range r.slots {
for _, slotInfo := range slotsForSnap {
if slotInfo.Interface != plugInfo.Interface {
@@ -1130,22 +1140,23 @@ func (r *Repository) AutoConnectCandidateSlots(plugSnapName, plugName string, po
iface := slotInfo.Interface
// declaration based checks disallow
- ok, err := policyCheck(NewConnectedPlug(plugInfo, nil, nil), NewConnectedSlot(slotInfo, nil, nil))
+ ok, arity, err := policyCheck(NewConnectedPlug(plugInfo, nil, nil), NewConnectedSlot(slotInfo, nil, nil))
if !ok || err != nil {
continue
}
if r.ifaces[iface].AutoConnect(plugInfo, slotInfo) {
candidates = append(candidates, slotInfo)
+ arities = append(arities, arity)
}
}
}
- return candidates
+ return candidates, arities
}
// AutoConnectCandidatePlugs finds and returns viable auto-connection candidates
// for a given slot.
-func (r *Repository) AutoConnectCandidatePlugs(slotSnapName, slotName string, policyCheck func(*ConnectedPlug, *ConnectedSlot) (bool, error)) []*snap.PlugInfo {
+func (r *Repository) AutoConnectCandidatePlugs(slotSnapName, slotName string, policyCheck func(*ConnectedPlug, *ConnectedSlot) (bool, SideArity, error)) []*snap.PlugInfo {
r.m.Lock()
defer r.m.Unlock()
@@ -1163,7 +1174,7 @@ func (r *Repository) AutoConnectCandidatePlugs(slotSnapName, slotName string, po
iface := slotInfo.Interface
// declaration based checks disallow
- ok, err := policyCheck(NewConnectedPlug(plugInfo, nil, nil), NewConnectedSlot(slotInfo, nil, nil))
+ ok, _, err := policyCheck(NewConnectedPlug(plugInfo, nil, nil), NewConnectedSlot(slotInfo, nil, nil))
if !ok || err != nil {
continue
}
diff --git a/interfaces/repo_test.go b/interfaces/repo_test.go
index e6a6ad28a9..2a6f25ba16 100644
--- a/interfaces/repo_test.go
+++ b/interfaces/repo_test.go
@@ -21,6 +21,7 @@ package interfaces_test
import (
"fmt"
+ "strings"
. "gopkg.in/check.v1"
@@ -1683,6 +1684,14 @@ func (s *RepositorySuite) TestSnapSpecificationFailureWithPermanentSnippets(c *C
c.Assert(spec, IsNil)
}
+type testSideArity struct {
+ sideSnapName string
+}
+
+func (a *testSideArity) SlotsPerPlugAny() bool {
+ return strings.HasSuffix(a.sideSnapName, "2")
+}
+
func (s *RepositorySuite) TestAutoConnectCandidatePlugsAndSlots(c *C) {
// Add two interfaces, one with automatic connections, one with manual
repo := s.emptyRepo
@@ -1691,8 +1700,8 @@ func (s *RepositorySuite) TestAutoConnectCandidatePlugsAndSlots(c *C) {
err = repo.AddInterface(&ifacetest.TestInterface{InterfaceName: "manual"})
c.Assert(err, IsNil)
- policyCheck := func(plug *ConnectedPlug, slot *ConnectedSlot) (bool, error) {
- return slot.Interface() == "auto", nil
+ policyCheck := func(plug *ConnectedPlug, slot *ConnectedSlot) (bool, SideArity, error) {
+ return slot.Interface() == "auto", &testSideArity{plug.Snap().InstanceName()}, nil
}
// Add a pair of snaps with plugs/slots using those two interfaces
@@ -1716,11 +1725,13 @@ slots:
err = repo.AddSnap(consumer)
c.Assert(err, IsNil)
- candidateSlots := repo.AutoConnectCandidateSlots("consumer", "auto", policyCheck)
+ candidateSlots, arities := repo.AutoConnectCandidateSlots("consumer", "auto", policyCheck)
c.Assert(candidateSlots, HasLen, 1)
c.Check(candidateSlots[0].Snap.InstanceName(), Equals, "producer")
c.Check(candidateSlots[0].Interface, Equals, "auto")
c.Check(candidateSlots[0].Name, Equals, "auto")
+ c.Assert(arities, HasLen, 1)
+ c.Check(arities[0].SlotsPerPlugAny(), Equals, false)
candidatePlugs := repo.AutoConnectCandidatePlugs("producer", "auto", policyCheck)
c.Assert(candidatePlugs, HasLen, 1)
@@ -1735,8 +1746,8 @@ func (s *RepositorySuite) TestAutoConnectCandidatePlugsAndSlotsSymmetry(c *C) {
err := repo.AddInterface(&ifacetest.TestInterface{InterfaceName: "auto"})
c.Assert(err, IsNil)
- policyCheck := func(plug *ConnectedPlug, slot *ConnectedSlot) (bool, error) {
- return slot.Interface() == "auto", nil
+ policyCheck := func(plug *ConnectedPlug, slot *ConnectedSlot) (bool, SideArity, error) {
+ return slot.Interface() == "auto", &testSideArity{plug.Snap().InstanceName()}, nil
}
// Add a producer snap for "auto"
@@ -1773,17 +1784,21 @@ plugs:
c.Assert(err, IsNil)
// Both can auto-connect
- candidateSlots := repo.AutoConnectCandidateSlots("consumer1", "auto", policyCheck)
+ candidateSlots, arities := repo.AutoConnectCandidateSlots("consumer1", "auto", policyCheck)
c.Assert(candidateSlots, HasLen, 1)
c.Check(candidateSlots[0].Snap.InstanceName(), Equals, "producer")
c.Check(candidateSlots[0].Interface, Equals, "auto")
c.Check(candidateSlots[0].Name, Equals, "auto")
+ c.Assert(arities, HasLen, 1)
+ c.Check(arities[0].SlotsPerPlugAny(), Equals, false)
- candidateSlots = repo.AutoConnectCandidateSlots("consumer2", "auto", policyCheck)
+ candidateSlots, arities = repo.AutoConnectCandidateSlots("consumer2", "auto", policyCheck)
c.Assert(candidateSlots, HasLen, 1)
c.Check(candidateSlots[0].Snap.InstanceName(), Equals, "producer")
c.Check(candidateSlots[0].Interface, Equals, "auto")
c.Check(candidateSlots[0].Name, Equals, "auto")
+ c.Assert(arities, HasLen, 1)
+ c.Check(arities[0].SlotsPerPlugAny(), Equals, true)
// Plugs candidates seen from the producer (for example if
// it's installed after) should be the same
@@ -1791,6 +1806,69 @@ plugs:
c.Assert(candidatePlugs, HasLen, 2)
}
+func (s *RepositorySuite) TestAutoConnectCandidateSlotsSideArity(c *C) {
+ repo := s.emptyRepo
+ // Add a "auto" interface
+ err := repo.AddInterface(&ifacetest.TestInterface{InterfaceName: "auto"})
+ c.Assert(err, IsNil)
+
+ policyCheck := func(plug *ConnectedPlug, slot *ConnectedSlot) (bool, SideArity, error) {
+ return slot.Interface() == "auto", &testSideArity{slot.Snap().InstanceName()}, nil
+ }
+
+ // Add two producer snaps for "auto"
+ producer1 := snaptest.MockInfo(c, `
+name: producer1
+version: 0
+slots:
+ auto:
+`, nil)
+ err = repo.AddSnap(producer1)
+ c.Assert(err, IsNil)
+
+ producer2 := snaptest.MockInfo(c, `
+name: producer2
+version: 0
+slots:
+ auto:
+`, nil)
+ err = repo.AddSnap(producer2)
+ c.Assert(err, IsNil)
+
+ // Add a consumer snap for "auto"
+ consumer := snaptest.MockInfo(c, `
+name: consumer
+version: 0
+plugs:
+ auto:
+`, nil)
+ err = repo.AddSnap(consumer)
+ c.Assert(err, IsNil)
+
+ // Both slots could auto-connect
+ seenProducers := make(map[string]bool)
+ candidateSlots, arities := repo.AutoConnectCandidateSlots("consumer", "auto", policyCheck)
+ c.Assert(candidateSlots, HasLen, 2)
+ c.Assert(arities, HasLen, 2)
+ for i, candSlot := range candidateSlots {
+ c.Check(candSlot.Interface, Equals, "auto")
+ c.Check(candSlot.Name, Equals, "auto")
+ producerName := candSlot.Snap.InstanceName()
+ // SideArities match
+ switch producerName {
+ case "producer1":
+ c.Check(arities[i].SlotsPerPlugAny(), Equals, false)
+ case "producer2":
+ c.Check(arities[i].SlotsPerPlugAny(), Equals, true)
+ }
+ seenProducers[producerName] = true
+ }
+ c.Check(seenProducers, DeepEquals, map[string]bool{
+ "producer1": true,
+ "producer2": true,
+ })
+}
+
// Tests for AddSnap and RemoveSnap
type AddRemoveSuite struct {
@@ -2064,8 +2142,8 @@ func (s *DisconnectSnapSuite) TestParallelInstances(c *C) {
c.Check(affected, testutil.Contains, "s2_instance")
}
-func contentPolicyCheck(plug *ConnectedPlug, slot *ConnectedSlot) (bool, error) {
- return plug.Snap().Publisher.ID == slot.Snap().Publisher.ID, nil
+func contentPolicyCheck(plug *ConnectedPlug, slot *ConnectedSlot) (bool, SideArity, error) {
+ return plug.Snap().Publisher.ID == slot.Snap().Publisher.ID, nil, nil
}
func contentAutoConnect(plug *snap.PlugInfo, slot *snap.SlotInfo) bool {
@@ -2106,7 +2184,7 @@ slots:
func (s *RepositorySuite) TestAutoConnectContentInterfaceSimple(c *C) {
repo, _, _ := makeContentConnectionTestSnaps(c, "mylib", "mylib")
- candidateSlots := repo.AutoConnectCandidateSlots("content-plug-snap", "imported-content", contentPolicyCheck)
+ candidateSlots, _ := repo.AutoConnectCandidateSlots("content-plug-snap", "imported-content", contentPolicyCheck)
c.Assert(candidateSlots, HasLen, 1)
c.Check(candidateSlots[0].Name, Equals, "exported-content")
candidatePlugs := repo.AutoConnectCandidatePlugs("content-slot-snap", "exported-content", contentPolicyCheck)
@@ -2118,7 +2196,7 @@ func (s *RepositorySuite) TestAutoConnectContentInterfaceOSWorksCorrectly(c *C)
repo, _, slotSnap := makeContentConnectionTestSnaps(c, "mylib", "otherlib")
slotSnap.SnapType = snap.TypeOS
- candidateSlots := repo.AutoConnectCandidateSlots("content-plug-snap", "imported-content", contentPolicyCheck)
+ candidateSlots, _ := repo.AutoConnectCandidateSlots("content-plug-snap", "imported-content", contentPolicyCheck)
c.Check(candidateSlots, HasLen, 0)
candidatePlugs := repo.AutoConnectCandidatePlugs("content-slot-snap", "exported-content", contentPolicyCheck)
c.Assert(candidatePlugs, HasLen, 0)
@@ -2126,7 +2204,7 @@ func (s *RepositorySuite) TestAutoConnectContentInterfaceOSWorksCorrectly(c *C)
func (s *RepositorySuite) TestAutoConnectContentInterfaceNoMatchingContent(c *C) {
repo, _, _ := makeContentConnectionTestSnaps(c, "mylib", "otherlib")
- candidateSlots := repo.AutoConnectCandidateSlots("content-plug-snap", "imported-content", contentPolicyCheck)
+ candidateSlots, _ := repo.AutoConnectCandidateSlots("content-plug-snap", "imported-content", contentPolicyCheck)
c.Check(candidateSlots, HasLen, 0)
candidatePlugs := repo.AutoConnectCandidatePlugs("content-slot-snap", "exported-content", contentPolicyCheck)
c.Assert(candidatePlugs, HasLen, 0)
@@ -2138,7 +2216,7 @@ func (s *RepositorySuite) TestAutoConnectContentInterfaceNoMatchingDeveloper(c *
plugSnap.Publisher.ID = "fooid"
slotSnap.Publisher.ID = "barid"
- candidateSlots := repo.AutoConnectCandidateSlots("content-plug-snap", "imported-content", contentPolicyCheck)
+ candidateSlots, _ := repo.AutoConnectCandidateSlots("content-plug-snap", "imported-content", contentPolicyCheck)
c.Check(candidateSlots, HasLen, 0)
candidatePlugs := repo.AutoConnectCandidatePlugs("content-slot-snap", "exported-content", contentPolicyCheck)
c.Assert(candidatePlugs, HasLen, 0)
diff --git a/overlord/devicestate/devicestate.go b/overlord/devicestate/devicestate.go
index 523f500a0b..9f09fb0f61 100644
--- a/overlord/devicestate/devicestate.go
+++ b/overlord/devicestate/devicestate.go
@@ -319,6 +319,15 @@ func extractDownloadInstallEdgesFromTs(ts *state.TaskSet) (firstDl, lastDl, firs
return firstDl, tasks[edgeTaskIndex], tasks[edgeTaskIndex+1], lastInst, nil
}
+func notInstalled(st *state.State, name string) (bool, error) {
+ _, err := snapstate.CurrentInfo(st, name)
+ _, isNotInstalled := err.(*snap.NotInstalledError)
+ if isNotInstalled {
+ return true, nil
+ }
+ return false, err
+}
+
func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Model, deviceCtx snapstate.DeviceContext, fromChange string) ([]*state.TaskSet, error) {
userID := 0
var tss []*state.TaskSet
@@ -331,14 +340,33 @@ func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Mo
}
tss = append(tss, ts)
}
+
+ var ts *state.TaskSet
if current.Kernel() != new.Kernel() {
- // TODO: we need to support corner cases here like:
- // 0. start with "old-kernel"
- // 1. remodel to "new-kernel"
- // 2. remodel back to "old-kernel"
- // In step (2) we will get a "already-installed" error
- // here right now (workaround: remove "old-kernel")
- ts, err := snapstateInstallWithDeviceContext(ctx, st, new.Kernel(), &snapstate.RevisionOptions{Channel: new.KernelTrack()}, userID, snapstate.Flags{}, deviceCtx, fromChange)
+ needsInstall, err := notInstalled(st, new.Kernel())
+ if err != nil {
+ return nil, err
+ }
+ if needsInstall {
+ ts, err = snapstateInstallWithDeviceContext(ctx, st, new.Kernel(), &snapstate.RevisionOptions{Channel: new.KernelTrack()}, userID, snapstate.Flags{}, deviceCtx, fromChange)
+ } else {
+ ts, err = snapstate.LinkNewBaseOrKernel(st, new.Base())
+ }
+ if err != nil {
+ return nil, err
+ }
+ tss = append(tss, ts)
+ }
+ if current.Base() != new.Base() {
+ needsInstall, err := notInstalled(st, new.Base())
+ if err != nil {
+ return nil, err
+ }
+ if needsInstall {
+ ts, err = snapstateInstallWithDeviceContext(ctx, st, new.Base(), nil, userID, snapstate.Flags{}, deviceCtx, fromChange)
+ } else {
+ ts, err = snapstate.LinkNewBaseOrKernel(st, new.Base())
+ }
if err != nil {
return nil, err
}
@@ -365,16 +393,17 @@ func remodelTasks(ctx context.Context, st *state.State, current, new *asserts.Mo
for _, snapRef := range new.RequiredNoEssentialSnaps() {
// TODO|XXX: have methods that take refs directly
// to respect the snap ids
- _, err := snapstate.CurrentInfo(st, snapRef.SnapName())
- // If the snap is not installed we need to install it now.
- if _, ok := err.(*snap.NotInstalledError); ok {
+ needsInstall, err := notInstalled(st, snapRef.SnapName())
+ if err != nil {
+ return nil, err
+ }
+ if needsInstall {
+ // If the snap is not installed we need to install it now.
ts, err := snapstateInstallWithDeviceContext(ctx, st, snapRef.SnapName(), nil, userID, snapstate.Flags{Required: true}, deviceCtx, fromChange)
if err != nil {
return nil, err
}
tss = append(tss, ts)
- } else if err != nil {
- return nil, err
}
}
// TODO: Validate that all bases and default-providers are part
@@ -490,9 +519,9 @@ func Remodel(st *state.State, new *asserts.Model) (*state.Change, error) {
}
// calculate snap differences between the two models
- // FIXME: this needs work to switch the base to boot as well
- if current.Base() != new.Base() {
- return nil, fmt.Errorf("cannot remodel to different bases yet")
+ // FIXME: this needs work to switch from core->bases
+ if current.Base() == "" && new.Base() != "" {
+ return nil, fmt.Errorf("cannot remodel from core to bases yet")
}
// TODO: should we run a remodel only while no other change is
diff --git a/overlord/devicestate/devicestate_remodel_test.go b/overlord/devicestate/devicestate_remodel_test.go
index e7c70f3049..f2058b424f 100644
--- a/overlord/devicestate/devicestate_remodel_test.go
+++ b/overlord/devicestate/devicestate_remodel_test.go
@@ -80,7 +80,6 @@ func (s *deviceMgrRemodelSuite) TestRemodelUnhappy(c *C) {
"architecture": "amd64",
"kernel": "pc-kernel",
"gadget": "pc",
- "base": "core18",
}
s.makeModelAssertionInState(c, cur["brand"], cur["model"], map[string]interface{}{
"architecture": cur["architecture"],
@@ -99,7 +98,7 @@ func (s *deviceMgrRemodelSuite) TestRemodelUnhappy(c *C) {
errStr string
}{
{map[string]string{"architecture": "pdp-7"}, "cannot remodel to different architectures yet"},
- {map[string]string{"base": "core20"}, "cannot remodel to different bases yet"},
+ {map[string]string{"base": "core20"}, "cannot remodel from core to bases yet"},
} {
// copy current model unless new model test data is different
for k, v := range cur {
@@ -1402,3 +1401,59 @@ func (s *deviceMgrRemodelSuite) TestRemodelGadgetAssetsParanoidCheck(c *C) {
c.Check(gadgetUpdateCalled, Equals, false)
c.Check(s.restartRequests, HasLen, 0)
}
+
+func (s *deviceMgrSuite) TestRemodelSwitchBase(c *C) {
+ s.state.Lock()
+ defer s.state.Unlock()
+ s.state.Set("seeded", true)
+ s.state.Set("refresh-privacy-key", "some-privacy-key")
+
+ var testDeviceCtx snapstate.DeviceContext
+
+ var snapstateInstallWithDeviceContextCalled int
+ restore := devicestate.MockSnapstateInstallWithDeviceContext(func(ctx context.Context, st *state.State, name string, opts *snapstate.RevisionOptions, userID int, flags snapstate.Flags, deviceCtx snapstate.DeviceContext, fromChange string) (*state.TaskSet, error) {
+ snapstateInstallWithDeviceContextCalled++
+ c.Check(name, Equals, "core20")
+
+ tDownload := s.state.NewTask("fake-download", fmt.Sprintf("Download %s", name))
+ tValidate := s.state.NewTask("validate-snap", fmt.Sprintf("Validate %s", name))
+ tValidate.WaitFor(tDownload)
+ tInstall := s.state.NewTask("fake-install", fmt.Sprintf("Install %s", name))
+ tInstall.WaitFor(tValidate)
+ ts := state.NewTaskSet(tDownload, tValidate, tInstall)
+ ts.MarkEdge(tValidate, snapstate.DownloadAndChecksDoneEdge)
+ return ts, nil
+ })
+ defer restore()
+
+ // set a model assertion
+ current := s.brands.Model("canonical", "pc-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core18",
+ })
+ err := assertstate.Add(s.state, current)
+ c.Assert(err, IsNil)
+ devicestatetest.SetDevice(s.state, &auth.DeviceState{
+ Brand: "canonical",
+ Model: "pc-model",
+ })
+
+ new := s.brands.Model("canonical", "pc-model", map[string]interface{}{
+ "architecture": "amd64",
+ "kernel": "pc-kernel",
+ "gadget": "pc",
+ "base": "core20",
+ "revision": "1",
+ })
+
+ testDeviceCtx = &snapstatetest.TrivialDeviceContext{Remodeling: true}
+
+ tss, err := devicestate.RemodelTasks(context.Background(), s.state, current, new, testDeviceCtx, "99")
+ c.Assert(err, IsNil)
+ // 1 switch to a new base plus the remodel task
+ c.Assert(tss, HasLen, 2)
+ // API was hit
+ c.Assert(snapstateInstallWithDeviceContextCalled, Equals, 1)
+}
diff --git a/overlord/ifacestate/handlers.go b/overlord/ifacestate/handlers.go
index 8ea11532d6..98ee7465e4 100644
--- a/overlord/ifacestate/handlers.go
+++ b/overlord/ifacestate/handlers.go
@@ -465,7 +465,10 @@ func (m *InterfaceManager) doConnect(task *state.Task, _ *tomb.Tomb) error {
if err != nil {
return err
}
- policyChecker = autochecker.check
+ policyChecker = func(plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) (bool, error) {
+ ok, _, err := autochecker.check(plug, slot)
+ return ok, err
+ }
} else {
policyCheck, err := newConnectChecker(st, deviceCtx)
if err != nil {
diff --git a/overlord/ifacestate/helpers.go b/overlord/ifacestate/helpers.go
index 486a1f3cb5..a083fdcd1e 100644
--- a/overlord/ifacestate/helpers.go
+++ b/overlord/ifacestate/helpers.go
@@ -493,7 +493,7 @@ func (c *autoConnectChecker) snapDeclaration(snapID string) (*asserts.SnapDeclar
return snapDecl, nil
}
-func (c *autoConnectChecker) check(plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) (bool, error) {
+func (c *autoConnectChecker) check(plug *interfaces.ConnectedPlug, slot *interfaces.ConnectedSlot) (bool, interfaces.SideArity, error) {
modelAs := c.deviceCtx.Model()
var storeAs *asserts.Store
@@ -501,7 +501,7 @@ func (c *autoConnectChecker) check(plug *interfaces.ConnectedPlug, slot *interfa
var err error
storeAs, err = assertstate.Store(c.st, modelAs.Store())
if err != nil && !asserts.IsNotFound(err) {
- return false, err
+ return false, nil, err
}
}
@@ -511,7 +511,7 @@ func (c *autoConnectChecker) check(plug *interfaces.ConnectedPlug, slot *interfa
plugDecl, err = c.snapDeclaration(plug.Snap().SnapID)
if err != nil {
logger.Noticef("error: cannot find snap declaration for %q: %v", plug.Snap().InstanceName(), err)
- return false, nil
+ return false, nil, nil
}
}
@@ -521,7 +521,7 @@ func (c *autoConnectChecker) check(plug *interfaces.ConnectedPlug, slot *interfa
slotDecl, err = c.snapDeclaration(slot.Snap().SnapID)
if err != nil {
logger.Noticef("error: cannot find snap declaration for %q: %v", slot.Snap().InstanceName(), err)
- return false, nil
+ return false, nil, nil
}
}
@@ -536,22 +536,29 @@ func (c *autoConnectChecker) check(plug *interfaces.ConnectedPlug, slot *interfa
Store: storeAs,
}
- return ic.CheckAutoConnect() == nil, nil
+ arity, err := ic.CheckAutoConnect()
+ if err == nil {
+ return true, arity, nil
+ }
+
+ return false, nil, nil
}
// filterUbuntuCoreSlots filters out any ubuntu-core slots,
// if there are both ubuntu-core and core slots. This would occur
// during a ubuntu-core -> core transition.
-func filterUbuntuCoreSlots(candidates []*snap.SlotInfo) []*snap.SlotInfo {
+func filterUbuntuCoreSlots(candidates []*snap.SlotInfo, arities []interfaces.SideArity) ([]*snap.SlotInfo, []interfaces.SideArity) {
hasCore := false
hasUbuntuCore := false
var withoutUbuntuCore []*snap.SlotInfo
+ var withoutUbuntuCoreArities []interfaces.SideArity
for i, candSlot := range candidates {
switch candSlot.Snap.InstanceName() {
case "ubuntu-core":
if !hasUbuntuCore {
hasUbuntuCore = true
withoutUbuntuCore = append(withoutUbuntuCore, candidates[:i]...)
+ withoutUbuntuCoreArities = append(withoutUbuntuCoreArities, arities[:i]...)
}
case "core":
hasCore = true
@@ -559,13 +566,15 @@ func filterUbuntuCoreSlots(candidates []*snap.SlotInfo) []*snap.SlotInfo {
default:
if hasUbuntuCore {
withoutUbuntuCore = append(withoutUbuntuCore, candSlot)
+ withoutUbuntuCoreArities = append(withoutUbuntuCoreArities, arities[i])
}
}
}
if hasCore && hasUbuntuCore {
candidates = withoutUbuntuCore
+ arities = withoutUbuntuCoreArities
}
- return candidates
+ return candidates, arities
}
// addAutoConnections adds to newconns any applicable auto-connections
@@ -576,7 +585,7 @@ func filterUbuntuCoreSlots(candidates []*snap.SlotInfo) []*snap.SlotInfo {
// to handle checkAutoconnectConflicts errors.
func (c *autoConnectChecker) addAutoConnections(newconns map[string]*interfaces.ConnRef, plugs []*snap.PlugInfo, filter func([]*snap.SlotInfo) []*snap.SlotInfo, conns map[string]*connState, cannotAutoConnectLog func(plug *snap.PlugInfo, candRefs []string) string, conflictError func(*state.Retry, error) error) error {
for _, plug := range plugs {
- candSlots := c.repo.AutoConnectCandidateSlots(plug.Snap.InstanceName(), plug.Name, c.check)
+ candSlots, arities := c.repo.AutoConnectCandidateSlots(plug.Snap.InstanceName(), plug.Name, c.check)
if len(candSlots) == 0 {
continue
@@ -587,12 +596,18 @@ func (c *autoConnectChecker) addAutoConnections(newconns map[string]*interfaces.
// providing the same interface. In that situation we
// want to ignore any candidates in ubuntu-core and
// simply go with those from the new core snap.
- candSlots = filterUbuntuCoreSlots(candSlots)
+ candSlots, arities = filterUbuntuCoreSlots(candSlots, arities)
applicable := candSlots
// candidate arity check
- if len(candSlots) != 1 {
- applicable = nil
+ for _, arity := range arities {
+ if !arity.SlotsPerPlugAny() {
+ // ATM not any (*) => none or exactly one
+ if len(candSlots) != 1 {
+ applicable = nil
+ }
+ break
+ }
}
if filter != nil {
diff --git a/overlord/ifacestate/ifacestate_test.go b/overlord/ifacestate/ifacestate_test.go
index 553be90c04..1b95a5dc69 100644
--- a/overlord/ifacestate/ifacestate_test.go
+++ b/overlord/ifacestate/ifacestate_test.go
@@ -7224,3 +7224,195 @@ func (s *interfaceManagerSuite) TestTransitionConnectionsCoreMigration(c *C) {
c.Assert(err, IsNil)
c.Assert(repoConns, HasLen, 0)
}
+
+func (s *interfaceManagerSuite) TestDoSetupSnapSecurityAutoConnectsDeclBasedAnySlotsPerPlugPlugSide(c *C) {
+ s.MockModel(c, nil)
+
+ // the producer snap
+ s.MockSnapDecl(c, "theme1", "one-publisher", nil)
+
+ // 2nd producer snap
+ s.MockSnapDecl(c, "theme2", "one-publisher", nil)
+
+ // the consumer
+ s.MockSnapDecl(c, "theme-consumer", "one-publisher", map[string]interface{}{
+ "format": "1",
+ "plugs": map[string]interface{}{
+ "content": map[string]interface{}{
+ "allow-auto-connection": map[string]interface{}{
+ "slots-per-plug": "*",
+ },
+ },
+ },
+ })
+
+ check := func(conns map[string]interface{}, repoConns []*interfaces.ConnRef) {
+ c.Check(repoConns, HasLen, 2)
+
+ c.Check(conns, DeepEquals, map[string]interface{}{
+ "theme-consumer:plug theme1:slot": map[string]interface{}{
+ "auto": true,
+ "interface": "content",
+ "plug-static": map[string]interface{}{"content": "themes"},
+ "slot-static": map[string]interface{}{"content": "themes"},
+ },
+ "theme-consumer:plug theme2:slot": map[string]interface{}{
+ "auto": true,
+ "interface": "content",
+ "plug-static": map[string]interface{}{"content": "themes"},
+ "slot-static": map[string]interface{}{"content": "themes"},
+ },
+ })
+ }
+
+ s.testDoSetupSnapSecurityAutoConnectsDeclBasedAnySlotsPerPlug(c, check)
+}
+
+func (s *interfaceManagerSuite) testDoSetupSnapSecurityAutoConnectsDeclBasedAnySlotsPerPlug(c *C, check func(map[string]interface{}, []*interfaces.ConnRef)) {
+ const theme1Yaml = `
+name: theme1
+version: 1
+slots:
+ slot:
+ interface: content
+ content: themes
+`
+ s.mockSnap(c, theme1Yaml)
+ const theme2Yaml = `
+name: theme2
+version: 1
+slots:
+ slot:
+ interface: content
+ content: themes
+`
+ s.mockSnap(c, theme2Yaml)
+
+ mgr := s.manager(c)
+
+ const themeConsumerYaml = `
+name: theme-consumer
+version: 1
+plugs:
+ plug:
+ interface: content
+ content: themes
+`
+ snapInfo := s.mockSnap(c, themeConsumerYaml)
+
+ // Run the setup-snap-security task and let it finish.
+ change := s.addSetupSnapSecurityChange(c, &snapstate.SnapSetup{
+ SideInfo: &snap.SideInfo{
+ RealName: snapInfo.SnapName(),
+ SnapID: snapInfo.SnapID,
+ Revision: snapInfo.Revision,
+ },
+ })
+ s.settle(c)
+
+ s.state.Lock()
+ defer s.state.Unlock()
+
+ // Ensure that the task succeeded.
+ c.Assert(change.Status(), Equals, state.DoneStatus)
+
+ var conns map[string]interface{}
+ _ = s.state.Get("conns", &conns)
+
+ repo := mgr.Repository()
+ plug := repo.Plug("theme-consumer", "plug")
+ c.Assert(plug, Not(IsNil))
+
+ check(conns, repo.Interfaces().Connections)
+}
+
+func (s *interfaceManagerSuite) TestDoSetupSnapSecurityAutoConnectsDeclBasedAnySlotsPerPlugSlotSide(c *C) {
+ s.MockModel(c, nil)
+
+ // the producer snap
+ s.MockSnapDecl(c, "theme1", "one-publisher", map[string]interface{}{
+ "format": "1",
+ "slots": map[string]interface{}{
+ "content": map[string]interface{}{
+ "allow-auto-connection": map[string]interface{}{
+ "slots-per-plug": "*",
+ },
+ },
+ },
+ })
+
+ // 2nd producer snap
+ s.MockSnapDecl(c, "theme2", "one-publisher", map[string]interface{}{
+ "format": "1",
+ "slots": map[string]interface{}{
+ "content": map[string]interface{}{
+ "allow-auto-connection": map[string]interface{}{
+ "slots-per-plug": "*",
+ },
+ },
+ },
+ })
+
+ // the consumer
+ s.MockSnapDecl(c, "theme-consumer", "one-publisher", nil)
+
+ check := func(conns map[string]interface{}, repoConns []*interfaces.ConnRef) {
+ c.Check(repoConns, HasLen, 2)
+
+ c.Check(conns, DeepEquals, map[string]interface{}{
+ "theme-consumer:plug theme1:slot": map[string]interface{}{
+ "auto": true,
+ "interface": "content",
+ "plug-static": map[string]interface{}{"content": "themes"},
+ "slot-static": map[string]interface{}{"content": "themes"},
+ },
+ "theme-consumer:plug theme2:slot": map[string]interface{}{
+ "auto": true,
+ "interface": "content",
+ "plug-static": map[string]interface{}{"content": "themes"},
+ "slot-static": map[string]interface{}{"content": "themes"},
+ },
+ })
+ }
+
+ s.testDoSetupSnapSecurityAutoConnectsDeclBasedAnySlotsPerPlug(c, check)
+}
+
+func (s *interfaceManagerSuite) TestDoSetupSnapSecurityAutoConnectsDeclBasedAnySlotsPerPlugAmbiguity(c *C) {
+ s.MockModel(c, nil)
+
+ // the producer snap
+ s.MockSnapDecl(c, "theme1", "one-publisher", map[string]interface{}{
+ "format": "1",
+ "slots": map[string]interface{}{
+ "content": map[string]interface{}{
+ "allow-auto-connection": map[string]interface{}{
+ "slots-per-plug": "*",
+ },
+ },
+ },
+ })
+
+ // 2nd producer snap
+ s.MockSnapDecl(c, "theme2", "one-publisher", map[string]interface{}{
+ "format": "1",
+ "slots": map[string]interface{}{
+ "content": map[string]interface{}{
+ "allow-auto-connection": map[string]interface{}{
+ "slots-per-plug": "1",
+ },
+ },
+ },
+ })
+
+ // the consumer
+ s.MockSnapDecl(c, "theme-consumer", "one-publisher", nil)
+
+ check := func(conns map[string]interface{}, repoConns []*interfaces.ConnRef) {
+ // slots-per-plug were ambigous, nothing was connected
+ c.Check(repoConns, HasLen, 0)
+ c.Check(conns, HasLen, 0)
+ }
+
+ s.testDoSetupSnapSecurityAutoConnectsDeclBasedAnySlotsPerPlug(c, check)
+}
diff --git a/overlord/managers_test.go b/overlord/managers_test.go
index 5ae42ca935..d2771d46fc 100644
--- a/overlord/managers_test.go
+++ b/overlord/managers_test.go
@@ -3414,8 +3414,10 @@ func validateInstallTasks(c *C, tasks []*state.Task, name, revno string, flags i
i++
c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Start snap "%s" (%s) services`, name, revno))
i++
- c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Run configure hook of "%s" snap if present`, name))
- i++
+ if flags&noConfigure == 0 {
+ c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Run configure hook of "%s" snap if present`, name))
+ i++
+ }
c.Assert(tasks[i].Summary(), Equals, fmt.Sprintf(`Run health check of "%s" snap`, name))
i++
return i
@@ -3688,10 +3690,432 @@ type: base`
})
chg, err := devicestate.Remodel(st, newModel)
- c.Assert(err, ErrorMatches, "cannot remodel to different bases yet")
+ c.Assert(err, ErrorMatches, "cannot remodel from core to bases yet")
c.Assert(chg, IsNil)
}
+func (ms *mgrsSuite) TestRemodelSwitchToDifferentBase(c *C) {
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
+ defer bootloader.Force(nil)
+ bloader.SetBootVars(map[string]string{
+ "snap_mode": "",
+ "snap_core": "core18_1.snap",
+ "snap_kernel": "pc-kernel_1.snap",
+ })
+
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ mockServer := ms.mockStore(c)
+ defer mockServer.Close()
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ si := &snap.SideInfo{RealName: "core18", SnapID: fakeSnapID("core18"), Revision: snap.R(1)}
+ snapstate.Set(st, "core18", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{si},
+ Current: snap.R(1),
+ SnapType: "base",
+ })
+ si2 := &snap.SideInfo{RealName: "pc", SnapID: fakeSnapID("pc"), Revision: snap.R(1)}
+ gadgetSnapYaml := "name: pc\nversion: 1.0\ntype: gadget"
+ snapstate.Set(st, "pc", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{si2},
+ Current: snap.R(1),
+ SnapType: "gadget",
+ })
+ gadgetYaml := `
+volumes:
+ volume-id:
+ bootloader: grub
+`
+ snaptest.MockSnapWithFiles(c, gadgetSnapYaml, si2, [][]string{
+ {"meta/gadget.yaml", gadgetYaml},
+ })
+
+ // add "core20" snap to fake store
+ const core20Yaml = `name: core20
+type: base
+version: 20.04`
+ ms.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "core20",
+ "publisher-id": "can0nical",
+ })
+ snapPath, _ := ms.makeStoreTestSnap(c, core20Yaml, "2")
+ ms.serveSnap(snapPath, "2")
+
+ // add "foo" snap to fake store
+ ms.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "foo",
+ })
+ snapPath, _ = ms.makeStoreTestSnap(c, `{name: "foo", version: 1.0}`, "1")
+ ms.serveSnap(snapPath, "1")
+
+ // create/set custom model assertion
+ model := ms.brands.Model("can0nical", "my-model", modelDefaults, map[string]interface{}{
+ "base": "core18",
+ })
+
+ // setup model assertion
+ devicestatetest.SetDevice(st, &auth.DeviceState{
+ Brand: "can0nical",
+ Model: "my-model",
+ Serial: "serialserialserial",
+ })
+ err := assertstate.Add(st, model)
+ c.Assert(err, IsNil)
+
+ // create a new model
+ newModel := ms.brands.Model("can0nical", "my-model", modelDefaults, map[string]interface{}{
+ "base": "core20",
+ "revision": "1",
+ "required-snaps": []interface{}{"foo"},
+ })
+
+ chg, err := devicestate.Remodel(st, newModel)
+ c.Assert(err, IsNil)
+
+ st.Unlock()
+ err = ms.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+ c.Assert(chg.Err(), IsNil)
+
+ // system waits for a restart because of the new base
+ t := findKind(chg, "auto-connect")
+ c.Assert(t, NotNil)
+ c.Assert(t.Status(), Equals, state.DoingStatus)
+
+ // check that the boot vars got updated as expected
+ bvars, err := bloader.GetBootVars("snap_mode", "snap_core", "snap_try_core", "snap_kernel", "snap_try_kernel")
+ c.Assert(err, IsNil)
+ c.Assert(bvars, DeepEquals, map[string]string{
+ "snap_mode": "try",
+ "snap_core": "core18_1.snap",
+ "snap_try_core": "core20_2.snap",
+ "snap_kernel": "pc-kernel_1.snap",
+ "snap_try_kernel": "",
+ })
+
+ // simulate successful restart happened and that the bootvars
+ // got updated
+ state.MockRestarting(st, state.RestartUnset)
+ bloader.SetBootVars(map[string]string{
+ "snap_mode": "",
+ "snap_core": "core20_2.snap",
+ "snap_kernel": "pc-kernel_1.snap",
+ })
+
+ // continue
+ st.Unlock()
+ err = ms.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.DoneStatus, Commentf("upgrade-snap change failed with: %v", chg.Err()))
+
+ // ensure tasks were run in the right order
+ tasks := chg.Tasks()
+ sort.Sort(byReadyTime(tasks))
+
+ // first all downloads/checks in sequential order
+ var i int
+ i += validateDownloadCheckTasks(c, tasks[i:], "core20", "2", "stable")
+ i += validateDownloadCheckTasks(c, tasks[i:], "foo", "1", "stable")
+
+ // then all installs in sequential order
+ i += validateInstallTasks(c, tasks[i:], "core20", "2", noConfigure)
+ i += validateInstallTasks(c, tasks[i:], "foo", "1", 0)
+
+ // ensure that we only have the tasks we checked (plus the one
+ // extra "set-model" task)
+ c.Assert(tasks, HasLen, i+1)
+}
+
+func (ms *mgrsSuite) TestRemodelSwitchToDifferentBaseUndo(c *C) {
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
+ defer bootloader.Force(nil)
+ bloader.SetBootVars(map[string]string{
+ "snap_mode": "",
+ "snap_core": "core18_1.snap",
+ "snap_kernel": "pc-kernel_1.snap",
+ })
+
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ mockServer := ms.mockStore(c)
+ defer mockServer.Close()
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ si := &snap.SideInfo{RealName: "core18", SnapID: fakeSnapID("core18"), Revision: snap.R(1)}
+ snapstate.Set(st, "core18", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{si},
+ Current: snap.R(1),
+ SnapType: "base",
+ })
+ snaptest.MockSnapWithFiles(c, "name: core18\ntype: base\nversion: 1.0", si, nil)
+
+ si2 := &snap.SideInfo{RealName: "pc", SnapID: fakeSnapID("pc"), Revision: snap.R(1)}
+ gadgetSnapYaml := "name: pc\nversion: 1.0\ntype: gadget"
+ snapstate.Set(st, "pc", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{si2},
+ Current: snap.R(1),
+ SnapType: "gadget",
+ })
+ gadgetYaml := `
+volumes:
+ volume-id:
+ bootloader: grub
+`
+ snaptest.MockSnapWithFiles(c, gadgetSnapYaml, si2, [][]string{
+ {"meta/gadget.yaml", gadgetYaml},
+ })
+
+ // add "core20" snap to fake store
+ const core20Yaml = `name: core20
+type: base
+version: 20.04`
+ ms.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "core20",
+ "publisher-id": "can0nical",
+ })
+ snapPath, _ := ms.makeStoreTestSnap(c, core20Yaml, "2")
+ ms.serveSnap(snapPath, "2")
+
+ // add "foo" snap to fake store
+ ms.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "foo",
+ })
+ snapPath, _ = ms.makeStoreTestSnap(c, `{name: "foo", version: 1.0}`, "1")
+ ms.serveSnap(snapPath, "1")
+
+ // create/set custom model assertion
+ model := ms.brands.Model("can0nical", "my-model", modelDefaults, map[string]interface{}{
+ "base": "core18",
+ })
+
+ // setup model assertion
+ devicestatetest.SetDevice(st, &auth.DeviceState{
+ Brand: "can0nical",
+ Model: "my-model",
+ Serial: "serialserialserial",
+ })
+ err := assertstate.Add(st, model)
+ c.Assert(err, IsNil)
+
+ // create a new model
+ newModel := ms.brands.Model("can0nical", "my-model", modelDefaults, map[string]interface{}{
+ "base": "core20",
+ "revision": "1",
+ "required-snaps": []interface{}{"foo"},
+ })
+
+ devicestate.InjectSetModelError(fmt.Errorf("boom"))
+ defer devicestate.InjectSetModelError(nil)
+
+ chg, err := devicestate.Remodel(st, newModel)
+ c.Assert(err, IsNil)
+
+ st.Unlock()
+ err = ms.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+ c.Assert(chg.Err(), IsNil)
+
+ // system waits for a restart because of the new base
+ t := findKind(chg, "auto-connect")
+ c.Assert(t, NotNil)
+ c.Assert(t.Status(), Equals, state.DoingStatus)
+
+ // check that the boot vars got updated as expected
+ c.Assert(bloader.BootVars, DeepEquals, map[string]string{
+ "snap_mode": "try",
+ "snap_core": "core18_1.snap",
+ "snap_try_core": "core20_2.snap",
+ "snap_kernel": "pc-kernel_1.snap",
+ })
+ // simulate successful restart happened
+ ms.mockSuccessfulReboot(c, bloader)
+ c.Assert(bloader.BootVars, DeepEquals, map[string]string{
+ "snap_mode": "",
+ "snap_core": "core20_2.snap",
+ "snap_try_core": "",
+ "snap_kernel": "pc-kernel_1.snap",
+ "snap_try_kernel": "",
+ })
+
+ // continue
+ st.Unlock()
+ err = ms.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.ErrorStatus)
+
+ // and we are in restarting state
+ restarting, restartType := st.Restarting()
+ c.Check(restarting, Equals, true)
+ c.Check(restartType, Equals, state.RestartSystem)
+
+ // and the undo gave us our old kernel back
+ c.Assert(bloader.BootVars, DeepEquals, map[string]string{
+ "snap_core": "core20_2.snap",
+ "snap_try_core": "core18_1.snap",
+ "snap_kernel": "pc-kernel_1.snap",
+ "snap_try_kernel": "",
+ "snap_mode": "try",
+ })
+}
+
+func (ms *mgrsSuite) TestRemodelSwitchToDifferentBaseUndoOnRollback(c *C) {
+ bloader := bootloadertest.Mock("mock", c.MkDir())
+ bootloader.Force(bloader)
+ defer bootloader.Force(nil)
+ bloader.SetBootVars(map[string]string{
+ "snap_mode": "",
+ "snap_core": "core18_1.snap",
+ "snap_kernel": "pc-kernel_1.snap",
+ })
+
+ restore := release.MockOnClassic(false)
+ defer restore()
+
+ mockServer := ms.mockStore(c)
+ defer mockServer.Close()
+
+ st := ms.o.State()
+ st.Lock()
+ defer st.Unlock()
+
+ si := &snap.SideInfo{RealName: "core18", SnapID: fakeSnapID("core18"), Revision: snap.R(1)}
+ snapstate.Set(st, "core18", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{si},
+ Current: snap.R(1),
+ SnapType: "base",
+ })
+ snaptest.MockSnapWithFiles(c, "name: core18\ntype: base\nversion: 1.0", si, nil)
+
+ si2 := &snap.SideInfo{RealName: "pc", SnapID: fakeSnapID("pc"), Revision: snap.R(1)}
+ gadgetSnapYaml := "name: pc\nversion: 1.0\ntype: gadget"
+ snapstate.Set(st, "pc", &snapstate.SnapState{
+ Active: true,
+ Sequence: []*snap.SideInfo{si2},
+ Current: snap.R(1),
+ SnapType: "gadget",
+ })
+ gadgetYaml := `
+volumes:
+ volume-id:
+ bootloader: grub
+`
+ snaptest.MockSnapWithFiles(c, gadgetSnapYaml, si2, [][]string{
+ {"meta/gadget.yaml", gadgetYaml},
+ })
+
+ // add "core20" snap to fake store
+ const core20Yaml = `name: core20
+type: base
+version: 20.04`
+ ms.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "core20",
+ "publisher-id": "can0nical",
+ })
+ snapPath, _ := ms.makeStoreTestSnap(c, core20Yaml, "2")
+ ms.serveSnap(snapPath, "2")
+
+ // add "foo" snap to fake store
+ ms.prereqSnapAssertions(c, map[string]interface{}{
+ "snap-name": "foo",
+ })
+ snapPath, _ = ms.makeStoreTestSnap(c, `{name: "foo", version: 1.0}`, "1")
+ ms.serveSnap(snapPath, "1")
+
+ // create/set custom model assertion
+ model := ms.brands.Model("can0nical", "my-model", modelDefaults, map[string]interface{}{
+ "base": "core18",
+ })
+
+ // setup model assertion
+ devicestatetest.SetDevice(st, &auth.DeviceState{
+ Brand: "can0nical",
+ Model: "my-model",
+ Serial: "serialserialserial",
+ })
+ err := assertstate.Add(st, model)
+ c.Assert(err, IsNil)
+
+ // create a new model
+ newModel := ms.brands.Model("can0nical", "my-model", modelDefaults, map[string]interface{}{
+ "base": "core20",
+ "revision": "1",
+ "required-snaps": []interface{}{"foo"},
+ })
+
+ chg, err := devicestate.Remodel(st, newModel)
+ c.Assert(err, IsNil)
+
+ st.Unlock()
+ err = ms.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+ c.Assert(chg.Err(), IsNil)
+
+ // system waits for a restart because of the new base
+ t := findKind(chg, "auto-connect")
+ c.Assert(t, NotNil)
+ c.Assert(t.Status(), Equals, state.DoingStatus)
+
+ // check that the boot vars got updated as expected
+ c.Assert(bloader.BootVars, DeepEquals, map[string]string{
+ "snap_mode": "try",
+ "snap_core": "core18_1.snap",
+ "snap_try_core": "core20_2.snap",
+ "snap_kernel": "pc-kernel_1.snap",
+ })
+ // simulate successful restart happened
+ ms.mockRollbackAcrossReboot(c, bloader)
+ c.Assert(bloader.BootVars, DeepEquals, map[string]string{
+ "snap_mode": "",
+ "snap_core": "core18_1.snap",
+ "snap_try_core": "",
+ "snap_kernel": "pc-kernel_1.snap",
+ "snap_try_kernel": "",
+ })
+
+ // continue
+ st.Unlock()
+ err = ms.o.Settle(settleTimeout)
+ st.Lock()
+ c.Assert(err, IsNil)
+
+ c.Assert(chg.Status(), Equals, state.ErrorStatus)
+
+ // and we are *not* in restarting state
+ restarting, _ := st.Restarting()
+ c.Check(restarting, Equals, false)
+ // bootvars unchanged
+ c.Assert(bloader.BootVars, DeepEquals, map[string]string{
+ "snap_mode": "",
+ "snap_core": "core18_1.snap",
+ "snap_try_core": "",
+ "snap_kernel": "pc-kernel_1.snap",
+ "snap_try_kernel": "",
+ })
+}
+
type kernelSuite struct {
baseMgrsSuite
diff --git a/overlord/snapstate/snapstate.go b/overlord/snapstate/snapstate.go
index a65e215243..e3e047d758 100644
--- a/overlord/snapstate/snapstate.go
+++ b/overlord/snapstate/snapstate.go
@@ -1561,6 +1561,55 @@ func AutoRefresh(ctx context.Context, st *state.State) ([]string, []*state.TaskS
return UpdateMany(ctx, st, nil, userID, &Flags{IsAutoRefresh: true})
}
+// LinkNewBaseOrKernel will create prepare/link-snap tasks for a remodel
+func LinkNewBaseOrKernel(st *state.State, name string) (*state.TaskSet, error) {
+ var snapst SnapState
+ err := Get(st, name, &snapst)
+ if err == state.ErrNoState {
+ return nil, &snap.NotInstalledError{Snap: name}
+ }
+ if err != nil {
+ return nil, err
+ }
+
+ if err := CheckChangeConflict(st, name, nil); err != nil {
+ return nil, err
+ }
+
+ info, err := snapst.CurrentInfo()
+ if err != nil {
+ return nil, err
+ }
+
+ switch info.GetType() {
+ case snap.TypeOS, snap.TypeBase, snap.TypeKernel:
+ // good
+ default:
+ // bad
+ return nil, fmt.Errorf("cannot link type %v", info.GetType())
+ }
+
+ snapsup := &SnapSetup{
+ SideInfo: snapst.CurrentSideInfo(),
+ Flags: snapst.Flags.ForSnapSetup(),
+ Type: info.GetType(),
+ PlugsOnly: len(info.Slots) == 0,
+ InstanceKey: snapst.InstanceKey,
+ }
+
+ prepareSnap := st.NewTask("prepare-snap", fmt.Sprintf(i18n.G("Prepare snap %q (%s) for remodel"), snapsup.InstanceName(), snapst.Current))
+ prepareSnap.Set("snap-setup", &snapsup)
+
+ linkSnap := st.NewTask("link-snap", fmt.Sprintf(i18n.G("Make snap %q (%s) available to the system during remodel"), snapsup.InstanceName(), snapst.Current))
+ linkSnap.Set("snap-setup-task", prepareSnap.ID())
+ linkSnap.WaitFor(prepareSnap)
+
+ // we need this for remodel
+ ts := state.NewTaskSet(prepareSnap, linkSnap)
+ ts.MarkEdge(prepareSnap, DownloadAndChecksDoneEdge)
+ return ts, nil
+}
+
// Enable sets a snap to the active state
func Enable(st *state.State, name string) (*state.TaskSet, error) {
var snapst SnapState
diff --git a/seed/seed20.go b/seed/seed20.go
index 30ad574775..cf2a183d1c 100644
--- a/seed/seed20.go
+++ b/seed/seed20.go
@@ -102,6 +102,10 @@ func (s *seed20) LoadAssertions(db asserts.RODatabase, commitTo func(*asserts.Ba
}
modelRef := refs[0]
+ if len(declRefs) != len(revRefs) {
+ return fmt.Errorf("system unexpectedly holds a different number of snap-declaration than snap-revision assertions")
+ }
+
// this also verifies the consistency of all of them
if err := commitTo(batch); err != nil {
return err
@@ -217,7 +221,7 @@ func (s *seed20) lookupVerifiedRevision(snapRef naming.SnapRef) (snapPath string
snapRev = s.snapRevsByID[snapID]
if snapRev == nil {
- return "", nil, nil, fmt.Errorf("cannot find snap-revision for snap-id: %s", snapID)
+ return "", nil, nil, fmt.Errorf("internal error: cannot find snap-revision for snap-id: %s", snapID)
}
snapName := snapDecl.SnapName()
@@ -225,11 +229,11 @@ func (s *seed20) lookupVerifiedRevision(snapRef naming.SnapRef) (snapPath string
fi, err := os.Stat(snapPath)
if err != nil {
- return "", nil, nil, fmt.Errorf("cannot stat snap %q: %v", snapPath, err)
+ return "", nil, nil, fmt.Errorf("cannot stat snap: %v", err)
}
if fi.Size() != int64(snapRev.SnapSize()) {
- return "", nil, nil, fmt.Errorf("cannot validate snap %q for snap %q (snap-id %q), wrong size", snapPath, snapName, snapID)
+ return "", nil, nil, fmt.Errorf("cannot validate %q for snap %q (snap-id %q), wrong size", snapPath, snapName, snapID)
}
snapSHA3_384, _, err := asserts.SnapFileSHA3_384(snapPath)
@@ -238,7 +242,7 @@ func (s *seed20) lookupVerifiedRevision(snapRef naming.SnapRef) (snapPath string
}
if snapSHA3_384 != snapRev.SnapSHA3_384() {
- return "", nil, nil, fmt.Errorf("cannot validate snap %q for snap %q (snap-id %q), hash mismatch with snap-revision", snapPath, snapName, snapID)
+ return "", nil, nil, fmt.Errorf("cannot validate %q for snap %q (snap-id %q), hash mismatch with snap-revision", snapPath, snapName, snapID)
}
diff --git a/seed/seed20_test.go b/seed/seed20_test.go
index 59a78478bd..70f52f2243 100644
--- a/seed/seed20_test.go
+++ b/seed/seed20_test.go
@@ -20,12 +20,16 @@
package seed_test
import (
+ "os"
"path/filepath"
+ "strings"
+ "time"
. "gopkg.in/check.v1"
"github.com/snapcore/snapd/asserts"
"github.com/snapcore/snapd/asserts/assertstest"
+ "github.com/snapcore/snapd/osutil"
"github.com/snapcore/snapd/seed"
"github.com/snapcore/snapd/seed/seedtest"
"github.com/snapcore/snapd/snap"
@@ -162,6 +166,366 @@ func (s *seed20Suite) TestLoadMetaCore20Minimal(c *C) {
c.Check(runSnaps, HasLen, 0)
}
+func (s *seed20Suite) makeCore20MinimalSeed(c *C, sysLabel string) string {
+ s.makeSnap(c, "snapd", "")
+ s.makeSnap(c, "core20", "")
+ s.makeSnap(c, "pc-kernel=20", "")
+ s.makeSnap(c, "pc=20", "")
+
+ s.MakeSeed(c, sysLabel, "my-brand", "my-model", map[string]interface{}{
+ "display-name": "my model",
+ "architecture": "amd64",
+ "base": "core20",
+ "snaps": []interface{}{
+ map[string]interface{}{
+ "name": "pc-kernel",
+ "id": s.AssertedSnapID("pc-kernel"),
+ "type": "kernel",
+ "default-channel": "20",
+ },
+ map[string]interface{}{
+ "name": "pc",
+ "id": s.AssertedSnapID("pc"),
+ "type": "gadget",
+ "default-channel": "20",
+ }},
+ })
+
+ return filepath.Join(s.SeedDir, "systems", sysLabel)
+}
+
+func (s *seed20Suite) TestLoadAssertionsModelTempDBHappy(c *C) {
+ r := seed.MockTrusted(s.StoreSigning.Trusted)
+ defer r()
+
+ sysLabel := "20191031"
+ s.makeCore20MinimalSeed(c, sysLabel)
+
+ seed20, err := seed.Open(s.SeedDir, sysLabel)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadAssertions(nil, nil)
+ c.Assert(err, IsNil)
+
+ model, err := seed20.Model()
+ c.Assert(err, IsNil)
+ c.Check(model.Model(), Equals, "my-model")
+ c.Check(model.Base(), Equals, "core20")
+}
+
+func (s *seed20Suite) TestLoadAssertionsMultiModels(c *C) {
+ sysLabel := "20191031"
+ sysDir := s.makeCore20MinimalSeed(c, sysLabel)
+
+ err := osutil.CopyFile(filepath.Join(sysDir, "model"), filepath.Join(sysDir, "assertions", "model2"), 0)
+ c.Assert(err, IsNil)
+
+ seed20, err := seed.Open(s.SeedDir, sysLabel)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadAssertions(s.db, s.commitTo)
+ c.Check(err, ErrorMatches, `system cannot have any model assertion but the one in the system model assertion file`)
+}
+
+func (s *seed20Suite) TestLoadAssertionsInvalidModelAssertFile(c *C) {
+ sysLabel := "20191031"
+ sysDir := s.makeCore20MinimalSeed(c, sysLabel)
+
+ modelAssertFn := filepath.Join(sysDir, "model")
+
+ // copy over multiple assertions
+ err := osutil.CopyFile(filepath.Join(sysDir, "assertions", "model-etc"), modelAssertFn, osutil.CopyFlagOverwrite)
+ c.Assert(err, IsNil)
+
+ seed20, err := seed.Open(s.SeedDir, sysLabel)
+ c.Assert(err, IsNil)
+ err = seed20.LoadAssertions(s.db, s.commitTo)
+ c.Check(err, ErrorMatches, `system model assertion file must contain exactly the model assertion`)
+
+ // write whatever single non model assertion
+ seedtest.WriteAssertions(modelAssertFn, s.AssertedSnapRevision("snapd"))
+
+ seed20, err = seed.Open(s.SeedDir, sysLabel)
+ c.Assert(err, IsNil)
+ err = seed20.LoadAssertions(s.db, s.commitTo)
+ c.Check(err, ErrorMatches, `system model assertion file must contain exactly the model assertion`)
+}
+
+func (s *seed20Suite) massageAssertions(c *C, fn string, filter func(asserts.Assertion) asserts.Assertion) {
+ assertions := seedtest.ReadAssertions(c, fn)
+ filtered := make([]asserts.Assertion, 0, len(assertions))
+ for _, a := range assertions {
+ a1 := filter(a)
+ if a1 != nil {
+ filtered = append(filtered, a1)
+ }
+ }
+ seedtest.WriteAssertions(fn, filtered...)
+}
+
+func (s *seed20Suite) TestLoadAssertionsUnbalancedDeclsAndRevs(c *C) {
+ sysLabel := "20191031"
+ sysDir := s.makeCore20MinimalSeed(c, sysLabel)
+
+ s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion {
+ if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("core20") {
+ return nil
+ }
+ return a
+ })
+
+ seed20, err := seed.Open(s.SeedDir, sysLabel)
+ c.Assert(err, IsNil)
+ err = seed20.LoadAssertions(s.db, s.commitTo)
+ c.Check(err, ErrorMatches, `system unexpectedly holds a different number of snap-declaration than snap-revision assertions`)
+}
+
+func (s *seed20Suite) TestLoadAssertionsMultiSnapRev(c *C) {
+ sysLabel := "20191031"
+ sysDir := s.makeCore20MinimalSeed(c, sysLabel)
+
+ spuriousRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
+ "snap-sha3-384": strings.Repeat("B", 64),
+ "snap-size": "1000",
+ "snap-id": s.AssertedSnapID("core20"),
+ "developer-id": "canonical",
+ "snap-revision": "99",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion {
+ if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("snapd") {
+ return spuriousRev
+ }
+ return a
+ })
+
+ seed20, err := seed.Open(s.SeedDir, sysLabel)
+ c.Assert(err, IsNil)
+ err = seed20.LoadAssertions(s.db, s.commitTo)
+ c.Check(err, ErrorMatches, `cannot have multiple snap-revisions for the same snap-id: core20ididididididididididididid`)
+}
+
+func (s *seed20Suite) TestLoadAssertionsMultiSnapDecl(c *C) {
+ sysLabel := "20191031"
+ sysDir := s.makeCore20MinimalSeed(c, sysLabel)
+
+ spuriousDecl, err := s.StoreSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": "idididididididididididididididid",
+ "publisher-id": "canonical",
+ "snap-name": "core20",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ spuriousRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
+ "snap-sha3-384": strings.Repeat("B", 64),
+ "snap-size": "1000",
+ "snap-id": s.AssertedSnapID("core20"),
+ "developer-id": "canonical",
+ "snap-revision": "99",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion {
+ if a.Type() == asserts.SnapDeclarationType && a.HeaderString("snap-name") == "snapd" {
+ return spuriousDecl
+ }
+ if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("snapd") {
+ return spuriousRev
+ }
+ return a
+ })
+
+ seed20, err := seed.Open(s.SeedDir, sysLabel)
+ c.Assert(err, IsNil)
+ err = seed20.LoadAssertions(s.db, s.commitTo)
+ c.Check(err, ErrorMatches, `cannot have multiple snap-declarations for the same snap-name: core20`)
+}
+
+func (s *seed20Suite) TestLoadMetaMissingSnapDeclByName(c *C) {
+ sysLabel := "20191031"
+ sysDir := s.makeCore20MinimalSeed(c, sysLabel)
+
+ wrongDecl, err := s.StoreSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": "idididididididididididididididid",
+ "publisher-id": "canonical",
+ "snap-name": "core20X",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ wrongRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
+ "snap-sha3-384": strings.Repeat("B", 64),
+ "snap-size": "1000",
+ "snap-id": "idididididididididididididididid",
+ "developer-id": "canonical",
+ "snap-revision": "99",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion {
+ if a.Type() == asserts.SnapDeclarationType && a.HeaderString("snap-name") == "core20" {
+ return wrongDecl
+ }
+ if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("core20") {
+ return wrongRev
+ }
+ return a
+ })
+
+ seed20, err := seed.Open(s.SeedDir, sysLabel)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadMeta(s.perfTimings)
+ c.Check(err, ErrorMatches, `cannot find snap-declaration for snap name: core20`)
+}
+
+func (s *seed20Suite) TestLoadMetaMissingSnapDeclByID(c *C) {
+ sysLabel := "20191031"
+ sysDir := s.makeCore20MinimalSeed(c, sysLabel)
+
+ wrongDecl, err := s.StoreSigning.Sign(asserts.SnapDeclarationType, map[string]interface{}{
+ "series": "16",
+ "snap-id": "idididididididididididididididid",
+ "publisher-id": "canonical",
+ "snap-name": "pc",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ wrongRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
+ "snap-sha3-384": strings.Repeat("B", 64),
+ "snap-size": "1000",
+ "snap-id": "idididididididididididididididid",
+ "developer-id": "canonical",
+ "snap-revision": "99",
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion {
+ if a.Type() == asserts.SnapDeclarationType && a.HeaderString("snap-name") == "pc" {
+ return wrongDecl
+ }
+ if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("pc") {
+ return wrongRev
+ }
+ return a
+ })
+
+ seed20, err := seed.Open(s.SeedDir, sysLabel)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadMeta(s.perfTimings)
+ c.Check(err, ErrorMatches, `cannot find snap-declaration for snap-id: pcididididididididididididididid`)
+}
+
+func (s *seed20Suite) TestLoadMetaMissingSnap(c *C) {
+ sysLabel := "20191031"
+ s.makeCore20MinimalSeed(c, sysLabel)
+
+ err := os.Remove(filepath.Join(s.SeedDir, "snaps", "pc_1.snap"))
+ c.Assert(err, IsNil)
+
+ seed20, err := seed.Open(s.SeedDir, sysLabel)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadMeta(s.perfTimings)
+ c.Check(err, ErrorMatches, `cannot stat snap:.*pc_1\.snap.*`)
+}
+
+func (s *seed20Suite) TestLoadMetaWrongSizeSnap(c *C) {
+ sysLabel := "20191031"
+ s.makeCore20MinimalSeed(c, sysLabel)
+
+ err := os.Truncate(filepath.Join(s.SeedDir, "snaps", "pc_1.snap"), 5)
+ c.Assert(err, IsNil)
+
+ seed20, err := seed.Open(s.SeedDir, sysLabel)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadMeta(s.perfTimings)
+ c.Check(err, ErrorMatches, `cannot validate ".*pc_1\.snap" for snap "pc" \(snap-id "pc.*"\), wrong size`)
+}
+
+func (s *seed20Suite) TestLoadMetaWrongHashSnap(c *C) {
+ sysLabel := "20191031"
+ sysDir := s.makeCore20MinimalSeed(c, sysLabel)
+
+ pcRev := s.AssertedSnapRevision("pc")
+ wrongRev, err := s.StoreSigning.Sign(asserts.SnapRevisionType, map[string]interface{}{
+ "snap-sha3-384": strings.Repeat("B", 64),
+ "snap-size": pcRev.HeaderString("snap-size"),
+ "snap-id": s.AssertedSnapID("pc"),
+ "developer-id": "canonical",
+ "snap-revision": pcRev.HeaderString("snap-revision"),
+ "timestamp": time.Now().UTC().Format(time.RFC3339),
+ }, nil, "")
+ c.Assert(err, IsNil)
+
+ s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion {
+ if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("pc") {
+ return wrongRev
+ }
+ return a
+ })
+
+ seed20, err := seed.Open(s.SeedDir, sysLabel)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadMeta(s.perfTimings)
+ c.Check(err, ErrorMatches, `cannot validate ".*pc_1\.snap" for snap "pc" \(snap-id "pc.*"\), hash mismatch with snap-revision`)
+}
+
+func (s *seed20Suite) TestLoadMetaWrongGadgetBase(c *C) {
+ sysLabel := "20191031"
+ sysDir := s.makeCore20MinimalSeed(c, sysLabel)
+
+ // pc with base: core18
+ pc18Decl, pc18Rev := s.MakeAssertedSnap(c, snapYaml["pc=18"], nil, snap.R(2), "canonical")
+ err := os.Rename(s.AssertedSnap("pc"), filepath.Join(s.SeedDir, "snaps", "pc_2.snap"))
+ c.Assert(err, IsNil)
+ s.massageAssertions(c, filepath.Join(sysDir, "assertions", "snaps"), func(a asserts.Assertion) asserts.Assertion {
+ if a.Type() == asserts.SnapDeclarationType && a.HeaderString("snap-name") == "pc" {
+ return pc18Decl
+ }
+ if a.Type() == asserts.SnapRevisionType && a.HeaderString("snap-id") == s.AssertedSnapID("pc") {
+ return pc18Rev
+ }
+ return a
+ })
+
+ seed20, err := seed.Open(s.SeedDir, sysLabel)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadAssertions(s.db, s.commitTo)
+ c.Assert(err, IsNil)
+
+ err = seed20.LoadMeta(s.perfTimings)
+ c.Check(err, ErrorMatches, `cannot use gadget snap because its base "core18" is different from model base "core20"`)
+}
+
func (s *seed20Suite) TestLoadMetaCore20(c *C) {
s.makeSnap(c, "snapd", "")
s.makeSnap(c, "core20", "")
diff --git a/seed/seedtest/seedtest.go b/seed/seedtest/seedtest.go
index dd3da3a7c5..8c5d17e86f 100644
--- a/seed/seedtest/seedtest.go
+++ b/seed/seedtest/seedtest.go
@@ -21,6 +21,7 @@ package seedtest
import (
"fmt"
+ "io"
"os"
"path/filepath"
"strings"
@@ -188,6 +189,24 @@ func WriteAssertions(fn string, assertions ...asserts.Assertion) {
}
}
+func ReadAssertions(c *C, fn string) []asserts.Assertion {
+ f, err := os.Open(fn)
+ c.Assert(err, IsNil)
+
+ var as []asserts.Assertion
+ dec := asserts.NewDecoder(f)
+ for {
+ a, err := dec.Decode()
+ if err == io.EOF {
+ break
+ }
+ c.Assert(err, IsNil)
+ as = append(as, a)
+ }
+
+ return as
+}
+
// TestingSeed20 helps setting up a populated Core 20 testing seed directory.
type TestingSeed20 struct {
SeedSnaps
diff --git a/seed/seedwriter/writer_test.go b/seed/seedwriter/writer_test.go
index 39f2f58ba6..bbe0231286 100644
--- a/seed/seedwriter/writer_test.go
+++ b/seed/seedwriter/writer_test.go
@@ -22,7 +22,6 @@ package seedwriter_test
import (
"encoding/json"
"fmt"
- "io"
"io/ioutil"
"os"
"path/filepath"
@@ -755,24 +754,6 @@ func (s *writerSuite) TestDownloadedCheckTypeCore(c *C) {
c.Check(err, ErrorMatches, `core snap has unexpected type: base`)
}
-func readAssertions(c *C, fn string) []asserts.Assertion {
- f, err := os.Open(fn)
- c.Assert(err, IsNil)
-
- var as []asserts.Assertion
- dec := asserts.NewDecoder(f)
- for {
- a, err := dec.Decode()
- if err == io.EOF {
- break
- }
- c.Assert(err, IsNil)
- as = append(as, a)
- }
-
- return as
-}
-
func (s *writerSuite) TestSeedSnapsWriteMetaCore18(c *C) {
model := s.Brands.Model("my-brand", "my-model", map[string]interface{}{
"display-name": "my model",
@@ -865,7 +846,7 @@ func (s *writerSuite) TestSeedSnapsWriteMetaCore18(c *C) {
c.Check(filepath.Join(seedAssertsDir, "model"), testutil.FileEquals, asserts.Encode(model))
- acct := readAssertions(c, filepath.Join(seedAssertsDir, "my-brand.account"))
+ acct := seedtest.ReadAssertions(c, filepath.Join(seedAssertsDir, "my-brand.account"))
c.Assert(acct, HasLen, 1)
c.Check(acct[0].Type(), Equals, asserts.AccountType)
c.Check(acct[0].HeaderString("account-id"), Equals, "my-brand")
@@ -873,12 +854,12 @@ func (s *writerSuite) TestSeedSnapsWriteMetaCore18(c *C) {
// check the snap assertions are also in place
for _, snapName := range []string{"snapd", "pc-kernel", "core18", "pc", "cont-consumer", "cont-producer"} {
p := filepath.Join(seedAssertsDir, fmt.Sprintf("16,%s.snap-declaration", s.AssertedSnapID(snapName)))
- decl := readAssertions(c, p)
+ decl := seedtest.ReadAssertions(c, p)
c.Assert(decl, HasLen, 1)
c.Check(decl[0].Type(), Equals, asserts.SnapDeclarationType)
c.Check(decl[0].HeaderString("snap-name"), Equals, snapName)
p = filepath.Join(seedAssertsDir, fmt.Sprintf("%s.snap-revision", s.AssertedSnapRevision(snapName).SnapSHA3_384()))
- rev := readAssertions(c, p)
+ rev := seedtest.ReadAssertions(c, p)
c.Assert(rev, HasLen, 1)
c.Check(rev[0].Type(), Equals, asserts.SnapRevisionType)
c.Check(rev[0].HeaderString("snap-id"), Equals, s.AssertedSnapID(snapName))
@@ -1090,12 +1071,12 @@ func (s *writerSuite) TestLocalSnapsCore18FullUse(c *C) {
seedAssertsDir := filepath.Join(s.opts.SeedDir, "assertions")
for _, snapName := range []string{"snapd", "cont-producer"} {
p := filepath.Join(seedAssertsDir, fmt.Sprintf("16,%s.snap-declaration", s.AssertedSnapID(snapName)))
- decl := readAssertions(c, p)
+ decl := seedtest.ReadAssertions(c, p)
c.Assert(decl, HasLen, 1)
c.Check(decl[0].Type(), Equals, asserts.SnapDeclarationType)
c.Check(decl[0].HeaderString("snap-name"), Equals, snapName)
p = filepath.Join(seedAssertsDir, fmt.Sprintf("%s.snap-revision", s.AssertedSnapRevision(snapName).SnapSHA3_384()))
- rev := readAssertions(c, p)
+ rev := seedtest.ReadAssertions(c, p)
c.Assert(rev, HasLen, 1)
c.Check(rev[0].Type(), Equals, asserts.SnapRevisionType)
c.Check(rev[0].HeaderString("snap-id"), Equals, s.AssertedSnapID(snapName))
@@ -1471,12 +1452,12 @@ func (s *writerSuite) TestSeedSnapsWriteMetaExtraSnaps(c *C) {
seedAssertsDir := filepath.Join(s.opts.SeedDir, "assertions")
for _, snapName := range []string{"snapd", "core", "pc-kernel", "core18", "pc", "cont-consumer", "cont-producer", "required"} {
p := filepath.Join(seedAssertsDir, fmt.Sprintf("16,%s.snap-declaration", s.AssertedSnapID(snapName)))
- decl := readAssertions(c, p)
+ decl := seedtest.ReadAssertions(c, p)
c.Assert(decl, HasLen, 1)
c.Check(decl[0].Type(), Equals, asserts.SnapDeclarationType)
c.Check(decl[0].HeaderString("snap-name"), Equals, snapName)
p = filepath.Join(seedAssertsDir, fmt.Sprintf("%s.snap-revision", s.AssertedSnapRevision(snapName).SnapSHA3_384()))
- rev := readAssertions(c, p)
+ rev := seedtest.ReadAssertions(c, p)
c.Assert(rev, HasLen, 1)
c.Check(rev[0].Type(), Equals, asserts.SnapRevisionType)
c.Check(rev[0].HeaderString("snap-id"), Equals, s.AssertedSnapID(snapName))
@@ -1741,7 +1722,7 @@ func (s *writerSuite) TestSeedSnapsWriteMetaCore20(c *C) {
c.Check(filepath.Join(systemDir, "model"), testutil.FileEquals, asserts.Encode(model))
assertsDir := filepath.Join(systemDir, "assertions")
- modelEtc := readAssertions(c, filepath.Join(assertsDir, "model-etc"))
+ modelEtc := seedtest.ReadAssertions(c, filepath.Join(assertsDir, "model-etc"))
c.Check(modelEtc, HasLen, 4)
keyPKs := make(map[string]bool)
@@ -1763,7 +1744,7 @@ func (s *writerSuite) TestSeedSnapsWriteMetaCore20(c *C) {
})
// check snap assertions
- snapAsserts := readAssertions(c, filepath.Join(assertsDir, "snaps"))
+ snapAsserts := seedtest.ReadAssertions(c, filepath.Join(assertsDir, "snaps"))
seen := make(map[string]bool)
for _, a := range snapAsserts {
diff --git a/spread.yaml b/spread.yaml
index aabdff5179..185098d5b1 100644
--- a/spread.yaml
+++ b/spread.yaml
@@ -29,6 +29,7 @@ environment:
TRUST_TEST_KEYS: '$(HOST: echo "${SPREAD_TRUST_TEST_KEYS:-true}")'
MANAGED_DEVICE: "false"
CORE_CHANNEL: '$(HOST: echo "${SPREAD_CORE_CHANNEL:-edge}")'
+ BASE_CHANNEL: '$(HOST: echo "${SPREAD_BASE_CHANNEL:-edge}")'
KERNEL_CHANNEL: '$(HOST: echo "${SPREAD_KERNEL_CHANNEL:-edge}")'
GADGET_CHANNEL: '$(HOST: echo "${SPREAD_GADGET_CHANNEL:-edge}")'
SNAPD_CHANNEL: '$(HOST: echo "${SPREAD_SNAPD_CHANNEL:-edge}")'
diff --git a/tests/lib/assertions/developer1-pc-18-new-base.model b/tests/lib/assertions/developer1-pc-18-new-base.model
new file mode 100644
index 0000000000..7cd5d58035
--- /dev/null
+++ b/tests/lib/assertions/developer1-pc-18-new-base.model
@@ -0,0 +1,23 @@
+type: model
+authority-id: developer1
+revision: 2
+series: 16
+brand-id: developer1
+model: my-model
+architecture: amd64
+base: test-snapd-core18
+gadget: pc=18
+kernel: pc-kernel=18
+timestamp: 2017-05-28T19:40:00+00:00
+sign-key-sha3-384: EAD4DbLxK_kn0gzNCXOs3kd6DeMU3f-L6BEsSEuJGBqCORR0gXkdDxMbOm11mRFu
+
+AcLBUgQAAQoABgUCXXkbOAAACgsQAF7CSfowmuUnv5R1QMrGtzFGJurjQvgLVHc9olSygOZl2H/m
+Gmb8whFKbUsWjQFO6S2BMUQ+L3C5BUsVLnHclNTq9RrkaLDsPMNuso7uqCcVSk83r+N/ptXXzZhm
+wEL2WLu6F5WUBxdwIljlorwuw9V4uP+1gdQ+jEEIJ9Hl0RRdAt1Pm39/5+nWRMKSy6TBH3E8y70I
+B5Qu0K4HCG1Tz11XZGIf541rIGa7mBzeuJ7FWcCsdvcKcvFsQXYq6FV1qzBwT0gM+Aqg3rgdVE7y
+V4b8UUv1CBDNn5lvkXGDV3Kbl5odX4sbmCqTTT5jFoWyhDXISpg31f+UIMXcg5EUjSfBP22F3NjI
+fdpMY9CBYpecHmVhsBpbtDjI2bfDuuAFMKWkeKFySjbhRrcpTm4iEfq8ezrpRJrusUzWPb6jTB7S
+cb7Dp9Hq8FzFi2GrV2w4NVQCGihwS3HuQAvQ5ZxDtgS1Nv+5oTRSiqkZRGr8h9LA8PPUDvIrBAg0
+TrZgbql7Qwjo4PkjtZPQBNzyOcHFC38mKVq9r1b5ZGkF6yGvPK76a2Oe7vmWPw+EyQKDc+5+UYiU
+vYXFbqWwtAkF0fkMis2TiSqd6FfBWRM64OE+8/n/UrEgbS69i4/XXjm34iscc8umbmZ1w5Z/hsC0
+sDZUfqBALcc31dvgi1ClJUTEnotw
diff --git a/tests/lib/state.sh b/tests/lib/state.sh
index 297592c654..5690af700a 100755
--- a/tests/lib/state.sh
+++ b/tests/lib/state.sh
@@ -47,6 +47,8 @@ save_snapd_state() {
cp -rf /var/cache/snapd "$SNAPD_STATE_PATH"/snapd-cache
cp -rf "$boot_path" "$SNAPD_STATE_PATH"/boot
cp -f /etc/systemd/system/snap-*core*.mount "$SNAPD_STATE_PATH"/system-units
+ mkdir -p "$SNAPD_STATE_PATH"/var-snap
+ cp -a /var/snap/* "$SNAPD_STATE_PATH"/var-snap/
else
systemctl daemon-reload
escaped_snap_mount_dir="$(systemd-escape --path "$SNAP_MOUNT_DIR")"
@@ -94,11 +96,14 @@ restore_snapd_state() {
cp -rf "$SNAPD_STATE_PATH"/snapd-cache/* /var/cache/snapd
cp -rf "$SNAPD_STATE_PATH"/boot/* "$boot_path"
cp -f "$SNAPD_STATE_PATH"/system-units/* /etc/systemd/system
+ rm -rf /var/snap/*
+ cp -a "$SNAPD_STATE_PATH"/var-snap/* /var/snap/
else
# Purge all the systemd service units config
rm -rf /etc/systemd/system/snapd.service.d
rm -rf /etc/systemd/system/snapd.socket.d
+ # TODO: remove files created by the test
tar -C/ -xf "$SNAPD_STATE_FILE"
fi
diff --git a/tests/main/remodel-base/task.yaml b/tests/main/remodel-base/task.yaml
new file mode 100644
index 0000000000..1520f0fd8a
--- /dev/null
+++ b/tests/main/remodel-base/task.yaml
@@ -0,0 +1,122 @@
+summary: Test a remodel that switches to a new base
+environment:
+ OLD_BASE: core18
+ NEW_BASE: test-snapd-core18
+
+systems: [ubuntu-core-18-64]
+
+prepare: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ #shellcheck source=tests/lib/systemd.sh
+ . "$TESTSLIB"/systemd.sh
+ systemctl stop snapd.service snapd.socket
+ rm -rf /var/lib/snapd/assertions/*
+ rm -rf /var/lib/snapd/device
+ rm -rf /var/lib/snapd/state.json
+ mv /var/lib/snapd/seed/assertions/model model.bak
+ cp "$TESTSLIB"/assertions/developer1.account /var/lib/snapd/seed/assertions
+ cp "$TESTSLIB"/assertions/developer1.account-key /var/lib/snapd/seed/assertions
+ cp "$TESTSLIB"/assertions/developer1-pc-18.model /var/lib/snapd/seed/assertions
+ cp "$TESTSLIB"/assertions/testrootorg-store.account-key /var/lib/snapd/seed/assertions
+ # kick first boot again
+ systemctl start snapd.service snapd.socket
+ retry-tool -n 60 --wait 5 sh -c 'snap changes | grep -q "Done.*Initialize system state"'
+
+restore: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ #shellcheck source=tests/lib/systemd.sh
+ . "$TESTSLIB"/systemd.sh
+ systemctl stop snapd.service snapd.socket
+ rm -rf /var/lib/snapd/assertions/*
+ rm -rf /var/lib/snapd/device
+ rm -rf /var/lib/snapd/state.json
+ rm -f /var/lib/snapd/seed/assertions/developer1.account
+ rm -f /var/lib/snapd/seed/assertions/developer1.account-key
+ rm -f /var/lib/snapd/seed/assertions/developer1-pc-18.model
+ rm -f /var/lib/snapd/seed/assertions/testrootorg-store.account-key
+ mv model.bak /var/lib/snapd/seed/assertions/model
+ rm -f ./*.bak
+ # kick first boot again
+ systemctl start snapd.service snapd.socket
+ # wait for first boot to be done
+ snap wait system seed.loaded
+ retry-tool -n 60 --wait 5 sh -c 'snap changes | grep -q "Done.*Initialize system state"'
+ # extra paranoia because failure to cleanup earlier took us a long time
+ # to find
+ if [ -e /var/snap/$NEW_BASE/current ]; then
+ echo "Leftover $NEW_BASE data dir found, test does not "
+ echo "properly cleanup"
+ echo "see https://github.com/snapcore/snapd/pull/6620"
+ echo
+ find /var/snap
+ exit 1
+ fi
+execute: |
+ if [ "$TRUST_TEST_KEYS" = "false" ]; then
+ echo "This test needs test keys to be trusted"
+ exit
+ fi
+ #shellcheck source=tests/lib/boot.sh
+ . "$TESTSLIB"/boot.sh
+ wait_change_done() {
+ chg_summary="$1"
+ for _ in $(seq 10); do
+ if snap changes | MATCH "[0-9]+\\ +Done\\ +.* $chg_summary"; then
+ break
+ fi
+ # some debug output
+ snap changes
+ # wait a bit
+ sleep 5
+ done
+ snap changes | MATCH "[0-9]+\\ +Done\\ +.* $chg_summary"
+ }
+ # initial boot with the current model
+ if [ "$SPREAD_REBOOT" = 0 ]; then
+ # sanity check
+ snap list "$OLD_BASE"
+
+ echo "We have the right model assertion"
+ snap debug model|MATCH "model: my-model"
+ echo "Now we remodel"
+ snap remodel "$TESTSLIB"/assertions/developer1-pc-18-new-base.model
+ echo "Double check that we boot into the right base"
+ MATCH "snap_try_core=$NEW_BASE" < /boot/grub/grubenv
+ echo "reboot to finish the change"
+ REBOOT
+ fi
+ # first boot with the new model base
+ if [ "$SPREAD_REBOOT" = 1 ]; then
+ echo "and we have the new base snap installed"
+ snap list "$NEW_BASE"
+ echo "And are using it"
+ wait_core_post_boot
+ MATCH "snap_core=$NEW_BASE" < /boot/grub/grubenv
+ echo "and we got the new model assertion"
+ wait_change_done "Refresh model assertion from revision 0 to 2"
+ snap debug model|MATCH "revision: 2"
+ echo "and we cannot remove the base snap"
+ not snap remove "$NEW_BASE"
+ # TODO: test when keeping the old base, test removing the old base
+ # (not possible here as the pc gadget uses core18 as its base)
+ echo "And we can remodel again and remove the new base"
+ snap remodel "$TESTSLIB"/assertions/developer1-pc-18-revno3.model
+ REBOOT
+ fi
+ # reboot from new model to undo the new model again (to not pollute tests)
+ if [ "$SPREAD_REBOOT" = 2 ]; then
+ wait_core_post_boot
+ MATCH "snap_core=$OLD_BASE" < /boot/grub/grubenv
+ wait_change_done "Refresh model assertion from revision 2 to 3"
+ snap debug model|MATCH "revision: 3"
+ echo "cleanup"
+ snap remove "$NEW_BASE"
+ snap refresh --channel="$BASE_CHANNEL" "$OLD_BASE"
+ REBOOT
+ fi