launchpad.net/~rogpeppe/juju-core/500-errgo-fix@v0.0.0-20140213181702-000000002356/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  	"io"
     9  	"os"
    10  	"strings"
    11  
    12  	"github.com/errgo/errgo"
    13  	"launchpad.net/gnuflag"
    14  
    15  	"launchpad.net/juju-core/charm"
    16  	"launchpad.net/juju-core/cmd"
    17  	"launchpad.net/juju-core/constraints"
    18  	"launchpad.net/juju-core/environs"
    19  	"launchpad.net/juju-core/environs/bootstrap"
    20  	"launchpad.net/juju-core/environs/config"
    21  	"launchpad.net/juju-core/environs/configstore"
    22  	"launchpad.net/juju-core/environs/imagemetadata"
    23  	"launchpad.net/juju-core/environs/sync"
    24  	"launchpad.net/juju-core/environs/tools"
    25  	"launchpad.net/juju-core/errors"
    26  	"launchpad.net/juju-core/provider"
    27  	"launchpad.net/juju-core/utils/set"
    28  	"launchpad.net/juju-core/version"
    29  )
    30  
    31  const bootstrapDoc = `
    32  bootstrap starts a new environment of the current type (it will return an error
    33  if the environment has already been bootstrapped).  Bootstrapping an environment
    34  will provision a new machine in the environment and run the juju state server on
    35  that machine.
    36  
    37  If constraints are specified in the bootstrap command, they will apply to the 
    38  machine provisioned for the juju state server.  They will also be set as default
    39  constraints on the environment for all future machines, exactly as if the
    40  constraints were set with juju set-constraints.
    41  
    42  Bootstrap initializes the cloud environment synchronously and displays information
    43  about the current installation steps.  The time for bootstrap to complete varies 
    44  across cloud providers from a few seconds to several minutes.  Once bootstrap has 
    45  completed, you can run other juju commands against your environment. You can change
    46  the default timeout and retry delays used during the bootstrap by changing the
    47  following settings in your environments.yaml (all values represent number of seconds):
    48  
    49      # How long to wait for a connection to the state server.
    50      bootstrap-timeout: 600 # default: 10 minutes
    51      # How long to wait between connection attempts to a state server address.
    52      bootstrap-retry-delay: 5 # default: 5 seconds
    53      # How often to refresh state server addresses from the API server.
    54      bootstrap-addresses-delay: 10 # default: 10 seconds
    55  
    56  Private clouds may need to specify their own custom image metadata, and possibly upload
    57  Juju tools to cloud storage if no outgoing Internet access is available. In this case,
    58  use the --metadata-source paramater to tell bootstrap a local directory from which to
    59  upload tools and/or image metadata.
    60  
    61  See Also:
    62     juju help switch
    63     juju help constraints
    64     juju help set-constraints
    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  	cmd.EnvCommandBase
    71  	Constraints    constraints.Value
    72  	UploadTools    bool
    73  	Series         []string
    74  	MetadataSource string
    75  }
    76  
    77  func (c *BootstrapCommand) Info() *cmd.Info {
    78  	return &cmd.Info{
    79  		Name:    "bootstrap",
    80  		Purpose: "start up an environment from scratch",
    81  		Doc:     bootstrapDoc,
    82  	}
    83  }
    84  
    85  func (c *BootstrapCommand) SetFlags(f *gnuflag.FlagSet) {
    86  	c.EnvCommandBase.SetFlags(f)
    87  	f.Var(constraints.ConstraintsValue{&c.Constraints}, "constraints", "set environment constraints")
    88  	f.BoolVar(&c.UploadTools, "upload-tools", false, "upload local version of tools before bootstrapping")
    89  	f.Var(seriesVar{&c.Series}, "series", "upload tools for supplied comma-separated series list")
    90  	f.StringVar(&c.MetadataSource, "metadata-source", "", "local path to use as tools and/or metadata source")
    91  }
    92  
    93  func (c *BootstrapCommand) Init(args []string) (err error) {
    94  	if len(c.Series) > 0 && !c.UploadTools {
    95  		return fmt.Errorf("--series requires --upload-tools")
    96  	}
    97  	return cmd.CheckEmpty(args)
    98  }
    99  
   100  type bootstrapContext struct {
   101  	*cmd.Context
   102  }
   103  
   104  func (c bootstrapContext) Stdin() io.Reader {
   105  	return c.Context.Stdin
   106  }
   107  
   108  func (c bootstrapContext) Stdout() io.Writer {
   109  	return c.Context.Stdout
   110  }
   111  
   112  func (c bootstrapContext) Stderr() io.Writer {
   113  	return c.Context.Stderr
   114  }
   115  
   116  func destroyPreparedEnviron(env environs.Environ, store configstore.Storage, err *error, action string) {
   117  	if *err == nil {
   118  		return
   119  	}
   120  	if err := environs.Destroy(env, store); err != nil {
   121  		logger.Errorf("%s failed, and the environment could not be destroyed: %v", action, err)
   122  	}
   123  }
   124  
   125  // Run connects to the environment specified on the command line and bootstraps
   126  // a juju in that environment if none already exists. If there is as yet no environments.yaml file,
   127  // the user is informed how to create one.
   128  func (c *BootstrapCommand) Run(ctx *cmd.Context) (resultErr error) {
   129  	store, err := configstore.Default()
   130  	if err != nil {
   131  		return err
   132  	}
   133  	var existing bool
   134  	if _, err := store.ReadInfo(c.EnvName); !errors.IsNotFoundError(err) {
   135  		existing = true
   136  	}
   137  	environ, err := environs.PrepareFromName(c.EnvName, store)
   138  	if err != nil {
   139  		return err
   140  	}
   141  	if !existing {
   142  		defer destroyPreparedEnviron(environ, store, &resultErr, "Bootstrap")
   143  	}
   144  	bootstrapContext := bootstrapContext{ctx}
   145  	// If the environment has a special bootstrap Storage, use it wherever
   146  	// we'd otherwise use environ.Storage.
   147  	if bs, ok := environ.(environs.BootstrapStorager); ok {
   148  		if err := bs.EnableBootstrapStorage(bootstrapContext); err != nil {
   149  			return errgo.Annotate(err, "failed to enable bootstrap storage")
   150  		}
   151  	}
   152  	if err := bootstrap.EnsureNotBootstrapped(environ); err != nil {
   153  		return err
   154  	}
   155  	// If --metadata-source is specified, override the default tools metadata source so
   156  	// SyncTools can use it, and also upload any image metadata.
   157  	if c.MetadataSource != "" {
   158  		metadataDir := ctx.AbsPath(c.MetadataSource)
   159  		logger.Infof("Setting default tools and image metadata sources: %s", metadataDir)
   160  		tools.DefaultBaseURL = metadataDir
   161  		if err := imagemetadata.UploadImageMetadata(environ.Storage(), metadataDir); err != nil {
   162  			// Do not error if image metadata directory doesn't exist.
   163  			if !os.IsNotExist(err) {
   164  				return fmt.Errorf("uploading image metadata: %v", err)
   165  			}
   166  		} else {
   167  			logger.Infof("custom image metadata uploaded")
   168  		}
   169  	}
   170  	// TODO (wallyworld): 2013-09-20 bug 1227931
   171  	// We can set a custom tools data source instead of doing an
   172  	// unnecessary upload.
   173  	if environ.Config().Type() == provider.Local {
   174  		c.UploadTools = true
   175  	}
   176  	if c.UploadTools {
   177  		err = c.uploadTools(environ)
   178  		if err != nil {
   179  			return err
   180  		}
   181  	}
   182  	return bootstrap.Bootstrap(bootstrapContext, environ, c.Constraints)
   183  }
   184  
   185  func (c *BootstrapCommand) uploadTools(environ environs.Environ) error {
   186  	// Force version.Current, for consistency with subsequent upgrade-juju
   187  	// (see UpgradeJujuCommand).
   188  	forceVersion := uploadVersion(version.Current.Number, nil)
   189  	cfg := environ.Config()
   190  	series := getUploadSeries(cfg, c.Series)
   191  	agenttools, err := sync.Upload(environ.Storage(), &forceVersion, series...)
   192  	if err != nil {
   193  		return err
   194  	}
   195  	cfg, err = cfg.Apply(map[string]interface{}{
   196  		"agent-version": agenttools.Version.Number.String(),
   197  	})
   198  	if err == nil {
   199  		err = environ.SetConfig(cfg)
   200  	}
   201  	if err != nil {
   202  		return fmt.Errorf("failed to update environment configuration: %v", err)
   203  	}
   204  	return nil
   205  }
   206  
   207  type seriesVar struct {
   208  	target *[]string
   209  }
   210  
   211  func (v seriesVar) Set(value string) error {
   212  	names := strings.Split(value, ",")
   213  	for _, name := range names {
   214  		if !charm.IsValidSeries(name) {
   215  			return fmt.Errorf("invalid series name %q", name)
   216  		}
   217  	}
   218  	*v.target = names
   219  	return nil
   220  }
   221  
   222  func (v seriesVar) String() string {
   223  	return strings.Join(*v.target, ",")
   224  }
   225  
   226  // getUploadSeries returns the supplied series with duplicates removed if
   227  // non-empty; otherwise it returns a default list of series we should
   228  // probably upload, based on cfg.
   229  func getUploadSeries(cfg *config.Config, series []string) []string {
   230  	unique := set.NewStrings(series...)
   231  	if unique.IsEmpty() {
   232  		unique.Add(version.Current.Series)
   233  		unique.Add(config.DefaultSeries)
   234  		unique.Add(cfg.DefaultSeries())
   235  	}
   236  	return unique.Values()
   237  }