Merge lp:~hduran-8/juju-core/add_state_network_check_on_specified_machine into lp:~go-bot/juju-core/trunk

Proposed by Horacio Durán
Status: Work in progress
Proposed branch: lp:~hduran-8/juju-core/add_state_network_check_on_specified_machine
Merge into: lp:~go-bot/juju-core/trunk
Diff against target: 415 lines (+278/-68)
3 files modified
state/machine.go (+42/-0)
state/network_assign_test.go (+88/-0)
state/unit.go (+148/-68)
To merge this branch: bzr merge lp:~hduran-8/juju-core/add_state_network_check_on_specified_machine
Reviewer Review Type Date Requested Status
Juju Engineering Pending
Review via email: mp+215143@code.launchpad.net

Description of the change

Add check for requested networks compatibility

This is still work in progress, the idea is to
check if the requested networks for a unit and
for a machine are compatible.
So far check looks that there is no requesting
to inlude nets excluded in the other and vice
versa.

https://codereview.appspot.com/86430043/

To post a comment you must log in.
2580. By Horacio Durán

Changed the way networks are obtained.
Fixed the corresponding tests
Added a draft of check for changes

2581. By Horacio Durán

Go Formatted, added extra comments for context

Revision history for this message
William Reade (fwereade) wrote :
Download full text (6.0 KiB)

firehose of suggestions :)

https://codereview.appspot.com/86430043/diff/20001/state/unit.go
File state/unit.go (right):

https://codereview.appspot.com/86430043/diff/20001/state/unit.go#newcode539
state/unit.go:539: return readRequestedNetworks(u.st, u.globalKey())
don't think we need this method now

https://codereview.appspot.com/86430043/diff/20001/state/unit.go#newcode833
state/unit.go:833: machineIncludedNetworks, machineExcludedNetworks, _
:= m.RequestedNetworks()
Basically never ignore errors. Certainly not here ;p.

https://codereview.appspot.com/86430043/diff/20001/state/unit.go#newcode839
state/unit.go:839: unitIncludedNetworks, unitExcludedNetworks, _ :=
networked_service.Networks()
readRequestedNetworks(u.st, serviceGlobalKey(u.doc.Service))

and don't ignore errors :).

https://codereview.appspot.com/86430043/diff/20001/state/unit.go#newcode852
state/unit.go:852: return fmt.Errorf("The requested network %q is
explicitly excluded in the requested machine %q ", includedNetwork,
m.Id())
We tend not to capitalise error messages

https://codereview.appspot.com/86430043/diff/20001/state/unit.go#newcode859
state/unit.go:859: }
This all might be easier with the methods on utils/set.Strings

https://codereview.appspot.com/86430043/diff/20001/state/unit.go#newcode864
state/unit.go:864: Assert: bson.D{{"txn-revno",
networked_service.doc.TxnRevno}},
We definitely don't want to assert on the service doc's txn-revno -- we
care about changes to the networks, which are on a different doc.

Ofc, you don't have access to them at the moment. I'd add a TxnRevno
field to the requestedNetworks doc, and have readRequestedNetworks
return that too; the RequestedNetworks methods can ignore it, and this
method can use it in txns.

Make sure you return -1 if it's missing, though, and assert DocMissing
:).

https://codereview.appspot.com/86430043/diff/20001/state/unit.go#newcode866
state/unit.go:866: err = u.st.runTransaction(ops)
When building this sort of construction:

if err := u.st.runTransaction(ops); err != txn.ErrAborted {
     return err
}

...replaces this line and the subsequent two blocks.

*but* asserting at this point does us no good -- we have to build the
correct asserts and include them in the transaction, below, that
actually does the assignment.

