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  }