github.com/altoros/juju-vmware@v0.0.0-20150312064031-f19ae857ccca/cmd/juju/bootstrap.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package main
     5  
     6  import (
     7  	"fmt"
     8  	"os"
     9  
    10  	"github.com/juju/cmd"
    11  	"github.com/juju/errors"
    12  	"gopkg.in/juju/charm.v4"
    13  	"launchpad.net/gnuflag"
    14  
    15  	"github.com/juju/juju/cmd/envcmd"
    16  	"github.com/juju/juju/constraints"
    17  	"github.com/juju/juju/environs"
    18  	"github.com/juju/juju/environs/bootstrap"
    19  	"github.com/juju/juju/instance"
    20  	"github.com/juju/juju/juju"
    21  	"github.com/juju/juju/network"
    22  	"github.com/juju/juju/provider"
    23  )
    24  
    25  const bootstrapDoc = `
    26  bootstrap starts a new environment of the current type (it will return an error
    27  if the environment has already been bootstrapped).  Bootstrapping an environment
    28  will provision a new machine in the environment and run the juju state server on
    29  that machine.
    30  
    31  If constraints are specified in the bootstrap command, they will apply to the
    32  machine provisioned for the juju state server.  They will also be set as default
    33  constraints on the environment for all future machines, exactly as if the
    34  constraints were set with juju set-constraints.
    35  
    36  It is possible to override constraints and the automatic machine selection
    37  algorithm by using the "--to" flag. The value associated with "--to" is a
    38  "placement directive", which tells Juju how to identify the first machine to use.
    39  For more information on placement directives, see "juju help placement".
    40  
    41  Bootstrap initializes the cloud environment synchronously and displays information
    42  about the current installation steps.  The time for bootstrap to complete varies
    43  across cloud providers from a few seconds to several minutes.  Once bootstrap has
    44  completed, you can run other juju commands against your environment. You can change
    45  the default timeout and retry delays used during the bootstrap by changing the
    46  following settings in your environments.yaml (all values represent number of seconds):
    47  
    48      # How long to wait for a connection to the state server.
    49      bootstrap-timeout: 600 # default: 10 minutes
    50      # How long to wait between connection attempts to a state server address.
    51      bootstrap-retry-delay: 5 # default: 5 seconds
    52      # How often to refresh state server addresses from the API server.
    53      bootstrap-addresses-delay: 10 # default: 10 seconds
    54  
    55  Private clouds may need to specify their own custom image metadata, and possibly upload
    56  Juju tools to cloud storage if no outgoing Internet access is available. In this case,
    57  use the --metadata-source paramater to tell bootstrap a local directory from which to
    58  upload tools and/or image metadata.
    59  
    60  See Also:
    61     juju help switch
    62     juju help constraints
    63     juju help set-constraints
    64     juju help placement
    65  `
    66  
    67  // BootstrapCommand is responsible for launching the first machine in a juju
    68  // environment, and setting up everything necessary to continue working.
    69  type BootstrapCommand struct {
    70  	envcmd.EnvCommandBase
    71  	Constraints           constraints.Value
    72  	UploadTools           bool
    73  	Series                []string
    74  	seriesOld             []string
    75  	MetadataSource        string
    76  	Placement             string
    77  	KeepBrokenEnvironment bool
    78  }
    79  
    80  func (c *BootstrapCommand) Info() *cmd.Info {
    81  	return &cmd.Info{
    82  		Name:    "bootstrap",
    83  		Purpose: "start up an environment from scratch",
    84  		Doc:     bootstrapDoc,
    85  	}
    86  }
    87  
    88  func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) {
    89  	f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "set environment constraints")
    90  	f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools before bootstrapping")
    91  	f.Var(newSeriesValue(nil, &c.Series), "upload-series", "upload tools for supplied comma-separated series list (OBSOLETE)")
    92  	f.Var(newSeriesValue(nil, &c.seriesOld), "series", "see --upload-series (OBSOLETE)")
    93  	f.StringVar(&c.MetadataSource, "metadata-source", "", "local path to use as tools and/or metadata source")
    94  	f.StringVar(&c.Placement, "to", "", "a placement directive indicating an instance to bootstrap")
    95  	f.BoolVar(&c.KeepBrokenEnvironment, "keep-broken", false, "do not destroy the environment if bootstrap fails")
    96  }
    97  
    98  func (c *BootstrapCommand) Init(args []string) (err error) {
    99  	if len(c.Series) > 0 && !c.UploadTools {
   100  		return fmt.Errorf("--upload-series requires --upload-tools")
   101  	}
   102  	if len(c.seriesOld) > 0 && !c.UploadTools {
   103  		return fmt.Errorf("--series requires --upload-tools")
   104  	}
   105  	if len(c.Series) > 0 && len(c.seriesOld) > 0 {
   106  		return fmt.Errorf("--upload-series and --series can't be used together")
   107  	}
   108  
   109  	// Parse the placement directive. Bootstrap currently only
   110  	// supports provider-specific placement directives.
   111  	if c.Placement != "" {
   112  		_, err = instance.ParsePlacement(c.Placement)
   113  		if err != instance.ErrPlacementScopeMissing {
   114  			// We only support unscoped placement directives for bootstrap.
   115  			return fmt.Errorf("unsupported bootstrap placement directive %q", c.Placement)
   116  		}
   117  	}
   118  	return cmd.CheckEmpty(args)
   119  }
   120  
   121  type seriesValue struct {
   122  	*cmd.StringsValue
   123  }
   124  
   125  // newSeriesValue is used to create the type passed into the gnuflag.FlagSet Var function.
   126  func newSeriesValue(defaultValue []string, target *[]string) *seriesValue {
   127  	v := seriesValue{(*cmd.StringsValue)(target)}
   128  	*(v.StringsValue) = defaultValue
   129  	return &v
   130  }
   131  
   132  // Implements gnuflag.Value Set.
   133  func (v *seriesValue) Set(s string) error {
   134  	if err := v.StringsValue.Set(s); err != nil {
   135  		return err
   136  	}
   137  	for _, name := range *(v.StringsValue) {
   138  		if !charm.IsValidSeries(name) {
   139  			v.StringsValue = nil
   140  			return fmt.Errorf("invalid series name %q", name)
   141  		}
   142  	}
   143  	return nil
   144  }
   145  
   146  // bootstrap functionality that Run calls to support cleaner testing
   147  type BootstrapInterface interface {
   148  	EnsureNotBootstrapped(env environs.Environ) error
   149  	Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error
   150  }
   151  
   152  type bootstrapFuncs struct{}
   153  
   154  func (b bootstrapFuncs) EnsureNotBootstrapped(env environs.Environ) error {
   155  	return bootstrap.EnsureNotBootstrapped(env)
   156  }
   157  
   158  func (b bootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args bootstrap.BootstrapParams) error {
   159  	return bootstrap.Bootstrap(ctx, env, args)
   160  }
   161  
   162  var getBootstrapFuncs = func() BootstrapInterface {
   163  	return &bootstrapFuncs{}
   164  }
   165  
   166  // Run connects to the environment specified on the command line and bootstraps
   167  // a juju in that environment if none already exists. If there is as yet no environments.yaml file,
   168  // the user is informed how to create one.
   169  func (c *BootstrapCommand) Run(ctx *cmd.Context) (resultErr error) {
   170  	bootstrapFuncs := getBootstrapFuncs()
   171  
   172  	if len(c.seriesOld) > 0 {
   173  		fmt.Fprintln(ctx.Stderr, "Use of --series is obsolete. --upload-tools now expands to all supported series of the same operating system.")
   174  	}
   175  	if len(c.Series) > 0 {
   176  		fmt.Fprintln(ctx.Stderr, "Use of --upload-series is obsolete. --upload-tools now expands to all supported series of the same operating system.")
   177  	}
   178  
   179  	if c.ConnectionName() == "" {
   180  		return fmt.Errorf("the name of the environment must be specified")
   181  	}
   182  
   183  	environ, cleanup, err := environFromName(
   184  		ctx,
   185  		c.ConnectionName(),
   186  		"Bootstrap",
   187  		bootstrapFuncs.EnsureNotBootstrapped,
   188  	)
   189  
   190  	// If we error out for any reason, clean up the environment.
   191  	defer func() {
   192  		if resultErr != nil && cleanup != nil {
   193  			if c.KeepBrokenEnvironment {
   194  				logger.Warningf("bootstrap failed but --keep-broken was specified so environment is not being destroyed.\n" +
   195  					"When you are finished diagnosing the problem, remember to run juju destroy-environment --force\n" +
   196  					"to clean up the environment.")
   197  			} else {
   198  				handleBootstrapError(ctx, resultErr, cleanup)
   199  			}
   200  		}
   201  	}()
   202  
   203  	// Handle any errors from environFromName(...).
   204  	if err != nil {
   205  		return errors.Annotatef(err, "there was an issue examining the environment")
   206  	}
   207  
   208  	// Check to see if this environment is already bootstrapped. If it
   209  	// is, we inform the user and exit early. If an error is returned
   210  	// but it is not that the environment is already bootstrapped,
   211  	// then we're in an unknown state.
   212  	if err := bootstrapFuncs.EnsureNotBootstrapped(environ); nil != err {
   213  		if environs.ErrAlreadyBootstrapped == err {
   214  			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.")
   215  			return err
   216  		}
   217  		return errors.Annotatef(err, "cannot determine if environment is already bootstrapped.")
   218  	}
   219  
   220  	// Block interruption during bootstrap. Providers may also
   221  	// register for interrupt notification so they can exit early.
   222  	interrupted := make(chan os.Signal, 1)
   223  	defer close(interrupted)
   224  	ctx.InterruptNotify(interrupted)
   225  	defer ctx.StopInterruptNotify(interrupted)
   226  	go func() {
   227  		for _ = range interrupted {
   228  			ctx.Infof("Interrupt signalled: waiting for bootstrap to exit")
   229  		}
   230  	}()
   231  
   232  	// If --metadata-source is specified, override the default tools metadata source so
   233  	// SyncTools can use it, and also upload any image metadata.
   234  	var metadataDir string
   235  	if c.MetadataSource != "" {
   236  		metadataDir = ctx.AbsPath(c.MetadataSource)
   237  	}
   238  
   239  	// TODO (wallyworld): 2013-09-20 bug 1227931
   240  	// We can set a custom tools data source instead of doing an
   241  	// unnecessary upload.
   242  	if environ.Config().Type() == provider.Local {
   243  		c.UploadTools = true
   244  	}
   245  
   246  	err = bootstrapFuncs.Bootstrap(envcmd.BootstrapContext(ctx), environ, bootstrap.BootstrapParams{
   247  		Constraints: c.Constraints,
   248  		Placement:   c.Placement,
   249  		UploadTools: c.UploadTools,
   250  		MetadataDir: metadataDir,
   251  	})
   252  	if err != nil {
   253  		return errors.Annotate(err, "failed to bootstrap environment")
   254  	}
   255  	return c.SetBootstrapEndpointAddress(environ)
   256  }
   257  
   258  // handleBootstrapError is called to clean up if bootstrap fails.
   259  func handleBootstrapError(ctx *cmd.Context, err error, cleanup func()) {
   260  	ch := make(chan os.Signal, 1)
   261  	ctx.InterruptNotify(ch)
   262  	defer ctx.StopInterruptNotify(ch)
   263  	defer close(ch)
   264  	go func() {
   265  		for _ = range ch {
   266  			fmt.Fprintln(ctx.GetStderr(), "Cleaning up failed bootstrap")
   267  		}
   268  	}()
   269  	cleanup()
   270  }
   271  
   272  var allInstances = func(environ environs.Environ) ([]instance.Instance, error) {
   273  	return environ.AllInstances()
   274  }
   275  
   276  var prepareEndpointsForCaching = juju.PrepareEndpointsForCaching
   277  
   278  // SetBootstrapEndpointAddress writes the API endpoint address of the
   279  // bootstrap server into the connection information. This should only be run
   280  // once directly after Bootstrap. It assumes that there is just one instance
   281  // in the environment - the bootstrap instance.
   282  func (c *BootstrapCommand) SetBootstrapEndpointAddress(environ environs.Environ) error {
   283  	instances, err := allInstances(environ)
   284  	if err != nil {
   285  		return errors.Trace(err)
   286  	}
   287  	length := len(instances)
   288  	if length == 0 {
   289  		return errors.Errorf("found no instances, expected at least one")
   290  	}
   291  	if length > 1 {
   292  		logger.Warningf("expected one instance, got %d", length)
   293  	}
   294  	bootstrapInstance := instances[0]
   295  	cfg := environ.Config()
   296  	info, err := envcmd.ConnectionInfoForName(c.ConnectionName())
   297  	if err != nil {
   298  		return errors.Annotate(err, "failed to get connection info")
   299  	}
   300  
   301  	// Don't use c.ConnectionEndpoint as it attempts to contact the state
   302  	// server if no addresses are found in connection info.
   303  	endpoint := info.APIEndpoint()
   304  	netAddrs, err := bootstrapInstance.Addresses()
   305  	if err != nil {
   306  		return errors.Annotate(err, "failed to get bootstrap instance addresses")
   307  	}
   308  	apiPort := cfg.APIPort()
   309  	apiHostPorts := network.AddressesWithPort(netAddrs, apiPort)
   310  	addrs, hosts, addrsChanged := prepareEndpointsForCaching(
   311  		info, [][]network.HostPort{apiHostPorts}, network.HostPort{},
   312  	)
   313  	if !addrsChanged {
   314  		// Something's wrong we already have cached addresses?
   315  		return errors.Annotate(err, "cached API endpoints unexpectedly exist")
   316  	}
   317  	endpoint.Addresses = addrs
   318  	endpoint.Hostnames = hosts
   319  	writer, err := c.ConnectionWriter()
   320  	if err != nil {
   321  		return errors.Annotate(err, "failed to get connection writer")
   322  	}
   323  	writer.SetAPIEndpoint(endpoint)
   324  	err = writer.Write()
   325  	if err != nil {
   326  		return errors.Annotate(err, "failed to write API endpoint to connection info")
   327  	}
   328  	return nil
   329  }