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