github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/cmd/juju/controller/destroy.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package controller 5 6 import ( 7 "bufio" 8 "bytes" 9 "fmt" 10 "io" 11 "strings" 12 "time" 13 14 "github.com/juju/cmd" 15 "github.com/juju/errors" 16 "github.com/juju/gnuflag" 17 "github.com/juju/utils/clock" 18 "gopkg.in/juju/names.v2" 19 20 "github.com/juju/juju/api/base" 21 "github.com/juju/juju/api/controller" 22 "github.com/juju/juju/apiserver/params" 23 "github.com/juju/juju/cmd/juju/block" 24 "github.com/juju/juju/cmd/modelcmd" 25 "github.com/juju/juju/environs" 26 "github.com/juju/juju/environs/config" 27 "github.com/juju/juju/jujuclient" 28 ) 29 30 // NewDestroyCommand returns a command to destroy a controller. 31 func NewDestroyCommand() cmd.Command { 32 // Even though this command is all about destroying a controller we end up 33 // needing environment endpoints so we can fall back to the client destroy 34 // environment method. This shouldn't really matter in practice as the 35 // user trying to take down the controller will need to have access to the 36 // controller environment anyway. 37 return modelcmd.WrapController( 38 &destroyCommand{}, 39 modelcmd.WrapControllerSkipControllerFlags, 40 modelcmd.WrapControllerSkipDefaultController, 41 ) 42 } 43 44 // destroyCommand destroys the specified controller. 45 type destroyCommand struct { 46 destroyCommandBase 47 destroyModels bool 48 } 49 50 // usageDetails has backticks which we want to keep for markdown processing. 51 // TODO(cheryl): Do we want the usage, options, examples, and see also text in 52 // backticks for markdown? 53 var usageDetails = ` 54 All models (initial model plus all workload/hosted) associated with the 55 controller will first need to be destroyed, either in advance, or by 56 specifying `[1:] + "`--destroy-all-models`." + ` 57 58 Examples: 59 juju destroy-controller --destroy-all-models mycontroller 60 61 See also: 62 kill-controller 63 unregister` 64 65 var usageSummary = ` 66 Destroys a controller.`[1:] 67 68 var destroySysMsg = ` 69 WARNING! This command will destroy the %q controller. 70 This includes all machines, applications, data and other resources. 71 72 Continue? (y/N):`[1:] 73 74 // destroyControllerAPI defines the methods on the controller API endpoint 75 // that the destroy command calls. 76 type destroyControllerAPI interface { 77 Close() error 78 ModelConfig() (map[string]interface{}, error) 79 HostedModelConfigs() ([]controller.HostedConfig, error) 80 CloudSpec(names.ModelTag) (environs.CloudSpec, error) 81 DestroyController(destroyModels bool) error 82 ListBlockedModels() ([]params.ModelBlockInfo, error) 83 ModelStatus(models ...names.ModelTag) ([]base.ModelStatus, error) 84 AllModels() ([]base.UserModel, error) 85 } 86 87 // destroyClientAPI defines the methods on the client API endpoint that the 88 // destroy command might call. 89 type destroyClientAPI interface { 90 Close() error 91 ModelGet() (map[string]interface{}, error) 92 DestroyModel() error 93 } 94 95 // Info implements Command.Info. 96 func (c *destroyCommand) Info() *cmd.Info { 97 return &cmd.Info{ 98 Name: "destroy-controller", 99 Args: "<controller name>", 100 Purpose: usageSummary, 101 Doc: usageDetails, 102 } 103 } 104 105 // SetFlags implements Command.SetFlags. 106 func (c *destroyCommand) SetFlags(f *gnuflag.FlagSet) { 107 c.destroyCommandBase.SetFlags(f) 108 f.BoolVar(&c.destroyModels, "destroy-all-models", false, "Destroy all hosted models in the controller") 109 } 110 111 // Run implements Command.Run 112 func (c *destroyCommand) Run(ctx *cmd.Context) error { 113 controllerName := c.ControllerName() 114 store := c.ClientStore() 115 if !c.assumeYes { 116 if err := confirmDestruction(ctx, c.ControllerName()); err != nil { 117 return err 118 } 119 } 120 121 // Attempt to connect to the API. If we can't, fail the destroy. Users will 122 // need to use the controller kill command if we can't connect. 123 api, err := c.getControllerAPI() 124 if err != nil { 125 return c.ensureUserFriendlyErrorLog(errors.Annotate(err, "cannot connect to API"), ctx, nil) 126 } 127 defer api.Close() 128 129 // Obtain controller environ so we can clean up afterwards. 130 controllerEnviron, err := c.getControllerEnviron(ctx, store, controllerName, api) 131 if err != nil { 132 return errors.Annotate(err, "getting controller environ") 133 } 134 135 for { 136 // Attempt to destroy the controller. 137 ctx.Infof("Destroying controller") 138 var hasHostedModels bool 139 err = api.DestroyController(c.destroyModels) 140 if err != nil { 141 if params.IsCodeHasHostedModels(err) { 142 hasHostedModels = true 143 } else { 144 return c.ensureUserFriendlyErrorLog( 145 errors.Annotate(err, "cannot destroy controller"), 146 ctx, api, 147 ) 148 } 149 } 150 151 updateStatus := newTimedStatusUpdater(ctx, api, controllerEnviron.Config().UUID(), clock.WallClock) 152 ctrStatus, modelsStatus := updateStatus(0) 153 if !c.destroyModels { 154 if err := c.checkNoAliveHostedModels(ctx, modelsStatus); err != nil { 155 return errors.Trace(err) 156 } 157 if hasHostedModels && !hasUnDeadModels(modelsStatus) { 158 // When we called DestroyController before, we were 159 // informed that there were hosted models remaining. 160 // When we checked just now, there were none. We should 161 // try destroying again. 162 continue 163 } 164 } 165 166 // Even if we've not just requested for hosted models to be destroyed, 167 // there may be some being destroyed already. We need to wait for them. 168 ctx.Infof("Waiting for hosted model resources to be reclaimed") 169 for ; hasUnDeadModels(modelsStatus); ctrStatus, modelsStatus = updateStatus(2 * time.Second) { 170 ctx.Infof(fmtCtrStatus(ctrStatus)) 171 for _, model := range modelsStatus { 172 ctx.Verbosef(fmtModelStatus(model)) 173 } 174 } 175 ctx.Infof("All hosted models reclaimed, cleaning up controller machines") 176 return environs.Destroy(c.ControllerName(), controllerEnviron, store) 177 } 178 } 179 180 // checkNoAliveHostedModels ensures that the given set of hosted models 181 // contains none that are Alive. If there are, an message is printed 182 // out to 183 func (c *destroyCommand) checkNoAliveHostedModels(ctx *cmd.Context, models []modelData) error { 184 if !hasAliveModels(models) { 185 return nil 186 } 187 // The user did not specify --destroy-all-models, 188 // and there are models still alive. 189 var buf bytes.Buffer 190 for _, model := range models { 191 if model.Life != string(params.Alive) { 192 continue 193 } 194 buf.WriteString(fmtModelStatus(model)) 195 buf.WriteRune('\n') 196 } 197 return errors.Errorf(`cannot destroy controller %q 198 199 The controller has live hosted models. If you want 200 to destroy all hosted models in the controller, 201 run this command again with the --destroy-all-models 202 flag. 203 204 Models: 205 %s`, c.ControllerName(), buf.String()) 206 } 207 208 // ensureUserFriendlyErrorLog ensures that error will be logged and displayed 209 // in a user-friendly manner with readable and digestable error message. 210 func (c *destroyCommand) ensureUserFriendlyErrorLog(destroyErr error, ctx *cmd.Context, api destroyControllerAPI) error { 211 if destroyErr == nil { 212 return nil 213 } 214 if params.IsCodeOperationBlocked(destroyErr) { 215 logger.Errorf(destroyControllerBlockedMsg) 216 if api != nil { 217 models, err := api.ListBlockedModels() 218 out := &bytes.Buffer{} 219 if err == nil { 220 var info interface{} 221 info, err = block.FormatModelBlockInfo(models) 222 if err != nil { 223 return errors.Trace(err) 224 } 225 err = block.FormatTabularBlockedModels(out, info) 226 } 227 if err != nil { 228 logger.Errorf("Unable to list models: %s", err) 229 return cmd.ErrSilent 230 } 231 ctx.Infof(out.String()) 232 } 233 return cmd.ErrSilent 234 } 235 if params.IsCodeHasHostedModels(destroyErr) { 236 return destroyErr 237 } 238 logger.Errorf(stdFailureMsg, c.ControllerName()) 239 return destroyErr 240 } 241 242 const destroyControllerBlockedMsg = `there are models with disabled commands preventing controller destruction 243 244 To enable controller destruction, please run: 245 246 juju enable-destroy-controller 247 248 ` 249 250 // TODO(axw) this should only be printed out if we couldn't 251 // connect to the controller. 252 const stdFailureMsg = `failed to destroy controller %q 253 254 If the controller is unusable, then you may run 255 256 juju kill-controller 257 258 to forcibly destroy the controller. Upon doing so, review 259 your cloud provider console for any resources that need 260 to be cleaned up. 261 262 ` 263 264 // destroyCommandBase provides common attributes and methods that both the controller 265 // destroy and controller kill commands require. 266 type destroyCommandBase struct { 267 modelcmd.ControllerCommandBase 268 assumeYes bool 269 270 // The following fields are for mocking out 271 // api behavior for testing. 272 api destroyControllerAPI 273 apierr error 274 clientapi destroyClientAPI 275 } 276 277 func (c *destroyCommandBase) getControllerAPI() (destroyControllerAPI, error) { 278 if c.api != nil { 279 return c.api, c.apierr 280 } 281 root, err := c.NewAPIRoot() 282 if err != nil { 283 return nil, errors.Trace(err) 284 } 285 return controller.NewClient(root), nil 286 } 287 288 // SetFlags implements Command.SetFlags. 289 func (c *destroyCommandBase) SetFlags(f *gnuflag.FlagSet) { 290 c.ControllerCommandBase.SetFlags(f) 291 f.BoolVar(&c.assumeYes, "y", false, "Do not ask for confirmation") 292 f.BoolVar(&c.assumeYes, "yes", false, "") 293 } 294 295 // Init implements Command.Init. 296 func (c *destroyCommandBase) Init(args []string) error { 297 switch len(args) { 298 case 0: 299 return errors.New("no controller specified") 300 case 1: 301 return c.SetControllerName(args[0]) 302 default: 303 return cmd.CheckEmpty(args[1:]) 304 } 305 } 306 307 // getControllerEnviron returns the Environ for the controller model. 308 // 309 // getControllerEnviron gets the information required to get the 310 // Environ by first checking the config store, then querying the 311 // API if the information is not in the store. 312 func (c *destroyCommandBase) getControllerEnviron( 313 ctx *cmd.Context, 314 store jujuclient.ClientStore, 315 controllerName string, 316 sysAPI destroyControllerAPI, 317 ) (environs.Environ, error) { 318 env, err := c.getControllerEnvironFromStore(ctx, store, controllerName) 319 if errors.IsNotFound(err) { 320 return c.getControllerEnvironFromAPI(sysAPI, controllerName) 321 } else if err != nil { 322 return nil, errors.Annotate(err, "getting environ using bootstrap config from client store") 323 } 324 return env, nil 325 } 326 327 func (c *destroyCommandBase) getControllerEnvironFromStore( 328 ctx *cmd.Context, 329 store jujuclient.ClientStore, 330 controllerName string, 331 ) (environs.Environ, error) { 332 bootstrapConfig, params, err := modelcmd.NewGetBootstrapConfigParamsFunc(ctx, store)(controllerName) 333 if err != nil { 334 return nil, errors.Trace(err) 335 } 336 provider, err := environs.Provider(bootstrapConfig.CloudType) 337 if err != nil { 338 return nil, errors.Trace(err) 339 } 340 cfg, err := provider.PrepareConfig(*params) 341 if err != nil { 342 return nil, errors.Trace(err) 343 } 344 return environs.New(environs.OpenParams{ 345 Cloud: params.Cloud, 346 Config: cfg, 347 }) 348 } 349 350 func (c *destroyCommandBase) getControllerEnvironFromAPI( 351 api destroyControllerAPI, 352 controllerName string, 353 ) (environs.Environ, error) { 354 if api == nil { 355 return nil, errors.New( 356 "unable to get bootstrap information from client store or API", 357 ) 358 } 359 attrs, err := api.ModelConfig() 360 if err != nil { 361 return nil, errors.Annotate(err, "getting model config from API") 362 } 363 cfg, err := config.New(config.NoDefaults, attrs) 364 if err != nil { 365 return nil, errors.Trace(err) 366 } 367 cloudSpec, err := api.CloudSpec(names.NewModelTag(cfg.UUID())) 368 if err != nil { 369 return nil, errors.Annotate(err, "getting cloud spec from API") 370 } 371 return environs.New(environs.OpenParams{ 372 Cloud: cloudSpec, 373 Config: cfg, 374 }) 375 } 376 377 func confirmDestruction(ctx *cmd.Context, controllerName string) error { 378 // Get confirmation from the user that they want to continue 379 fmt.Fprintf(ctx.Stdout, destroySysMsg, controllerName) 380 381 scanner := bufio.NewScanner(ctx.Stdin) 382 scanner.Scan() 383 err := scanner.Err() 384 if err != nil && err != io.EOF { 385 return errors.Annotate(err, "controller destruction aborted") 386 } 387 answer := strings.ToLower(scanner.Text()) 388 if answer != "y" && answer != "yes" { 389 return errors.New("controller destruction aborted") 390 } 391 392 return nil 393 }