github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/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 "text/tabwriter" 13 "time" 14 15 "github.com/juju/cmd" 16 "github.com/juju/errors" 17 "github.com/juju/names" 18 "launchpad.net/gnuflag" 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.ControllerSkipFlags, 40 modelcmd.ControllerSkipDefault, 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 64 var usageSummary = ` 65 Destroys a controller.`[1:] 66 67 var destroySysMsg = ` 68 WARNING! This command will destroy the %q controller. 69 This includes all machines, services, data and other resources. 70 71 Continue [y/N]? `[1:] 72 73 // destroyControllerAPI defines the methods on the controller API endpoint 74 // that the destroy command calls. 75 type destroyControllerAPI interface { 76 Close() error 77 ModelConfig() (map[string]interface{}, error) 78 DestroyController(destroyModels bool) error 79 ListBlockedModels() ([]params.ModelBlockInfo, error) 80 ModelStatus(models ...names.ModelTag) ([]base.ModelStatus, error) 81 AllModels() ([]base.UserModel, error) 82 } 83 84 // destroyClientAPI defines the methods on the client API endpoint that the 85 // destroy command might call. 86 type destroyClientAPI interface { 87 Close() error 88 ModelGet() (map[string]interface{}, error) 89 DestroyModel() error 90 } 91 92 // Info implements Command.Info. 93 func (c *destroyCommand) Info() *cmd.Info { 94 return &cmd.Info{ 95 Name: "destroy-controller", 96 Args: "<controller name>", 97 Purpose: usageSummary, 98 Doc: usageDetails, 99 } 100 } 101 102 // SetFlags implements Command.SetFlags. 103 func (c *destroyCommand) SetFlags(f *gnuflag.FlagSet) { 104 f.BoolVar(&c.destroyModels, "destroy-all-models", false, "Destroy all hosted models in the controller") 105 c.destroyCommandBase.SetFlags(f) 106 } 107 108 // Run implements Command.Run 109 func (c *destroyCommand) Run(ctx *cmd.Context) error { 110 controllerName := c.ControllerName() 111 store := c.ClientStore() 112 controllerDetails, err := store.ControllerByName(controllerName) 113 if err != nil { 114 return errors.Annotate(err, "cannot read controller info") 115 } 116 117 if !c.assumeYes { 118 if err = confirmDestruction(ctx, c.ControllerName()); err != nil { 119 return err 120 } 121 } 122 123 // Attempt to connect to the API. If we can't, fail the destroy. Users will 124 // need to use the controller kill command if we can't connect. 125 api, err := c.getControllerAPI() 126 if err != nil { 127 return c.ensureUserFriendlyErrorLog(errors.Annotate(err, "cannot connect to API"), ctx, nil) 128 } 129 defer api.Close() 130 131 // Obtain controller environ so we can clean up afterwards. 132 controllerEnviron, err := c.getControllerEnviron(store, controllerName, api) 133 if err != nil { 134 return errors.Annotate(err, "getting controller environ") 135 } 136 137 for { 138 // Attempt to destroy the controller. 139 ctx.Infof("Destroying controller") 140 var hasHostedModels bool 141 err = api.DestroyController(c.destroyModels) 142 if err != nil { 143 if params.IsCodeHasHostedModels(err) { 144 hasHostedModels = true 145 } else { 146 return c.ensureUserFriendlyErrorLog( 147 errors.Annotate(err, "cannot destroy controller"), 148 ctx, api, 149 ) 150 } 151 } 152 153 updateStatus := newTimedStatusUpdater(ctx, api, controllerDetails.ControllerUUID) 154 ctrStatus, modelsStatus := updateStatus(0) 155 if !c.destroyModels { 156 if err := c.checkNoAliveHostedModels(ctx, modelsStatus); err != nil { 157 return errors.Trace(err) 158 } 159 if hasHostedModels && !hasUnDeadModels(modelsStatus) { 160 // When we called DestroyController before, we were 161 // informed that there were hosted models remaining. 162 // When we checked just now, there were none. We should 163 // try destroying again. 164 continue 165 } 166 } 167 168 // Even if we've not just requested for hosted models to be destroyed, 169 // there may be some being destroyed already. We need to wait for them. 170 ctx.Infof("Waiting for hosted model resources to be reclaimed") 171 for ; hasUnDeadModels(modelsStatus); ctrStatus, modelsStatus = updateStatus(2 * time.Second) { 172 ctx.Infof(fmtCtrStatus(ctrStatus)) 173 for _, model := range modelsStatus { 174 ctx.Verbosef(fmtModelStatus(model)) 175 } 176 } 177 ctx.Infof("All hosted models reclaimed, cleaning up controller machines") 178 return environs.Destroy(c.ControllerName(), controllerEnviron, store) 179 } 180 } 181 182 // checkNoAliveHostedModels ensures that the given set of hosted models 183 // contains none that are Alive. If there are, an message is printed 184 // out to 185 func (c *destroyCommand) checkNoAliveHostedModels(ctx *cmd.Context, models []modelData) error { 186 if !hasAliveModels(models) { 187 return nil 188 } 189 // The user did not specify --destroy-all-models, 190 // and there are models still alive. 191 var buf bytes.Buffer 192 for _, model := range models { 193 if model.Life != params.Alive { 194 continue 195 } 196 buf.WriteString(fmtModelStatus(model)) 197 buf.WriteRune('\n') 198 } 199 return errors.Errorf(`cannot destroy controller %q 200 201 The controller has live hosted models. If you want 202 to destroy all hosted models in the controller, 203 run this command again with the --destroy-all-models 204 flag. 205 206 Models: 207 %s`, c.ControllerName(), buf.String()) 208 } 209 210 // ensureUserFriendlyErrorLog ensures that error will be logged and displayed 211 // in a user-friendly manner with readable and digestable error message. 212 func (c *destroyCommand) ensureUserFriendlyErrorLog(destroyErr error, ctx *cmd.Context, api destroyControllerAPI) error { 213 if destroyErr == nil { 214 return nil 215 } 216 if params.IsCodeOperationBlocked(destroyErr) { 217 logger.Errorf(destroyControllerBlockedMsg) 218 if api != nil { 219 models, err := api.ListBlockedModels() 220 var bytes []byte 221 if err == nil { 222 bytes, err = formatTabularBlockedModels(models) 223 } 224 if err != nil { 225 logger.Errorf("Unable to list blocked models: %s", err) 226 return cmd.ErrSilent 227 } 228 ctx.Infof(string(bytes)) 229 } 230 return cmd.ErrSilent 231 } 232 if params.IsCodeHasHostedModels(destroyErr) { 233 return destroyErr 234 } 235 logger.Errorf(stdFailureMsg, c.ControllerName()) 236 return destroyErr 237 } 238 239 const destroyControllerBlockedMsg = `there are blocks preventing controller destruction 240 To remove all blocks in the controller, please run: 241 242 juju controller remove-blocks 243 244 ` 245 246 // TODO(axw) this should only be printed out if we couldn't 247 // connect to the controller. 248 const stdFailureMsg = `failed to destroy controller %q 249 250 If the controller is unusable, then you may run 251 252 juju kill-controller 253 254 to forcibly destroy the controller. Upon doing so, review 255 your cloud provider console for any resources that need 256 to be cleaned up. 257 258 ` 259 260 func formatTabularBlockedModels(value interface{}) ([]byte, error) { 261 models, ok := value.([]params.ModelBlockInfo) 262 if !ok { 263 return nil, errors.Errorf("expected value of type %T, got %T", models, value) 264 } 265 266 var out bytes.Buffer 267 const ( 268 // To format things into columns. 269 minwidth = 0 270 tabwidth = 1 271 padding = 2 272 padchar = ' ' 273 flags = 0 274 ) 275 tw := tabwriter.NewWriter(&out, minwidth, tabwidth, padding, padchar, flags) 276 fmt.Fprintf(tw, "NAME\tMODEL UUID\tOWNER\tBLOCKS\n") 277 for _, model := range models { 278 fmt.Fprintf(tw, "%s\t%s\t%s\t%s\n", model.Name, model.UUID, model.OwnerTag, blocksToStr(model.Blocks)) 279 } 280 tw.Flush() 281 return out.Bytes(), nil 282 } 283 284 func blocksToStr(blocks []string) string { 285 result := "" 286 sep := "" 287 for _, blk := range blocks { 288 result = result + sep + block.OperationFromType(blk) 289 sep = "," 290 } 291 292 return result 293 } 294 295 // destroyCommandBase provides common attributes and methods that both the controller 296 // destroy and controller kill commands require. 297 type destroyCommandBase struct { 298 modelcmd.ControllerCommandBase 299 assumeYes bool 300 301 // The following fields are for mocking out 302 // api behavior for testing. 303 api destroyControllerAPI 304 apierr error 305 clientapi destroyClientAPI 306 } 307 308 func (c *destroyCommandBase) getControllerAPI() (destroyControllerAPI, error) { 309 if c.api != nil { 310 return c.api, c.apierr 311 } 312 root, err := c.NewAPIRoot() 313 if err != nil { 314 return nil, errors.Trace(err) 315 } 316 return controller.NewClient(root), nil 317 } 318 319 // SetFlags implements Command.SetFlags. 320 func (c *destroyCommandBase) SetFlags(f *gnuflag.FlagSet) { 321 f.BoolVar(&c.assumeYes, "y", false, "Do not ask for confirmation") 322 f.BoolVar(&c.assumeYes, "yes", false, "") 323 } 324 325 // Init implements Command.Init. 326 func (c *destroyCommandBase) Init(args []string) error { 327 switch len(args) { 328 case 0: 329 return errors.New("no controller specified") 330 case 1: 331 return c.SetControllerName(args[0]) 332 default: 333 return cmd.CheckEmpty(args[1:]) 334 } 335 } 336 337 // getControllerEnviron returns the Environ for the controller model. 338 // 339 // getControllerEnviron gets the information required to get the 340 // Environ by first checking the config store, then querying the 341 // API if the information is not in the store. 342 func (c *destroyCommandBase) getControllerEnviron( 343 store jujuclient.ClientStore, controllerName string, sysAPI destroyControllerAPI, 344 ) (_ environs.Environ, err error) { 345 cfg, err := modelcmd.NewGetBootstrapConfigFunc(store)(controllerName) 346 if errors.IsNotFound(err) { 347 if sysAPI == nil { 348 return nil, errors.New( 349 "unable to get bootstrap information from client store or API", 350 ) 351 } 352 bootstrapConfig, err := sysAPI.ModelConfig() 353 if err != nil { 354 return nil, errors.Annotate(err, "getting bootstrap config from API") 355 } 356 cfg, err = config.New(config.NoDefaults, bootstrapConfig) 357 if err != nil { 358 return nil, errors.Trace(err) 359 } 360 } else if err != nil { 361 return nil, errors.Annotate(err, "getting bootstrap config from client store") 362 } 363 return environs.New(cfg) 364 } 365 366 func confirmDestruction(ctx *cmd.Context, controllerName string) error { 367 // Get confirmation from the user that they want to continue 368 fmt.Fprintf(ctx.Stdout, destroySysMsg, controllerName) 369 370 scanner := bufio.NewScanner(ctx.Stdin) 371 scanner.Scan() 372 err := scanner.Err() 373 if err != nil && err != io.EOF { 374 return errors.Annotate(err, "controller destruction aborted") 375 } 376 answer := strings.ToLower(scanner.Text()) 377 if answer != "y" && answer != "yes" { 378 return errors.New("controller destruction aborted") 379 } 380 381 return nil 382 }