diff options
| author | Pawel Stolowski <stolowski@gmail.com> | 2019-11-07 10:34:20 +0100 |
|---|---|---|
| committer | Pawel Stolowski <stolowski@gmail.com> | 2019-11-07 10:34:20 +0100 |
| commit | b407e74241ee41b94166b7357857de89946e28b9 (patch) | |
| tree | 09ca88033a73b8728dc613c3578aeabc1a70625d | |
| parent | 75fcfbb6326d21e065a1cca559d9a54aa359e8de (diff) | |
| parent | 780f4a86b268d4c7232afb8411b569992c9a3a3e (diff) | |
Merge branch 'master' into remove-autoconnectionsremove-autoconnections
| -rw-r--r-- | asserts/ifacedecls.go | 29 | ||||
| -rw-r--r-- | asserts/ifacedecls_test.go | 59 | ||||
| -rw-r--r-- | interfaces/policy/basedeclaration_test.go | 76 | ||||
| -rw-r--r-- | interfaces/policy/helpers.go | 29 | ||||
| -rw-r--r-- | interfaces/policy/policy.go | 52 | ||||
| -rw-r--r-- | interfaces/policy/policy_test.go | 110 | ||||
| -rw-r--r-- | interfaces/repo.go | 23 | ||||
| -rw-r--r-- | interfaces/repo_test.go | 104 | ||||
| -rw-r--r-- | overlord/devicestate/devicestate.go | 59 | ||||
| -rw-r--r-- | overlord/devicestate/devicestate_remodel_test.go | 59 | ||||
| -rw-r--r-- | overlord/ifacestate/handlers.go | 5 | ||||
| -rw-r--r-- | overlord/ifacestate/helpers.go | 37 | ||||
| -rw-r--r-- | overlord/ifacestate/ifacestate_test.go | 192 | ||||
| -rw-r--r-- | overlord/managers_test.go | 430 | ||||
| -rw-r--r-- | overlord/snapstate/snapstate.go | 49 | ||||
| -rw-r--r-- | seed/seed20.go | 12 | ||||
| -rw-r--r-- | seed/seed20_test.go | 364 | ||||
| -rw-r--r-- | seed/seedtest/seedtest.go | 19 | ||||
| -rw-r--r-- | seed/seedwriter/writer_test.go | 37 | ||||
| -rw-r--r-- | spread.yaml | 1 | ||||
| -rw-r--r-- | tests/lib/assertions/developer1-pc-18-new-base.model | 23 | ||||
| -rwxr-xr-x | tests/lib/state.sh | 5 | ||||
| -rw-r--r-- | tests/main/remodel-base/task.yaml | 122 |
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 |