https://codereview.appspot.com/86430043/diff/20001/state/unit.go#newcode887
state/unit.go:887: assert := append(isAliveDoc, bson.D{
(rename this unitAssert for clarity's sake)

https://codereview.appspot.com/86430043/diff/20001/state/unit.go#newcode893
state/unit.go:893: massert := isAliveDoc
(and this to machineAssert)

https://codereview.appspot.com/86430043/diff/20001/state/unit.go#newcode932
state/unit.go:932: }
I would recommend starting off by refactoring the method to have the
shape we want, so it can handle retries, and making sure all original
tests still pass; and then adding in the extra bits. So, something like:

func (u *Unit) assignToMachine(m *Machine, unused bool) (err error) {
     // On the expectation that we'll have more than one possible nil
return,
     // defer the updates to local state.
     defer func() {
         if err != nil {
             u.doc.MachineId = m.doc.Id...

Read more...

2582. By Horacio Durán

Refactored assignToMachine Added proper tests in separate file, added clone to unit and machine

2583. By Horacio Durán

Go formatted

2584. By Horacio Durán

Go formatte

2585. By Horacio Durán

Ensured there is a retry when txnrevno of unit does not match

Revision history for this message
William Reade (fwereade) wrote :

couple of comments, let's chat live

https://codereview.appspot.com/86430043/diff/40001/state/unit.go
File state/unit.go (right):

https://codereview.appspot.com/86430043/diff/40001/state/unit.go#newcode869
state/unit.go:869: if unit.doc.MachineId != machine.Id() {
This in particular needs to go inside the loop -- unit and machine
assignments can change.

(also, should we be checking that the machine's principals are empty if
unused is true? I think we should...)

https://codereview.appspot.com/86430043/diff/40001/state/unit.go#newcode881
state/unit.go:881: if err := unit.st.supportsUnitPlacement(); err != nil
{
I don't think this is mutable state, is it? we can check it outside the
loop.

https://codereview.appspot.com/86430043/diff/40001/state/unit.go#newcode923
state/unit.go:923: }
Everything from here up to the top of the loop feels like it should all
be inside the assignToMachineOps method -- calculating the ops and
verifying that they're sane should go hand in hand. (this applies to
stuff like the life checks too -- check them while building the txn,
don't just build it blind, wait for it to fail, and then look up why.)

https://codereview.appspot.com/86430043/diff/40001/state/unit.go#newcode940
state/unit.go:940: case unit.doc.Life != Alive:
You shouldn't need this code here. It should all be checked as we
construct the transaction -- we have access to latest-known-state up at
the top of the loop, and we can and should check unit life etc *before*
we try to run a transaction asserting that it's the case.

Apart from anything else, you're inspecting old state here -- it doesn't
necessarily have any bearing on why the txn mechanism rejected the ops.

https://codereview.appspot.com/86430043/

Revision history for this message
Horacio Durán (hduran-8) wrote :

Wow i was not aware it was so bad, right now i am on holiday and on a
touristic place very far from a pc. ill Ping you on monday as soon as i
begin my day

El jueves, 17 de abril de 2014, <email address hidden> escribió:

> couple of comments, let's chat live
>
>
> https://codereview.appspot.com/86430043/diff/40001/state/unit.go
> File state/unit.go (right):
>
> https://codereview.appspot.com/86430043/diff/40001/state/
> unit.go#newcode869
> state/unit.go:869: if unit.doc.MachineId != machine.Id() {
> This in particular needs to go inside the loop -- unit and machine
> assignments can change.
>
> (also, should we be checking that the machine's principals are empty if
> unused is true? I think we should...)
>
> https://codereview.appspot.com/86430043/diff/40001/state/
> unit.go#newcode881
> state/unit.go:881: if err := unit.st.supportsUnitPlacement(); err != nil
> {
> I don't think this is mutable state, is it? we can check it outside the
> loop.
>
> https://codereview.appspot.com/86430043/diff/40001/state/
> unit.go#newcode923
> state/unit.go:923: }
> Everything from here up to the top of the loop feels like it should all
> be inside the assignToMachineOps method -- calculating the ops and
> verifying that they're sane should go hand in hand. (this applies to
> stuff like the life checks too -- check them while building the txn,
> don't just build it blind, wait for it to fail, and then look up why.)
>
> https://codereview.appspot.com/86430043/diff/40001/state/
> unit.go#newcode940
> state/unit.go:940: case unit.doc.Life != Alive:
> You shouldn't need this code here. It should all be checked as we
> construct the transaction -- we have access to latest-known-state up at
> the top of the loop, and we can and should check unit life etc *before*
> we try to run a transaction asserting that it's the case.
>
> Apart from anything else, you're inspecting old state here -- it doesn't
> necessarily have any bearing on why the txn mechanism rejected the ops.
>
> https://codereview.appspot.com/86430043/
>

Unmerged revisions

2585. By Horacio Durán

Ensured there is a retry when txnrevno of unit does not match

2584. By Horacio Durán

Go formatte

2583. By Horacio Durán

Go formatted

2582. By Horacio Durán

Refactored assignToMachine Added proper tests in separate file, added clone to unit and machine

2581. By Horacio Durán

Go Formatted, added extra comments for context

2580. By Horacio Durán

Changed the way networks are obtained.
Fixed the corresponding tests
Added a draft of check for changes

2579. By Horacio Durán

Ran go fmt

2578. By Horacio Durán

Added rough check for network compatibility when adding to machine

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'state/machine.go'
--- state/machine.go 2014-04-13 11:06:42 +0000
+++ state/machine.go 2014-04-14 23:52:31 +0000
@@ -134,6 +134,48 @@
134 return machine134 return machine
135}135}
136136
137func (m *Machine) Clone() *Machine {
138 machineClone := &Machine{
139 st: m.st,
140 doc: m.doc,
141 }
142
143 machinePrincipals := make([]string, len(m.doc.Principals))
144 for key, value := range m.doc.Principals {
145 machinePrincipals[key] = value
146 }
147 machineClone.doc.Principals = machinePrincipals
148 machineJobs := make([]MachineJob, len(m.doc.Jobs))
149 for key, value := range m.doc.Jobs {
150 machineJobs[key] = value
151 }
152 machineClone.doc.Jobs = machineJobs
153 machineAddresses := make([]address, len(m.doc.Addresses))
154 for key, value := range m.doc.Addresses {
155 machineAddresses[key] = value
156 }
157 machineClone.doc.Addresses = machineAddresses
158 machineMachineAddresses := make([]address, len(m.doc.MachineAddresses))
159 for key, value := range m.doc.MachineAddresses {
160 machineMachineAddresses[key] = value
161 }
162 machineClone.doc.MachineAddresses = machineMachineAddresses
163
164 machineSupportedContainers := make([]instance.ContainerType, len(m.doc.SupportedContainers))
165 for key, value := range m.doc.SupportedContainers {
166 machineSupportedContainers[key] = value
167 }
168 machineClone.doc.SupportedContainers = machineSupportedContainers
169
170 machineClone.annotator = annotator{
171 globalKey: machineClone.globalKey(),
172 tag: machineClone.Tag(),
173 st: machineClone.st,
174 }
175 return machineClone
176
177}
178
137// Id returns the machine id.179// Id returns the machine id.
138func (m *Machine) Id() string {180func (m *Machine) Id() string {
139 return m.doc.Id181 return m.doc.Id
140182
=== added file 'state/network_assign_test.go'
--- state/network_assign_test.go 1970-01-01 00:00:00 +0000
+++ state/network_assign_test.go 2014-04-14 23:52:31 +0000
@@ -0,0 +1,88 @@
1// Copyright 2012, 2013 Canonical Ltd.
2// Licensed under the AGPLv3, see LICENCE file for details.
3
4package state_test
5
6import (
7 "fmt"
8
9 gc "launchpad.net/gocheck"
10
11 "launchpad.net/juju-core/state"
12)
13
14type NetworkedUnitSuite struct {
15 UnitSuite
16}
17
18var _ = gc.Suite(&NetworkedUnitSuite{})
19
20func (s *NetworkedUnitSuite) SetUpTest(c *gc.C) {
21 includeNetworks := []string{"net1", "net3", "net5"}
22 excludeNetworks := []string{"net2", "net4", "net6"}
23 s.ConnSuite.SetUpTest(c)
24 s.charm = s.AddTestingCharm(c, "wordpress")
25 var err error
26 s.service = s.AddTestingServiceWithNetworks(c, "wordpress", s.charm, includeNetworks, excludeNetworks)
27 c.Assert(err, gc.IsNil)
28 s.unit, err = s.service.AddUnit()
29 c.Assert(err, gc.IsNil)
30 c.Assert(s.unit.Series(), gc.Equals, "quantal")
31}
32
33func (s *NetworkedUnitSuite) TestIncludeExcludeNetworksCompatibilityCheckPasses(c *gc.C) {
34 includeNetworks := []string{"net1", "net3", "net5"}
35 excludeNetworks := []string{"net2", "net4", "net6"}
36
37 template := state.MachineTemplate{
38 Series: "quantal",
39 Jobs: []state.MachineJob{state.JobHostUnits},
40 IncludeNetworks: includeNetworks,
41 ExcludeNetworks: excludeNetworks,
42 }
43
44 machine, err := s.State.AddOneMachine(template)
45 c.Assert(err, gc.IsNil)
46
47 // TODO: (hduran-8) I really need to find a way to trigger reload
48 // So I can test that there is a retry but could not think
49 // a proper transactionhook
50
51 err = s.unit.AssignToMachine(machine)
52 c.Assert(err, gc.IsNil)
53}
54
55func (s *NetworkedUnitSuite) TestExcludeNetworksCompatibilityCheckFails(c *gc.C) {
56 includeNetworks := []string{"net2", "net4", "net6"}
57 excludeNetworks := []string{"net1", "net3", "net5"}
58
59 template := state.MachineTemplate{
60 Series: "quantal",
61 Jobs: []state.MachineJob{state.JobHostUnits},
62 IncludeNetworks: includeNetworks,
63 ExcludeNetworks: excludeNetworks,
64 }
65 machine, err := s.State.AddOneMachine(template)
66 c.Assert(err, gc.IsNil)
67 err = s.unit.AssignToMachine(machine)
68 expected_error := fmt.Errorf("cannot assign unit \"wordpress/0\" to machine 0: the requested networks [\"net1\" \"net3\" \"net5\"] are explicitly excluded in the requested machine \"0\" ")
69 c.Assert(err, gc.DeepEquals, expected_error)
70
71}
72
73func (s *NetworkedUnitSuite) TestInvludeNetworksCompatibilityCheckFails(c *gc.C) {
74 includeNetworks := []string{"net2", "net4", "net6"}
75 excludeNetworks := []string{"net7", "net9", "net11"}
76
77 template := state.MachineTemplate{
78 Series: "quantal",
79 Jobs: []state.MachineJob{state.JobHostUnits},
80 IncludeNetworks: includeNetworks,
81 ExcludeNetworks: excludeNetworks,
82 }
83 machine, err := s.State.AddOneMachine(template)
84 c.Assert(err, gc.IsNil)
85 err = s.unit.AssignToMachine(machine)
86 expected_error := fmt.Errorf("cannot assign unit \"wordpress/0\" to machine 0: the requested networks [\"net2\" \"net4\" \"net6\"] are explicitly included in the requested machine \"0\" ")
87 c.Assert(err, gc.DeepEquals, expected_error)
88}
089
=== modified file 'state/unit.go'
--- state/unit.go 2014-04-12 05:53:58 +0000
+++ state/unit.go 2014-04-14 23:52:31 +0000
@@ -22,6 +22,7 @@
22 "launchpad.net/juju-core/state/presence"22 "launchpad.net/juju-core/state/presence"
23 "launchpad.net/juju-core/tools"23 "launchpad.net/juju-core/tools"
24 "launchpad.net/juju-core/utils"24 "launchpad.net/juju-core/utils"
25 "launchpad.net/juju-core/utils/set"
25 "launchpad.net/juju-core/version"26 "launchpad.net/juju-core/version"
26)27)
2728
@@ -102,6 +103,32 @@
102 return unit103 return unit
103}104}
104105
106// Provides a reazonably deep copy of the given unit
107func (u *Unit) Clone() *Unit {
108 // The following fields have been omitted from explicit copy
109 // because I did not find reason to deep copy them
110 // CharmURL *charm.URL
111 // Tools *tools.Tools `bson:",omitempty"`
112 unitClone := &Unit{st: u.st, doc: u.doc}
113 unitSubordinates := make([]string, len(u.doc.Subordinates))
114 for key, value := range u.doc.Subordinates {
115 unitSubordinates[key] = value
116 }
117 unitClone.doc.Subordinates = unitSubordinates
118 unitPorts := make([]instance.Port, len(u.doc.Ports))
119 for key, value := range u.doc.Ports {
120 unitPorts[key] = value
121 }
122 unitClone.doc.Ports = unitPorts
123
124 unitClone.annotator = annotator{
125 globalKey: unitClone.globalKey(),
126 tag: unitClone.Tag(),
127 st: unitClone.st,
128 }
129 return unitClone
130}
131
105// Service returns the service.132// Service returns the service.
106func (u *Unit) Service() (*Service, error) {133func (u *Unit) Service() (*Service, error) {
107 return u.st.Service(u.doc.Service)134 return u.st.Service(u.doc.Service)
@@ -269,7 +296,7 @@
269 u.doc.Life = Dying296 u.doc.Life = Dying
270 }297 }
271 }()298 }()
272 unit := &Unit{st: u.st, doc: u.doc}299 unit := u.Clone()
273 for i := 0; i < 5; i++ {300 for i := 0; i < 5; i++ {
274 switch ops, err := unit.destroyOps(); err {301 switch ops, err := unit.destroyOps(); err {
275 case errRefresh:302 case errRefresh:
@@ -441,7 +468,7 @@
441468
442 // Now we're sure we haven't left any scopes occupied by this unit, we469 // Now we're sure we haven't left any scopes occupied by this unit, we
443 // can safely remove the document.470 // can safely remove the document.
444 unit := &Unit{st: u.st, doc: u.doc}471 unit := u.Clone()
445 for i := 0; i < 5; i++ {472 for i := 0; i < 5; i++ {
446 switch ops, err := unit.removeOps(isDeadDoc); err {473 switch ops, err := unit.removeOps(isDeadDoc); err {
447 case errRefresh:474 case errRefresh:
@@ -816,6 +843,33 @@
816 inUseErr = stderrors.New("machine is not unused")843 inUseErr = stderrors.New("machine is not unused")
817)844)
818845
846func (u *Unit) assignToMachineOps(m *Machine, unused bool) []txn.Op {
847 assert := append(isAliveDoc, bson.D{
848 {"$or", []bson.D{
849 {{"machineid", ""}},
850 {{"machineid", m.Id()}},
851 }},
852 }...)
853 massert := isAliveDoc
854 if unused {
855 massert = append(massert, bson.D{{"clean", bson.D{{"$ne", false}}}}...)
856 }
857 assert = append(assert, bson.D{{"txn-revno", u.doc.TxnRevno}}...)
858 ops := []txn.Op{
859 {
860 C: u.st.units.Name,
861 Id: u.doc.Name,
862 Assert: assert,
863 Update: bson.D{{"$set", bson.D{{"machineid", m.doc.Id}}}},
864 }, {
865 C: u.st.machines.Name,
866 Id: m.doc.Id,
867 Assert: massert,
868 Update: bson.D{{"$addToSet", bson.D{{"principals", u.doc.Name}}}, {"$set", bson.D{{"clean", false}}}},
869 }}
870 return ops
871}
872
819// assignToMachine is the internal version of AssignToMachine,873// assignToMachine is the internal version of AssignToMachine,
820// also used by AssignToUnusedMachine. It returns specific errors874// also used by AssignToUnusedMachine. It returns specific errors
821// in some cases:875// in some cases:
@@ -824,80 +878,106 @@
824// - alreadyAssignedErr when the unit has already been assigned878// - alreadyAssignedErr when the unit has already been assigned
825// - inUseErr when the machine already has a unit assigned (if unused is true)879// - inUseErr when the machine already has a unit assigned (if unused is true)
826func (u *Unit) assignToMachine(m *Machine, unused bool) (err error) {880func (u *Unit) assignToMachine(m *Machine, unused bool) (err error) {
827 if u.doc.Series != m.doc.Series {881
882 unit := u.Clone()
883 machine := m.Clone()
884
885 // Static checking
886 if unit.doc.Series != machine.doc.Series {
828 return fmt.Errorf("series does not match")887 return fmt.Errorf("series does not match")
829 }888 }
830 if u.doc.MachineId != "" {889 if unit.doc.MachineId != "" {
831 if u.doc.MachineId != m.Id() {890 if unit.doc.MachineId != machine.Id() {
832 return alreadyAssignedErr891 return alreadyAssignedErr
833 }892 }
834 return nil893 return nil
835 }894 }
836 if u.doc.Principal != "" {895 if unit.doc.Principal != "" {
837 return fmt.Errorf("unit is a subordinate")896 return fmt.Errorf("unit is a subordinate")
838 }897 }
839 canHost := false898 for i := 0; i < 3; i++ {
840 for _, j := range m.doc.Jobs {899
841 if j == JobHostUnits {900 // assignToMachine implies assignment to an existing machine,
842 canHost = true901 // which is only permitted if unit placement is supported.
843 break902 if err := unit.st.supportsUnitPlacement(); err != nil {
844 }903 return err
845 }904 }
846 if !canHost {905
847 return fmt.Errorf("machine %q cannot host units", m)906 // Jobs
848 }907 canHost := false
849 // assignToMachine implies assignment to an existing machine,908 for _, j := range m.doc.Jobs {
850 // which is only permitted if unit placement is supported.909 if j == JobHostUnits {
851 if err := u.st.supportsUnitPlacement(); err != nil {910 canHost = true
852 return err911 break
853 }912 }
854 assert := append(isAliveDoc, bson.D{913 }
855 {"$or", []bson.D{914 if !canHost {
856 {{"machineid", ""}},915 return fmt.Errorf("machine %q cannot host units", m)
857 {{"machineid", m.Id()}},916 }
858 }},917
859 }...)918 // Check non colliding network settings
860 massert := isAliveDoc919 machineIncludedNetworks, machineExcludedNetworks, err := machine.RequestedNetworks()
861 if unused {920 if err != nil {
862 massert = append(massert, bson.D{{"clean", bson.D{{"$ne", false}}}}...)921 return err
863 }922 }
864 ops := []txn.Op{{923
865 C: u.st.units.Name,924 unitIncludedNetworks, unitExcludedNetworks, err := readRequestedNetworks(unit.st, serviceGlobalKey(unit.doc.Service))
866 Id: u.doc.Name,925 if err != nil {
867 Assert: assert,926 return err
868 Update: bson.D{{"$set", bson.D{{"machineid", m.doc.Id}}}},927 }
869 }, {928
870 C: u.st.machines.Name,929 machineExcludedSet := set.NewStrings(machineExcludedNetworks...)
871 Id: m.doc.Id,930 machineIncludedSet := set.NewStrings(machineIncludedNetworks...)
872 Assert: massert,931 unitExcludedSet := set.NewStrings(unitExcludedNetworks...)
873 Update: bson.D{{"$addToSet", bson.D{{"principals", u.doc.Name}}}, {"$set", bson.D{{"clean", false}}}},932 unitIncludedSet := set.NewStrings(unitIncludedNetworks...)
874 }}933
875 err = u.st.runTransaction(ops)934 mExcludeUIncludeIntersection := unitIncludedSet.Intersection(machineExcludedSet)
876 if err == nil {935
877 u.doc.MachineId = m.doc.Id936 if mExcludeUIncludeIntersection.Size() > 0 {
878 m.doc.Clean = false937 return fmt.Errorf("the requested networks %q are explicitly excluded in the requested machine %q ", mExcludeUIncludeIntersection.Values(), machine.Id())
879 return nil938 }
880 }939
881 if err != txn.ErrAborted {940 mIncludeUExcludeIntersection := unitExcludedSet.Intersection(machineIncludedSet)
882 return err941
883 }942 if mIncludeUExcludeIntersection.Size() > 0 {
884 u0, err := u.st.Unit(u.Name())943 return fmt.Errorf("the requested networks %q are explicitly included in the requested machine %q ", mIncludeUExcludeIntersection.Values(), machine.Id())
885 if err != nil {944 }
886 return err945
887 }946 // ops
888 m0, err := u.st.Machine(m.Id())947 ops := unit.assignToMachineOps(machine, unused)
889 if err != nil {948 err = u.st.runTransaction(ops)
890 return err949
891 }950 if err == nil {
892 switch {951 u.doc.MachineId = m.doc.Id
893 case u0.Life() != Alive:952 m.doc.Clean = false
894 return unitNotAliveErr953 return nil
895 case m0.Life() != Alive:954 }
896 return machineNotAliveErr955
897 case u0.doc.MachineId != "" || !unused:956 if i > 1 {
898 return alreadyAssignedErr957 if err != txn.ErrAborted {
899 }958 return err
900 return inUseErr959 }
960
961 switch {
962 case unit.doc.Life != Alive:
963 return unitNotAliveErr
964 case machine.doc.Life != Alive:
965 return machineNotAliveErr
966 case u.doc.MachineId != "" || !unused:
967 return alreadyAssignedErr
968 }
969 }
970
971 if err := unit.Refresh(); err != nil {
972 return err
973 }
974 if err := machine.Refresh(); err != nil {
975 return err
976 }
977
978 }
979
980 return ErrExcessiveContention
901}981}
902982
903func assignContextf(err *error, unit *Unit, target string) {983func assignContextf(err *error, unit *Unit, target string) {

Subscribers

People subscribed via source and target branches

to status/vote changes: