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  }