github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/cmd/juju/commands/bootstrap.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package commands
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  	"strings"
    10  	"time"
    11  
    12  	"github.com/juju/cmd"
    13  	"github.com/juju/errors"
    14  	"github.com/juju/utils"
    15  	"github.com/juju/utils/featureflag"
    16  	"gopkg.in/juju/charm.v5"
    17  	"launchpad.net/gnuflag"
    18  
    19  	apiblock "github.com/juju/juju/api/block"
    20  	"github.com/juju/juju/apiserver"
    21  	"github.com/juju/juju/cmd/envcmd"
    22  	"github.com/juju/juju/cmd/juju/block"
    23  	"github.com/juju/juju/constraints"
    24  	"github.com/juju/juju/environs"
    25  	"github.com/juju/juju/environs/bootstrap"
    26  	"github.com/juju/juju/environs/configstore"
    27  	"github.com/juju/juju/feature"
    28  	"github.com/juju/juju/instance"
    29  	"github.com/juju/juju/juju"
    30  	"github.com/juju/juju/juju/osenv"
    31  	"github.com/juju/juju/network"
    32  	"github.com/juju/juju/provider"
    33  	"github.com/juju/juju/version"
    34  )
    35  
    36  // provisionalProviders is the names of providers that are hidden behind
    37  // feature flags.
    38  var provisionalProviders = map[string]string{
    39  	"cloudsigma": feature.CloudSigma,
    40  	"vsphere":    feature.VSphereProvider,
    41  }
    42  
    43  const bootstrapDoc = `
    44  bootstrap starts a new environment of the current type (it will return an error
    45  if the environment has already been bootstrapped).  Bootstrapping an environment
    46  will provision a new machine in the environment and run the juju state server on
    47  that machine.
    48  
    49  If constraints are specified in the bootstrap command, they will apply to the
    50  machine provisioned for the juju state server.  They will also be set as default
    51  constraints on the environment for all future machines, exactly as if the
    52  constraints were set with juju set-constraints.
    53  
    54  It is possible to override constraints and the automatic machine selection
    55  algorithm by using the "--to" flag. The value associated with "--to" is a
    56  "placement directive", which tells Juju how to identify the first machine to use.
    57  For more information on placement directives, see "juju help placement".
    58  
    59  Bootstrap initialises the cloud environment synchronously and displays information
    60  about the current installation steps.  The time for bootstrap to complete varies
    61  across cloud providers from a few seconds to several minutes.  Once bootstrap has
    62  completed, you can run other juju commands against your environment. You can change
    63  the default timeout and retry delays used during the bootstrap by changing the
    64  following settings in your environments.yaml (all values represent number of seconds):
    65  
    66      # How long to wait for a connection to the state server.
    67      bootstrap-timeout: 600 # default: 10 minutes
    68      # How long to wait between connection attempts to a state server address.
    69      bootstrap-retry-delay: 5 # default: 5 seconds
    70      # How often to refresh state server addresses from the API server.
    71      bootstrap-addresses-delay: 10 # default: 10 seconds
    72  
    73  Private clouds may need to specify their own custom image metadata, and
    74  possibly upload Juju tools to cloud storage if no outgoing Internet access is
    75  available. In this case, use the --metadata-source parameter to point
    76  bootstrap to a local directory from which to upload tools and/or image
    77  metadata.
    78  
    79  If agent-version is specifed, this is the default tools version to use when running the Juju agents.
    80  Only the numeric version is relevant. To enable ease of scripting, the full binary version
    81  is accepted (eg 1.24.4-trusty-amd64) but only the numeric version (eg 1.24.4) is used.
    82  An alias for bootstrapping Juju with the exact same version as the client is to use the
    83  --no-auto-upgrade parameter.
    84  
    85  See Also:
    86     juju help switch
    87     juju help constraints
    88     juju help set-constraints
    89     juju help placement
    90  `
    91  
    92  // BootstrapCommand is responsible for launching the first machine in a juju
    93  // environment, and setting up everything necessary to continue working.
    94  type BootstrapCommand struct {
    95  	envcmd.EnvCommandBase
    96  	Constraints           constraints.Value
    97  	UploadTools           bool
    98  	Series                []string
    99  	seriesOld             []string
   100  	MetadataSource        string
   101  	Placement             string
   102  	KeepBrokenEnvironment bool
   103  	NoAutoUpgrade         bool
   104  	AgentVersionParam     string
   105  	AgentVersion          *version.Number
   106  }
   107  
   108  func (c *BootstrapCommand) Info() *cmd.Info {
   109  	return &cmd.Info{
   110  		Name:    "bootstrap",
   111  		Purpose: "start up an environment from scratch",
   112  		Doc:     bootstrapDoc,
   113  	}
   114  }
   115  
   116  func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) {
   117  	f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "set environment constraints")
   118  	f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools before bootstrapping")
   119  	f.Var(newSeriesValue(nil, &c.Series), "upload-series", "upload tools for supplied comma-separated series list (OBSOLETE)")
   120  	f.Var(newSeriesValue(nil, &c.seriesOld), "series", "see --upload-series (OBSOLETE)")
   121  	f.StringVar(&c.MetadataSource, "metadata-source", "", "local path to use as tools and/or metadata source")
   122  	f.StringVar(&c.Placement, "to", "", "a placement directive indicating an instance to bootstrap")
   123  	f.BoolVar(&c.KeepBrokenEnvironment, "keep-broken", false, "do not destroy the environment if bootstrap fails")
   124  	f.BoolVar(&c.NoAutoUpgrade, "no-auto-upgrade", false, "do not upgrade to newer tools on first bootstrap")
   125  	f.StringVar(&c.AgentVersionParam, "agent-version", "", "the version of tools to initially use for Juju agents")
   126  }
   127  
   128  func (c *BootstrapCommand) Init(args []string) (err error) {
   129  	if len(c.Series) > 0 && !c.UploadTools {
   130  		return fmt.Errorf("--upload-series requires --upload-tools")
   131  	}
   132  	if len(c.seriesOld) > 0 && !c.UploadTools {
   133  		return fmt.Errorf("--series requires --upload-tools")
   134  	}
   135  	if len(c.Series) > 0 && len(c.seriesOld) > 0 {
   136  		return fmt.Errorf("--upload-series and --series can't be used together")
   137  	}
   138  	if c.AgentVersionParam != "" && c.UploadTools {
   139  		return fmt.Errorf("--agent-version and --upload-tools can't be used together")
   140  	}
   141  	if c.AgentVersionParam != "" && c.NoAutoUpgrade {
   142  		return fmt.Errorf("--agent-version and --no-auto-upgrade can't be used together")
   143  	}
   144  
   145  	// Parse the placement directive. Bootstrap currently only
   146  	// supports provider-specific placement directives.
   147  	if c.Placement != "" {
   148  		_, err = instance.ParsePlacement(c.Placement)
   149  		if err != instance.ErrPlacementScopeMissing {
   150  			// We only support unscoped placement directives for bootstrap.
   151  			return fmt.Errorf("unsupported bootstrap placement directive %q", c.Placement)
   152  		}
   153  	}
   154  	if c.NoAutoUpgrade {
   155  		vers := version.Current.Number
   156  		c.AgentVersion = &vers
   157  	} else if c.AgentVersionParam != "" {
   158  		if vers, err := version.ParseBinary(c.AgentVersionParam); err == nil {
   159  			c.AgentVersion = &vers.Number
   160  		} else if vers, err := version.Parse(c.AgentVersionParam); err == nil {
   161  			c.AgentVersion = &vers
   162  		} else {
   163  			return err
   164  		}
   165  	}
   166  	if c.AgentVersion != nil && (c.AgentVersion.Major != version.Current.Major || c.AgentVersion.Minor != version.Current.Minor) {
   167  		return fmt.Errorf("requested agent version major.minor mismatch")
   168  	}
   169  	return cmd.CheckEmpty(args)
   170  }
   171  
   172  type seriesValue struct {
   173  	*cmd.StringsValue
   174  }
   175  
   176  // newSeriesValue is used to create the type passed into the gnuflag.FlagSet Var function.
   177  func newSeriesValue(defaultValue []string, target *[]string) *seriesValue {
   178  	v := seriesValue{(*cmd.StringsValue)(target)}
   179  	*(v.StringsValue) = defaultValue
   180  	return &v
   181  }
   182  
   183  // Implements gnuflag.Value Set.
   184  func (v *seriesValue) Set(s string) error {
   185  	if err := v.StringsValue.Set(s); err != nil {
   186  		return err
   187  	}
   188  	for _, name := range *(v.StringsValue) {
   189  		if !charm.IsValidSeries(name) {
   190  			v.StringsValue = nil
   191  			return fmt.Errorf("invalid series name %q", name)
   192  		}
   193  	}
   194  	return nil
   195  }
   196  
   197  // bootstrap functionality that Run calls to support cleaner testing
   198  type BootstrapInterface interface {
   199  	EnsureNotBootstrapped(env environs.Environ) error
   200  	Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error
   201  }
   202  
   203  type bootstrapFuncs struct{}
   204  
   205  func (b bootstrapFuncs) EnsureNotBootstrapped(env environs.Environ) error {
   206  	return bootstrap.EnsureNotBootstrapped(env)
   207  }
   208  
   209  func (b bootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args bootstrap.BootstrapParams) error {
   210  	return bootstrap.Bootstrap(ctx, env, args)
   211  }
   212  
   213  var getBootstrapFuncs = func() BootstrapInterface {
   214  	return &bootstrapFuncs{}
   215  }
   216  
   217  var getEnvName = func(c *BootstrapCommand) string {
   218  	return c.ConnectionName()
   219  }
   220  
   221  // Run connects to the environment specified on the command line and bootstraps
   222  // a juju in that environment if none already exists. If there is as yet no environments.yaml file,
   223  // the user is informed how to create one.
   224  func (c *BootstrapCommand) Run(ctx *cmd.Context) (resultErr error) {
   225  	bootstrapFuncs := getBootstrapFuncs()
   226  
   227  	if len(c.seriesOld) > 0 {
   228  		fmt.Fprintln(ctx.Stderr, "Use of --series is obsolete. --upload-tools now expands to all supported series of the same operating system.")
   229  	}
   230  	if len(c.Series) > 0 {
   231  		fmt.Fprintln(ctx.Stderr, "Use of --upload-series is obsolete. --upload-tools now expands to all supported series of the same operating system.")
   232  	}
   233  
   234  	envName := getEnvName(c)
   235  	if envName == "" {
   236  		return errors.Errorf("the name of the environment must be specified")
   237  	}
   238  	if err := checkProviderType(envName); errors.IsNotFound(err) {
   239  		// This error will get handled later.
   240  	} else if err != nil {
   241  		return errors.Trace(err)
   242  	}
   243  
   244  	environ, cleanup, err := environFromName(
   245  		ctx,
   246  		envName,
   247  		"Bootstrap",
   248  		bootstrapFuncs.EnsureNotBootstrapped,
   249  	)
   250  
   251  	// If we error out for any reason, clean up the environment.
   252  	defer func() {
   253  		if resultErr != nil && cleanup != nil {
   254  			if c.KeepBrokenEnvironment {
   255  				logger.Warningf("bootstrap failed but --keep-broken was specified so environment is not being destroyed.\n" +
   256  					"When you are finished diagnosing the problem, remember to run juju destroy-environment --force\n" +
   257  					"to clean up the environment.")
   258  			} else {
   259  				handleBootstrapError(ctx, resultErr, cleanup)
   260  			}
   261  		}
   262  	}()
   263  
   264  	// Handle any errors from environFromName(...).
   265  	if err != nil {
   266  		return errors.Annotatef(err, "there was an issue examining the environment")
   267  	}
   268  
   269  	// Check to see if this environment is already bootstrapped. If it
   270  	// is, we inform the user and exit early. If an error is returned
   271  	// but it is not that the environment is already bootstrapped,
   272  	// then we're in an unknown state.
   273  	if err := bootstrapFuncs.EnsureNotBootstrapped(environ); nil != err {
   274  		if environs.ErrAlreadyBootstrapped == err {
   275  			logger.Warningf("This juju environment is already bootstrapped. If you want to start a new Juju\nenvironment, first run juju destroy-environment to clean up, or switch to an\nalternative environment.")
   276  			return err
   277  		}
   278  		return errors.Annotatef(err, "cannot determine if environment is already bootstrapped.")
   279  	}
   280  
   281  	// Block interruption during bootstrap. Providers may also
   282  	// register for interrupt notification so they can exit early.
   283  	interrupted := make(chan os.Signal, 1)
   284  	defer close(interrupted)
   285  	ctx.InterruptNotify(interrupted)
   286  	defer ctx.StopInterruptNotify(interrupted)
   287  	go func() {
   288  		for _ = range interrupted {
   289  			ctx.Infof("Interrupt signalled: waiting for bootstrap to exit")
   290  		}
   291  	}()
   292  
   293  	// If --metadata-source is specified, override the default tools metadata source so
   294  	// SyncTools can use it, and also upload any image metadata.
   295  	var metadataDir string
   296  	if c.MetadataSource != "" {
   297  		metadataDir = ctx.AbsPath(c.MetadataSource)
   298  	}
   299  
   300  	// TODO (wallyworld): 2013-09-20 bug 1227931
   301  	// We can set a custom tools data source instead of doing an
   302  	// unnecessary upload.
   303  	if environ.Config().Type() == provider.Local {
   304  		c.UploadTools = true
   305  	}
   306  
   307  	err = bootstrapFuncs.Bootstrap(envcmd.BootstrapContext(ctx), environ, bootstrap.BootstrapParams{
   308  		Constraints:  c.Constraints,
   309  		Placement:    c.Placement,
   310  		UploadTools:  c.UploadTools,
   311  		AgentVersion: c.AgentVersion,
   312  		MetadataDir:  metadataDir,
   313  	})
   314  	if err != nil {
   315  		return errors.Annotate(err, "failed to bootstrap environment")
   316  	}
   317  	err = c.SetBootstrapEndpointAddress(environ)
   318  	if err != nil {
   319  		return errors.Annotate(err, "saving bootstrap endpoint address")
   320  	}
   321  	// To avoid race conditions when running scripted bootstraps, wait
   322  	// for the state server's machine agent to be ready to accept commands
   323  	// before exiting this bootstrap command.
   324  	return c.waitForAgentInitialisation(ctx)
   325  }
   326  
   327  var (
   328  	bootstrapReadyPollDelay = 1 * time.Second
   329  	bootstrapReadyPollCount = 60
   330  	blockAPI                = getBlockAPI
   331  )
   332  
   333  // getBlockAPI returns a block api for listing blocks.
   334  func getBlockAPI(c *envcmd.EnvCommandBase) (block.BlockListAPI, error) {
   335  	root, err := c.NewAPIRoot()
   336  	if err != nil {
   337  		return nil, err
   338  	}
   339  	return apiblock.NewClient(root), nil
   340  }
   341  
   342  // waitForAgentInitialisation polls the bootstrapped state server with a read-only
   343  // command which will fail until the state server is fully initialised.
   344  // TODO(wallyworld) - add a bespoke command to maybe the admin facade for this purpose.
   345  func (c *BootstrapCommand) waitForAgentInitialisation(ctx *cmd.Context) (err error) {
   346  	attempts := utils.AttemptStrategy{
   347  		Min:   bootstrapReadyPollCount,
   348  		Delay: bootstrapReadyPollDelay,
   349  	}
   350  	var client block.BlockListAPI
   351  	for attempt := attempts.Start(); attempt.Next(); {
   352  		client, err = blockAPI(&c.EnvCommandBase)
   353  		if err != nil {
   354  			return err
   355  		}
   356  		_, err = client.List()
   357  		client.Close()
   358  		if err == nil {
   359  			ctx.Infof("Bootstrap complete")
   360  			return nil
   361  		}
   362  		// As the API server is coming up, it goes through a number of steps.
   363  		// Initially the upgrade steps run, but the api server allows some
   364  		// calls to be processed during the upgrade, but not the list blocks.
   365  		// It is also possible that the underlying database causes connections
   366  		// to be dropped as it is initialising, or reconfiguring. These can
   367  		// lead to EOF or "connection is shut down" error messages. We skip
   368  		// these too, hoping that things come back up before the end of the
   369  		// retry poll count.
   370  		errorMessage := err.Error()
   371  		if strings.Contains(errorMessage, apiserver.UpgradeInProgressError.Error()) ||
   372  			strings.HasSuffix(errorMessage, "EOF") ||
   373  			strings.HasSuffix(errorMessage, "connection is shut down") {
   374  			ctx.Infof("Waiting for API to become available")
   375  			continue
   376  		}
   377  		return err
   378  	}
   379  	return err
   380  }
   381  
   382  var environType = func(envName string) (string, error) {
   383  	store, err := configstore.Default()
   384  	if err != nil {
   385  		return "", errors.Trace(err)
   386  	}
   387  	cfg, _, err := environs.ConfigForName(envName, store)
   388  	if err != nil {
   389  		return "", errors.Trace(err)
   390  	}
   391  	return cfg.Type(), nil
   392  }
   393  
   394  // checkProviderType ensures the provider type is okay.
   395  func checkProviderType(envName string) error {
   396  	envType, err := environType(envName)
   397  	if err != nil {
   398  		return errors.Trace(err)
   399  	}
   400  
   401  	featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey)
   402  	flag, ok := provisionalProviders[envType]
   403  	if ok && !featureflag.Enabled(flag) {
   404  		msg := `the %q provider is provisional in this version of Juju. To use it anyway, set JUJU_DEV_FEATURE_FLAGS="%s" in your shell environment`
   405  		return errors.Errorf(msg, envType, flag)
   406  	}
   407  
   408  	return nil
   409  }
   410  
   411  // handleBootstrapError is called to clean up if bootstrap fails.
   412  func handleBootstrapError(ctx *cmd.Context, err error, cleanup func()) {
   413  	ch := make(chan os.Signal, 1)
   414  	ctx.InterruptNotify(ch)
   415  	defer ctx.StopInterruptNotify(ch)
   416  	defer close(ch)
   417  	go func() {
   418  		for _ = range ch {
   419  			fmt.Fprintln(ctx.GetStderr(), "Cleaning up failed bootstrap")
   420  		}
   421  	}()
   422  	cleanup()
   423  }
   424  
   425  var allInstances = func(environ environs.Environ) ([]instance.Instance, error) {
   426  	return environ.AllInstances()
   427  }
   428  
   429  var prepareEndpointsForCaching = juju.PrepareEndpointsForCaching
   430  
   431  // SetBootstrapEndpointAddress writes the API endpoint address of the
   432  // bootstrap server into the connection information. This should only be run
   433  // once directly after Bootstrap. It assumes that there is just one instance
   434  // in the environment - the bootstrap instance.
   435  func (c *BootstrapCommand) SetBootstrapEndpointAddress(environ environs.Environ) error {
   436  	instances, err := allInstances(environ)
   437  	if err != nil {
   438  		return errors.Trace(err)
   439  	}
   440  	length := len(instances)
   441  	if length == 0 {
   442  		return errors.Errorf("found no instances, expected at least one")
   443  	}
   444  	if length > 1 {
   445  		logger.Warningf("expected one instance, got %d", length)
   446  	}
   447  	bootstrapInstance := instances[0]
   448  	cfg := environ.Config()
   449  	info, err := envcmd.ConnectionInfoForName(c.ConnectionName())
   450  	if err != nil {
   451  		return errors.Annotate(err, "failed to get connection info")
   452  	}
   453  
   454  	// Don't use c.ConnectionEndpoint as it attempts to contact the state
   455  	// server if no addresses are found in connection info.
   456  	endpoint := info.APIEndpoint()
   457  	netAddrs, err := bootstrapInstance.Addresses()
   458  	if err != nil {
   459  		return errors.Annotate(err, "failed to get bootstrap instance addresses")
   460  	}
   461  	apiPort := cfg.APIPort()
   462  	apiHostPorts := network.AddressesWithPort(netAddrs, apiPort)
   463  	addrs, hosts, addrsChanged := prepareEndpointsForCaching(
   464  		info, [][]network.HostPort{apiHostPorts}, network.HostPort{},
   465  	)
   466  	if !addrsChanged {
   467  		// Something's wrong we already have cached addresses?
   468  		return errors.Annotate(err, "cached API endpoints unexpectedly exist")
   469  	}
   470  	endpoint.Addresses = addrs
   471  	endpoint.Hostnames = hosts
   472  	writer, err := c.ConnectionWriter()
   473  	if err != nil {
   474  		return errors.Annotate(err, "failed to get connection writer")
   475  	}
   476  	writer.SetAPIEndpoint(endpoint)
   477  	err = writer.Write()
   478  	if err != nil {
   479  		return errors.Annotate(err, "failed to write API endpoint to connection info")
   480  	}
   481  	return nil
   482  }