github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/model/destroy.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for infos. 3 4 package model 5 6 import ( 7 "bytes" 8 "fmt" 9 "io" 10 "os" 11 "time" 12 13 jujuclock "github.com/juju/clock" 14 "github.com/juju/cmd" 15 "github.com/juju/errors" 16 "github.com/juju/gnuflag" 17 "github.com/juju/loggo" 18 "github.com/juju/romulus/api/budget" 19 "gopkg.in/juju/names.v2" 20 "gopkg.in/macaroon-bakery.v2-unstable/httpbakery" 21 22 "github.com/juju/juju/api/base" 23 "github.com/juju/juju/api/modelconfig" 24 "github.com/juju/juju/api/modelmanager" 25 "github.com/juju/juju/api/storage" 26 "github.com/juju/juju/apiserver/params" 27 jujucmd "github.com/juju/juju/cmd" 28 "github.com/juju/juju/cmd/juju/block" 29 rcmd "github.com/juju/juju/cmd/juju/romulus" 30 "github.com/juju/juju/cmd/modelcmd" 31 "github.com/juju/juju/cmd/output" 32 "github.com/juju/juju/core/model" 33 corestatus "github.com/juju/juju/core/status" 34 ) 35 36 const ( 37 slaUnsupported = "unsupported" 38 ) 39 40 var logger = loggo.GetLogger("juju.cmd.juju.model") 41 42 // NewDestroyCommand returns a command used to destroy a model. 43 func NewDestroyCommand() cmd.Command { 44 destroyCmd := &destroyCommand{ 45 clock: jujuclock.WallClock, 46 } 47 destroyCmd.CanClearCurrentModel = true 48 return modelcmd.Wrap( 49 destroyCmd, 50 modelcmd.WrapSkipDefaultModel, 51 modelcmd.WrapSkipModelFlags, 52 ) 53 } 54 55 // destroyCommand destroys the specified model. 56 type destroyCommand struct { 57 modelcmd.ModelCommandBase 58 59 clock jujuclock.Clock 60 61 assumeYes bool 62 timeout time.Duration 63 destroyStorage bool 64 releaseStorage bool 65 api DestroyModelAPI 66 configAPI ModelConfigAPI 67 storageAPI StorageAPI 68 } 69 70 var destroyDoc = ` 71 Destroys the specified model. This will result in the non-recoverable 72 removal of all the units operating in the model and any resources stored 73 there. Due to the irreversible nature of the command, it will prompt for 74 confirmation (unless overridden with the '-y' option) before taking any 75 action. 76 77 If there is persistent storage in any of the models managed by the 78 controller, then you must choose to either destroy or release the 79 storage, using --destroy-storage or --release-storage respectively. 80 81 Examples: 82 83 juju destroy-model test 84 juju destroy-model -y mymodel 85 juju destroy-model -y mymodel --destroy-storage 86 juju destroy-model -y mymodel --release-storage 87 88 See also: 89 destroy-controller 90 ` 91 var destroyIAASModelMsg = ` 92 WARNING! This command will destroy the %q model. 93 This includes all machines, applications, data and other resources. 94 95 Continue [y/N]? `[1:] 96 97 var destroyCAASModelMsg = ` 98 WARNING! This command will destroy the %q model. 99 This includes all containers, applications, data and other resources. 100 101 Continue [y/N]? `[1:] 102 103 // DestroyModelAPI defines the methods on the modelmanager 104 // API that the destroy command calls. It is exported for mocking in tests. 105 type DestroyModelAPI interface { 106 Close() error 107 BestAPIVersion() int 108 DestroyModel(tag names.ModelTag, destroyStorage *bool) error 109 ModelStatus(models ...names.ModelTag) ([]base.ModelStatus, error) 110 } 111 112 // ModelConfigAPI defines the methods on the modelconfig 113 // API that the destroy command calls. It is exported for mocking in tests. 114 type ModelConfigAPI interface { 115 Close() error 116 SLALevel() (string, error) 117 } 118 119 // Info implements Command.Info. 120 func (c *destroyCommand) Info() *cmd.Info { 121 return jujucmd.Info(&cmd.Info{ 122 Name: "destroy-model", 123 Args: "[<controller name>:]<model name>", 124 Purpose: "Terminate all machines/containers and resources for a non-controller model.", 125 Doc: destroyDoc, 126 }) 127 } 128 129 // SetFlags implements Command.SetFlags. 130 func (c *destroyCommand) SetFlags(f *gnuflag.FlagSet) { 131 c.ModelCommandBase.SetFlags(f) 132 f.BoolVar(&c.assumeYes, "y", false, "Do not prompt for confirmation") 133 f.BoolVar(&c.assumeYes, "yes", false, "") 134 f.DurationVar(&c.timeout, "t", 30*time.Minute, "Timeout before model destruction is aborted") 135 f.DurationVar(&c.timeout, "timeout", 30*time.Minute, "") 136 f.BoolVar(&c.destroyStorage, "destroy-storage", false, "Destroy all storage instances in the model") 137 f.BoolVar(&c.releaseStorage, "release-storage", false, "Release all storage instances from the model, and management of the controller, without destroying them") 138 } 139 140 // Init implements Command.Init. 141 func (c *destroyCommand) Init(args []string) error { 142 if c.destroyStorage && c.releaseStorage { 143 return errors.New("--destroy-storage and --release-storage cannot both be specified") 144 } 145 switch len(args) { 146 case 0: 147 return errors.New("no model specified") 148 case 1: 149 return c.SetModelName(args[0], false) 150 default: 151 return cmd.CheckEmpty(args[1:]) 152 } 153 } 154 155 func (c *destroyCommand) getAPI() (DestroyModelAPI, error) { 156 if c.api != nil { 157 return c.api, nil 158 } 159 root, err := c.NewControllerAPIRoot() 160 if err != nil { 161 return nil, errors.Trace(err) 162 } 163 return modelmanager.NewClient(root), nil 164 } 165 166 func (c *destroyCommand) getModelConfigAPI() (ModelConfigAPI, error) { 167 if c.configAPI != nil { 168 return c.configAPI, nil 169 } 170 root, err := c.NewAPIRoot() 171 if err != nil { 172 return nil, errors.Trace(err) 173 } 174 return modelconfig.NewClient(root), nil 175 } 176 177 func (c *destroyCommand) getStorageAPI() (StorageAPI, error) { 178 if c.storageAPI != nil { 179 return c.storageAPI, nil 180 } 181 root, err := c.NewAPIRoot() 182 if err != nil { 183 return nil, errors.Trace(err) 184 } 185 return storage.NewClient(root), nil 186 } 187 188 // Run implements Command.Run 189 func (c *destroyCommand) Run(ctx *cmd.Context) error { 190 store := c.ClientStore() 191 controllerName, err := c.ControllerName() 192 if err != nil { 193 return errors.Trace(err) 194 } 195 196 controllerDetails, err := store.ControllerByName(controllerName) 197 if err != nil { 198 return errors.Annotate(err, "cannot read controller details") 199 } 200 modelName, modelDetails, err := c.ModelDetails() 201 if err != nil { 202 return errors.Trace(err) 203 } 204 205 if modelDetails.ModelUUID == controllerDetails.ControllerUUID { 206 return errors.Errorf("%q is a controller; use 'juju destroy-controller' to destroy it", modelName) 207 } 208 209 if !c.assumeYes { 210 modelType, err := c.ModelType() 211 if err != nil { 212 return errors.Trace(err) 213 } 214 msg := destroyIAASModelMsg 215 if modelType == model.CAAS { 216 msg = destroyCAASModelMsg 217 } 218 fmt.Fprintf(ctx.Stdout, msg, modelName) 219 220 if err := jujucmd.UserConfirmYes(ctx); err != nil { 221 return errors.Annotate(err, "model destruction") 222 } 223 } 224 225 // Attempt to connect to the API. If we can't, fail the destroy. 226 api, err := c.getAPI() 227 if err != nil { 228 return errors.Annotate(err, "cannot connect to API") 229 } 230 defer api.Close() 231 232 configAPI, err := c.getModelConfigAPI() 233 if err != nil { 234 return errors.Annotate(err, "cannot connect to API") 235 } 236 defer configAPI.Close() 237 238 // Check if the model has an SLA set. 239 slaIsSet := false 240 slaLevel, err := configAPI.SLALevel() 241 if err == nil { 242 slaIsSet = slaLevel != "" && slaLevel != slaUnsupported 243 } else { 244 logger.Debugf("could not determine model SLA level: %v", err) 245 } 246 247 if api.BestAPIVersion() < 4 { 248 // Versions before 4 support only destroying the storage, 249 // and will not raise an error if there is storage in the 250 // controller. Force the user to specify up-front. 251 if c.releaseStorage { 252 return errors.New("this juju controller only supports destroying storage") 253 } 254 if !c.destroyStorage { 255 storageAPI, err := c.getStorageAPI() 256 if err != nil { 257 return errors.Trace(err) 258 } 259 defer storageAPI.Close() 260 261 storage, err := storageAPI.ListStorageDetails() 262 if err != nil { 263 return errors.Trace(err) 264 } 265 if len(storage) > 0 { 266 return errors.Errorf(`cannot destroy model %q 267 268 Destroying this model will destroy the storage, but you 269 have not indicated that you want to do that. 270 271 Please run the the command again with --destroy-storage 272 to confirm that you want to destroy the storage along 273 with the model. 274 275 If instead you want to keep the storage, you must first 276 upgrade the controller to version 2.3 or greater. 277 278 `, modelName) 279 } 280 c.destroyStorage = true 281 } 282 } 283 284 // Attempt to destroy the model. 285 ctx.Infof("Destroying model") 286 var destroyStorage *bool 287 if c.destroyStorage || c.releaseStorage { 288 destroyStorage = &c.destroyStorage 289 } 290 modelTag := names.NewModelTag(modelDetails.ModelUUID) 291 if err := api.DestroyModel(modelTag, destroyStorage); err != nil { 292 return c.handleError( 293 modelTag, modelName, api, 294 errors.Annotate(err, "cannot destroy model"), 295 ) 296 } 297 298 // Wait for model to be destroyed. 299 if err := waitForModelDestroyed( 300 ctx, api, 301 names.NewModelTag(modelDetails.ModelUUID), 302 c.timeout, 303 c.clock, 304 ); err != nil { 305 return err 306 } 307 308 // Check if the model has an sla auth. 309 if slaIsSet { 310 err = c.removeModelBudget(modelDetails.ModelUUID) 311 if err != nil { 312 ctx.Warningf("model allocation not removed: %v", err) 313 } 314 } 315 316 c.RemoveModelFromClientStore(store, controllerName, modelName) 317 return nil 318 } 319 320 func (c *destroyCommand) removeModelBudget(uuid string) error { 321 bakeryClient, err := c.BakeryClient() 322 if err != nil { 323 return errors.Trace(err) 324 } 325 326 budgetAPIRoot, err := rcmd.GetMeteringURLForModelCmd(&c.ModelCommandBase) 327 if err != nil { 328 return errors.Trace(err) 329 } 330 budgetClient, err := getBudgetAPIClient(budgetAPIRoot, bakeryClient) 331 if err != nil { 332 return errors.Trace(err) 333 } 334 335 resp, err := budgetClient.DeleteBudget(uuid) 336 if err != nil { 337 return errors.Trace(err) 338 } 339 if resp != "" { 340 logger.Infof(resp) 341 } 342 return nil 343 } 344 345 type modelData struct { 346 machineCount int 347 applicationCount int 348 volumeCount int 349 filesystemCount int 350 errorCount int 351 } 352 353 func waitForModelDestroyed( 354 ctx *cmd.Context, 355 api DestroyModelAPI, 356 tag names.ModelTag, 357 timeout time.Duration, 358 clock jujuclock.Clock, 359 ) error { 360 361 interrupted := make(chan os.Signal, 1) 362 defer close(interrupted) 363 ctx.InterruptNotify(interrupted) 364 defer ctx.StopInterruptNotify(interrupted) 365 366 var data *modelData 367 var erroredStatuses modelResourceErrorStatusSummary 368 369 printErrors := func() { 370 erroredStatuses.PrettyPrint(ctx.Stdout) 371 } 372 373 // no wait for 1st time. 374 intervalSeconds := 0 * time.Second 375 timeoutAfter := clock.After(timeout) 376 for { 377 select { 378 case <-interrupted: 379 ctx.Infof("ctrl+c detected, aborting...") 380 printErrors() 381 return cmd.ErrSilent 382 case <-timeoutAfter: 383 printErrors() 384 return errors.Timeoutf("timeout after %v", timeout) 385 case <-clock.After(intervalSeconds): 386 data, erroredStatuses = getModelStatus(ctx, api, tag) 387 if data == nil { 388 // model has been destroyed successfully. 389 return nil 390 } 391 ctx.Infof(formatDestroyModelInfo(data) + "...") 392 intervalSeconds = 2 * time.Second 393 } 394 } 395 } 396 397 type modelResourceErrorStatus struct { 398 ID, Message string 399 } 400 401 type modelResourceErrorStatusSummary struct { 402 Machines []modelResourceErrorStatus 403 Filesystems []modelResourceErrorStatus 404 Volumes []modelResourceErrorStatus 405 } 406 407 func (s modelResourceErrorStatusSummary) Count() int { 408 return len(s.Machines) + len(s.Filesystems) + len(s.Volumes) 409 } 410 411 func (s modelResourceErrorStatusSummary) PrettyPrint(writer io.Writer) error { 412 if s.Count() == 0 { 413 return nil 414 } 415 416 tw := output.TabWriter(writer) 417 w := output.Wrapper{tw} 418 w.Println(` 419 The following errors were encountered during destroying the model. 420 You can fix the problem causing the errors and run destroy-model again. 421 `) 422 w.Println("Resource", "Id", "Message") 423 for _, resources := range []map[string][]modelResourceErrorStatus{ 424 {"Machine": s.Machines}, 425 {"Filesystem": s.Filesystems}, 426 {"Volume": s.Volumes}, 427 } { 428 for k, v := range resources { 429 resourceType := k 430 for _, r := range v { 431 w.Println(resourceType, r.ID, r.Message) 432 resourceType = "" 433 } 434 } 435 } 436 tw.Flush() 437 return nil 438 } 439 440 func getModelStatus(ctx *cmd.Context, api DestroyModelAPI, tag names.ModelTag) (*modelData, modelResourceErrorStatusSummary) { 441 var erroredStatuses modelResourceErrorStatusSummary 442 443 status, err := api.ModelStatus(tag) 444 if err == nil && len(status) == 1 && status[0].Error != nil { 445 // In 2.2 an error of one model generate an error for the entire request, 446 // in 2.3 this was corrected to just be an error for the requested model. 447 err = status[0].Error 448 } 449 if err != nil { 450 if params.IsCodeNotFound(err) { 451 ctx.Infof("Model destroyed.") 452 } else { 453 ctx.Infof("Unable to get the model status from the API: %v.", err) 454 } 455 return nil, erroredStatuses 456 } 457 isError := func(s string) bool { 458 return corestatus.Error.Matches(corestatus.Status(s)) 459 } 460 for _, s := range status { 461 for _, v := range s.Machines { 462 if isError(v.Status) { 463 erroredStatuses.Machines = append(erroredStatuses.Machines, modelResourceErrorStatus{ 464 ID: v.Id, 465 Message: v.Message, 466 }) 467 } 468 } 469 for _, v := range s.Filesystems { 470 if isError(v.Status) { 471 erroredStatuses.Filesystems = append(erroredStatuses.Filesystems, modelResourceErrorStatus{ 472 ID: v.Id, 473 Message: v.Message, 474 }) 475 } 476 } 477 for _, v := range s.Volumes { 478 if isError(v.Status) { 479 erroredStatuses.Volumes = append(erroredStatuses.Volumes, modelResourceErrorStatus{ 480 ID: v.Id, 481 Message: v.Message, 482 }) 483 } 484 } 485 } 486 487 if l := len(status); l != 1 { 488 ctx.Infof("error finding model status: expected one result, got %d", l) 489 return nil, erroredStatuses 490 } 491 return &modelData{ 492 machineCount: status[0].HostedMachineCount, 493 applicationCount: status[0].ApplicationCount, 494 volumeCount: len(status[0].Volumes), 495 filesystemCount: len(status[0].Filesystems), 496 errorCount: erroredStatuses.Count(), 497 }, erroredStatuses 498 } 499 500 func formatDestroyModelInfo(data *modelData) string { 501 out := "Waiting on model to be removed" 502 if data.errorCount > 0 { 503 // always shows errorCount even if no machines and applications left. 504 out += fmt.Sprintf(", %d error(s)", data.errorCount) 505 } 506 if data.machineCount == 0 && data.applicationCount == 0 { 507 return out 508 } 509 if data.machineCount > 0 { 510 out += fmt.Sprintf(", %d machine(s)", data.machineCount) 511 } 512 if data.applicationCount > 0 { 513 out += fmt.Sprintf(", %d application(s)", data.applicationCount) 514 } 515 if data.volumeCount > 0 { 516 out += fmt.Sprintf(", %d volume(s)", data.volumeCount) 517 } 518 if data.filesystemCount > 0 { 519 out += fmt.Sprintf(", %d filesystems(s)", data.filesystemCount) 520 } 521 return out 522 } 523 524 func (c *destroyCommand) handleError( 525 modelTag names.ModelTag, 526 modelName string, 527 api DestroyModelAPI, 528 err error, 529 ) error { 530 if params.IsCodeOperationBlocked(err) { 531 return block.ProcessBlockedError(err, block.BlockDestroy) 532 } 533 if params.IsCodeHasPersistentStorage(err) { 534 return handlePersistentStorageError(modelTag, modelName, api) 535 } 536 logger.Errorf(`failed to destroy model %q`, modelName) 537 return err 538 } 539 540 func handlePersistentStorageError( 541 modelTag names.ModelTag, 542 modelName string, 543 api DestroyModelAPI, 544 ) error { 545 modelStatuses, err := api.ModelStatus(modelTag) 546 if err != nil { 547 return errors.Annotate(err, "getting model status") 548 } 549 if l := len(modelStatuses); l != 1 { 550 return errors.Errorf("error finding model status: expected one result, got %d", l) 551 } 552 modelStatus := modelStatuses[0] 553 if modelStatus.Error != nil { 554 if errors.IsNotFound(modelStatus.Error) { 555 // This most likely occurred because a model was 556 // destroyed half-way through the call. 557 return nil 558 } 559 return errors.Annotate(err, "getting model status") 560 } 561 562 var buf bytes.Buffer 563 var persistentVolumes, persistentFilesystems int 564 for _, v := range modelStatus.Volumes { 565 if v.Detachable { 566 persistentVolumes++ 567 } 568 } 569 for _, f := range modelStatus.Filesystems { 570 if f.Detachable { 571 persistentFilesystems++ 572 } 573 } 574 if n := persistentVolumes; n > 0 { 575 fmt.Fprintf(&buf, "%d volume", n) 576 if n > 1 { 577 buf.WriteRune('s') 578 } 579 if persistentFilesystems > 0 { 580 buf.WriteString(" and ") 581 } 582 } 583 if n := persistentFilesystems; n > 0 { 584 fmt.Fprintf(&buf, "%d filesystem", n) 585 if n > 1 { 586 buf.WriteRune('s') 587 } 588 } 589 590 return errors.Errorf(`cannot destroy model %q 591 592 The model has persistent storage remaining: 593 %s 594 595 To destroy the storage, run the destroy-model 596 command again with the "--destroy-storage" option. 597 598 To release the storage from Juju's management 599 without destroying it, use the "--release-storage" 600 option instead. The storage can then be imported 601 into another Juju model. 602 603 `, modelName, buf.String()) 604 } 605 606 var getBudgetAPIClient = getBudgetAPIClientImpl 607 608 func getBudgetAPIClientImpl(apiRoot string, bakeryClient *httpbakery.Client) (BudgetAPIClient, error) { 609 return budget.NewClient(budget.APIRoot(apiRoot), budget.HTTPClient(bakeryClient)) 610 } 611 612 // BudgetAPIClient defines the budget API client interface. 613 type BudgetAPIClient interface { 614 DeleteBudget(string) (string, error) 615 } 616 617 // StorageAPI defines the storage client API interface. 618 type StorageAPI interface { 619 Close() error 620 ListStorageDetails() ([]params.StorageDetails, error) 621 }