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 }