github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/cmd/juju/controller/kill.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  	"fmt"
     8  	"time"
     9  
    10  	"github.com/juju/clock"
    11  	"github.com/juju/cmd"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/gnuflag"
    14  
    15  	"github.com/juju/juju/api/controller"
    16  	"github.com/juju/juju/api/credentialmanager"
    17  	"github.com/juju/juju/apiserver/common"
    18  	jujucmd "github.com/juju/juju/cmd"
    19  	"github.com/juju/juju/cmd/modelcmd"
    20  	"github.com/juju/juju/environs"
    21  	"github.com/juju/juju/environs/config"
    22  )
    23  
    24  const killDoc = `
    25  Forcibly destroy the specified controller.  If the API server is accessible,
    26  this command will attempt to destroy the controller model and all hosted models
    27  and their resources.
    28  
    29  If the API server is unreachable, the machines of the controller model will be
    30  destroyed through the cloud provisioner.  If there are additional machines,
    31  including machines within hosted models, these machines will not be destroyed
    32  and will never be reconnected to the Juju controller being destroyed.
    33  
    34  The normal process of killing the controller will involve watching the hosted
    35  models as they are brought down in a controlled manner. If for some reason the
    36  models do not stop cleanly, there is a default five minute timeout. If no change
    37  in the model state occurs for the duration of this timeout, the command will
    38  stop watching and destroy the models directly through the cloud provider.
    39  
    40  See also:
    41      destroy-controller
    42      unregister
    43  `
    44  
    45  // NewKillCommand returns a command to kill a controller. Killing is a
    46  // forceful destroy.
    47  func NewKillCommand() modelcmd.Command {
    48  	cmd := killCommand{clock: clock.WallClock}
    49  	cmd.environsDestroy = environs.Destroy
    50  	return wrapKillCommand(&cmd)
    51  }
    52  
    53  // wrapKillCommand provides the common wrapping used by tests and
    54  // the default NewKillCommand above.
    55  func wrapKillCommand(kill *killCommand) modelcmd.Command {
    56  	return modelcmd.WrapController(
    57  		kill,
    58  		modelcmd.WrapControllerSkipControllerFlags,
    59  		modelcmd.WrapControllerSkipDefaultController,
    60  	)
    61  }
    62  
    63  // killCommand kills the specified controller.
    64  type killCommand struct {
    65  	destroyCommandBase
    66  
    67  	clock   clock.Clock
    68  	timeout time.Duration
    69  }
    70  
    71  // SetFlags implements Command.SetFlags.
    72  func (c *killCommand) SetFlags(f *gnuflag.FlagSet) {
    73  	c.destroyCommandBase.SetFlags(f)
    74  	f.Var(newDurationValue(time.Minute*5, &c.timeout), "t", "Timeout before direct destruction")
    75  	f.Var(newDurationValue(time.Minute*5, &c.timeout), "timeout", "")
    76  }
    77  
    78  // Info implements Command.Info.
    79  func (c *killCommand) Info() *cmd.Info {
    80  	return jujucmd.Info(&cmd.Info{
    81  		Name:    "kill-controller",
    82  		Args:    "<controller name>",
    83  		Purpose: "Forcibly terminate all machines and other associated resources for a Juju controller.",
    84  		Doc:     killDoc,
    85  	})
    86  }
    87  
    88  // Init implements Command.Init.
    89  func (c *killCommand) Init(args []string) error {
    90  	return c.destroyCommandBase.Init(args)
    91  }
    92  
    93  var errConnTimedOut = errors.New("open connection timed out")
    94  
    95  // Run implements Command.Run
    96  func (c *killCommand) Run(ctx *cmd.Context) error {
    97  	controllerName, err := c.ControllerName()
    98  	if err != nil {
    99  		return errors.Trace(err)
   100  	}
   101  	store := c.ClientStore()
   102  	if !c.assumeYes {
   103  		if err := confirmDestruction(ctx, controllerName); err != nil {
   104  			return err
   105  		}
   106  	}
   107  
   108  	// Attempt to connect to the API.
   109  	api, err := c.getControllerAPIWithTimeout(10 * time.Second)
   110  	switch errors.Cause(err) {
   111  	case nil:
   112  		defer api.Close()
   113  	case common.ErrPerm:
   114  		return errors.Annotate(err, "cannot destroy controller")
   115  	default:
   116  		ctx.Infof("Unable to open API: %s\n", err)
   117  	}
   118  
   119  	// Obtain controller environ so we can clean up afterwards.
   120  	controllerEnviron, err := c.getControllerEnviron(ctx, store, controllerName, api)
   121  	if err != nil {
   122  		return errors.Annotate(err, "getting controller environ")
   123  	}
   124  	cloudCallCtx := cloudCallContext(c.controllerCredentialAPIFunc)
   125  	// If we were unable to connect to the API, just destroy the controller through
   126  	// the environs interface.
   127  	if api == nil {
   128  		ctx.Infof("Unable to connect to the API server, destroying through provider")
   129  		return c.environsDestroy(controllerName, controllerEnviron, cloudCallCtx, store)
   130  	}
   131  
   132  	// Attempt to destroy the controller and all models and storage.
   133  	destroyStorage := true
   134  	err = api.DestroyController(controller.DestroyControllerParams{
   135  		DestroyModels:  true,
   136  		DestroyStorage: &destroyStorage,
   137  	})
   138  	if err != nil {
   139  		ctx.Infof("Unable to destroy controller through the API: %s\nDestroying through provider", err)
   140  		return c.environsDestroy(controllerName, controllerEnviron, cloudCallCtx, store)
   141  	}
   142  
   143  	ctx.Infof("Destroying controller %q\nWaiting for resources to be reclaimed", controllerName)
   144  
   145  	uuid := controllerEnviron.Config().UUID()
   146  	if err := c.WaitForModels(ctx, api, uuid); err != nil {
   147  		c.DirectDestroyRemaining(ctx, api)
   148  	}
   149  	return c.environsDestroy(controllerName, controllerEnviron, cloudCallCtx, store)
   150  }
   151  
   152  func (c *killCommand) getControllerAPIWithTimeout(timeout time.Duration) (destroyControllerAPI, error) {
   153  	type result struct {
   154  		c   destroyControllerAPI
   155  		err error
   156  	}
   157  	resultc := make(chan result)
   158  	done := make(chan struct{})
   159  	go func() {
   160  		api, err := c.getControllerAPI()
   161  		select {
   162  		case resultc <- result{api, err}:
   163  		case <-done:
   164  			if api != nil {
   165  				api.Close()
   166  			}
   167  		}
   168  	}()
   169  	select {
   170  	case r := <-resultc:
   171  		return r.c, r.err
   172  	case <-c.clock.After(timeout):
   173  		close(done)
   174  		return nil, errConnTimedOut
   175  	}
   176  }
   177  
   178  // DirectDestroyRemaining will attempt to directly destroy any remaining
   179  // models that have machines left.
   180  func (c *killCommand) DirectDestroyRemaining(ctx *cmd.Context, api destroyControllerAPI) {
   181  	hasErrors := false
   182  	hostedConfig, err := api.HostedModelConfigs()
   183  	if err != nil {
   184  		hasErrors = true
   185  		logger.Errorf("unable to retrieve hosted model config: %v", err)
   186  	}
   187  	for _, model := range hostedConfig {
   188  		if model.Error != nil {
   189  			// We can only display model name here since
   190  			// the error coming from api can be anything
   191  			// including the parsing of the model owner tag.
   192  			// Only model name is guaranteed to be set in the result
   193  			// when an error is returned.
   194  			hasErrors = true
   195  			logger.Errorf("could not kill %s directly: %v", model.Name, model.Error)
   196  			continue
   197  		}
   198  		ctx.Infof("Killing %s/%s directly", model.Owner.Id(), model.Name)
   199  		cfg, err := config.New(config.NoDefaults, model.Config)
   200  		if err != nil {
   201  			logger.Errorf(err.Error())
   202  			hasErrors = true
   203  			continue
   204  		}
   205  		p, err := environs.Provider(model.CloudSpec.Type)
   206  		if err != nil {
   207  			logger.Errorf(err.Error())
   208  			hasErrors = true
   209  			continue
   210  		}
   211  		// TODO(caas) - only cloud providers support Destroy()
   212  		if cloudProvider, ok := p.(environs.CloudEnvironProvider); ok {
   213  			env, err := environs.Open(cloudProvider, environs.OpenParams{
   214  				Cloud:  model.CloudSpec,
   215  				Config: cfg,
   216  			})
   217  			if err != nil {
   218  				logger.Errorf(err.Error())
   219  				hasErrors = true
   220  				continue
   221  			}
   222  			cloudCallCtx := cloudCallContext(c.credentialAPIFunctionForModel(model.Name))
   223  			if err := env.Destroy(cloudCallCtx); err != nil {
   224  				logger.Errorf(err.Error())
   225  				hasErrors = true
   226  				continue
   227  			}
   228  		}
   229  		ctx.Infof("  done")
   230  	}
   231  	if hasErrors {
   232  		logger.Errorf("there were problems destroying some models, manual intervention may be necessary to ensure resources are released")
   233  	} else {
   234  		ctx.Infof("All hosted models destroyed, cleaning up controller machines")
   235  	}
   236  }
   237  
   238  func (c *killCommand) credentialAPIFunctionForModel(modelName string) newCredentialAPIFunc {
   239  	f := func(api CredentialAPI, err error) newCredentialAPIFunc {
   240  		return func() (CredentialAPI, error) {
   241  			return api, err
   242  		}
   243  	}
   244  	root, err := c.NewModelAPIRoot(modelName)
   245  	if err != nil {
   246  		return f(nil, errors.Trace(err))
   247  	}
   248  	return f(credentialmanager.NewClient(root), nil)
   249  }
   250  
   251  // WaitForModels will wait for the models to bring themselves down nicely.
   252  // It will return the UUIDs of any models that need to be removed forceably.
   253  func (c *killCommand) WaitForModels(ctx *cmd.Context, api destroyControllerAPI, uuid string) error {
   254  	thirtySeconds := (time.Second * 30)
   255  	updateStatus := newTimedStatusUpdater(ctx, api, uuid, c.clock)
   256  
   257  	envStatus := updateStatus(0)
   258  	lastStatus := envStatus.controller
   259  	lastChange := c.clock.Now().Truncate(time.Second)
   260  	deadline := lastChange.Add(c.timeout)
   261  	// Check for both undead models and live machines, as machines may be
   262  	// in the controller model.
   263  	for ; hasUnreclaimedResources(envStatus) && (deadline.After(c.clock.Now())); envStatus = updateStatus(5 * time.Second) {
   264  		now := c.clock.Now().Truncate(time.Second)
   265  		if envStatus.controller != lastStatus {
   266  			lastStatus = envStatus.controller
   267  			lastChange = now
   268  			deadline = lastChange.Add(c.timeout)
   269  		}
   270  		timeSinceLastChange := now.Sub(lastChange)
   271  		timeUntilDestruction := deadline.Sub(now)
   272  		warning := ""
   273  		// We want to show the warning if it has been more than 30 seconds since
   274  		// the last change, or we are within 30 seconds of our timeout.
   275  		if timeSinceLastChange > thirtySeconds || timeUntilDestruction < thirtySeconds {
   276  			warning = fmt.Sprintf(", will kill machines directly in %s", timeUntilDestruction)
   277  		}
   278  		ctx.Infof("%s%s", fmtCtrStatus(envStatus.controller), warning)
   279  		for _, modelStatus := range envStatus.models {
   280  			ctx.Verbosef(fmtModelStatus(modelStatus))
   281  		}
   282  	}
   283  	if hasUnreclaimedResources(envStatus) {
   284  		return errors.New("timed out")
   285  	} else {
   286  		ctx.Infof("All hosted models reclaimed, cleaning up controller machines")
   287  	}
   288  	return nil
   289  }
   290  
   291  type durationValue time.Duration
   292  
   293  func newDurationValue(value time.Duration, p *time.Duration) *durationValue {
   294  	*p = value
   295  	return (*durationValue)(p)
   296  }
   297  
   298  func (d *durationValue) Set(s string) error {
   299  	v, err := time.ParseDuration(s)
   300  	if err != nil {
   301  		return err
   302  	}
   303  	*d = durationValue(v)
   304  	return err
   305  }
   306  
   307  func (d *durationValue) String() string { return (*time.Duration)(d).String() }