github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/environs/bootstrap/bootstrap.go (about) 1 // Copyright 2012, 2013 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package bootstrap 5 6 import ( 7 "archive/tar" 8 "compress/bzip2" 9 "crypto/sha256" 10 "fmt" 11 "io" 12 "io/ioutil" 13 "os" 14 "path/filepath" 15 "strings" 16 17 "github.com/juju/errors" 18 "github.com/juju/loggo" 19 "github.com/juju/utils" 20 "github.com/juju/utils/series" 21 "github.com/juju/utils/ssh" 22 "github.com/juju/version" 23 24 "github.com/juju/juju/cloudconfig/instancecfg" 25 "github.com/juju/juju/constraints" 26 "github.com/juju/juju/environs" 27 "github.com/juju/juju/environs/gui" 28 "github.com/juju/juju/environs/imagemetadata" 29 "github.com/juju/juju/environs/simplestreams" 30 "github.com/juju/juju/environs/storage" 31 "github.com/juju/juju/environs/sync" 32 "github.com/juju/juju/environs/tools" 33 "github.com/juju/juju/network" 34 coretools "github.com/juju/juju/tools" 35 jujuversion "github.com/juju/juju/version" 36 ) 37 38 const noToolsMessage = `Juju cannot bootstrap because no tools are available for your model. 39 You may want to use the 'agent-metadata-url' configuration setting to specify the tools location. 40 ` 41 42 var ( 43 logger = loggo.GetLogger("juju.environs.bootstrap") 44 ) 45 46 // BootstrapParams holds the parameters for bootstrapping an environment. 47 type BootstrapParams struct { 48 // ModelConstraints are merged with the bootstrap constraints 49 // to choose the initial instance, and will be stored in the 50 // initial models' states. 51 ModelConstraints constraints.Value 52 53 // BootstrapConstraints are used to choose the initial instance. 54 // BootstrapConstraints does not affect the model constraints. 55 BootstrapConstraints constraints.Value 56 57 // BootstrapSeries, if specified, is the series to use for the 58 // initial bootstrap machine. 59 BootstrapSeries string 60 61 // BootstrapImage, if specified, is the image ID to use for the 62 // initial bootstrap machine. 63 BootstrapImage string 64 65 // HostedModelConfig is the set of config attributes to be overlaid 66 // on the controller config to construct the initial hosted model 67 // config. 68 HostedModelConfig map[string]interface{} 69 70 // Placement, if non-empty, holds an environment-specific placement 71 // directive used to choose the initial instance. 72 Placement string 73 74 // UploadTools reports whether we should upload the local tools and 75 // override the environment's specified agent-version. It is an error 76 // to specify UploadTools with a nil BuildToolsTarball. 77 UploadTools bool 78 79 // BuildToolsTarball, if non-nil, is a function that may be used to 80 // build tools to upload. If this is nil, tools uploading will never 81 // take place. 82 BuildToolsTarball sync.BuildToolsTarballFunc 83 84 // MetadataDir is an optional path to a local directory containing 85 // tools and/or image metadata. 86 MetadataDir string 87 88 // AgentVersion, if set, determines the exact tools version that 89 // will be used to start the Juju agents. 90 AgentVersion *version.Number 91 92 // GUIDataSourceBaseURL holds the simplestreams data source base URL 93 // used to retrieve the Juju GUI archive installed in the controller. 94 // If not set, the Juju GUI is not installed from simplestreams. 95 GUIDataSourceBaseURL string 96 } 97 98 // Bootstrap bootstraps the given environment. The supplied constraints are 99 // used to provision the instance, and are also set within the bootstrapped 100 // environment. 101 func Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args BootstrapParams) error { 102 cfg := environ.Config() 103 network.SetPreferIPv6(cfg.PreferIPv6()) 104 if secret := cfg.AdminSecret(); secret == "" { 105 return errors.Errorf("model configuration has no admin-secret") 106 } 107 if authKeys := ssh.SplitAuthorisedKeys(cfg.AuthorizedKeys()); len(authKeys) == 0 { 108 // Apparently this can never happen, so it's not tested. But, one day, 109 // Config will act differently (it's pretty crazy that, AFAICT, the 110 // authorized-keys are optional config settings... but it's impossible 111 // to actually *create* a config without them)... and when it does, 112 // we'll be here to catch this problem early. 113 return errors.Errorf("model configuration has no authorized-keys") 114 } 115 if _, hasCACert := cfg.CACert(); !hasCACert { 116 return errors.Errorf("model configuration has no ca-cert") 117 } 118 if _, hasCAKey := cfg.CAPrivateKey(); !hasCAKey { 119 return errors.Errorf("model configuration has no ca-private-key") 120 } 121 122 // Set default tools metadata source, add image metadata source, 123 // then verify constraints. Providers may rely on image metadata 124 // for constraint validation. 125 var customImageMetadata []*imagemetadata.ImageMetadata 126 if args.MetadataDir != "" { 127 var err error 128 customImageMetadata, err = setPrivateMetadataSources(environ, args.MetadataDir) 129 if err != nil { 130 return err 131 } 132 } 133 if err := validateConstraints(environ, args.ModelConstraints); err != nil { 134 return err 135 } 136 if err := validateConstraints(environ, args.BootstrapConstraints); err != nil { 137 return err 138 } 139 140 constraintsValidator, err := environ.ConstraintsValidator() 141 if err != nil { 142 return err 143 } 144 bootstrapConstraints, err := constraintsValidator.Merge( 145 args.ModelConstraints, args.BootstrapConstraints, 146 ) 147 if err != nil { 148 return err 149 } 150 151 _, supportsNetworking := environs.SupportsNetworking(environ) 152 153 var bootstrapSeries *string 154 if args.BootstrapSeries != "" { 155 bootstrapSeries = &args.BootstrapSeries 156 } 157 158 ctx.Infof("Bootstrapping model %q", cfg.Name()) 159 logger.Debugf("model %q supports service/machine networks: %v", cfg.Name(), supportsNetworking) 160 disableNetworkManagement, _ := cfg.DisableNetworkManagement() 161 logger.Debugf("network management by juju enabled: %v", !disableNetworkManagement) 162 availableTools, err := findAvailableTools( 163 environ, args.AgentVersion, bootstrapConstraints.Arch, 164 bootstrapSeries, args.UploadTools, args.BuildToolsTarball != nil, 165 ) 166 if errors.IsNotFound(err) { 167 return errors.New(noToolsMessage) 168 } else if err != nil { 169 return err 170 } 171 172 if lxcMTU, ok := cfg.LXCDefaultMTU(); ok { 173 logger.Debugf("using MTU %v for all created LXC containers' network interfaces", lxcMTU) 174 } 175 176 imageMetadata, err := bootstrapImageMetadata( 177 environ, availableTools, 178 args.BootstrapImage, 179 &customImageMetadata, 180 ) 181 if err != nil { 182 return errors.Trace(err) 183 } 184 185 // If we're uploading, we must override agent-version; 186 // if we're not uploading, we want to ensure we have an 187 // agent-version set anyway, to appease FinishInstanceConfig. 188 // In the latter case, setBootstrapTools will later set 189 // agent-version to the correct thing. 190 agentVersion := jujuversion.Current 191 if args.AgentVersion != nil { 192 agentVersion = *args.AgentVersion 193 } 194 if cfg, err = cfg.Apply(map[string]interface{}{ 195 "agent-version": agentVersion.String(), 196 }); err != nil { 197 return err 198 } 199 if err = environ.SetConfig(cfg); err != nil { 200 return err 201 } 202 203 ctx.Infof("Starting new instance for initial controller") 204 result, err := environ.Bootstrap(ctx, environs.BootstrapParams{ 205 ModelConstraints: args.ModelConstraints, 206 BootstrapConstraints: args.BootstrapConstraints, 207 BootstrapSeries: args.BootstrapSeries, 208 Placement: args.Placement, 209 AvailableTools: availableTools, 210 ImageMetadata: imageMetadata, 211 }) 212 if err != nil { 213 return err 214 } 215 216 matchingTools, err := availableTools.Match(coretools.Filter{ 217 Arch: result.Arch, 218 Series: result.Series, 219 }) 220 if err != nil { 221 return err 222 } 223 selectedToolsList, err := setBootstrapTools(environ, matchingTools) 224 if err != nil { 225 return err 226 } 227 havePrepackaged := false 228 for i, selectedTools := range selectedToolsList { 229 if selectedTools.URL != "" { 230 havePrepackaged = true 231 continue 232 } 233 ctx.Infof("Building tools to upload (%s)", selectedTools.Version) 234 builtTools, err := args.BuildToolsTarball(&selectedTools.Version.Number, cfg.AgentStream()) 235 if err != nil { 236 return errors.Annotate(err, "cannot upload bootstrap tools") 237 } 238 defer os.RemoveAll(builtTools.Dir) 239 filename := filepath.Join(builtTools.Dir, builtTools.StorageName) 240 selectedTools.URL = fmt.Sprintf("file://%s", filename) 241 selectedTools.Size = builtTools.Size 242 selectedTools.SHA256 = builtTools.Sha256Hash 243 selectedToolsList[i] = selectedTools 244 } 245 if !havePrepackaged && !args.UploadTools { 246 // There are no prepackaged agents, so we must upload 247 // even though the user didn't ask for it. We only do 248 // this when the image-stream is not "released" and 249 // the agent version hasn't been specified. 250 logger.Warningf("no prepackaged tools available") 251 } 252 253 ctx.Infof("Installing Juju agent on bootstrap instance") 254 publicKey, err := userPublicSigningKey() 255 if err != nil { 256 return err 257 } 258 instanceConfig, err := instancecfg.NewBootstrapInstanceConfig( 259 args.BootstrapConstraints, args.ModelConstraints, result.Series, publicKey, 260 ) 261 if err != nil { 262 return err 263 } 264 if err := instanceConfig.SetTools(selectedToolsList); err != nil { 265 return errors.Trace(err) 266 } 267 instanceConfig.CustomImageMetadata = customImageMetadata 268 instanceConfig.HostedModelConfig = args.HostedModelConfig 269 270 instanceConfig.GUI = guiArchive(args.GUIDataSourceBaseURL, func(msg string) { 271 ctx.Infof(msg) 272 }) 273 274 if err := result.Finalize(ctx, instanceConfig); err != nil { 275 return err 276 } 277 ctx.Infof("Bootstrap agent installed") 278 return nil 279 } 280 281 func userPublicSigningKey() (string, error) { 282 signingKeyFile := os.Getenv("JUJU_STREAMS_PUBLICKEY_FILE") 283 signingKey := "" 284 if signingKeyFile != "" { 285 path, err := utils.NormalizePath(signingKeyFile) 286 if err != nil { 287 return "", errors.Annotatef(err, "cannot expand key file path: %s", signingKeyFile) 288 } 289 b, err := ioutil.ReadFile(path) 290 if err != nil { 291 return "", errors.Annotatef(err, "invalid public key file: %s", path) 292 } 293 signingKey = string(b) 294 } 295 return signingKey, nil 296 } 297 298 // bootstrapImageMetadata returns the image metadata to use for bootstrapping 299 // the given environment. If the environment provider does not make use of 300 // simplestreams, no metadata will be returned. 301 // 302 // If a bootstrap image ID is specified, image metadata will be synthesised 303 // using that image ID, and the architecture and series specified by the 304 // initiator. In addition, the custom image metadata that is saved into the 305 // state database will have the synthesised image metadata added to it. 306 func bootstrapImageMetadata( 307 environ environs.Environ, 308 availableTools coretools.List, 309 bootstrapImageId string, 310 customImageMetadata *[]*imagemetadata.ImageMetadata, 311 ) ([]*imagemetadata.ImageMetadata, error) { 312 313 hasRegion, ok := environ.(simplestreams.HasRegion) 314 if !ok { 315 if bootstrapImageId != "" { 316 // We only support specifying image IDs for providers 317 // that use simplestreams for now. 318 return nil, errors.NotSupportedf( 319 "specifying bootstrap image for %q provider", 320 environ.Config().Type(), 321 ) 322 } 323 // No region, no metadata. 324 return nil, nil 325 } 326 region, err := hasRegion.Region() 327 if err != nil { 328 return nil, errors.Trace(err) 329 } 330 331 if bootstrapImageId != "" { 332 arches := availableTools.Arches() 333 if len(arches) != 1 { 334 return nil, errors.NotValidf("multiple architectures with bootstrap image") 335 } 336 allSeries := availableTools.AllSeries() 337 if len(allSeries) != 1 { 338 return nil, errors.NotValidf("multiple series with bootstrap image") 339 } 340 seriesVersion, err := series.SeriesVersion(allSeries[0]) 341 if err != nil { 342 return nil, errors.Trace(err) 343 } 344 // The returned metadata does not have information about the 345 // storage or virtualisation type. Any provider that wants to 346 // filter on those properties should allow for empty values. 347 meta := &imagemetadata.ImageMetadata{ 348 Id: bootstrapImageId, 349 Arch: arches[0], 350 Version: seriesVersion, 351 RegionName: region.Region, 352 Endpoint: region.Endpoint, 353 Stream: environ.Config().ImageStream(), 354 } 355 *customImageMetadata = append(*customImageMetadata, meta) 356 return []*imagemetadata.ImageMetadata{meta}, nil 357 } 358 359 // For providers that support making use of simplestreams 360 // image metadata, search public image metadata. We need 361 // to pass this onto Bootstrap for selecting images. 362 sources, err := environs.ImageMetadataSources(environ) 363 if err != nil { 364 return nil, errors.Trace(err) 365 } 366 imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{ 367 CloudSpec: region, 368 Series: availableTools.AllSeries(), 369 Arches: availableTools.Arches(), 370 Stream: environ.Config().ImageStream(), 371 }) 372 logger.Debugf("constraints for image metadata lookup %v", imageConstraint) 373 374 // Get image metadata from all data sources. 375 // Since order of data source matters, order of image metadata matters too. Append is important here. 376 var publicImageMetadata []*imagemetadata.ImageMetadata 377 for _, source := range sources { 378 sourceMetadata, _, err := imagemetadata.Fetch([]simplestreams.DataSource{source}, imageConstraint) 379 if err != nil { 380 logger.Debugf("ignoring image metadata in %s: %v", source.Description(), err) 381 // Just keep looking... 382 continue 383 } 384 logger.Debugf("found %d image metadata in %s", len(sourceMetadata), source.Description()) 385 publicImageMetadata = append(publicImageMetadata, sourceMetadata...) 386 } 387 388 logger.Debugf("found %d image metadata from all image data sources", len(publicImageMetadata)) 389 if len(publicImageMetadata) == 0 { 390 return nil, errors.New("no image metadata found") 391 } 392 return publicImageMetadata, nil 393 } 394 395 // setBootstrapTools returns the newest tools from the given tools list, 396 // and updates the agent-version configuration attribute. 397 func setBootstrapTools(environ environs.Environ, possibleTools coretools.List) (coretools.List, error) { 398 if len(possibleTools) == 0 { 399 return nil, fmt.Errorf("no bootstrap tools available") 400 } 401 var newVersion version.Number 402 newVersion, toolsList := possibleTools.Newest() 403 logger.Infof("newest version: %s", newVersion) 404 cfg := environ.Config() 405 if agentVersion, _ := cfg.AgentVersion(); agentVersion != newVersion { 406 cfg, err := cfg.Apply(map[string]interface{}{ 407 "agent-version": newVersion.String(), 408 }) 409 if err == nil { 410 err = environ.SetConfig(cfg) 411 } 412 if err != nil { 413 return nil, fmt.Errorf("failed to update model configuration: %v", err) 414 } 415 } 416 bootstrapVersion := newVersion 417 // We should only ever bootstrap the exact same version as the client, 418 // or we risk bootstrap incompatibility. We still set agent-version to 419 // the newest version, so the agent will immediately upgrade itself. 420 if !isCompatibleVersion(newVersion, jujuversion.Current) { 421 compatibleVersion, compatibleTools := findCompatibleTools(possibleTools, jujuversion.Current) 422 if len(compatibleTools) == 0 { 423 logger.Warningf( 424 "failed to find %s tools, will attempt to use %s", 425 jujuversion.Current, newVersion, 426 ) 427 } else { 428 bootstrapVersion, toolsList = compatibleVersion, compatibleTools 429 } 430 } 431 logger.Infof("picked bootstrap tools version: %s", bootstrapVersion) 432 return toolsList, nil 433 } 434 435 // findCompatibleTools finds tools in the list that have the same major, minor 436 // and patch level as jujuversion.Current. 437 // 438 // Build number is not important to match; uploaded tools will have 439 // incremented build number, and we want to match them. 440 func findCompatibleTools(possibleTools coretools.List, version version.Number) (version.Number, coretools.List) { 441 var compatibleTools coretools.List 442 for _, tools := range possibleTools { 443 if isCompatibleVersion(tools.Version.Number, version) { 444 compatibleTools = append(compatibleTools, tools) 445 } 446 } 447 return compatibleTools.Newest() 448 } 449 450 func isCompatibleVersion(v1, v2 version.Number) bool { 451 v1.Build = 0 452 v2.Build = 0 453 return v1.Compare(v2) == 0 454 } 455 456 // setPrivateMetadataSources sets the default tools metadata source 457 // for tools syncing, and adds an image metadata source after verifying 458 // the contents. 459 func setPrivateMetadataSources(env environs.Environ, metadataDir string) ([]*imagemetadata.ImageMetadata, error) { 460 logger.Infof("Setting default tools and image metadata sources: %s", metadataDir) 461 tools.DefaultBaseURL = metadataDir 462 463 imageMetadataDir := filepath.Join(metadataDir, storage.BaseImagesPath) 464 if _, err := os.Stat(imageMetadataDir); err != nil { 465 if !os.IsNotExist(err) { 466 return nil, errors.Annotate(err, "cannot access image metadata") 467 } 468 return nil, nil 469 } 470 471 baseURL := fmt.Sprintf("file://%s", filepath.ToSlash(imageMetadataDir)) 472 publicKey, _ := simplestreams.UserPublicSigningKey() 473 datasource := simplestreams.NewURLSignedDataSource("bootstrap metadata", baseURL, publicKey, utils.NoVerifySSLHostnames, simplestreams.CUSTOM_CLOUD_DATA, false) 474 475 // Read the image metadata, as we'll want to upload it to the environment. 476 imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{}) 477 existingMetadata, _, err := imagemetadata.Fetch([]simplestreams.DataSource{datasource}, imageConstraint) 478 if err != nil && !errors.IsNotFound(err) { 479 return nil, errors.Annotate(err, "cannot read image metadata") 480 } 481 482 // Add an image metadata datasource for constraint validation, etc. 483 environs.RegisterUserImageDataSourceFunc("bootstrap metadata", func(environs.Environ) (simplestreams.DataSource, error) { 484 return datasource, nil 485 }) 486 logger.Infof("custom image metadata added to search path") 487 return existingMetadata, nil 488 } 489 490 func validateConstraints(env environs.Environ, cons constraints.Value) error { 491 validator, err := env.ConstraintsValidator() 492 if err != nil { 493 return err 494 } 495 unsupported, err := validator.Validate(cons) 496 if len(unsupported) > 0 { 497 logger.Warningf("unsupported constraints: %v", unsupported) 498 } 499 return err 500 } 501 502 // guiArchive returns information on the GUI archive that will be uploaded 503 // to the controller. Possible errors in retrieving the GUI archive information 504 // do not prevent the model to be bootstrapped. If dataSourceBaseURL is 505 // non-empty, remote GUI archive info is retrieved from simplestreams using it 506 // as the base URL. The given logProgress function is used to inform users 507 // about errors or progress in setting up the Juju GUI. 508 func guiArchive(dataSourceBaseURL string, logProgress func(string)) *coretools.GUIArchive { 509 // The environment variable is only used for development purposes. 510 path := os.Getenv("JUJU_GUI") 511 if path != "" { 512 vers, err := guiVersion(path) 513 if err != nil { 514 logProgress(fmt.Sprintf("Cannot use Juju GUI at %q: %s", path, err)) 515 return nil 516 } 517 hash, size, err := hashAndSize(path) 518 if err != nil { 519 logProgress(fmt.Sprintf("Cannot use Juju GUI at %q: %s", path, err)) 520 return nil 521 } 522 logProgress(fmt.Sprintf("Preparing for Juju GUI %s installation from local archive", vers)) 523 return &coretools.GUIArchive{ 524 Version: vers, 525 URL: "file://" + filepath.ToSlash(path), 526 SHA256: hash, 527 Size: size, 528 } 529 } 530 // Check if the user requested to bootstrap with no GUI. 531 if dataSourceBaseURL == "" { 532 logProgress("Juju GUI installation has been disabled") 533 return nil 534 } 535 // Fetch GUI archives info from simplestreams. 536 source := gui.NewDataSource(dataSourceBaseURL) 537 allMeta, err := guiFetchMetadata(gui.ReleasedStream, source) 538 if err != nil { 539 logProgress(fmt.Sprintf("Unable to fetch Juju GUI info: %s", err)) 540 return nil 541 } 542 if len(allMeta) == 0 { 543 logProgress("No available Juju GUI archives found") 544 return nil 545 } 546 // Metadata info are returned in descending version order. 547 logProgress(fmt.Sprintf("Preparing for Juju GUI %s release installation", allMeta[0].Version)) 548 return &coretools.GUIArchive{ 549 Version: allMeta[0].Version, 550 URL: allMeta[0].FullPath, 551 SHA256: allMeta[0].SHA256, 552 Size: allMeta[0].Size, 553 } 554 } 555 556 // guiFetchMetadata is defined for testing purposes. 557 var guiFetchMetadata = gui.FetchMetadata 558 559 // guiVersion retrieves the GUI version from the juju-gui-* directory included 560 // in the bz2 archive at the given path. 561 func guiVersion(path string) (version.Number, error) { 562 var number version.Number 563 f, err := os.Open(path) 564 if err != nil { 565 return number, errors.Annotate(err, "cannot open Juju GUI archive") 566 } 567 defer f.Close() 568 prefix := "jujugui-" 569 r := tar.NewReader(bzip2.NewReader(f)) 570 for { 571 hdr, err := r.Next() 572 if err == io.EOF { 573 break 574 } 575 if err != nil { 576 return number, errors.New("cannot read Juju GUI archive") 577 } 578 info := hdr.FileInfo() 579 if !info.IsDir() || !strings.HasPrefix(hdr.Name, prefix) { 580 continue 581 } 582 n := info.Name()[len(prefix):] 583 number, err = version.Parse(n) 584 if err != nil { 585 return number, errors.Errorf("cannot parse version %q", n) 586 } 587 return number, nil 588 } 589 return number, errors.New("cannot find Juju GUI version") 590 } 591 592 // hashAndSize calculates and returns the SHA256 hash and the size of the file 593 // located at the given path. 594 func hashAndSize(path string) (hash string, size int64, err error) { 595 f, err := os.Open(path) 596 if err != nil { 597 return "", 0, errors.Mask(err) 598 } 599 defer f.Close() 600 h := sha256.New() 601 size, err = io.Copy(h, f) 602 if err != nil { 603 return "", 0, errors.Mask(err) 604 } 605 return fmt.Sprintf("%x", h.Sum(nil)), size, nil 606 }