Merge lp:~axwalk/juju-core/provider-unit-assignment-policy into lp:~go-bot/juju-core/trunk
- provider-unit-assignment-policy
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Andrew Wilkins |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2812 |
Proposed branch: | lp:~axwalk/juju-core/provider-unit-assignment-policy |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
544 lines (+383/-50) 8 files modified
environs/statepolicy.go (+13/-0) state/apiserver/provisioner/provisioner.go (+6/-27) state/conn_test.go (+8/-0) state/distribution.go (+77/-0) state/distribution_test.go (+185/-0) state/policy.go (+30/-8) state/service.go (+7/-3) state/unit.go (+57/-12) |
To merge this branch: | bzr merge lp:~axwalk/juju-core/provider-unit-assignment-policy |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+221481@code.launchpad.net |
Commit message
Introduce InstanceDistributor policy
This change introduces an InstanceDistributor
policy, which will be used to implement automatic
Availability Zone management for AWS and OpenStack.
When an attempt is made to assign a unit to a clean
machine, we gather the instances that have already
been provisioned, and provide those and the unit's
distribution group to the provider. The provider
may select as many or as few of the candidates as it
wishes to allow; the assignment policy will then
choose one of them for assigning to. If no candidates
are returned, the unit will be assigned to a new
machine.
There are currently no implementations of this policy;
they will be forthcoming when manual AZ support lands
in the amazon/openstack providers.
Description of the change
Introduce InstanceDistributor policy
This change introduces an InstanceDistributor
policy, which will be used to implement automatic
Availability Zone management for AWS and OpenStack.
When an attempt is made to assign a unit to a clean
machine, we gather the instances that have already
been provisioned, and provide those and the unit's
distribution group to the provider. The provider
may select as many or as few of the candidates as it
wishes to allow; the assignment policy will then
choose one of them for assigning to. If no candidates
are returned, the unit will be assigned to a new
machine.
There are currently no implementations of this policy;
they will be forthcoming when manual AZ support lands
in the amazon/openstack providers.
Andrew Wilkins (axwalk) wrote : | # |
William Reade (fwereade) wrote : | # |
LGTM with a bit of mindful rearrangement.
https:/
File state/distribut
https:/
state/distribut
service.
fwiw, this doesn't feel *quite* right because we don't actually ever
need/use the *Service directly -- the AllUnits could be figured out from
the unit name alone, so grabbing the service doc is just an unnecessary
db hit (I think?).
But it's a balance: using the methods we already have for
familarity/
on ids and less on instantiated objects and *might* eventually lead us
in a leaner/cleaner direction. I have a slight preference for the latter
but I'll leave it to your judgment.
https:/
state/distribut
([]instance.Id, error) {
Suggestion: ServiceInstance
over spreading the methods on a particular type across files, but just
making the receiver a regular param pretty much eliminates that feeling
of ick. I'm not sure it's *entirely* rational, but...
Anyway, same applies to Unit.distribute above. Doesn't feel like it's
quite in the right place.
https:/
File state/policy.go (right):
https:/
state/policy.
where to? ;p
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File state/distribut
https:/
state/distribut
service.
On 2014/05/30 08:51:12, fwereade wrote:
> fwiw, this doesn't feel *quite* right because we don't actually ever
need/use
> the *Service directly -- the AllUnits could be figured out from the
unit name
> alone, so grabbing the service doc is just an unnecessary db hit (I
think?).
Indeed it is unnecessary. I've changed ServiceInstances to take a
service name, and extracted the logic of Service.AllUnits out to a free
function which takes a service name.
> But it's a balance: using the methods we already have for
familarity/
> sake, vs writing more primitives that operate more on ids and less on
> instantiated objects and *might* eventually lead us in a
leaner/cleaner
> direction. I have a slight preference for the latter but I'll leave it
to your
> judgment.
https:/
state/distribut
([]instance.Id, error) {
On 2014/05/30 08:51:12, fwereade wrote:
> Suggestion: ServiceInstance
discomfort over
> spreading the methods on a particular type across files, but just
making the
> receiver a regular param pretty much eliminates that feeling of ick.
I'm not
> sure it's *entirely* rational, but...
> Anyway, same applies to Unit.distribute above. Doesn't feel like it's
quite in
> the right place.
Changed both to free functions.
https:/
File state/policy.go (right):
https:/
state/policy.
On 2014/05/30 08:51:12, fwereade wrote:
> where to? ;p
Yeah, it's probably best just left here.
Preview Diff
1 | === modified file 'environs/statepolicy.go' | |||
2 | --- environs/statepolicy.go 2014-04-24 12:33:19 +0000 | |||
3 | +++ environs/statepolicy.go 2014-05-30 12:33:30 +0000 | |||
4 | @@ -4,6 +4,8 @@ | |||
5 | 4 | package environs | 4 | package environs |
6 | 5 | 5 | ||
7 | 6 | import ( | 6 | import ( |
8 | 7 | "github.com/juju/errors" | ||
9 | 8 | |||
10 | 7 | "launchpad.net/juju-core/constraints" | 9 | "launchpad.net/juju-core/constraints" |
11 | 8 | "launchpad.net/juju-core/environs/config" | 10 | "launchpad.net/juju-core/environs/config" |
12 | 9 | "launchpad.net/juju-core/state" | 11 | "launchpad.net/juju-core/state" |
13 | @@ -44,3 +46,14 @@ | |||
14 | 44 | } | 46 | } |
15 | 45 | return env.ConstraintsValidator() | 47 | return env.ConstraintsValidator() |
16 | 46 | } | 48 | } |
17 | 49 | |||
18 | 50 | func (environStatePolicy) InstanceDistributor(cfg *config.Config) (state.InstanceDistributor, error) { | ||
19 | 51 | env, err := New(cfg) | ||
20 | 52 | if err != nil { | ||
21 | 53 | return nil, err | ||
22 | 54 | } | ||
23 | 55 | if p, ok := env.(state.InstanceDistributor); ok { | ||
24 | 56 | return p, nil | ||
25 | 57 | } | ||
26 | 58 | return nil, errors.NotImplementedf("InstanceDistributor") | ||
27 | 59 | } | ||
28 | 47 | 60 | ||
29 | === modified file 'state/apiserver/provisioner/provisioner.go' | |||
30 | --- state/apiserver/provisioner/provisioner.go 2014-05-13 12:57:53 +0000 | |||
31 | +++ state/apiserver/provisioner/provisioner.go 2014-05-30 12:33:30 +0000 | |||
32 | @@ -416,33 +416,12 @@ | |||
33 | 416 | if !unit.IsPrincipal() { | 416 | if !unit.IsPrincipal() { |
34 | 417 | continue | 417 | continue |
35 | 418 | } | 418 | } |
63 | 419 | service, err := unit.Service() | 419 | instanceIds, err := state.ServiceInstances(st, unit.ServiceName()) |
64 | 420 | if err != nil { | 420 | if err != nil { |
65 | 421 | return nil, err | 421 | return nil, err |
66 | 422 | } | 422 | } |
67 | 423 | allUnits, err := service.AllUnits() | 423 | for _, instanceId := range instanceIds { |
68 | 424 | if err != nil { | 424 | instanceIdSet.Add(string(instanceId)) |
42 | 425 | return nil, err | ||
43 | 426 | } | ||
44 | 427 | for _, unit := range allUnits { | ||
45 | 428 | machineId, err := unit.AssignedMachineId() | ||
46 | 429 | if state.IsNotAssigned(err) { | ||
47 | 430 | continue | ||
48 | 431 | } else if err != nil { | ||
49 | 432 | return nil, err | ||
50 | 433 | } | ||
51 | 434 | machine, err := st.Machine(machineId) | ||
52 | 435 | if err != nil { | ||
53 | 436 | return nil, err | ||
54 | 437 | } | ||
55 | 438 | instanceId, err := machine.InstanceId() | ||
56 | 439 | if err == nil { | ||
57 | 440 | instanceIdSet.Add(string(instanceId)) | ||
58 | 441 | } else if state.IsNotProvisionedError(err) { | ||
59 | 442 | continue | ||
60 | 443 | } else { | ||
61 | 444 | return nil, err | ||
62 | 445 | } | ||
69 | 446 | } | 425 | } |
70 | 447 | } | 426 | } |
71 | 448 | instanceIds := make([]instance.Id, instanceIdSet.Size()) | 427 | instanceIds := make([]instance.Id, instanceIdSet.Size()) |
72 | 449 | 428 | ||
73 | === modified file 'state/conn_test.go' | |||
74 | --- state/conn_test.go 2014-05-20 04:27:02 +0000 | |||
75 | +++ state/conn_test.go 2014-05-30 12:33:30 +0000 | |||
76 | @@ -105,6 +105,7 @@ | |||
77 | 105 | getConfigValidator func(string) (state.ConfigValidator, error) | 105 | getConfigValidator func(string) (state.ConfigValidator, error) |
78 | 106 | getEnvironCapability func(*config.Config) (state.EnvironCapability, error) | 106 | getEnvironCapability func(*config.Config) (state.EnvironCapability, error) |
79 | 107 | getConstraintsValidator func(*config.Config) (constraints.Validator, error) | 107 | getConstraintsValidator func(*config.Config) (constraints.Validator, error) |
80 | 108 | getInstanceDistributor func(*config.Config) (state.InstanceDistributor, error) | ||
81 | 108 | } | 109 | } |
82 | 109 | 110 | ||
83 | 110 | func (p *mockPolicy) Prechecker(cfg *config.Config) (state.Prechecker, error) { | 111 | func (p *mockPolicy) Prechecker(cfg *config.Config) (state.Prechecker, error) { |
84 | @@ -134,3 +135,10 @@ | |||
85 | 134 | } | 135 | } |
86 | 135 | return nil, errors.NewNotImplemented(nil, "ConstraintsValidator") | 136 | return nil, errors.NewNotImplemented(nil, "ConstraintsValidator") |
87 | 136 | } | 137 | } |
88 | 138 | |||
89 | 139 | func (p *mockPolicy) InstanceDistributor(cfg *config.Config) (state.InstanceDistributor, error) { | ||
90 | 140 | if p.getInstanceDistributor != nil { | ||
91 | 141 | return p.getInstanceDistributor(cfg) | ||
92 | 142 | } | ||
93 | 143 | return nil, errors.NewNotImplemented(nil, "InstanceDistributor") | ||
94 | 144 | } | ||
95 | 137 | 145 | ||
96 | === added file 'state/distribution.go' | |||
97 | --- state/distribution.go 1970-01-01 00:00:00 +0000 | |||
98 | +++ state/distribution.go 2014-05-30 12:33:30 +0000 | |||
99 | @@ -0,0 +1,77 @@ | |||
100 | 1 | // Copyright 2014 Canonical Ltd. | ||
101 | 2 | // Licensed under the AGPLv3, see LICENCE file for details. | ||
102 | 3 | |||
103 | 4 | package state | ||
104 | 5 | |||
105 | 6 | import ( | ||
106 | 7 | "fmt" | ||
107 | 8 | |||
108 | 9 | "github.com/juju/errors" | ||
109 | 10 | |||
110 | 11 | "launchpad.net/juju-core/instance" | ||
111 | 12 | ) | ||
112 | 13 | |||
113 | 14 | // distributeuUnit takes a unit and set of clean, possibly empty, instances | ||
114 | 15 | // and asks the InstanceDistributor policy (if any) which ones are suitable | ||
115 | 16 | // for assigning the unit to. If there is no InstanceDistributor, or the | ||
116 | 17 | // distribution group is empty, then all of the candidates will be returned. | ||
117 | 18 | func distributeUnit(u *Unit, candidates []instance.Id) ([]instance.Id, error) { | ||
118 | 19 | if len(candidates) == 0 { | ||
119 | 20 | return nil, nil | ||
120 | 21 | } | ||
121 | 22 | if u.st.policy == nil { | ||
122 | 23 | return candidates, nil | ||
123 | 24 | } | ||
124 | 25 | cfg, err := u.st.EnvironConfig() | ||
125 | 26 | if err != nil { | ||
126 | 27 | return nil, err | ||
127 | 28 | } | ||
128 | 29 | distributor, err := u.st.policy.InstanceDistributor(cfg) | ||
129 | 30 | if errors.IsNotImplemented(err) { | ||
130 | 31 | return candidates, nil | ||
131 | 32 | } else if err != nil { | ||
132 | 33 | return nil, err | ||
133 | 34 | } | ||
134 | 35 | if distributor == nil { | ||
135 | 36 | return nil, fmt.Errorf("policy returned nil instance distributor without an error") | ||
136 | 37 | } | ||
137 | 38 | distributionGroup, err := ServiceInstances(u.st, u.doc.Service) | ||
138 | 39 | if err != nil { | ||
139 | 40 | return nil, err | ||
140 | 41 | } | ||
141 | 42 | if len(distributionGroup) == 0 { | ||
142 | 43 | return candidates, nil | ||
143 | 44 | } | ||
144 | 45 | return distributor.DistributeInstances(candidates, distributionGroup) | ||
145 | 46 | } | ||
146 | 47 | |||
147 | 48 | // ServiceInstances returns the instance IDs of provisioned | ||
148 | 49 | // machines that are assigned units of the specified service. | ||
149 | 50 | func ServiceInstances(st *State, service string) ([]instance.Id, error) { | ||
150 | 51 | units, err := allUnits(st, service) | ||
151 | 52 | if err != nil { | ||
152 | 53 | return nil, err | ||
153 | 54 | } | ||
154 | 55 | instanceIds := make([]instance.Id, 0, len(units)) | ||
155 | 56 | for _, unit := range units { | ||
156 | 57 | machineId, err := unit.AssignedMachineId() | ||
157 | 58 | if IsNotAssigned(err) { | ||
158 | 59 | continue | ||
159 | 60 | } else if err != nil { | ||
160 | 61 | return nil, err | ||
161 | 62 | } | ||
162 | 63 | machine, err := st.Machine(machineId) | ||
163 | 64 | if err != nil { | ||
164 | 65 | return nil, err | ||
165 | 66 | } | ||
166 | 67 | instanceId, err := machine.InstanceId() | ||
167 | 68 | if err == nil { | ||
168 | 69 | instanceIds = append(instanceIds, instanceId) | ||
169 | 70 | } else if IsNotProvisionedError(err) { | ||
170 | 71 | continue | ||
171 | 72 | } else { | ||
172 | 73 | return nil, err | ||
173 | 74 | } | ||
174 | 75 | } | ||
175 | 76 | return instanceIds, nil | ||
176 | 77 | } | ||
177 | 0 | 78 | ||
178 | === added file 'state/distribution_test.go' | |||
179 | --- state/distribution_test.go 1970-01-01 00:00:00 +0000 | |||
180 | +++ state/distribution_test.go 2014-05-30 12:33:30 +0000 | |||
181 | @@ -0,0 +1,185 @@ | |||
182 | 1 | // Copyright 2014 Canonical Ltd. | ||
183 | 2 | // Licensed under the AGPLv3, see LICENCE file for details. | ||
184 | 3 | |||
185 | 4 | package state_test | ||
186 | 5 | |||
187 | 6 | import ( | ||
188 | 7 | "fmt" | ||
189 | 8 | |||
190 | 9 | "github.com/juju/errors" | ||
191 | 10 | jc "github.com/juju/testing/checkers" | ||
192 | 11 | gc "launchpad.net/gocheck" | ||
193 | 12 | |||
194 | 13 | "launchpad.net/juju-core/environs/config" | ||
195 | 14 | "launchpad.net/juju-core/instance" | ||
196 | 15 | "launchpad.net/juju-core/state" | ||
197 | 16 | ) | ||
198 | 17 | |||
199 | 18 | type InstanceDistributorSuite struct { | ||
200 | 19 | ConnSuite | ||
201 | 20 | distributor mockInstanceDistributor | ||
202 | 21 | wordpress *state.Service | ||
203 | 22 | machines []*state.Machine | ||
204 | 23 | } | ||
205 | 24 | |||
206 | 25 | var _ = gc.Suite(&InstanceDistributorSuite{}) | ||
207 | 26 | |||
208 | 27 | type mockInstanceDistributor struct { | ||
209 | 28 | candidates []instance.Id | ||
210 | 29 | distributionGroup []instance.Id | ||
211 | 30 | result []instance.Id | ||
212 | 31 | err error | ||
213 | 32 | } | ||
214 | 33 | |||
215 | 34 | func (p *mockInstanceDistributor) DistributeInstances(candidates, distributionGroup []instance.Id) ([]instance.Id, error) { | ||
216 | 35 | p.candidates = candidates | ||
217 | 36 | p.distributionGroup = distributionGroup | ||
218 | 37 | result := p.result | ||
219 | 38 | if result == nil { | ||
220 | 39 | result = candidates | ||
221 | 40 | } | ||
222 | 41 | return result, p.err | ||
223 | 42 | } | ||
224 | 43 | |||
225 | 44 | func (s *InstanceDistributorSuite) SetUpTest(c *gc.C) { | ||
226 | 45 | s.ConnSuite.SetUpTest(c) | ||
227 | 46 | s.distributor = mockInstanceDistributor{} | ||
228 | 47 | s.policy.getInstanceDistributor = func(*config.Config) (state.InstanceDistributor, error) { | ||
229 | 48 | return &s.distributor, nil | ||
230 | 49 | } | ||
231 | 50 | s.wordpress = s.AddTestingServiceWithNetworks( | ||
232 | 51 | c, | ||
233 | 52 | "wordpress", | ||
234 | 53 | s.AddTestingCharm(c, "wordpress"), | ||
235 | 54 | []string{"net1", "net2"}, | ||
236 | 55 | []string{"net3", "net4"}, | ||
237 | 56 | ) | ||
238 | 57 | s.machines = make([]*state.Machine, 3) | ||
239 | 58 | for i := range s.machines { | ||
240 | 59 | var err error | ||
241 | 60 | s.machines[i], err = s.State.AddOneMachine(state.MachineTemplate{ | ||
242 | 61 | Series: "quantal", | ||
243 | 62 | Jobs: []state.MachineJob{state.JobHostUnits}, | ||
244 | 63 | }) | ||
245 | 64 | c.Assert(err, gc.IsNil) | ||
246 | 65 | } | ||
247 | 66 | } | ||
248 | 67 | |||
249 | 68 | func (s *InstanceDistributorSuite) setupScenario(c *gc.C) { | ||
250 | 69 | // Assign a unit so we have a non-empty distribution group, and | ||
251 | 70 | // provision all instances so we have candidates. | ||
252 | 71 | unit, err := s.wordpress.AddUnit() | ||
253 | 72 | c.Assert(err, gc.IsNil) | ||
254 | 73 | err = unit.AssignToMachine(s.machines[0]) | ||
255 | 74 | c.Assert(err, gc.IsNil) | ||
256 | 75 | for i, m := range s.machines { | ||
257 | 76 | instId := instance.Id(fmt.Sprintf("i-blah-%d", i)) | ||
258 | 77 | err = m.SetProvisioned(instId, "fake-nonce", nil) | ||
259 | 78 | c.Assert(err, gc.IsNil) | ||
260 | 79 | } | ||
261 | 80 | } | ||
262 | 81 | |||
263 | 82 | func (s *InstanceDistributorSuite) TestDistributeInstances(c *gc.C) { | ||
264 | 83 | s.setupScenario(c) | ||
265 | 84 | unit, err := s.wordpress.AddUnit() | ||
266 | 85 | c.Assert(err, gc.IsNil) | ||
267 | 86 | _, err = unit.AssignToCleanMachine() | ||
268 | 87 | c.Assert(err, gc.IsNil) | ||
269 | 88 | c.Assert(s.distributor.candidates, jc.SameContents, []instance.Id{"i-blah-1", "i-blah-2"}) | ||
270 | 89 | c.Assert(s.distributor.distributionGroup, jc.SameContents, []instance.Id{"i-blah-0"}) | ||
271 | 90 | s.distributor.result = []instance.Id{} | ||
272 | 91 | _, err = unit.AssignToCleanMachine() | ||
273 | 92 | c.Assert(err, gc.ErrorMatches, eligibleMachinesInUse) | ||
274 | 93 | } | ||
275 | 94 | |||
276 | 95 | func (s *InstanceDistributorSuite) TestDistributeInstancesInvalidInstances(c *gc.C) { | ||
277 | 96 | s.setupScenario(c) | ||
278 | 97 | unit, err := s.wordpress.AddUnit() | ||
279 | 98 | c.Assert(err, gc.IsNil) | ||
280 | 99 | s.distributor.result = []instance.Id{"notthere"} | ||
281 | 100 | _, err = unit.AssignToCleanMachine() | ||
282 | 101 | c.Assert(err, gc.ErrorMatches, `cannot assign unit "wordpress/1" to clean machine: invalid instance returned: notthere`) | ||
283 | 102 | } | ||
284 | 103 | |||
285 | 104 | func (s *InstanceDistributorSuite) TestDistributeInstancesNoEmptyMachines(c *gc.C) { | ||
286 | 105 | for i := range s.machines { | ||
287 | 106 | // Assign a unit so we have a non-empty distribution group. | ||
288 | 107 | unit, err := s.wordpress.AddUnit() | ||
289 | 108 | c.Assert(err, gc.IsNil) | ||
290 | 109 | m, err := unit.AssignToCleanMachine() | ||
291 | 110 | c.Assert(err, gc.IsNil) | ||
292 | 111 | instId := instance.Id(fmt.Sprintf("i-blah-%d", i)) | ||
293 | 112 | err = m.SetProvisioned(instId, "fake-nonce", nil) | ||
294 | 113 | c.Assert(err, gc.IsNil) | ||
295 | 114 | } | ||
296 | 115 | |||
297 | 116 | // InstanceDistributor is not called if there are no empty instances. | ||
298 | 117 | s.distributor.err = fmt.Errorf("no assignment for you") | ||
299 | 118 | unit, err := s.wordpress.AddUnit() | ||
300 | 119 | c.Assert(err, gc.IsNil) | ||
301 | 120 | _, err = unit.AssignToCleanMachine() | ||
302 | 121 | c.Assert(err, gc.ErrorMatches, eligibleMachinesInUse) | ||
303 | 122 | } | ||
304 | 123 | |||
305 | 124 | func (s *InstanceDistributorSuite) TestDistributeInstancesErrors(c *gc.C) { | ||
306 | 125 | s.setupScenario(c) | ||
307 | 126 | unit, err := s.wordpress.AddUnit() | ||
308 | 127 | c.Assert(err, gc.IsNil) | ||
309 | 128 | |||
310 | 129 | // Ensure that assignment fails when DistributeInstances returns an error. | ||
311 | 130 | s.distributor.err = fmt.Errorf("no assignment for you") | ||
312 | 131 | _, err = unit.AssignToCleanMachine() | ||
313 | 132 | c.Assert(err, gc.ErrorMatches, ".*no assignment for you") | ||
314 | 133 | _, err = unit.AssignToCleanEmptyMachine() | ||
315 | 134 | c.Assert(err, gc.ErrorMatches, ".*no assignment for you") | ||
316 | 135 | // If the policy's InstanceDistributor method fails, that will be returned first. | ||
317 | 136 | s.policy.getInstanceDistributor = func(*config.Config) (state.InstanceDistributor, error) { | ||
318 | 137 | return nil, fmt.Errorf("incapable of InstanceDistributor") | ||
319 | 138 | } | ||
320 | 139 | _, err = unit.AssignToCleanMachine() | ||
321 | 140 | c.Assert(err, gc.ErrorMatches, ".*incapable of InstanceDistributor") | ||
322 | 141 | } | ||
323 | 142 | |||
324 | 143 | func (s *InstanceDistributorSuite) TestDistributeInstancesEmptyDistributionGroup(c *gc.C) { | ||
325 | 144 | s.distributor.err = fmt.Errorf("no assignment for you") | ||
326 | 145 | |||
327 | 146 | // InstanceDistributor is not called if the distribution group is empty. | ||
328 | 147 | unit0, err := s.wordpress.AddUnit() | ||
329 | 148 | c.Assert(err, gc.IsNil) | ||
330 | 149 | _, err = unit0.AssignToCleanMachine() | ||
331 | 150 | c.Assert(err, gc.IsNil) | ||
332 | 151 | |||
333 | 152 | // Distribution group is still empty, because the machine assigned to has | ||
334 | 153 | // not been provisioned. | ||
335 | 154 | unit1, err := s.wordpress.AddUnit() | ||
336 | 155 | c.Assert(err, gc.IsNil) | ||
337 | 156 | _, err = unit1.AssignToCleanMachine() | ||
338 | 157 | c.Assert(err, gc.IsNil) | ||
339 | 158 | } | ||
340 | 159 | |||
341 | 160 | func (s *InstanceDistributorSuite) TestInstanceDistributorUnimplemented(c *gc.C) { | ||
342 | 161 | s.setupScenario(c) | ||
343 | 162 | var distributorErr error | ||
344 | 163 | s.policy.getInstanceDistributor = func(*config.Config) (state.InstanceDistributor, error) { | ||
345 | 164 | return nil, distributorErr | ||
346 | 165 | } | ||
347 | 166 | unit, err := s.wordpress.AddUnit() | ||
348 | 167 | c.Assert(err, gc.IsNil) | ||
349 | 168 | _, err = unit.AssignToCleanMachine() | ||
350 | 169 | c.Assert(err, gc.ErrorMatches, `cannot assign unit "wordpress/1" to clean machine: policy returned nil instance distributor without an error`) | ||
351 | 170 | distributorErr = errors.NotImplementedf("InstanceDistributor") | ||
352 | 171 | _, err = unit.AssignToCleanMachine() | ||
353 | 172 | c.Assert(err, gc.IsNil) | ||
354 | 173 | } | ||
355 | 174 | |||
356 | 175 | func (s *InstanceDistributorSuite) TestDistributeInstancesNoPolicy(c *gc.C) { | ||
357 | 176 | s.policy.getInstanceDistributor = func(*config.Config) (state.InstanceDistributor, error) { | ||
358 | 177 | c.Errorf("should not have been invoked") | ||
359 | 178 | return nil, nil | ||
360 | 179 | } | ||
361 | 180 | state.SetPolicy(s.State, nil) | ||
362 | 181 | unit, err := s.wordpress.AddUnit() | ||
363 | 182 | c.Assert(err, gc.IsNil) | ||
364 | 183 | _, err = unit.AssignToCleanMachine() | ||
365 | 184 | c.Assert(err, gc.IsNil) | ||
366 | 185 | } | ||
367 | 0 | 186 | ||
368 | === modified file 'state/policy.go' | |||
369 | --- state/policy.go 2014-05-13 04:30:48 +0000 | |||
370 | +++ state/policy.go 2014-05-30 12:33:30 +0000 | |||
371 | @@ -10,6 +10,7 @@ | |||
372 | 10 | 10 | ||
373 | 11 | "launchpad.net/juju-core/constraints" | 11 | "launchpad.net/juju-core/constraints" |
374 | 12 | "launchpad.net/juju-core/environs/config" | 12 | "launchpad.net/juju-core/environs/config" |
375 | 13 | "launchpad.net/juju-core/instance" | ||
376 | 13 | ) | 14 | ) |
377 | 14 | 15 | ||
378 | 15 | // Policy is an interface provided to State that may | 16 | // Policy is an interface provided to State that may |
379 | @@ -22,21 +23,24 @@ | |||
380 | 22 | // be ignored. Any other error will cause an error | 23 | // be ignored. Any other error will cause an error |
381 | 23 | // in the use of the policy. | 24 | // in the use of the policy. |
382 | 24 | type Policy interface { | 25 | type Policy interface { |
385 | 25 | // Prechecker takes a *config.Config and returns | 26 | // Prechecker takes a *config.Config and returns a Prechecker or an error. |
384 | 26 | // a (possibly nil) Prechecker or an error. | ||
386 | 27 | Prechecker(*config.Config) (Prechecker, error) | 27 | Prechecker(*config.Config) (Prechecker, error) |
387 | 28 | 28 | ||
390 | 29 | // ConfigValidator takes a provider type name and returns | 29 | // ConfigValidator takes a provider type name and returns a ConfigValidator |
391 | 30 | // a (possibly nil) ConfigValidator or an error. | 30 | // or an error. |
392 | 31 | ConfigValidator(providerType string) (ConfigValidator, error) | 31 | ConfigValidator(providerType string) (ConfigValidator, error) |
393 | 32 | 32 | ||
396 | 33 | // EnvironCapability takes a *config.Config and returns | 33 | // EnvironCapability takes a *config.Config and returns an EnvironCapability |
397 | 34 | // a (possibly nil) EnvironCapability or an error. | 34 | // or an error. |
398 | 35 | EnvironCapability(*config.Config) (EnvironCapability, error) | 35 | EnvironCapability(*config.Config) (EnvironCapability, error) |
399 | 36 | 36 | ||
402 | 37 | // ConstraintsValidator takes a *config.Config and returns | 37 | // ConstraintsValidator takes a *config.Config and returns a |
403 | 38 | // a (possibly nil) constraints.Validator or an error. | 38 | // constraints.Validator or an error. |
404 | 39 | ConstraintsValidator(*config.Config) (constraints.Validator, error) | 39 | ConstraintsValidator(*config.Config) (constraints.Validator, error) |
405 | 40 | |||
406 | 41 | // InstanceDistributor takes a *config.Config and returns an | ||
407 | 42 | // InstanceDistributor or an error. | ||
408 | 43 | InstanceDistributor(*config.Config) (InstanceDistributor, error) | ||
409 | 40 | } | 44 | } |
410 | 41 | 45 | ||
411 | 42 | // Prechecker is a policy interface that is provided to State | 46 | // Prechecker is a policy interface that is provided to State |
412 | @@ -187,3 +191,21 @@ | |||
413 | 187 | } | 191 | } |
414 | 188 | return capability.SupportsUnitPlacement() | 192 | return capability.SupportsUnitPlacement() |
415 | 189 | } | 193 | } |
416 | 194 | |||
417 | 195 | // InstanceDistributor is a policy interface that is provided | ||
418 | 196 | // to State to perform distribution of units across instances | ||
419 | 197 | // for high availability. | ||
420 | 198 | type InstanceDistributor interface { | ||
421 | 199 | // DistributeInstance takes a set of clean, empty | ||
422 | 200 | // instances, and a distribution group, and returns | ||
423 | 201 | // the subset of candidates which the policy will | ||
424 | 202 | // allow entry into the distribution group. | ||
425 | 203 | // | ||
426 | 204 | // The AssignClean and AssignCleanEmpty unit | ||
427 | 205 | // assignment policies will attempt to assign a | ||
428 | 206 | // unit to each of the resulting instances until | ||
429 | 207 | // one is successful. If no instances can be assigned | ||
430 | 208 | // to (e.g. because of concurrent deployments), then | ||
431 | 209 | // a new machine will be allocated. | ||
432 | 210 | DistributeInstances(candidates, distributionGroup []instance.Id) ([]instance.Id, error) | ||
433 | 211 | } | ||
434 | 190 | 212 | ||
435 | === modified file 'state/service.go' | |||
436 | --- state/service.go 2014-05-26 21:16:46 +0000 | |||
437 | +++ state/service.go 2014-05-30 12:33:30 +0000 | |||
438 | @@ -723,13 +723,17 @@ | |||
439 | 723 | 723 | ||
440 | 724 | // AllUnits returns all units of the service. | 724 | // AllUnits returns all units of the service. |
441 | 725 | func (s *Service) AllUnits() (units []*Unit, err error) { | 725 | func (s *Service) AllUnits() (units []*Unit, err error) { |
442 | 726 | return allUnits(s.st, s.doc.Name) | ||
443 | 727 | } | ||
444 | 728 | |||
445 | 729 | func allUnits(st *State, service string) (units []*Unit, err error) { | ||
446 | 726 | docs := []unitDoc{} | 730 | docs := []unitDoc{} |
448 | 727 | err = s.st.units.Find(bson.D{{"service", s.doc.Name}}).All(&docs) | 731 | err = st.units.Find(bson.D{{"service", service}}).All(&docs) |
449 | 728 | if err != nil { | 732 | if err != nil { |
451 | 729 | return nil, fmt.Errorf("cannot get all units from service %q: %v", s, err) | 733 | return nil, fmt.Errorf("cannot get all units from service %q: %v", service, err) |
452 | 730 | } | 734 | } |
453 | 731 | for i := range docs { | 735 | for i := range docs { |
455 | 732 | units = append(units, newUnit(s.st, &docs[i])) | 736 | units = append(units, newUnit(st, &docs[i])) |
456 | 733 | } | 737 | } |
457 | 734 | return units, nil | 738 | return units, nil |
458 | 735 | } | 739 | } |
459 | 736 | 740 | ||
460 | === modified file 'state/unit.go' | |||
461 | --- state/unit.go 2014-05-27 05:36:00 +0000 | |||
462 | +++ state/unit.go 2014-05-30 12:33:30 +0000 | |||
463 | @@ -1277,14 +1277,63 @@ | |||
464 | 1277 | return nil, err | 1277 | return nil, err |
465 | 1278 | } | 1278 | } |
466 | 1279 | 1279 | ||
475 | 1280 | // TODO(rog) Fix so this is more efficient when there are concurrent uses. | 1280 | // Find all of the candidate machines, and associated |
476 | 1281 | // Possible solution: pick the highest and the smallest id of all | 1281 | // instances for those that are provisioned. Instances |
477 | 1282 | // unused machines, and try to assign to the first one >= a random id in the | 1282 | // will be distributed across in preference to |
478 | 1283 | // middle. | 1283 | // unprovisioned machines. |
479 | 1284 | iter := query.Batch(1).Prefetch(0).Iter() | 1284 | var mdocs []*machineDoc |
480 | 1285 | var mdoc machineDoc | 1285 | if err := query.All(&mdocs); err != nil { |
481 | 1286 | for iter.Next(&mdoc) { | 1286 | assignContextf(&err, u, context) |
482 | 1287 | m := newMachine(u.st, &mdoc) | 1287 | return nil, err |
483 | 1288 | } | ||
484 | 1289 | var unprovisioned []*Machine | ||
485 | 1290 | var instances []instance.Id | ||
486 | 1291 | instanceMachines := make(map[instance.Id]*Machine) | ||
487 | 1292 | for _, mdoc := range mdocs { | ||
488 | 1293 | m := newMachine(u.st, mdoc) | ||
489 | 1294 | instance, err := m.InstanceId() | ||
490 | 1295 | if IsNotProvisionedError(err) { | ||
491 | 1296 | unprovisioned = append(unprovisioned, m) | ||
492 | 1297 | } else if err != nil { | ||
493 | 1298 | assignContextf(&err, u, context) | ||
494 | 1299 | return nil, err | ||
495 | 1300 | } else { | ||
496 | 1301 | instances = append(instances, instance) | ||
497 | 1302 | instanceMachines[instance] = m | ||
498 | 1303 | } | ||
499 | 1304 | } | ||
500 | 1305 | |||
501 | 1306 | // Filter the list of instances that are suitable for | ||
502 | 1307 | // distribution, and then map them back to machines. | ||
503 | 1308 | // | ||
504 | 1309 | // TODO(axw) 2014-05-30 #1324904 | ||
505 | 1310 | // Shuffle machines to reduce likelihood of collisions. | ||
506 | 1311 | // The partition of provisioned/unprovisioned machines | ||
507 | 1312 | // must be maintained. | ||
508 | 1313 | if instances, err = distributeUnit(u, instances); err != nil { | ||
509 | 1314 | assignContextf(&err, u, context) | ||
510 | 1315 | return nil, err | ||
511 | 1316 | } | ||
512 | 1317 | machines := make([]*Machine, len(instances), len(instances)+len(unprovisioned)) | ||
513 | 1318 | for i, instance := range instances { | ||
514 | 1319 | m, ok := instanceMachines[instance] | ||
515 | 1320 | if !ok { | ||
516 | 1321 | err := fmt.Errorf("invalid instance returned: %v", instance) | ||
517 | 1322 | assignContextf(&err, u, context) | ||
518 | 1323 | return nil, err | ||
519 | 1324 | } | ||
520 | 1325 | machines[i] = m | ||
521 | 1326 | } | ||
522 | 1327 | machines = append(machines, unprovisioned...) | ||
523 | 1328 | |||
524 | 1329 | // TODO(axw) 2014-05-30 #1253704 | ||
525 | 1330 | // We should not select a machine that is in the process | ||
526 | 1331 | // of being provisioned. There's no point asserting that | ||
527 | 1332 | // the machine hasn't been provisioned, as there'll still | ||
528 | 1333 | // be a period of time during which the machine may be | ||
529 | 1334 | // provisioned without the fact having yet been recorded | ||
530 | 1335 | // in state. | ||
531 | 1336 | for _, m := range machines { | ||
532 | 1288 | err := u.assignToMachine(m, true) | 1337 | err := u.assignToMachine(m, true) |
533 | 1289 | if err == nil { | 1338 | if err == nil { |
534 | 1290 | return m, nil | 1339 | return m, nil |
535 | @@ -1294,10 +1343,6 @@ | |||
536 | 1294 | return nil, err | 1343 | return nil, err |
537 | 1295 | } | 1344 | } |
538 | 1296 | } | 1345 | } |
539 | 1297 | if err := iter.Err(); err != nil { | ||
540 | 1298 | assignContextf(&err, u, context) | ||
541 | 1299 | return nil, err | ||
542 | 1300 | } | ||
543 | 1301 | return nil, noCleanMachines | 1346 | return nil, noCleanMachines |
544 | 1302 | } | 1347 | } |
545 | 1303 | 1348 |
Reviewers: mp+221481_ code.launchpad. net,
Message:
Please take a look.
Description:
Introduce InstanceDistributor policy
This change introduces an InstanceDistributor
policy, which will be used to implement automatic
Availability Zone management for AWS and OpenStack.
When an attempt is made to assign a unit to a clean
machine, we gather the instances that have already
been provisioned, and provide those and the unit's
distribution group to the provider. The provider
may select as many or as few of the candidates as it
wishes to allow; the assignment policy will then
choose one of them for assigning to. If no candidates
are returned, the unit will be assigned to a new
machine.
There are currently no implementations of this policy;
they will be forthcoming when manual AZ support lands
in the amazon/openstack providers.
https:/ /code.launchpad .net/~axwalk/ juju-core/ provider- unit-assignment -policy/ +merge/ 221481
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/99660047/
Affected files (+379, -40 lines): statepolicy. go /provisioner/ provisioner. go ion.go ion_test. go
A [revision details]
M environs/
M state/apiserver
M state/conn_test.go
A state/distribut
A state/distribut
M state/policy.go
M state/unit.go