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