github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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/clock" 15 "github.com/juju/cmd" 16 "github.com/juju/errors" 17 "github.com/juju/gnuflag" 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/api/credentialmanager" 23 "github.com/juju/juju/api/storage" 24 "github.com/juju/juju/apiserver/params" 25 jujucmd "github.com/juju/juju/cmd" 26 "github.com/juju/juju/cmd/juju/block" 27 "github.com/juju/juju/cmd/modelcmd" 28 "github.com/juju/juju/environs" 29 "github.com/juju/juju/environs/config" 30 "github.com/juju/juju/environs/context" 31 "github.com/juju/juju/jujuclient" 32 ) 33 34 // NewDestroyCommand returns a command to destroy a controller. 35 func NewDestroyCommand() cmd.Command { 36 cmd := destroyCommand{} 37 cmd.controllerCredentialAPIFunc = cmd.credentialAPIForControllerModel 38 cmd.environsDestroy = environs.Destroy 39 // Even though this command is all about destroying a controller we end up 40 // needing environment endpoints so we can fall back to the client destroy 41 // environment method. This shouldn't really matter in practice as the 42 // user trying to take down the controller will need to have access to the 43 // controller environment anyway. 44 return modelcmd.WrapController( 45 &cmd, 46 modelcmd.WrapControllerSkipControllerFlags, 47 modelcmd.WrapControllerSkipDefaultController, 48 ) 49 } 50 51 // destroyCommand destroys the specified controller. 52 type destroyCommand struct { 53 destroyCommandBase 54 storageAPI storageAPI 55 destroyModels bool 56 destroyStorage bool 57 releaseStorage bool 58 } 59 60 // usageDetails has backticks which we want to keep for markdown processing. 61 // TODO(cheryl): Do we want the usage, options, examples, and see also text in 62 // backticks for markdown? 63 var usageDetails = ` 64 All models (initial model plus all workload/hosted) associated with the 65 controller will first need to be destroyed, either in advance, or by 66 specifying `[1:] + "`--destroy-all-models`." + ` 67 68 If there is persistent storage in any of the models managed by the 69 controller, then you must choose to either destroy or release the 70 storage, using ` + "`--destroy-storage` or `--release-storage` respectively." + ` 71 72 Examples: 73 # Destroy the controller and all hosted models. If there is 74 # persistent storage remaining in any of the models, then 75 # this will prompt you to choose to either destroy or release 76 # the storage. 77 juju destroy-controller --destroy-all-models mycontroller 78 79 # Destroy the controller and all hosted models, destroying 80 # any remaining persistent storage. 81 juju destroy-controller --destroy-all-models --destroy-storage 82 83 # Destroy the controller and all hosted models, releasing 84 # any remaining persistent storage from Juju's control. 85 juju destroy-controller --destroy-all-models --release-storage 86 87 See also: 88 kill-controller 89 unregister` 90 91 var usageSummary = ` 92 Destroys a controller.`[1:] 93 94 var destroySysMsg = ` 95 WARNING! This command will destroy the %q controller. 96 This includes all machines, applications, data and other resources. 97 98 Continue? (y/N):`[1:] 99 100 // destroyControllerAPI defines the methods on the controller API endpoint 101 // that the destroy command calls. 102 type destroyControllerAPI interface { 103 Close() error 104 BestAPIVersion() int 105 ModelConfig() (map[string]interface{}, error) 106 HostedModelConfigs() ([]controller.HostedConfig, error) 107 CloudSpec(names.ModelTag) (environs.CloudSpec, error) 108 DestroyController(controller.DestroyControllerParams) error 109 ListBlockedModels() ([]params.ModelBlockInfo, error) 110 ModelStatus(models ...names.ModelTag) ([]base.ModelStatus, error) 111 AllModels() ([]base.UserModel, error) 112 } 113 114 type storageAPI interface { 115 Close() error 116 ListStorageDetails() ([]params.StorageDetails, error) 117 } 118 119 // destroyClientAPI defines the methods on the client API endpoint that the 120 // destroy command might call. 121 type destroyClientAPI interface { 122 Close() error 123 ModelGet() (map[string]interface{}, error) 124 DestroyModel() error 125 } 126 127 // Info implements Command.Info. 128 func (c *destroyCommand) Info() *cmd.Info { 129 return jujucmd.Info(&cmd.Info{ 130 Name: "destroy-controller", 131 Args: "<controller name>", 132 Purpose: usageSummary, 133 Doc: usageDetails, 134 }) 135 } 136 137 // SetFlags implements Command.SetFlags. 138 func (c *destroyCommand) SetFlags(f *gnuflag.FlagSet) { 139 c.destroyCommandBase.SetFlags(f) 140 f.BoolVar(&c.destroyModels, "destroy-all-models", false, "Destroy all hosted models in the controller") 141 f.BoolVar(&c.destroyStorage, "destroy-storage", false, "Destroy all storage instances managed by the controller") 142 f.BoolVar(&c.releaseStorage, "release-storage", false, "Release all storage instances from management of the controller, without destroying them") 143 } 144 145 // Init implements Command.Init. 146 func (c *destroyCommand) Init(args []string) error { 147 if c.destroyStorage && c.releaseStorage { 148 return errors.New("--destroy-storage and --release-storage cannot both be specified") 149 } 150 return c.destroyCommandBase.Init(args) 151 } 152 153 // Run implements Command.Run 154 func (c *destroyCommand) Run(ctx *cmd.Context) error { 155 controllerName, err := c.ControllerName() 156 if err != nil { 157 return errors.Trace(err) 158 } 159 store := c.ClientStore() 160 if !c.assumeYes { 161 if err := confirmDestruction(ctx, controllerName); err != nil { 162 return err 163 } 164 } 165 166 // Attempt to connect to the API. If we can't, fail the destroy. Users will 167 // need to use the controller kill command if we can't connect. 168 api, err := c.getControllerAPI() 169 if err != nil { 170 return c.ensureUserFriendlyErrorLog(errors.Annotate(err, "cannot connect to API"), ctx, nil) 171 } 172 defer api.Close() 173 174 if api.BestAPIVersion() < 4 { 175 // Versions before 4 support only destroying the storage, 176 // and will not raise an error if there is storage in the 177 // controller. Force the user to specify up-front. 178 if c.releaseStorage { 179 return errors.New("this juju controller only supports destroying storage") 180 } 181 if !c.destroyStorage { 182 models, err := api.AllModels() 183 if err != nil { 184 return errors.Trace(err) 185 } 186 var anyStorage bool 187 for _, model := range models { 188 hasStorage, err := c.modelHasStorage(model.Name) 189 if err != nil { 190 return errors.Trace(err) 191 } 192 if hasStorage { 193 anyStorage = true 194 break 195 } 196 } 197 if anyStorage { 198 return errors.Errorf(`cannot destroy controller %q 199 200 Destroying this controller will destroy the storage, 201 but you have not indicated that you want to do that. 202 203 Please run the the command again with --destroy-storage 204 to confirm that you want to destroy the storage along 205 with the controller. 206 207 If instead you want to keep the storage, you must first 208 upgrade the controller to version 2.3 or greater. 209 210 `, controllerName) 211 } 212 c.destroyStorage = true 213 } 214 } 215 216 // Obtain controller environ so we can clean up afterwards. 217 controllerEnviron, err := c.getControllerEnviron(ctx, store, controllerName, api) 218 if err != nil { 219 return errors.Annotate(err, "getting controller environ") 220 } 221 222 cloudCallCtx := cloudCallContext(c.controllerCredentialAPIFunc) 223 224 for { 225 // Attempt to destroy the controller. 226 ctx.Infof("Destroying controller") 227 var hasHostedModels bool 228 var hasPersistentStorage bool 229 var destroyStorage *bool 230 if c.destroyStorage || c.releaseStorage { 231 // Set destroyStorage to true or false, if 232 // --destroy-storage or --release-storage 233 // is specified, respectively. 234 destroyStorage = &c.destroyStorage 235 } 236 err = api.DestroyController(controller.DestroyControllerParams{ 237 DestroyModels: c.destroyModels, 238 DestroyStorage: destroyStorage, 239 }) 240 if err != nil { 241 if params.IsCodeHasHostedModels(err) { 242 hasHostedModels = true 243 } else if params.IsCodeHasPersistentStorage(err) { 244 hasPersistentStorage = true 245 } else { 246 return c.ensureUserFriendlyErrorLog( 247 errors.Annotate(err, "cannot destroy controller"), 248 ctx, api, 249 ) 250 } 251 } 252 253 updateStatus := newTimedStatusUpdater(ctx, api, controllerEnviron.Config().UUID(), clock.WallClock) 254 // wait for 2 seconds to let empty hosted models changed from alive to dying. 255 modelStatus := updateStatus(0) 256 if !c.destroyModels { 257 if err := c.checkNoAliveHostedModels(ctx, modelStatus.models); err != nil { 258 return errors.Trace(err) 259 } 260 if hasHostedModels && !hasUnDeadModels(modelStatus.models) { 261 // When we called DestroyController before, we were 262 // informed that there were hosted models remaining. 263 // When we checked just now, there were none. We should 264 // try destroying again. 265 continue 266 } 267 } 268 if !c.destroyStorage && !c.releaseStorage && hasPersistentStorage { 269 if err := c.checkNoPersistentStorage(ctx, modelStatus); err != nil { 270 return errors.Trace(err) 271 } 272 // When we called DestroyController before, we were 273 // informed that there was persistent storage remaining. 274 // When we checked just now, there was none. We should 275 // try destroying again. 276 continue 277 } 278 279 // Even if we've not just requested for hosted models to be destroyed, 280 // there may be some being destroyed already. We need to wait for them. 281 // Check for both undead models and live machines, as machines may be 282 // in the controller model. 283 ctx.Infof("Waiting for hosted model resources to be reclaimed") 284 for ; hasUnreclaimedResources(modelStatus); modelStatus = updateStatus(2 * time.Second) { 285 ctx.Infof(fmtCtrStatus(modelStatus.controller)) 286 for _, model := range modelStatus.models { 287 ctx.Verbosef(fmtModelStatus(model)) 288 } 289 } 290 ctx.Infof("All hosted models reclaimed, cleaning up controller machines") 291 return c.environsDestroy(controllerName, controllerEnviron, cloudCallCtx, store) 292 } 293 } 294 295 func (c *destroyCommand) modelHasStorage(modelName string) (bool, error) { 296 client, err := c.getStorageAPI(modelName) 297 if err != nil { 298 return false, errors.Trace(err) 299 } 300 defer client.Close() 301 302 storage, err := client.ListStorageDetails() 303 if err != nil { 304 return false, errors.Trace(err) 305 } 306 return len(storage) > 0, nil 307 } 308 309 // checkNoAliveHostedModels ensures that the given set of hosted models 310 // contains none that are Alive. If there are, an message is printed 311 // out to 312 func (c *destroyCommand) checkNoAliveHostedModels(ctx *cmd.Context, models []modelData) error { 313 if !hasAliveModels(models) { 314 return nil 315 } 316 // The user did not specify --destroy-all-models, 317 // and there are models still alive. 318 var buf bytes.Buffer 319 for _, model := range models { 320 if model.Life != string(params.Alive) { 321 continue 322 } 323 buf.WriteString(fmtModelStatus(model)) 324 buf.WriteRune('\n') 325 } 326 controllerName, err := c.ControllerName() 327 if err != nil { 328 return errors.Trace(err) 329 } 330 return errors.Errorf(`cannot destroy controller %q 331 332 The controller has live hosted models. If you want 333 to destroy all hosted models in the controller, 334 run this command again with the --destroy-all-models 335 option. 336 337 Models: 338 %s`, controllerName, buf.String()) 339 } 340 341 // checkNoPersistentStorage ensures that the controller contains 342 // no persistent storage. If there is any, a message is printed 343 // out informing the user that they must choose to destroy or 344 // release the storage. 345 func (c *destroyCommand) checkNoPersistentStorage(ctx *cmd.Context, envStatus environmentStatus) error { 346 models := append([]modelData{envStatus.controller.Model}, envStatus.models...) 347 348 var modelsWithPersistentStorage int 349 var persistentVolumesTotal int 350 var persistentFilesystemsTotal int 351 for _, m := range models { 352 if m.PersistentVolumeCount+m.PersistentFilesystemCount == 0 { 353 continue 354 } 355 modelsWithPersistentStorage++ 356 persistentVolumesTotal += m.PersistentVolumeCount 357 persistentFilesystemsTotal += m.PersistentFilesystemCount 358 } 359 360 var buf bytes.Buffer 361 if n := persistentVolumesTotal; n > 0 { 362 fmt.Fprintf(&buf, "%d volume", n) 363 if n > 1 { 364 buf.WriteRune('s') 365 } 366 if persistentFilesystemsTotal > 0 { 367 buf.WriteString(" and ") 368 } 369 } 370 if n := persistentFilesystemsTotal; n > 0 { 371 fmt.Fprintf(&buf, "%d filesystem", n) 372 if n > 1 { 373 buf.WriteRune('s') 374 } 375 } 376 buf.WriteRune(' ') 377 if n := modelsWithPersistentStorage; n == 1 { 378 buf.WriteString("in 1 model") 379 } else { 380 fmt.Fprintf(&buf, "across %d models", n) 381 } 382 383 controllerName, err := c.ControllerName() 384 if err != nil { 385 return errors.Trace(err) 386 } 387 388 return errors.Errorf(`cannot destroy controller %q 389 390 The controller has persistent storage remaining: 391 %s 392 393 To destroy the storage, run the destroy-controller 394 command again with the "--destroy-storage" option. 395 396 To release the storage from Juju's management 397 without destroying it, use the "--release-storage" 398 option instead. The storage can then be imported 399 into another Juju model. 400 401 `, controllerName, buf.String()) 402 } 403 404 // ensureUserFriendlyErrorLog ensures that error will be logged and displayed 405 // in a user-friendly manner with readable and digestable error message. 406 func (c *destroyCommand) ensureUserFriendlyErrorLog(destroyErr error, ctx *cmd.Context, api destroyControllerAPI) error { 407 if destroyErr == nil { 408 return nil 409 } 410 if params.IsCodeOperationBlocked(destroyErr) { 411 logger.Errorf(destroyControllerBlockedMsg) 412 if api != nil { 413 models, err := api.ListBlockedModels() 414 out := &bytes.Buffer{} 415 if err == nil { 416 var info interface{} 417 info, err = block.FormatModelBlockInfo(models) 418 if err != nil { 419 return errors.Trace(err) 420 } 421 err = block.FormatTabularBlockedModels(out, info) 422 } 423 if err != nil { 424 logger.Errorf("Unable to list models: %s", err) 425 return cmd.ErrSilent 426 } 427 ctx.Infof(out.String()) 428 } 429 return cmd.ErrSilent 430 } 431 controllerName, err := c.ControllerName() 432 if err != nil { 433 return errors.Trace(err) 434 } 435 logger.Errorf(stdFailureMsg, controllerName) 436 return destroyErr 437 } 438 439 const destroyControllerBlockedMsg = `there are models with disabled commands preventing controller destruction 440 441 To enable controller destruction, please run: 442 443 juju enable-destroy-controller 444 445 ` 446 447 // TODO(axw) this should only be printed out if we couldn't 448 // connect to the controller. 449 const stdFailureMsg = `failed to destroy controller %q 450 451 If the controller is unusable, then you may run 452 453 juju kill-controller 454 455 to forcibly destroy the controller. Upon doing so, review 456 your cloud provider console for any resources that need 457 to be cleaned up. 458 459 ` 460 461 // destroyCommandBase provides common attributes and methods that both the controller 462 // destroy and controller kill commands require. 463 type destroyCommandBase struct { 464 modelcmd.ControllerCommandBase 465 assumeYes bool 466 467 // The following fields are for mocking out 468 // api behavior for testing. 469 api destroyControllerAPI 470 apierr error 471 clientapi destroyClientAPI 472 473 controllerCredentialAPIFunc newCredentialAPIFunc 474 475 environsDestroy func(string, environs.ControllerDestroyer, context.ProviderCallContext, jujuclient.ControllerStore) error 476 } 477 478 func (c *destroyCommandBase) getControllerAPI() (destroyControllerAPI, error) { 479 // Note that some tests set c.api to a non-nil value 480 // even when c.apierr is non-nil, hence the separate test. 481 if c.apierr != nil { 482 return nil, c.apierr 483 } 484 if c.api != nil { 485 return c.api, nil 486 } 487 root, err := c.NewAPIRoot() 488 if err != nil { 489 return nil, errors.Trace(err) 490 } 491 return controller.NewClient(root), nil 492 } 493 494 func (c *destroyCommand) getStorageAPI(modelName string) (storageAPI, error) { 495 if c.storageAPI != nil { 496 return c.storageAPI, nil 497 } 498 root, err := c.NewModelAPIRoot(modelName) 499 if err != nil { 500 return nil, errors.Trace(err) 501 } 502 return storage.NewClient(root), nil 503 } 504 505 // SetFlags implements Command.SetFlags. 506 func (c *destroyCommandBase) SetFlags(f *gnuflag.FlagSet) { 507 c.ControllerCommandBase.SetFlags(f) 508 f.BoolVar(&c.assumeYes, "y", false, "Do not ask for confirmation") 509 f.BoolVar(&c.assumeYes, "yes", false, "") 510 } 511 512 // Init implements Command.Init. 513 func (c *destroyCommandBase) Init(args []string) error { 514 switch len(args) { 515 case 0: 516 return errors.New("no controller specified") 517 case 1: 518 return c.SetControllerName(args[0], false) 519 default: 520 return cmd.CheckEmpty(args[1:]) 521 } 522 } 523 524 // getControllerEnviron returns the Environ for the controller model. 525 // 526 // getControllerEnviron gets the information required to get the 527 // Environ by first checking the config store, then querying the 528 // API if the information is not in the store. 529 func (c *destroyCommandBase) getControllerEnviron( 530 ctx *cmd.Context, 531 store jujuclient.ClientStore, 532 controllerName string, 533 sysAPI destroyControllerAPI, 534 ) (environs.Environ, error) { 535 // TODO: (hml) 2018-08-01 536 // We should try to destroy via the API first, from store is a 537 // fall back position. 538 env, err := c.getControllerEnvironFromStore(ctx, store, controllerName) 539 if errors.IsNotFound(err) { 540 return c.getControllerEnvironFromAPI(sysAPI, controllerName) 541 } else if err != nil { 542 return nil, errors.Annotate(err, "getting environ using bootstrap config from client store") 543 } 544 return env, nil 545 } 546 547 func (c *destroyCommandBase) getControllerEnvironFromStore( 548 ctx *cmd.Context, 549 store jujuclient.ClientStore, 550 controllerName string, 551 ) (environs.Environ, error) { 552 bootstrapConfig, params, err := modelcmd.NewGetBootstrapConfigParamsFunc( 553 ctx, store, environs.GlobalProviderRegistry(), 554 )(controllerName) 555 if err != nil { 556 return nil, errors.Trace(err) 557 } 558 provider, err := environs.Provider(bootstrapConfig.CloudType) 559 if err != nil { 560 return nil, errors.Trace(err) 561 } 562 cfg, err := provider.PrepareConfig(*params) 563 if err != nil { 564 return nil, errors.Trace(err) 565 } 566 return environs.New(environs.OpenParams{ 567 Cloud: params.Cloud, 568 Config: cfg, 569 }) 570 } 571 572 func (c *destroyCommandBase) getControllerEnvironFromAPI( 573 api destroyControllerAPI, 574 controllerName string, 575 ) (environs.Environ, error) { 576 if api == nil { 577 return nil, errors.New( 578 "unable to get bootstrap information from client store or API", 579 ) 580 } 581 attrs, err := api.ModelConfig() 582 if err != nil { 583 return nil, errors.Annotate(err, "getting model config from API") 584 } 585 cfg, err := config.New(config.NoDefaults, attrs) 586 if err != nil { 587 return nil, errors.Trace(err) 588 } 589 cloudSpec, err := api.CloudSpec(names.NewModelTag(cfg.UUID())) 590 if err != nil { 591 return nil, errors.Annotate(err, "getting cloud spec from API") 592 } 593 return environs.New(environs.OpenParams{ 594 Cloud: cloudSpec, 595 Config: cfg, 596 }) 597 } 598 599 func confirmDestruction(ctx *cmd.Context, controllerName string) error { 600 // Get confirmation from the user that they want to continue 601 fmt.Fprintf(ctx.Stdout, destroySysMsg, controllerName) 602 603 scanner := bufio.NewScanner(ctx.Stdin) 604 scanner.Scan() 605 err := scanner.Err() 606 if err != nil && err != io.EOF { 607 return errors.Annotate(err, "controller destruction aborted") 608 } 609 answer := strings.ToLower(scanner.Text()) 610 if answer != "y" && answer != "yes" { 611 return errors.New("controller destruction aborted") 612 } 613 614 return nil 615 } 616 617 // CredentialAPI defines the methods on the credential API endpoint that the 618 // destroy command might call. 619 type CredentialAPI interface { 620 InvalidateModelCredential(string) error 621 Close() error 622 } 623 624 func (c *destroyCommandBase) credentialAPIForControllerModel() (CredentialAPI, error) { 625 // Note that the api here needs to operate on a controller model itself, 626 // as the controller model's cloud credential is the controller cloud credential. 627 root, err := c.NewAPIRoot() 628 if err != nil { 629 return nil, errors.Trace(err) 630 } 631 return credentialmanager.NewClient(root), nil 632 } 633 634 type newCredentialAPIFunc func() (CredentialAPI, error) 635 636 func cloudCallContext(newAPIFunc newCredentialAPIFunc) context.ProviderCallContext { 637 callCtx := context.NewCloudCallContext() 638 callCtx.InvalidateCredentialFunc = func(reason string) error { 639 api, err := newAPIFunc() 640 if err != nil { 641 return errors.Trace(err) 642 } 643 defer api.Close() 644 return api.InvalidateModelCredential(reason) 645 } 646 return callCtx 647 }