Merge lp:~axwalk/juju-core/destroy-environment-api into lp:~go-bot/juju-core/trunk
- destroy-environment-api
- Merge into trunk
Status: | Merged |
---|---|
Approved by: | Andrew Wilkins |
Approved revision: | no longer in the source branch. |
Merged at revision: | 2163 |
Proposed branch: | lp:~axwalk/juju-core/destroy-environment-api |
Merge into: | lp:~go-bot/juju-core/trunk |
Diff against target: |
794 lines (+534/-133) 10 files modified
cmd/juju/addmachine.go (+5/-1) cmd/juju/cmd_test.go (+0/-126) cmd/juju/destroyenvironment.go (+19/-5) cmd/juju/destroyenvironment_test.go (+160/-0) provider/dummy/environs.go (+3/-1) state/api/client.go (+8/-0) state/apiserver/client/destroy.go (+153/-0) state/apiserver/client/destroy_test.go (+123/-0) state/machine.go (+20/-0) state/machine_test.go (+43/-0) |
To merge this branch: | bzr merge lp:~axwalk/juju-core/destroy-environment-api |
Related bugs: |
Reviewer | Review Type | Date Requested | Status |
---|---|---|---|
Juju Engineering | Pending | ||
Review via email: mp+198698@code.launchpad.net |
Commit message
Implement DestroyEnvironment API
We introduce a new DestroyEnvironment client API
method, which:
- checks that there are no manual non-manager machines in state
- sets the environment to Dying, scheduling
cleanups for services and preventing addition
of new services and machines.
- destroys non-manager instances
"juju destroy-
ignoring the "no such method" error returned by old
API servers. If the API method succeeds, or it doesn't
exist, the provider.Destroy() method is called.
Description of the change
Implement DestroyEnvironment API
We introduce a new DestroyEnvironment client API
method, which:
- checks that there are no manual non-manager machines in state
- sets the environment to Dying, scheduling
cleanups for services and preventing addition
of new services and machines.
- destroys non-manager instances
"juju destroy-
ignoring the "no such method" error returned by old
API servers. If the API method succeeds, or it doesn't
exist, the provider.Destroy() method is called.
Andrew Wilkins (axwalk) wrote : | # |
William Reade (fwereade) wrote : | # |
LGTM with more-eager instance-killing, or a good reason not to, plus the
other trivials below
https:/
File state/apiserver
https:/
state/apiserver
checkManualMach
Hmm, can we consider the ordering of the check-and-abort vs the
stop-all-instances? I feel like it's probably best to kill what we can,
and then return an error like "can't stop state server yet, manual
machines x, y, z are still using it, please destroy-machine".
Also... it crosses my mind that the environ provisioner might take some
time to get the memo about the environment dying. Do we have any way to
block racy *provisioning* of recently-added machines? That can be a
followup, though.
https:/
state/apiserver
log the failed instances?
https:/
state/apiserver
strings.
Yeah, use the nonce -- if you need to make this a method on machine, so
be it.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
https:/
File state/apiserver
https:/
state/apiserver
checkManualMach
On 2013/12/18 12:52:20, fwereade wrote:
> Hmm, can we consider the ordering of the check-and-abort vs the
> stop-all-instances? I feel like it's probably best to kill what we
can, and then
> return an error like "can't stop state server yet, manual machines x,
y, z are
> still using it, please destroy-machine".
Moved the checkManualMachines after destroyInstances, and updated
destroyInstances to ignore manual machines.
> Also... it crosses my mind that the environ provisioner might take
some time to
> get the memo about the environment dying. Do we have any way to block
racy
> *provisioning* of recently-added machines? That can be a followup,
though.
I don't think we have any way of doing that yet.
https:/
state/apiserver
On 2013/12/18 12:52:20, fwereade wrote:
> log the failed instances?
Done.
https:/
state/apiserver
strings.
On 2013/12/18 12:52:20, fwereade wrote:
> Yeah, use the nonce -- if you need to make this a method on machine,
so be it.
Done.
Andrew Wilkins (axwalk) wrote : | # |
Please take a look.
Andrew Wilkins (axwalk) wrote : | # |
On 2013/12/19 14:25:38, axw wrote:
> Please take a look.
FYI, I added a --force flag to destroy-environment which skips the API
call. This is necessary for destroying broken environments, where the
API server is inaccessible or otherwise broken. There's also a drive-by
fix to add-machine, which was returning spurious errors due to nil
interface assignment.
Andrew Wilkins (axwalk) wrote : | # |
On 2013/12/19 14:25:38, axw wrote:
> Please take a look.
FYI, I added a --force flag to destroy-environment which skips the API
call. This is necessary for destroying broken environments, where the
API server is inaccessible or otherwise broken. There's also a drive-by
fix to add-machine, which was returning spurious errors due to nil
interface assignment.
Preview Diff
1 | === modified file 'cmd/juju/addmachine.go' |
2 | --- cmd/juju/addmachine.go 2013-12-17 18:21:26 +0000 |
3 | +++ cmd/juju/addmachine.go 2013-12-19 14:21:12 +0000 |
4 | @@ -175,7 +175,11 @@ |
5 | } else { |
6 | // Currently, only one machine is added, but in future there may be several added in one call. |
7 | machineInfo := results[0] |
8 | - machineId, err = machineInfo.Machine, machineInfo.Error |
9 | + var machineErr *params.Error |
10 | + machineId, machineErr = machineInfo.Machine, machineInfo.Error |
11 | + if machineErr != nil { |
12 | + err = machineErr |
13 | + } |
14 | } |
15 | if err != nil { |
16 | return err |
17 | |
18 | === modified file 'cmd/juju/cmd_test.go' |
19 | --- cmd/juju/cmd_test.go 2013-11-18 12:17:58 +0000 |
20 | +++ cmd/juju/cmd_test.go 2013-12-19 14:21:12 +0000 |
21 | @@ -4,7 +4,6 @@ |
22 | package main |
23 | |
24 | import ( |
25 | - "bytes" |
26 | "io" |
27 | "io/ioutil" |
28 | "os" |
29 | @@ -13,10 +12,6 @@ |
30 | gc "launchpad.net/gocheck" |
31 | |
32 | "launchpad.net/juju-core/cmd" |
33 | - "launchpad.net/juju-core/environs" |
34 | - "launchpad.net/juju-core/environs/configstore" |
35 | - "launchpad.net/juju-core/errors" |
36 | - "launchpad.net/juju-core/instance" |
37 | "launchpad.net/juju-core/juju/osenv" |
38 | "launchpad.net/juju-core/juju/testing" |
39 | "launchpad.net/juju-core/provider/dummy" |
40 | @@ -158,127 +153,6 @@ |
41 | return |
42 | } |
43 | |
44 | -func (*CmdSuite) TestDestroyEnvironmentCommand(c *gc.C) { |
45 | - // Prepare the environment so we can destroy it. |
46 | - store, err := configstore.Default() |
47 | - c.Assert(err, gc.IsNil) |
48 | - _, err = environs.PrepareFromName("", store) |
49 | - c.Assert(err, gc.IsNil) |
50 | - |
51 | - // Verify that the environment information exists. |
52 | - _, err = store.ReadInfo("peckham") |
53 | - c.Assert(err, gc.IsNil) |
54 | - |
55 | - // check environment is mandatory |
56 | - opc, errc := runCommand(nullContext(), new(DestroyEnvironmentCommand)) |
57 | - c.Check(<-errc, gc.Equals, NoEnvironmentError) |
58 | - |
59 | - // normal destroy |
60 | - opc, errc = runCommand(nullContext(), new(DestroyEnvironmentCommand), "peckham", "--yes") |
61 | - c.Check(<-errc, gc.IsNil) |
62 | - c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "peckham") |
63 | - |
64 | - // Verify that the environment information has been removed. |
65 | - _, err = store.ReadInfo("peckham") |
66 | - c.Assert(err, jc.Satisfies, errors.IsNotFoundError) |
67 | - |
68 | - _, err = environs.PrepareFromName("brokenenv", store) |
69 | - c.Assert(err, gc.IsNil) |
70 | - |
71 | - // destroy with broken environment |
72 | - opc, errc = runCommand(nullContext(), new(DestroyEnvironmentCommand), "brokenenv", "--yes") |
73 | - c.Check(<-opc, gc.IsNil) |
74 | - c.Check(<-errc, gc.ErrorMatches, "dummy.Destroy is broken") |
75 | - c.Check(<-opc, gc.IsNil) |
76 | -} |
77 | - |
78 | -func (*CmdSuite) TestDestroyEnvironmentCommandConfirmationFlag(c *gc.C) { |
79 | - com := new(DestroyEnvironmentCommand) |
80 | - c.Check(coretesting.InitCommand(com, []string{"peckham"}), gc.IsNil) |
81 | - c.Check(com.assumeYes, gc.Equals, false) |
82 | - |
83 | - com = new(DestroyEnvironmentCommand) |
84 | - c.Check(coretesting.InitCommand(com, []string{"peckham", "-y"}), gc.IsNil) |
85 | - c.Check(com.assumeYes, gc.Equals, true) |
86 | - |
87 | - com = new(DestroyEnvironmentCommand) |
88 | - c.Check(coretesting.InitCommand(com, []string{"peckham", "--yes"}), gc.IsNil) |
89 | - c.Check(com.assumeYes, gc.Equals, true) |
90 | -} |
91 | - |
92 | -func (*CmdSuite) TestDestroyEnvironmentCommandConfirmation(c *gc.C) { |
93 | - var stdin, stdout bytes.Buffer |
94 | - ctx := cmd.DefaultContext() |
95 | - ctx.Stdout = &stdout |
96 | - ctx.Stdin = &stdin |
97 | - |
98 | - store, err := configstore.Default() |
99 | - c.Assert(err, gc.IsNil) |
100 | - // Prepare the environment so we can destroy it. |
101 | - env, err := environs.PrepareFromName("", store) |
102 | - c.Assert(err, gc.IsNil) |
103 | - |
104 | - assertEnvironNotDestroyed(c, env, store) |
105 | - |
106 | - // Ensure confirmation is requested if "-y" is not specified. |
107 | - stdin.WriteString("n") |
108 | - opc, errc := runCommand(ctx, new(DestroyEnvironmentCommand), "peckham") |
109 | - c.Check(<-errc, gc.ErrorMatches, "Environment destruction aborted") |
110 | - c.Check(<-opc, gc.IsNil) |
111 | - c.Check(stdout.String(), gc.Matches, "WARNING!.*peckham.*\\(type: dummy\\)(.|\n)*") |
112 | - assertEnvironNotDestroyed(c, env, store) |
113 | - |
114 | - // EOF on stdin: equivalent to answering no. |
115 | - stdin.Reset() |
116 | - stdout.Reset() |
117 | - opc, errc = runCommand(ctx, new(DestroyEnvironmentCommand), "peckham") |
118 | - c.Check(<-opc, gc.IsNil) |
119 | - c.Check(<-errc, gc.ErrorMatches, "Environment destruction aborted") |
120 | - assertEnvironNotDestroyed(c, env, store) |
121 | - |
122 | - // "--yes" passed: no confirmation request. |
123 | - stdin.Reset() |
124 | - stdout.Reset() |
125 | - opc, errc = runCommand(ctx, new(DestroyEnvironmentCommand), "peckham", "--yes") |
126 | - c.Check(<-errc, gc.IsNil) |
127 | - c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "peckham") |
128 | - c.Check(stdout.String(), gc.Equals, "") |
129 | - assertEnvironDestroyed(c, env, store) |
130 | - |
131 | - // Any of casing of "y" and "yes" will confirm. |
132 | - for _, answer := range []string{"y", "Y", "yes", "YES"} { |
133 | - // Prepare the environment so we can destroy it. |
134 | - _, err := environs.PrepareFromName("", store) |
135 | - c.Assert(err, gc.IsNil) |
136 | - |
137 | - stdin.Reset() |
138 | - stdout.Reset() |
139 | - stdin.WriteString(answer) |
140 | - opc, errc = runCommand(ctx, new(DestroyEnvironmentCommand), "peckham") |
141 | - c.Check(<-errc, gc.IsNil) |
142 | - c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "peckham") |
143 | - c.Check(stdout.String(), gc.Matches, "WARNING!.*peckham.*\\(type: dummy\\)(.|\n)*") |
144 | - assertEnvironDestroyed(c, env, store) |
145 | - } |
146 | -} |
147 | - |
148 | -func assertEnvironDestroyed(c *gc.C, env environs.Environ, store configstore.Storage) { |
149 | - _, err := store.ReadInfo(env.Name()) |
150 | - c.Assert(err, jc.Satisfies, errors.IsNotFoundError) |
151 | - |
152 | - _, err = env.Instances([]instance.Id{"invalid"}) |
153 | - c.Assert(err, gc.ErrorMatches, "environment has been destroyed") |
154 | -} |
155 | - |
156 | -func assertEnvironNotDestroyed(c *gc.C, env environs.Environ, store configstore.Storage) { |
157 | - info, err := store.ReadInfo(env.Name()) |
158 | - c.Assert(err, gc.IsNil) |
159 | - c.Assert(info.Initialized(), jc.IsTrue) |
160 | - |
161 | - _, err = environs.NewFromName(env.Name(), store) |
162 | - c.Assert(err, gc.IsNil) |
163 | -} |
164 | - |
165 | var deployTests = []struct { |
166 | args []string |
167 | com *DeployCommand |
168 | |
169 | === modified file 'cmd/juju/destroyenvironment.go' |
170 | --- cmd/juju/destroyenvironment.go 2013-11-19 06:43:27 +0000 |
171 | +++ cmd/juju/destroyenvironment.go 2013-12-19 14:21:12 +0000 |
172 | @@ -15,6 +15,9 @@ |
173 | "launchpad.net/juju-core/cmd" |
174 | "launchpad.net/juju-core/environs" |
175 | "launchpad.net/juju-core/environs/configstore" |
176 | + "launchpad.net/juju-core/juju" |
177 | + "launchpad.net/juju-core/state/api" |
178 | + "launchpad.net/juju-core/state/api/params" |
179 | ) |
180 | |
181 | var NoEnvironmentError = errors.New("no environment specified") |
182 | @@ -24,6 +27,7 @@ |
183 | cmd.CommandBase |
184 | envName string |
185 | assumeYes bool |
186 | + force bool |
187 | } |
188 | |
189 | func (c *DestroyEnvironmentCommand) Info() *cmd.Info { |
190 | @@ -37,6 +41,7 @@ |
191 | func (c *DestroyEnvironmentCommand) SetFlags(f *gnuflag.FlagSet) { |
192 | f.BoolVar(&c.assumeYes, "y", false, "Do not ask for confirmation") |
193 | f.BoolVar(&c.assumeYes, "yes", false, "") |
194 | + f.BoolVar(&c.force, "force", false, "Forcefully destroy the environment, directly through the environment provider") |
195 | } |
196 | |
197 | func (c *DestroyEnvironmentCommand) Run(ctx *cmd.Context) error { |
198 | @@ -62,11 +67,20 @@ |
199 | return errors.New("Environment destruction aborted") |
200 | } |
201 | } |
202 | - |
203 | - // TODO(axw) 2013-08-30 bug 1218688 |
204 | - // destroy manually provisioned machines, or otherwise |
205 | - // block destroy-environment until all manually provisioned |
206 | - // machines have been manually "destroyed". |
207 | + // If --force is supplied, then don't attempt to use the API. |
208 | + // This is necessary to destroy broken environments, where the |
209 | + // API server is inaccessible or faulty. |
210 | + if !c.force { |
211 | + conn, err := juju.NewAPIConn(environ, api.DefaultDialOpts()) |
212 | + if err != nil { |
213 | + return err |
214 | + } |
215 | + defer conn.Close() |
216 | + err = conn.State.Client().DestroyEnvironment() |
217 | + if err != nil && !params.IsCodeNotImplemented(err) { |
218 | + return fmt.Errorf("destroying environment: %v", err) |
219 | + } |
220 | + } |
221 | return environs.Destroy(environ, store) |
222 | } |
223 | |
224 | |
225 | === added file 'cmd/juju/destroyenvironment_test.go' |
226 | --- cmd/juju/destroyenvironment_test.go 1970-01-01 00:00:00 +0000 |
227 | +++ cmd/juju/destroyenvironment_test.go 2013-12-19 14:21:12 +0000 |
228 | @@ -0,0 +1,160 @@ |
229 | +// Copyright 2012, 2013 Canonical Ltd. |
230 | +// Licensed under the AGPLv3, see LICENCE file for details. |
231 | + |
232 | +package main |
233 | + |
234 | +import ( |
235 | + "bytes" |
236 | + |
237 | + gc "launchpad.net/gocheck" |
238 | + |
239 | + "launchpad.net/juju-core/cmd" |
240 | + "launchpad.net/juju-core/environs" |
241 | + "launchpad.net/juju-core/environs/configstore" |
242 | + "launchpad.net/juju-core/errors" |
243 | + "launchpad.net/juju-core/instance" |
244 | + "launchpad.net/juju-core/juju/testing" |
245 | + "launchpad.net/juju-core/provider/dummy" |
246 | + coretesting "launchpad.net/juju-core/testing" |
247 | + jc "launchpad.net/juju-core/testing/checkers" |
248 | +) |
249 | + |
250 | +type destroyEnvSuite struct { |
251 | + testing.JujuConnSuite |
252 | +} |
253 | + |
254 | +var _ = gc.Suite(&destroyEnvSuite{}) |
255 | + |
256 | +func (s *destroyEnvSuite) TestDestroyEnvironmentCommand(c *gc.C) { |
257 | + // Prepare the environment so we can destroy it. |
258 | + _, err := environs.PrepareFromName("dummyenv", s.ConfigStore) |
259 | + c.Assert(err, gc.IsNil) |
260 | + |
261 | + // check environment is mandatory |
262 | + opc, errc := runCommand(nullContext(), new(DestroyEnvironmentCommand)) |
263 | + c.Check(<-errc, gc.Equals, NoEnvironmentError) |
264 | + |
265 | + // normal destroy |
266 | + opc, errc = runCommand(nullContext(), new(DestroyEnvironmentCommand), "dummyenv", "--yes") |
267 | + c.Check(<-errc, gc.IsNil) |
268 | + c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") |
269 | + |
270 | + // Verify that the environment information has been removed. |
271 | + _, err = s.ConfigStore.ReadInfo("dummyenv") |
272 | + c.Assert(err, jc.Satisfies, errors.IsNotFoundError) |
273 | +} |
274 | + |
275 | +func (s *destroyEnvSuite) TestDestroyEnvironmentCommandBroken(c *gc.C) { |
276 | + oldinfo, err := s.ConfigStore.ReadInfo("dummyenv") |
277 | + c.Assert(err, gc.IsNil) |
278 | + bootstrapConfig := oldinfo.BootstrapConfig() |
279 | + apiEndpoint := oldinfo.APIEndpoint() |
280 | + apiCredentials := oldinfo.APICredentials() |
281 | + err = oldinfo.Destroy() |
282 | + c.Assert(err, gc.IsNil) |
283 | + newinfo, err := s.ConfigStore.CreateInfo("dummyenv") |
284 | + c.Assert(err, gc.IsNil) |
285 | + |
286 | + bootstrapConfig["broken"] = "Destroy" |
287 | + newinfo.SetBootstrapConfig(bootstrapConfig) |
288 | + newinfo.SetAPIEndpoint(apiEndpoint) |
289 | + newinfo.SetAPICredentials(apiCredentials) |
290 | + err = newinfo.Write() |
291 | + c.Assert(err, gc.IsNil) |
292 | + |
293 | + // Prepare the environment so we can destroy it. |
294 | + _, err = environs.PrepareFromName("dummyenv", s.ConfigStore) |
295 | + c.Assert(err, gc.IsNil) |
296 | + |
297 | + // destroy with broken environment |
298 | + opc, errc := runCommand(nullContext(), new(DestroyEnvironmentCommand), "dummyenv", "--yes") |
299 | + c.Check(<-opc, gc.IsNil) |
300 | + c.Check(<-errc, gc.ErrorMatches, "dummy.Destroy is broken") |
301 | + c.Check(<-opc, gc.IsNil) |
302 | +} |
303 | + |
304 | +func (*destroyEnvSuite) TestDestroyEnvironmentCommandConfirmationFlag(c *gc.C) { |
305 | + com := new(DestroyEnvironmentCommand) |
306 | + c.Check(coretesting.InitCommand(com, []string{"dummyenv"}), gc.IsNil) |
307 | + c.Check(com.assumeYes, gc.Equals, false) |
308 | + |
309 | + com = new(DestroyEnvironmentCommand) |
310 | + c.Check(coretesting.InitCommand(com, []string{"dummyenv", "-y"}), gc.IsNil) |
311 | + c.Check(com.assumeYes, gc.Equals, true) |
312 | + |
313 | + com = new(DestroyEnvironmentCommand) |
314 | + c.Check(coretesting.InitCommand(com, []string{"dummyenv", "--yes"}), gc.IsNil) |
315 | + c.Check(com.assumeYes, gc.Equals, true) |
316 | +} |
317 | + |
318 | +func (s *destroyEnvSuite) TestDestroyEnvironmentCommandConfirmation(c *gc.C) { |
319 | + var stdin, stdout bytes.Buffer |
320 | + ctx := cmd.DefaultContext() |
321 | + ctx.Stdout = &stdout |
322 | + ctx.Stdin = &stdin |
323 | + |
324 | + // Prepare the environment so we can destroy it. |
325 | + env, err := environs.PrepareFromName("dummyenv", s.ConfigStore) |
326 | + c.Assert(err, gc.IsNil) |
327 | + |
328 | + assertEnvironNotDestroyed(c, env, s.ConfigStore) |
329 | + |
330 | + // Ensure confirmation is requested if "-y" is not specified. |
331 | + stdin.WriteString("n") |
332 | + opc, errc := runCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv") |
333 | + c.Check(<-errc, gc.ErrorMatches, "Environment destruction aborted") |
334 | + c.Check(<-opc, gc.IsNil) |
335 | + c.Check(stdout.String(), gc.Matches, "WARNING!.*dummyenv.*\\(type: dummy\\)(.|\n)*") |
336 | + assertEnvironNotDestroyed(c, env, s.ConfigStore) |
337 | + |
338 | + // EOF on stdin: equivalent to answering no. |
339 | + stdin.Reset() |
340 | + stdout.Reset() |
341 | + opc, errc = runCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv") |
342 | + c.Check(<-opc, gc.IsNil) |
343 | + c.Check(<-errc, gc.ErrorMatches, "Environment destruction aborted") |
344 | + assertEnvironNotDestroyed(c, env, s.ConfigStore) |
345 | + |
346 | + // "--yes" passed: no confirmation request. |
347 | + stdin.Reset() |
348 | + stdout.Reset() |
349 | + opc, errc = runCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv", "--yes") |
350 | + c.Check(<-errc, gc.IsNil) |
351 | + c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") |
352 | + c.Check(stdout.String(), gc.Equals, "") |
353 | + assertEnvironDestroyed(c, env, s.ConfigStore) |
354 | + |
355 | + // Any of casing of "y" and "yes" will confirm. |
356 | + for _, answer := range []string{"y", "Y", "yes", "YES"} { |
357 | + // Prepare the environment so we can destroy it. |
358 | + s.Reset(c) |
359 | + env, err := environs.PrepareFromName("dummyenv", s.ConfigStore) |
360 | + c.Assert(err, gc.IsNil) |
361 | + |
362 | + stdin.Reset() |
363 | + stdout.Reset() |
364 | + stdin.WriteString(answer) |
365 | + opc, errc = runCommand(ctx, new(DestroyEnvironmentCommand), "dummyenv") |
366 | + c.Check(<-errc, gc.IsNil) |
367 | + c.Check((<-opc).(dummy.OpDestroy).Env, gc.Equals, "dummyenv") |
368 | + c.Check(stdout.String(), gc.Matches, "WARNING!.*dummyenv.*\\(type: dummy\\)(.|\n)*") |
369 | + assertEnvironDestroyed(c, env, s.ConfigStore) |
370 | + } |
371 | +} |
372 | + |
373 | +func assertEnvironDestroyed(c *gc.C, env environs.Environ, store configstore.Storage) { |
374 | + _, err := store.ReadInfo(env.Name()) |
375 | + c.Assert(err, jc.Satisfies, errors.IsNotFoundError) |
376 | + |
377 | + _, err = env.Instances([]instance.Id{"invalid"}) |
378 | + c.Assert(err, gc.ErrorMatches, "environment has been destroyed") |
379 | +} |
380 | + |
381 | +func assertEnvironNotDestroyed(c *gc.C, env environs.Environ, store configstore.Storage) { |
382 | + info, err := store.ReadInfo(env.Name()) |
383 | + c.Assert(err, gc.IsNil) |
384 | + c.Assert(info.Initialized(), jc.IsTrue) |
385 | + |
386 | + _, err = environs.NewFromName(env.Name(), store) |
387 | + c.Assert(err, gc.IsNil) |
388 | +} |
389 | |
390 | === modified file 'provider/dummy/environs.go' |
391 | --- provider/dummy/environs.go 2013-12-16 09:53:26 +0000 |
392 | +++ provider/dummy/environs.go 2013-12-19 14:21:12 +0000 |
393 | @@ -786,8 +786,10 @@ |
394 | if inst == nil { |
395 | err = environs.ErrPartialInstances |
396 | notFound++ |
397 | + insts = append(insts, nil) |
398 | + } else { |
399 | + insts = append(insts, inst) |
400 | } |
401 | - insts = append(insts, inst) |
402 | } |
403 | if notFound == len(ids) { |
404 | return nil, environs.ErrNoInstances |
405 | |
406 | === modified file 'state/api/client.go' |
407 | --- state/api/client.go 2013-12-19 08:46:10 +0000 |
408 | +++ state/api/client.go 2013-12-19 14:21:12 +0000 |
409 | @@ -357,6 +357,14 @@ |
410 | return c.st.Call("Client", "", "SetEnvironAgentVersion", args, nil) |
411 | } |
412 | |
413 | +// DestroyEnvironment puts the environment into a "dying" state, |
414 | +// and removes all non-manager machine instances. DestroyEnvironment |
415 | +// will fail if there are any manually-provisioned non-manager machines |
416 | +// in state. |
417 | +func (c *Client) DestroyEnvironment() error { |
418 | + return c.st.Call("Client", "", "DestroyEnvironment", nil, nil) |
419 | +} |
420 | + |
421 | // AddLocalCharm prepares the given charm with a local: schema in its |
422 | // URL, and uploads it via the API server, returning the assigned |
423 | // charm URL. If the API server does not support charm uploads, an |
424 | |
425 | === added file 'state/apiserver/client/destroy.go' |
426 | --- state/apiserver/client/destroy.go 1970-01-01 00:00:00 +0000 |
427 | +++ state/apiserver/client/destroy.go 2013-12-19 14:21:12 +0000 |
428 | @@ -0,0 +1,153 @@ |
429 | +// Copyright 2013 Canonical Ltd. |
430 | +// Licensed under the AGPLv3, see LICENCE file for details. |
431 | + |
432 | +package client |
433 | + |
434 | +import ( |
435 | + "fmt" |
436 | + "strings" |
437 | + |
438 | + "launchpad.net/juju-core/environs" |
439 | + "launchpad.net/juju-core/instance" |
440 | + "launchpad.net/juju-core/state" |
441 | +) |
442 | + |
443 | +// DestroyEnvironment destroys all services and non-manager machine |
444 | +// instances in the environment. |
445 | +func (c *Client) DestroyEnvironment() error { |
446 | + // TODO(axw) 2013-08-30 bug 1218688 |
447 | + // |
448 | + // There's a race here: a client might add a manual machine |
449 | + // after another client checks. Destroy-environment will |
450 | + // still fail, but the environment will be in a state where |
451 | + // entities can only be destroyed. In the future we should |
452 | + // introduce a method of preventing Environment.Destroy() |
453 | + // from succeeding if machines have been added. |
454 | + |
455 | + // First, check for manual machines. We bail out if there are any, |
456 | + // to stop the user from prematurely hobbling the environment. |
457 | + machines, err := c.api.state.AllMachines() |
458 | + if err != nil { |
459 | + return err |
460 | + } |
461 | + if err := checkManualMachines(machines); err != nil { |
462 | + return err |
463 | + } |
464 | + |
465 | + // Set the environment to Dying, to lock out new machines and services. |
466 | + // Environment.Destroy() also schedules a cleanup for existing services. |
467 | + // Afterwards, refresh the machines in case any were added between the |
468 | + // first check and the Environment.Destroy(). |
469 | + env, err := c.api.state.Environment() |
470 | + if err != nil { |
471 | + return err |
472 | + } |
473 | + if err = env.Destroy(); err != nil { |
474 | + return err |
475 | + } |
476 | + machines, err = c.api.state.AllMachines() |
477 | + if err != nil { |
478 | + return err |
479 | + } |
480 | + |
481 | + // We must destroy instances server-side to support hosted Juju, |
482 | + // as there's no CLI to fall back on. In that case, we only ever |
483 | + // destroy non-state machines; we leave destroying state servers |
484 | + // in non-hosted environments to the CLI, as otherwise the API |
485 | + // server may get cut off. |
486 | + if err := destroyInstances(c.api.state, machines); err != nil { |
487 | + return err |
488 | + } |
489 | + |
490 | + // Make sure once again that there are no manually provisioned |
491 | + // non-manager machines. This caters for the race between the |
492 | + // first check and the Environment.Destroy(). |
493 | + if err := checkManualMachines(machines); err != nil { |
494 | + return err |
495 | + } |
496 | + |
497 | + // Return to the caller. If it's the CLI, it will finish up |
498 | + // by calling the provider's Destroy method, which will |
499 | + // destroy the state servers, any straggler instances, and |
500 | + // other provider-specific resources. |
501 | + return nil |
502 | +} |
503 | + |
504 | +// destroyInstances directly destroys all non-manager, |
505 | +// non-manual machine instances. |
506 | +func destroyInstances(st *state.State, machines []*state.Machine) error { |
507 | + var ids []instance.Id |
508 | + for _, m := range machines { |
509 | + if m.IsManager() { |
510 | + continue |
511 | + } |
512 | + manual, err := m.IsManual() |
513 | + if manual { |
514 | + continue |
515 | + } else if err != nil { |
516 | + return err |
517 | + } |
518 | + id, err := m.InstanceId() |
519 | + if err != nil { |
520 | + continue |
521 | + } |
522 | + ids = append(ids, id) |
523 | + } |
524 | + if len(ids) == 0 { |
525 | + return nil |
526 | + } |
527 | + envcfg, err := st.EnvironConfig() |
528 | + if err != nil { |
529 | + return err |
530 | + } |
531 | + env, err := environs.New(envcfg) |
532 | + if err != nil { |
533 | + return err |
534 | + } |
535 | + // TODO(axw) 2013-12-12 #1260171 |
536 | + // Modify InstanceBroker.StopInstances to take |
537 | + // a slice of IDs rather than Instances. |
538 | + instances, err := env.Instances(ids) |
539 | + switch err { |
540 | + case nil: |
541 | + default: |
542 | + return err |
543 | + case environs.ErrNoInstances: |
544 | + return nil |
545 | + case environs.ErrPartialInstances: |
546 | + var nonNilInstances []instance.Instance |
547 | + for i, inst := range instances { |
548 | + if inst == nil { |
549 | + logger.Warningf("unknown instance ID: %v", ids[i]) |
550 | + continue |
551 | + } |
552 | + nonNilInstances = append(nonNilInstances, inst) |
553 | + } |
554 | + instances = nonNilInstances |
555 | + } |
556 | + return env.StopInstances(instances) |
557 | +} |
558 | + |
559 | +// checkManualMachines checks if any of the machines in the slice were |
560 | +// manually provisioned, and are non-manager machines. These machines |
561 | +// must (currently) by manually destroyed via destroy-machine before |
562 | +// destroy-environment can successfully complete. |
563 | +func checkManualMachines(machines []*state.Machine) error { |
564 | + var ids []string |
565 | + for _, m := range machines { |
566 | + if m.IsManager() { |
567 | + continue |
568 | + } |
569 | + manual, err := m.IsManual() |
570 | + if err != nil { |
571 | + return err |
572 | + } |
573 | + if manual { |
574 | + ids = append(ids, m.Id()) |
575 | + } |
576 | + } |
577 | + if len(ids) > 0 { |
578 | + return fmt.Errorf("manually provisioned machines must first be destroyed with `juju destroy-machine %s`", strings.Join(ids, " ")) |
579 | + } |
580 | + return nil |
581 | +} |
582 | |
583 | === added file 'state/apiserver/client/destroy_test.go' |
584 | --- state/apiserver/client/destroy_test.go 1970-01-01 00:00:00 +0000 |
585 | +++ state/apiserver/client/destroy_test.go 2013-12-19 14:21:12 +0000 |
586 | @@ -0,0 +1,123 @@ |
587 | +// Copyright 2012, 2013 Canonical Ltd. |
588 | +// Licensed under the AGPLv3, see LICENCE file for details. |
589 | + |
590 | +package client_test |
591 | + |
592 | +import ( |
593 | + "fmt" |
594 | + |
595 | + gc "launchpad.net/gocheck" |
596 | + |
597 | + "launchpad.net/juju-core/environs" |
598 | + coreerrors "launchpad.net/juju-core/errors" |
599 | + "launchpad.net/juju-core/instance" |
600 | + "launchpad.net/juju-core/juju/testing" |
601 | + "launchpad.net/juju-core/state" |
602 | + jc "launchpad.net/juju-core/testing/checkers" |
603 | +) |
604 | + |
605 | +type destroyEnvironmentSuite struct { |
606 | + baseSuite |
607 | +} |
608 | + |
609 | +var _ = gc.Suite(&destroyEnvironmentSuite{}) |
610 | + |
611 | +// setUpManual adds "manually provisioned" machines to state: |
612 | +// one manager machine, and one non-manager. |
613 | +func (s *destroyEnvironmentSuite) setUpManual(c *gc.C) (m0, m1 *state.Machine) { |
614 | + m0, err := s.State.AddMachine("precise", state.JobManageEnviron, state.JobManageState) |
615 | + c.Assert(err, gc.IsNil) |
616 | + err = m0.SetProvisioned(instance.Id("manual:0"), "manual:0:fake_nonce", nil) |
617 | + c.Assert(err, gc.IsNil) |
618 | + m1, err = s.State.AddMachine("precise", state.JobHostUnits) |
619 | + c.Assert(err, gc.IsNil) |
620 | + err = m1.SetProvisioned(instance.Id("manual:1"), "manual:1:fake_nonce", nil) |
621 | + c.Assert(err, gc.IsNil) |
622 | + return m0, m1 |
623 | +} |
624 | + |
625 | +// setUpInstances adds machines to state backed by instances: |
626 | +// one manager machine, and one non-manager. |
627 | +func (s *destroyEnvironmentSuite) setUpInstances(c *gc.C) (m0, m1 *state.Machine) { |
628 | + m0, err := s.State.AddMachine("precise", state.JobManageEnviron, state.JobManageState) |
629 | + c.Assert(err, gc.IsNil) |
630 | + inst, _ := testing.AssertStartInstance(c, s.APIConn.Environ, m0.Id()) |
631 | + err = m0.SetProvisioned(inst.Id(), "fake_nonce", nil) |
632 | + c.Assert(err, gc.IsNil) |
633 | + m1, err = s.State.AddMachine("precise", state.JobHostUnits) |
634 | + c.Assert(err, gc.IsNil) |
635 | + inst, _ = testing.AssertStartInstance(c, s.APIConn.Environ, m1.Id()) |
636 | + err = m1.SetProvisioned(inst.Id(), "fake_nonce", nil) |
637 | + c.Assert(err, gc.IsNil) |
638 | + return m0, m1 |
639 | +} |
640 | + |
641 | +func (s *destroyEnvironmentSuite) TestDestroyEnvironmentManual(c *gc.C) { |
642 | + s.setUpScenario(c) |
643 | + _, nonManager := s.setUpManual(c) |
644 | + |
645 | + // If there are any non-manager manual machines in state, DestroyEnvironment will |
646 | + // error. It will not set the Dying flag on the environment. |
647 | + err := s.APIState.Client().DestroyEnvironment() |
648 | + c.Assert(err, gc.ErrorMatches, fmt.Sprintf("manually provisioned machines must first be destroyed with `juju destroy-machine %s`", nonManager.Id())) |
649 | + env, err := s.State.Environment() |
650 | + c.Assert(err, gc.IsNil) |
651 | + c.Assert(env.Life(), gc.Equals, state.Alive) |
652 | + |
653 | + // If we remove the non-manager machine, it should pass. |
654 | + // Manager machines will remain. |
655 | + err = nonManager.EnsureDead() |
656 | + c.Assert(err, gc.IsNil) |
657 | + err = nonManager.Remove() |
658 | + c.Assert(err, gc.IsNil) |
659 | + err = s.APIState.Client().DestroyEnvironment() |
660 | + c.Assert(err, gc.IsNil) |
661 | + err = env.Refresh() |
662 | + c.Assert(err, gc.IsNil) |
663 | + c.Assert(env.Life(), gc.Equals, state.Dying) |
664 | +} |
665 | + |
666 | +func (s *destroyEnvironmentSuite) TestDestroyEnvironment(c *gc.C) { |
667 | + s.setUpScenario(c) |
668 | + manager, nonManager := s.setUpInstances(c) |
669 | + managerId, _ := manager.InstanceId() |
670 | + nonManagerId, _ := nonManager.InstanceId() |
671 | + |
672 | + instances, err := s.APIConn.Environ.Instances([]instance.Id{managerId, nonManagerId}) |
673 | + c.Assert(err, gc.IsNil) |
674 | + for _, inst := range instances { |
675 | + c.Assert(inst, gc.NotNil) |
676 | + } |
677 | + |
678 | + services, err := s.State.AllServices() |
679 | + c.Assert(err, gc.IsNil) |
680 | + |
681 | + err = s.APIState.Client().DestroyEnvironment() |
682 | + c.Assert(err, gc.IsNil) |
683 | + |
684 | + // After DestroyEnvironment returns, we should have: |
685 | + // - all non-manager instances stopped |
686 | + instances, err = s.APIConn.Environ.Instances([]instance.Id{managerId, nonManagerId}) |
687 | + c.Assert(err, gc.Equals, environs.ErrPartialInstances) |
688 | + c.Assert(instances[0], gc.NotNil) |
689 | + c.Assert(instances[1], gc.IsNil) |
690 | + // - all services in state are Dying or Dead (or removed altogether), |
691 | + // after running the state Cleanups. |
692 | + needsCleanup, err := s.State.NeedsCleanup() |
693 | + c.Assert(err, gc.IsNil) |
694 | + c.Assert(needsCleanup, jc.IsTrue) |
695 | + err = s.State.Cleanup() |
696 | + c.Assert(err, gc.IsNil) |
697 | + for _, s := range services { |
698 | + err = s.Refresh() |
699 | + if err != nil { |
700 | + c.Assert(err, jc.Satisfies, coreerrors.IsNotFoundError) |
701 | + } else { |
702 | + c.Assert(s.Life(), gc.Not(gc.Equals), state.Alive) |
703 | + } |
704 | + } |
705 | + // - environment is Dying |
706 | + env, err := s.State.Environment() |
707 | + c.Assert(err, gc.IsNil) |
708 | + c.Assert(env.Life(), gc.Equals, state.Dying) |
709 | +} |
710 | |
711 | === modified file 'state/machine.go' |
712 | --- state/machine.go 2013-12-04 15:34:47 +0000 |
713 | +++ state/machine.go 2013-12-19 14:21:12 +0000 |
714 | @@ -206,6 +206,26 @@ |
715 | return false |
716 | } |
717 | |
718 | +// IsManual returns true if the machine was manually provisioned. |
719 | +func (m *Machine) IsManual() (bool, error) { |
720 | + // Apart from the bootstrap machine, manually provisioned |
721 | + // machines have a nonce prefixed with "manual:". This is |
722 | + // unique to manual provisioning. |
723 | + if strings.HasPrefix(m.doc.Nonce, "manual:") { |
724 | + return true, nil |
725 | + } |
726 | + // The bootstrap machine uses BootstrapNonce, so in that |
727 | + // case we need to check if its provider type is "null". |
728 | + if m.doc.Id == "0" { |
729 | + cfg, err := m.st.EnvironConfig() |
730 | + if err != nil { |
731 | + return false, err |
732 | + } |
733 | + return cfg.Type() == "null", nil |
734 | + } |
735 | + return false, nil |
736 | +} |
737 | + |
738 | // AgentTools returns the tools that the agent is currently running. |
739 | // It returns an error that satisfies IsNotFound if the tools have not yet been set. |
740 | func (m *Machine) AgentTools() (*tools.Tools, error) { |
741 | |
742 | === modified file 'state/machine_test.go' |
743 | --- state/machine_test.go 2013-12-12 17:23:45 +0000 |
744 | +++ state/machine_test.go 2013-12-19 14:21:12 +0000 |
745 | @@ -90,6 +90,49 @@ |
746 | } |
747 | } |
748 | |
749 | +func (s *MachineSuite) TestMachineIsManualBootstrap(c *gc.C) { |
750 | + cfg, err := s.State.EnvironConfig() |
751 | + c.Assert(err, gc.IsNil) |
752 | + c.Assert(cfg.Type(), gc.Not(gc.Equals), "null") |
753 | + c.Assert(s.machine.Id(), gc.Equals, "0") |
754 | + manual, err := s.machine.IsManual() |
755 | + c.Assert(err, gc.IsNil) |
756 | + c.Assert(manual, jc.IsFalse) |
757 | + newcfg, err := cfg.Apply(map[string]interface{}{"type": "null"}) |
758 | + c.Assert(err, gc.IsNil) |
759 | + err = s.State.SetEnvironConfig(newcfg, cfg) |
760 | + c.Assert(err, gc.IsNil) |
761 | + manual, err = s.machine.IsManual() |
762 | + c.Assert(err, gc.IsNil) |
763 | + c.Assert(manual, jc.IsTrue) |
764 | +} |
765 | + |
766 | +func (s *MachineSuite) TestMachineIsManual(c *gc.C) { |
767 | + tests := []struct { |
768 | + instanceId instance.Id |
769 | + nonce string |
770 | + isManual bool |
771 | + }{ |
772 | + {instanceId: "x", nonce: "y", isManual: false}, |
773 | + {instanceId: "manual:", nonce: "y", isManual: false}, |
774 | + {instanceId: "x", nonce: "manual:", isManual: true}, |
775 | + {instanceId: "x", nonce: "manual:y", isManual: true}, |
776 | + {instanceId: "x", nonce: "manual", isManual: false}, |
777 | + } |
778 | + for _, test := range tests { |
779 | + params := state.AddMachineParams{ |
780 | + Series: "quantal", |
781 | + Jobs: []state.MachineJob{state.JobHostUnits}, |
782 | + InstanceId: test.instanceId, |
783 | + Nonce: test.nonce, |
784 | + } |
785 | + m, err := s.State.InjectMachine(¶ms) |
786 | + c.Assert(err, gc.IsNil) |
787 | + isManual, err := m.IsManual() |
788 | + c.Assert(isManual, gc.Equals, test.isManual) |
789 | + } |
790 | +} |
791 | + |
792 | func (s *MachineSuite) TestLifeJobManageEnviron(c *gc.C) { |
793 | // A JobManageEnviron machine must never advance lifecycle. |
794 | m, err := s.State.AddMachine("quantal", state.JobManageEnviron) |
Reviewers: mp+198698_ code.launchpad. net,
Message:
Please take a look.
Description:
Implement DestroyEnvironment API
We introduce a new DestroyEnvironment client API
method, which:
- checks that there are no manual non-manager machines in state
- sets the environment to Dying, scheduling
cleanups for services and preventing addition
of new services and machines.
- destroys non-manager instances
"juju destroy- environment" will call this new method,
ignoring the "no such method" error returned by old
API servers. If the API method succeeds, or it doesn't
exist, the provider.Destroy() method is called.
https:/ /code.launchpad .net/~axwalk/ juju-core/ destroy- environment- api/+merge/ 198698
(do not edit description out of merge proposal)
Please review this at https:/ /codereview. appspot. com/41280043/
Affected files (+454, -132 lines): cmd_test. go destroyenvironm ent.go destroyenvironm ent_test. go dummy/environs. go /client/ destroy. go /client/ destroy_ test.go
A [revision details]
M cmd/juju/
M cmd/juju/
A cmd/juju/
M provider/
M state/api/client.go
A state/apiserver
A state/apiserver