Merge lp:~axwalk/juju-core/destroy-environment-api into lp:~go-bot/juju-core/trunk

Proposed by Andrew Wilkins
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
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-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://codereview.appspot.com/41280043/

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-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://codereview.appspot.com/41280043/

To post a comment you must log in.
Revision history for this message
Andrew Wilkins (axwalk) wrote :

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):
   A [revision details]
   M cmd/juju/cmd_test.go
   M cmd/juju/destroyenvironment.go
   A cmd/juju/destroyenvironment_test.go
   M provider/dummy/environs.go
   M state/api/client.go
   A state/apiserver/client/destroy.go
   A state/apiserver/client/destroy_test.go

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

LGTM with more-eager instance-killing, or a good reason not to, plus the
other trivials below

https://codereview.appspot.com/41280043/diff/1/state/apiserver/client/destroy.go
File state/apiserver/client/destroy.go (right):

https://codereview.appspot.com/41280043/diff/1/state/apiserver/client/destroy.go#newcode54
state/apiserver/client/destroy.go:54: if err :=
checkManualMachines(machines); err != nil {
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://codereview.appspot.com/41280043/diff/1/state/apiserver/client/destroy.go#newcode110
state/apiserver/client/destroy.go:110: if inst == nil {
log the failed instances?

https://codereview.appspot.com/41280043/diff/1/state/apiserver/client/destroy.go#newcode145
state/apiserver/client/destroy.go:145: return
strings.HasPrefix(string(iid), "manual:")
Yeah, use the nonce -- if you need to make this a method on machine, so
be it.

https://codereview.appspot.com/41280043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :

Please take a look.

https://codereview.appspot.com/41280043/diff/1/state/apiserver/client/destroy.go
File state/apiserver/client/destroy.go (right):

https://codereview.appspot.com/41280043/diff/1/state/apiserver/client/destroy.go#newcode54
state/apiserver/client/destroy.go:54: if err :=
checkManualMachines(machines); err != nil {
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://codereview.appspot.com/41280043/diff/1/state/apiserver/client/destroy.go#newcode110
state/apiserver/client/destroy.go:110: if inst == nil {
On 2013/12/18 12:52:20, fwereade wrote:
> log the failed instances?

Done.

https://codereview.appspot.com/41280043/diff/1/state/apiserver/client/destroy.go#newcode145
state/apiserver/client/destroy.go:145: return
strings.HasPrefix(string(iid), "manual:")
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.

https://codereview.appspot.com/41280043/

Revision history for this message
Andrew Wilkins (axwalk) wrote :
Revision history for this message
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.

https://codereview.appspot.com/41280043/

Revision history for this message
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.

https://codereview.appspot.com/41280043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
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(&params)
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)

Subscribers

People subscribed via source and target branches

to status/vote changes: