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