github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/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  	"launchpad.net/gnuflag"
    11  
    12  	"github.com/juju/juju/charm"
    13  	"github.com/juju/juju/cmd"
    14  	"github.com/juju/juju/cmd/envcmd"
    15  	"github.com/juju/juju/constraints"
    16  	"github.com/juju/juju/environs"
    17  	"github.com/juju/juju/environs/bootstrap"
    18  	"github.com/juju/juju/environs/imagemetadata"
    19  	"github.com/juju/juju/environs/tools"
    20  	"github.com/juju/juju/instance"
    21  	"github.com/juju/juju/provider"
    22  )
    23  
    24  const bootstrapDoc = `
    25  bootstrap starts a new environment of the current type (it will return an error
    26  if the environment has already been bootstrapped).  Bootstrapping an environment
    27  will provision a new machine in the environment and run the juju state server on
    28  that machine.
    29  
    30  If constraints are specified in the bootstrap command, they will apply to the 
    31  machine provisioned for the juju state server.  They will also be set as default
    32  constraints on the environment for all future machines, exactly as if the
    33  constraints were set with juju set-constraints.
    34  
    35  Bootstrap initializes the cloud environment synchronously and displays information
    36  about the current installation steps.  The time for bootstrap to complete varies 
    37  across cloud providers from a few seconds to several minutes.  Once bootstrap has 
    38  completed, you can run other juju commands against your environment. You can change
    39  the default timeout and retry delays used during the bootstrap by changing the
    40  following settings in your environments.yaml (all values represent number of seconds):
    41  
    42      # How long to wait for a connection to the state server.
    43      bootstrap-timeout: 600 # default: 10 minutes
    44      # How long to wait between connection attempts to a state server address.
    45      bootstrap-retry-delay: 5 # default: 5 seconds
    46      # How often to refresh state server addresses from the API server.
    47      bootstrap-addresses-delay: 10 # default: 10 seconds
    48  
    49  Private clouds may need to specify their own custom image metadata, and possibly upload
    50  Juju tools to cloud storage if no outgoing Internet access is available. In this case,
    51  use the --metadata-source paramater to tell bootstrap a local directory from which to
    52  upload tools and/or image metadata.
    53  
    54  See Also:
    55     juju help switch
    56     juju help constraints
    57     juju help set-constraints
    58  `
    59  
    60  // BootstrapCommand is responsible for launching the first machine in a juju
    61  // environment, and setting up everything necessary to continue working.
    62  type BootstrapCommand struct {
    63  	envcmd.EnvCommandBase
    64  	Constraints    constraints.Value
    65  	UploadTools    bool
    66  	Series         []string
    67  	seriesOld      []string
    68  	MetadataSource string
    69  	Placement      string
    70  }
    71  
    72  func (c *BootstrapCommand) Info() *cmd.Info {
    73  	return &cmd.Info{
    74  		Name:    "bootstrap",
    75  		Purpose: "start up an environment from scratch",
    76  		Doc:     bootstrapDoc,
    77  	}
    78  }
    79  
    80  func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) {
    81  	f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "set environment constraints")
    82  	f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools before bootstrapping")
    83  	f.Var(newSeriesValue(nil, &c.Series), "upload-series", "upload tools for supplied comma-separated series list")
    84  	f.Var(newSeriesValue(nil, &c.seriesOld), "series", "upload tools for supplied comma-separated series list (DEPRECATED, see --upload-series)")
    85  	f.StringVar(&c.MetadataSource, "metadata-source", "", "local path to use as tools and/or metadata source")
    86  	f.StringVar(&c.Placement, "to", "", "a placement directive indicating an instance to bootstrap")
    87  }
    88  
    89  func (c *BootstrapCommand) Init(args []string) (err error) {
    90  	if len(c.Series) > 0 && !c.UploadTools {
    91  		return fmt.Errorf("--upload-series requires --upload-tools")
    92  	}
    93  	if len(c.seriesOld) > 0 && !c.UploadTools {
    94  		return fmt.Errorf("--series requires --upload-tools")
    95  	}
    96  	if len(c.Series) > 0 && len(c.seriesOld) > 0 {
    97  		return fmt.Errorf("--upload-series and --series can't be used together")
    98  	}
    99  	if len(c.seriesOld) > 0 {
   100  		c.Series = c.seriesOld
   101  	}
   102  
   103  	// Parse the placement directive. Bootstrap currently only
   104  	// supports provider-specific placement directives.
   105  	if c.Placement != "" {
   106  		_, err = instance.ParsePlacement(c.Placement)
   107  		if err != instance.ErrPlacementScopeMissing {
   108  			// We only support unscoped placement directives for bootstrap.
   109  			return fmt.Errorf("unsupported bootstrap placement directive %q", c.Placement)
   110  		}
   111  	}
   112  	return cmd.CheckEmpty(args)
   113  }
   114  
   115  type seriesValue struct {
   116  	*cmd.StringsValue
   117  }
   118  
   119  // newSeriesValue is used to create the type passed into the gnuflag.FlagSet Var function.
   120  func newSeriesValue(defaultValue []string, target *[]string) *seriesValue {
   121  	v := seriesValue{(*cmd.StringsValue)(target)}
   122  	*(v.StringsValue) = defaultValue
   123  	return &v
   124  }
   125  
   126  // Implements gnuflag.Value Set.
   127  func (v *seriesValue) Set(s string) error {
   128  	if err := v.StringsValue.Set(s); err != nil {
   129  		return err
   130  	}
   131  	for _, name := range *(v.StringsValue) {
   132  		if !charm.IsValidSeries(name) {
   133  			v.StringsValue = nil
   134  			return fmt.Errorf("invalid series name %q", name)
   135  		}
   136  	}
   137  	return nil
   138  }
   139  
   140  // bootstrap functionality that Run calls to support cleaner testing
   141  type BootstrapInterface interface {
   142  	EnsureNotBootstrapped(env environs.Environ) error
   143  	UploadTools(environs.BootstrapContext, environs.Environ, *string, bool, ...string) error
   144  	Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args environs.BootstrapParams) error
   145  }
   146  
   147  type bootstrapFuncs struct{}
   148  
   149  func (b bootstrapFuncs) EnsureNotBootstrapped(env environs.Environ) error {
   150  	return bootstrap.EnsureNotBootstrapped(env)
   151  }
   152  
   153  func (b bootstrapFuncs) UploadTools(ctx environs.BootstrapContext, env environs.Environ, toolsArch *string, forceVersion bool, bootstrapSeries ...string) error {
   154  	return bootstrap.UploadTools(ctx, env, toolsArch, forceVersion, bootstrapSeries...)
   155  }
   156  
   157  func (b bootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args environs.BootstrapParams) error {
   158  	return bootstrap.Bootstrap(ctx, env, args)
   159  }
   160  
   161  var getBootstrapFuncs = func() BootstrapInterface {
   162  	return &bootstrapFuncs{}
   163  }
   164  
   165  // Run connects to the environment specified on the command line and bootstraps
   166  // a juju in that environment if none already exists. If there is as yet no environments.yaml file,
   167  // the user is informed how to create one.
   168  func (c *BootstrapCommand) Run(ctx *cmd.Context) (resultErr error) {
   169  	bootstrapFuncs := getBootstrapFuncs()
   170  
   171  	if len(c.seriesOld) > 0 {
   172  		fmt.Fprintln(ctx.Stderr, "Use of --series is deprecated. Please use --upload-series instead.")
   173  	}
   174  
   175  	environ, cleanup, err := environFromName(ctx, c.EnvName, &resultErr, "Bootstrap")
   176  	if err != nil {
   177  		return err
   178  	}
   179  	// We want to validate constraints early. However, if a custom image metadata
   180  	// source is specified, we can't validate the arch because that depends on what
   181  	// images metadata is to be uploaded. So we validate here if no custom metadata
   182  	// source is specified, and defer till later if not.
   183  	if c.MetadataSource == "" {
   184  		if err := validateConstraints(c.Constraints, environ); err != nil {
   185  			return err
   186  		}
   187  	}
   188  
   189  	defer cleanup()
   190  	if err := bootstrapFuncs.EnsureNotBootstrapped(environ); err != nil {
   191  		return err
   192  	}
   193  
   194  	// Block interruption during bootstrap. Providers may also
   195  	// register for interrupt notification so they can exit early.
   196  	interrupted := make(chan os.Signal, 1)
   197  	defer close(interrupted)
   198  	ctx.InterruptNotify(interrupted)
   199  	defer ctx.StopInterruptNotify(interrupted)
   200  	go func() {
   201  		for _ = range interrupted {
   202  			ctx.Infof("Interrupt signalled: waiting for bootstrap to exit")
   203  		}
   204  	}()
   205  
   206  	// If --metadata-source is specified, override the default tools metadata source so
   207  	// SyncTools can use it, and also upload any image metadata.
   208  	if c.MetadataSource != "" {
   209  		metadataDir := ctx.AbsPath(c.MetadataSource)
   210  		if err := uploadCustomMetadata(metadataDir, environ); err != nil {
   211  			return err
   212  		}
   213  		if err := validateConstraints(c.Constraints, environ); err != nil {
   214  			return err
   215  		}
   216  	}
   217  	// TODO (wallyworld): 2013-09-20 bug 1227931
   218  	// We can set a custom tools data source instead of doing an
   219  	// unnecessary upload.
   220  	if environ.Config().Type() == provider.Local {
   221  		c.UploadTools = true
   222  	}
   223  	if c.UploadTools {
   224  		err = bootstrapFuncs.UploadTools(ctx, environ, c.Constraints.Arch, true, c.Series...)
   225  		if err != nil {
   226  			return err
   227  		}
   228  	}
   229  	return bootstrapFuncs.Bootstrap(ctx, environ, environs.BootstrapParams{
   230  		Constraints: c.Constraints,
   231  		Placement:   c.Placement,
   232  	})
   233  }
   234  
   235  var uploadCustomMetadata = func(metadataDir string, env environs.Environ) error {
   236  	logger.Infof("Setting default tools and image metadata sources: %s", metadataDir)
   237  	tools.DefaultBaseURL = metadataDir
   238  	if err := imagemetadata.UploadImageMetadata(env.Storage(), metadataDir); err != nil {
   239  		// Do not error if image metadata directory doesn't exist.
   240  		if !os.IsNotExist(err) {
   241  			return fmt.Errorf("uploading image metadata: %v", err)
   242  		}
   243  	} else {
   244  		logger.Infof("custom image metadata uploaded")
   245  	}
   246  	return nil
   247  }
   248  
   249  var validateConstraints = func(cons constraints.Value, env environs.Environ) error {
   250  	validator, err := env.ConstraintsValidator()
   251  	if err != nil {
   252  		return err
   253  	}
   254  	unsupported, err := validator.Validate(cons)
   255  	if len(unsupported) > 0 {
   256  		logger.Warningf("unsupported constraints: %v", err)
   257  	} else if err != nil {
   258  		return err
   259  	}
   260  	return nil
   261  }