github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/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 11 "github.com/juju/cmd" 12 "github.com/juju/errors" 13 "github.com/juju/utils" 14 "github.com/juju/utils/featureflag" 15 "github.com/juju/version" 16 "gopkg.in/juju/charm.v6-unstable" 17 "launchpad.net/gnuflag" 18 19 jujucloud "github.com/juju/juju/cloud" 20 "github.com/juju/juju/cmd/juju/common" 21 "github.com/juju/juju/cmd/modelcmd" 22 "github.com/juju/juju/constraints" 23 "github.com/juju/juju/environs" 24 "github.com/juju/juju/environs/bootstrap" 25 "github.com/juju/juju/environs/config" 26 "github.com/juju/juju/environs/sync" 27 "github.com/juju/juju/feature" 28 "github.com/juju/juju/instance" 29 "github.com/juju/juju/juju/osenv" 30 "github.com/juju/juju/jujuclient" 31 jujuversion "github.com/juju/juju/version" 32 ) 33 34 // provisionalProviders is the names of providers that are hidden behind 35 // feature flags. 36 var provisionalProviders = map[string]string{ 37 "vsphere": feature.VSphereProvider, 38 } 39 40 var usageBootstrapSummary = ` 41 Initializes a cloud environment.`[1:] 42 43 var usageBootstrapDetails = ` 44 Initialization consists of creating an 'admin' model and provisioning a 45 machine to act as controller. 46 Credentials are set beforehand and are distinct from any other 47 configuration (see `[1:] + "`juju add-credential`" + `). 48 The 'admin' model typically does not run workloads. It should remain 49 pristine to run and manage Juju's own infrastructure for the corresponding 50 cloud. Additional (hosted) models should be created with ` + "`juju create-\nmodel`" + ` for workload purposes. 51 Note that a 'default' model is also created and becomes the current model 52 of the environment once the command completes. It can be discarded if 53 other models are created. 54 If '--bootstrap-constraints' is used, its values will also apply to any 55 future controllers provisioned for high availability (HA). 56 If '--constraints' is used, its values will be set as the default 57 constraints for all future workload machines in the model, exactly as if 58 the constraints were set with ` + "`juju set-model-constraints`" + `. 59 It is possible to override constraints and the automatic machine selection 60 algorithm by assigning a "placement directive" via the '--to' option. This 61 dictates what machine to use for the controller. This would typically be 62 used with the MAAS provider ('--to <host>.maas'). 63 You can change the default timeout and retry delays used during the 64 bootstrap by changing the following settings in your configuration file 65 (all values represent number of seconds): 66 # How long to wait for a connection to the controller 67 bootstrap-timeout: 600 # default: 10 minutes 68 # How long to wait between connection attempts to a controller 69 address. 70 bootstrap-retry-delay: 5 # default: 5 seconds 71 # How often to refresh controller addresses from the API server. 72 bootstrap-addresses-delay: 10 # default: 10 seconds 73 Private clouds may need to specify their own custom image metadata and 74 tools/agent. Use '--metadata-source' whose value is a local directory. 75 The value of '--agent-version' will become the default tools version to 76 use in all models for this controller. The full binary version is accepted 77 (e.g.: 2.0.1-xenial-amd64) but only the numeric version (e.g.: 2.0.1) is 78 used. Otherwise, by default, the version used is that of the client. 79 80 Examples: 81 juju bootstrap mycontroller google 82 juju bootstrap --config=~/config-rs.yaml mycontroller rackspace 83 juju bootstrap --config agent-version=1.25.3 mycontroller aws 84 juju bootstrap --config bootstrap-timeout=1200 mycontroller azure 85 86 See also: 87 add-credentials 88 add-model 89 set-constraints` 90 91 // defaultHostedModelName is the name of the hosted model created in each 92 // controller for deploying workloads to, in addition to the "admin" model. 93 const defaultHostedModelName = "default" 94 95 func newBootstrapCommand() cmd.Command { 96 return modelcmd.Wrap( 97 &bootstrapCommand{}, 98 modelcmd.ModelSkipFlags, modelcmd.ModelSkipDefault, 99 ) 100 } 101 102 // bootstrapCommand is responsible for launching the first machine in a juju 103 // environment, and setting up everything necessary to continue working. 104 type bootstrapCommand struct { 105 modelcmd.ModelCommandBase 106 107 Constraints constraints.Value 108 BootstrapConstraints constraints.Value 109 BootstrapSeries string 110 BootstrapImage string 111 UploadTools bool 112 MetadataSource string 113 Placement string 114 KeepBrokenEnvironment bool 115 AutoUpgrade bool 116 AgentVersionParam string 117 AgentVersion *version.Number 118 config common.ConfigFlag 119 120 controllerName string 121 hostedModelName string 122 CredentialName string 123 Cloud string 124 Region string 125 noGUI bool 126 } 127 128 func (c *bootstrapCommand) Info() *cmd.Info { 129 return &cmd.Info{ 130 Name: "bootstrap", 131 Args: "<controller name> <cloud name>[/region]", 132 Purpose: usageBootstrapSummary, 133 Doc: usageBootstrapDetails, 134 } 135 } 136 137 func (c *bootstrapCommand) SetFlags(f *gnuflag.FlagSet) { 138 f.Var(constraints.ConstraintsValue{Target: &c.Constraints}, "constraints", "Set model constraints") 139 f.Var(constraints.ConstraintsValue{Target: &c.BootstrapConstraints}, "bootstrap-constraints", "Specify bootstrap machine constraints") 140 f.StringVar(&c.BootstrapSeries, "bootstrap-series", "", "Specify the series of the bootstrap machine") 141 if featureflag.Enabled(feature.ImageMetadata) { 142 f.StringVar(&c.BootstrapImage, "bootstrap-image", "", "Specify the image of the bootstrap machine") 143 } 144 f.BoolVar(&c.UploadTools, "upload-tools", false, "Upload local version of tools before bootstrapping") 145 f.StringVar(&c.MetadataSource, "metadata-source", "", "Local path to use as tools and/or metadata source") 146 f.StringVar(&c.Placement, "to", "", "Placement directive indicating an instance to bootstrap") 147 f.BoolVar(&c.KeepBrokenEnvironment, "keep-broken", false, "Do not destroy the model if bootstrap fails") 148 f.BoolVar(&c.AutoUpgrade, "auto-upgrade", false, "Upgrade to the latest patch release tools on first bootstrap") 149 f.StringVar(&c.AgentVersionParam, "agent-version", "", "Version of tools to use for Juju agents") 150 f.StringVar(&c.CredentialName, "credential", "", "Credentials to use when bootstrapping") 151 f.Var(&c.config, "config", "Specify a controller configuration file, or one or more configuration\n options\n (--config config.yaml [--config key=value ...])") 152 f.StringVar(&c.hostedModelName, "d", defaultHostedModelName, "Name of the default hosted model for the controller") 153 f.StringVar(&c.hostedModelName, "default-model", defaultHostedModelName, "Name of the default hosted model for the controller") 154 f.BoolVar(&c.noGUI, "no-gui", false, "Do not install the Juju GUI in the controller when bootstrapping") 155 } 156 157 func (c *bootstrapCommand) Init(args []string) (err error) { 158 if c.AgentVersionParam != "" && c.UploadTools { 159 return fmt.Errorf("--agent-version and --upload-tools can't be used together") 160 } 161 if c.BootstrapSeries != "" && !charm.IsValidSeries(c.BootstrapSeries) { 162 return errors.NotValidf("series %q", c.BootstrapSeries) 163 } 164 if c.BootstrapImage != "" { 165 if c.BootstrapSeries == "" { 166 return errors.Errorf("--bootstrap-image must be used with --bootstrap-series") 167 } 168 cons, err := constraints.Merge(c.Constraints, c.BootstrapConstraints) 169 if err != nil { 170 return errors.Trace(err) 171 } 172 if !cons.HasArch() { 173 return errors.Errorf("--bootstrap-image must be used with --bootstrap-constraints, specifying architecture") 174 } 175 } 176 177 // Parse the placement directive. Bootstrap currently only 178 // supports provider-specific placement directives. 179 if c.Placement != "" { 180 _, err = instance.ParsePlacement(c.Placement) 181 if err != instance.ErrPlacementScopeMissing { 182 // We only support unscoped placement directives for bootstrap. 183 return fmt.Errorf("unsupported bootstrap placement directive %q", c.Placement) 184 } 185 } 186 if !c.AutoUpgrade { 187 // With no auto upgrade chosen, we default to the version matching the bootstrap client. 188 vers := jujuversion.Current 189 c.AgentVersion = &vers 190 } 191 if c.AgentVersionParam != "" { 192 if vers, err := version.ParseBinary(c.AgentVersionParam); err == nil { 193 c.AgentVersion = &vers.Number 194 } else if vers, err := version.Parse(c.AgentVersionParam); err == nil { 195 c.AgentVersion = &vers 196 } else { 197 return err 198 } 199 } 200 if c.AgentVersion != nil && (c.AgentVersion.Major != jujuversion.Current.Major || c.AgentVersion.Minor != jujuversion.Current.Minor) { 201 return fmt.Errorf("requested agent version major.minor mismatch") 202 } 203 204 // The user must specify two positional arguments: the controller name, 205 // and the cloud name (optionally with region specified). 206 if len(args) < 2 { 207 return errors.New("controller name and cloud name are required") 208 } 209 c.controllerName = bootstrappedControllerName(args[0]) 210 c.Cloud = args[1] 211 if i := strings.IndexRune(c.Cloud, '/'); i > 0 { 212 c.Cloud, c.Region = c.Cloud[:i], c.Cloud[i+1:] 213 } 214 return cmd.CheckEmpty(args[2:]) 215 } 216 217 var bootstrappedControllerName = func(controllerName string) string { 218 return fmt.Sprintf("local.%s", controllerName) 219 } 220 221 // BootstrapInterface provides bootstrap functionality that Run calls to support cleaner testing. 222 type BootstrapInterface interface { 223 Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args bootstrap.BootstrapParams) error 224 } 225 226 type bootstrapFuncs struct{} 227 228 func (b bootstrapFuncs) Bootstrap(ctx environs.BootstrapContext, env environs.Environ, args bootstrap.BootstrapParams) error { 229 return bootstrap.Bootstrap(ctx, env, args) 230 } 231 232 var getBootstrapFuncs = func() BootstrapInterface { 233 return &bootstrapFuncs{} 234 } 235 236 var ( 237 environsPrepare = environs.Prepare 238 environsDestroy = environs.Destroy 239 waitForAgentInitialisation = common.WaitForAgentInitialisation 240 ) 241 242 var ambiguousCredentialError = errors.New(` 243 more than one credential detected 244 run juju autoload-credentials and specify a credential using the --credential argument`[1:], 245 ) 246 247 // Run connects to the environment specified on the command line and bootstraps 248 // a juju in that environment if none already exists. If there is as yet no environments.yaml file, 249 // the user is informed how to create one. 250 func (c *bootstrapCommand) Run(ctx *cmd.Context) (resultErr error) { 251 bootstrapFuncs := getBootstrapFuncs() 252 253 // Get the cloud definition identified by c.Cloud. If c.Cloud does not 254 // identify a cloud in clouds.yaml, but is the name of a provider, and 255 // that provider implements environs.CloudRegionDetector, we'll 256 // synthesise a Cloud structure with the detected regions and no auth- 257 // types. 258 cloud, err := jujucloud.CloudByName(c.Cloud) 259 if errors.IsNotFound(err) { 260 ctx.Verbosef("cloud %q not found, trying as a provider name", c.Cloud) 261 provider, err := environs.Provider(c.Cloud) 262 if errors.IsNotFound(err) { 263 return errors.NewNotFound(nil, fmt.Sprintf("unknown cloud %q, please try %q", c.Cloud, "juju update-clouds")) 264 } else if err != nil { 265 return errors.Trace(err) 266 } 267 detector, ok := provider.(environs.CloudRegionDetector) 268 if !ok { 269 ctx.Verbosef( 270 "provider %q does not support detecting regions", 271 c.Cloud, 272 ) 273 return errors.NewNotFound(nil, fmt.Sprintf("unknown cloud %q, please try %q", c.Cloud, "juju update-clouds")) 274 } 275 regions, err := detector.DetectRegions() 276 if err != nil && !errors.IsNotFound(err) { 277 // It's not an error to have no regions. 278 return errors.Annotatef(err, 279 "detecting regions for %q cloud provider", 280 c.Cloud, 281 ) 282 } 283 cloud = &jujucloud.Cloud{ 284 Type: c.Cloud, 285 Regions: regions, 286 } 287 } else if err != nil { 288 return errors.Trace(err) 289 } 290 if err := checkProviderType(cloud.Type); errors.IsNotFound(err) { 291 // This error will get handled later. 292 } else if err != nil { 293 return errors.Trace(err) 294 } 295 296 // Get the credentials and region name. 297 store := c.ClientStore() 298 credential, credentialName, regionName, err := modelcmd.GetCredentials( 299 store, c.Region, c.CredentialName, c.Cloud, cloud.Type, 300 ) 301 if errors.IsNotFound(err) && c.CredentialName == "" { 302 // No credential was explicitly specified, and no credential 303 // was found in credentials.yaml; have the provider detect 304 // credentials from the environment. 305 ctx.Verbosef("no credentials found, checking environment") 306 detected, err := modelcmd.DetectCredential(c.Cloud, cloud.Type) 307 if errors.Cause(err) == modelcmd.ErrMultipleCredentials { 308 return ambiguousCredentialError 309 } else if err != nil { 310 return errors.Trace(err) 311 } 312 // We have one credential so extract it from the map. 313 var oneCredential jujucloud.Credential 314 for _, oneCredential = range detected.AuthCredentials { 315 } 316 credential = &oneCredential 317 regionName = c.Region 318 if regionName == "" { 319 regionName = detected.DefaultRegion 320 } 321 logger.Tracef("authenticating with region %q and %v", regionName, credential) 322 } else if err != nil { 323 return errors.Trace(err) 324 } 325 326 region, err := getRegion(cloud, c.Cloud, regionName) 327 if err != nil { 328 return errors.Trace(err) 329 } 330 331 hostedModelUUID, err := utils.NewUUID() 332 if err != nil { 333 return errors.Trace(err) 334 } 335 controllerUUID, err := utils.NewUUID() 336 if err != nil { 337 return errors.Trace(err) 338 } 339 340 // Create an environment config from the cloud and credentials. 341 configAttrs := map[string]interface{}{ 342 "type": cloud.Type, 343 "name": environs.ControllerModelName, 344 config.UUIDKey: controllerUUID.String(), 345 config.ControllerUUIDKey: controllerUUID.String(), 346 } 347 userConfigAttrs, err := c.config.ReadAttrs(ctx) 348 if err != nil { 349 return errors.Trace(err) 350 } 351 for k, v := range userConfigAttrs { 352 configAttrs[k] = v 353 } 354 logger.Debugf("preparing controller with config: %v", configAttrs) 355 356 // Read existing current controller, account, model so we can clean up on error. 357 var oldCurrentController string 358 oldCurrentController, err = modelcmd.ReadCurrentController() 359 if err != nil { 360 return errors.Annotate(err, "error reading current controller") 361 } 362 363 defer func() { 364 if resultErr == nil || errors.IsAlreadyExists(resultErr) { 365 return 366 } 367 if oldCurrentController != "" { 368 if err := modelcmd.WriteCurrentController(oldCurrentController); err != nil { 369 logger.Warningf( 370 "cannot reset current controller to %q: %v", 371 oldCurrentController, err, 372 ) 373 } 374 } 375 if err := store.RemoveController(c.controllerName); err != nil { 376 logger.Warningf( 377 "cannot destroy newly created controller %q details: %v", 378 c.controllerName, err, 379 ) 380 } 381 }() 382 383 environ, err := environsPrepare( 384 modelcmd.BootstrapContext(ctx), store, 385 environs.PrepareParams{ 386 BaseConfig: configAttrs, 387 ControllerName: c.controllerName, 388 CloudName: c.Cloud, 389 CloudRegion: region.Name, 390 CloudEndpoint: region.Endpoint, 391 CloudStorageEndpoint: region.StorageEndpoint, 392 Credential: *credential, 393 CredentialName: credentialName, 394 }, 395 ) 396 if err != nil { 397 return errors.Trace(err) 398 } 399 400 // Set the current model to the initial hosted model. 401 accountName, err := store.CurrentAccount(c.controllerName) 402 if err != nil { 403 return errors.Trace(err) 404 } 405 if err := store.UpdateModel(c.controllerName, accountName, c.hostedModelName, jujuclient.ModelDetails{ 406 hostedModelUUID.String(), 407 }); err != nil { 408 return errors.Trace(err) 409 } 410 if err := store.SetCurrentModel(c.controllerName, accountName, c.hostedModelName); err != nil { 411 return errors.Trace(err) 412 } 413 414 // Set the current controller so "juju status" can be run while 415 // bootstrapping is underway. 416 if err := modelcmd.WriteCurrentController(c.controllerName); err != nil { 417 return errors.Trace(err) 418 } 419 420 cloudRegion := c.Cloud 421 if region.Name != "" { 422 cloudRegion = fmt.Sprintf("%s/%s", cloudRegion, region.Name) 423 } 424 ctx.Infof( 425 "Creating Juju controller %q on %s", 426 c.controllerName, cloudRegion, 427 ) 428 429 // If we error out for any reason, clean up the environment. 430 defer func() { 431 if resultErr != nil { 432 if c.KeepBrokenEnvironment { 433 logger.Warningf(` 434 bootstrap failed but --keep-broken was specified so model is not being destroyed. 435 When you are finished diagnosing the problem, remember to run juju destroy-model --force 436 to clean up the model.`[1:]) 437 } else { 438 handleBootstrapError(ctx, resultErr, func() error { 439 return environsDestroy( 440 c.controllerName, environ, store, 441 ) 442 }) 443 } 444 } 445 }() 446 447 // Block interruption during bootstrap. Providers may also 448 // register for interrupt notification so they can exit early. 449 interrupted := make(chan os.Signal, 1) 450 defer close(interrupted) 451 ctx.InterruptNotify(interrupted) 452 defer ctx.StopInterruptNotify(interrupted) 453 go func() { 454 for _ = range interrupted { 455 ctx.Infof("Interrupt signalled: waiting for bootstrap to exit") 456 } 457 }() 458 459 // If --metadata-source is specified, override the default tools metadata source so 460 // SyncTools can use it, and also upload any image metadata. 461 var metadataDir string 462 if c.MetadataSource != "" { 463 metadataDir = ctx.AbsPath(c.MetadataSource) 464 } 465 466 // Merge environ and bootstrap-specific constraints. 467 constraintsValidator, err := environ.ConstraintsValidator() 468 if err != nil { 469 return errors.Trace(err) 470 } 471 bootstrapConstraints, err := constraintsValidator.Merge( 472 c.Constraints, c.BootstrapConstraints, 473 ) 474 if err != nil { 475 return errors.Trace(err) 476 } 477 logger.Infof("combined bootstrap constraints: %v", bootstrapConstraints) 478 479 hostedModelConfig := map[string]interface{}{ 480 "name": c.hostedModelName, 481 config.UUIDKey: hostedModelUUID.String(), 482 } 483 484 // We copy across any user supplied attributes to the hosted model config. 485 // But only if the attributes have not been removed from the controller 486 // model config as part of preparing the controller model. 487 controllerConfigAttrs := environ.Config().AllAttrs() 488 for k, v := range userConfigAttrs { 489 if _, ok := controllerConfigAttrs[k]; ok { 490 hostedModelConfig[k] = v 491 } 492 } 493 // Ensure that certain config attributes are not included in the hosted 494 // model config. These attributes may be modified during bootstrap; by 495 // removing them from this map, we ensure the modified values are 496 // inherited. 497 delete(hostedModelConfig, config.AuthKeysConfig) 498 delete(hostedModelConfig, config.AgentVersionKey) 499 500 // Check whether the Juju GUI must be installed in the controller. 501 // Leaving this value empty means no GUI will be installed. 502 var guiDataSourceBaseURL string 503 if !c.noGUI { 504 guiDataSourceBaseURL = common.GUIDataSourceBaseURL() 505 } 506 507 err = bootstrapFuncs.Bootstrap(modelcmd.BootstrapContext(ctx), environ, bootstrap.BootstrapParams{ 508 ModelConstraints: c.Constraints, 509 BootstrapConstraints: bootstrapConstraints, 510 BootstrapSeries: c.BootstrapSeries, 511 BootstrapImage: c.BootstrapImage, 512 Placement: c.Placement, 513 UploadTools: c.UploadTools, 514 BuildToolsTarball: sync.BuildToolsTarball, 515 AgentVersion: c.AgentVersion, 516 MetadataDir: metadataDir, 517 HostedModelConfig: hostedModelConfig, 518 GUIDataSourceBaseURL: guiDataSourceBaseURL, 519 }) 520 if err != nil { 521 return errors.Annotate(err, "failed to bootstrap model") 522 } 523 524 if err := c.SetModelName(c.hostedModelName); err != nil { 525 return errors.Trace(err) 526 } 527 528 err = common.SetBootstrapEndpointAddress(c.ClientStore(), c.controllerName, environ) 529 if err != nil { 530 return errors.Annotate(err, "saving bootstrap endpoint address") 531 } 532 533 // To avoid race conditions when running scripted bootstraps, wait 534 // for the controller's machine agent to be ready to accept commands 535 // before exiting this bootstrap command. 536 return waitForAgentInitialisation(ctx, &c.ModelCommandBase, c.controllerName) 537 } 538 539 // getRegion returns the cloud.Region to use, based on the specified 540 // region name, and the region name selected if none was specified. 541 // 542 // If no region name is specified, and there is at least one region, 543 // we use the first region in the list. 544 // 545 // If no region name is specified, and there are no regions at all, 546 // then we synthesise a region from the cloud's endpoint information 547 // and just pass this on to the provider. 548 func getRegion(cloud *jujucloud.Cloud, cloudName, regionName string) (jujucloud.Region, error) { 549 if len(cloud.Regions) == 0 { 550 // The cloud does not specify regions, so assume 551 // that the cloud provider does not have a concept 552 // of regions, or has no pre-defined regions, and 553 // defer validation to the provider. 554 region := jujucloud.Region{ 555 regionName, 556 cloud.Endpoint, 557 cloud.StorageEndpoint, 558 } 559 return region, nil 560 } 561 if regionName == "" { 562 // No region was specified, use the first region in the list. 563 return cloud.Regions[0], nil 564 } 565 for _, region := range cloud.Regions { 566 // Do a case-insensitive comparison 567 if strings.EqualFold(region.Name, regionName) { 568 return region, nil 569 } 570 } 571 return jujucloud.Region{}, errors.NewNotFound(nil, fmt.Sprintf( 572 "region %q in cloud %q not found (expected one of %q)\nalternatively, try %q", 573 regionName, cloudName, cloudRegionNames(cloud), "juju update-clouds", 574 )) 575 } 576 577 func cloudRegionNames(cloud *jujucloud.Cloud) []string { 578 var regionNames []string 579 for _, region := range cloud.Regions { 580 regionNames = append(regionNames, region.Name) 581 } 582 return regionNames 583 } 584 585 // checkProviderType ensures the provider type is okay. 586 func checkProviderType(envType string) error { 587 featureflag.SetFlagsFromEnvironment(osenv.JujuFeatureFlagEnvKey) 588 flag, ok := provisionalProviders[envType] 589 if ok && !featureflag.Enabled(flag) { 590 msg := `the %q provider is provisional in this version of Juju. To use it anyway, set JUJU_DEV_FEATURE_FLAGS="%s" in your shell model` 591 return errors.Errorf(msg, envType, flag) 592 } 593 return nil 594 } 595 596 // handleBootstrapError is called to clean up if bootstrap fails. 597 func handleBootstrapError(ctx *cmd.Context, err error, cleanup func() error) { 598 ch := make(chan os.Signal, 1) 599 ctx.InterruptNotify(ch) 600 defer ctx.StopInterruptNotify(ch) 601 defer close(ch) 602 go func() { 603 for _ = range ch { 604 fmt.Fprintln(ctx.GetStderr(), "Cleaning up failed bootstrap") 605 } 606 }() 607 if err := cleanup(); err != nil { 608 logger.Errorf("error cleaning up: %v", err) 609 } 610 }