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  }