1
// Copyright 2012, 2013 Canonical Ltd.
2
// Licensed under the AGPLv3, see LICENCE file for details.
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
"gopkg.in/juju/names.v2"
25
"github.com/juju/juju/api"
26
"github.com/juju/juju/apiserver/params"
27
"github.com/juju/juju/cloud"
28
"github.com/juju/juju/cloudconfig/instancecfg"
29
"github.com/juju/juju/constraints"
30
"github.com/juju/juju/controller"
31
"github.com/juju/juju/environs"
32
"github.com/juju/juju/environs/config"
33
"github.com/juju/juju/environs/gui"
34
"github.com/juju/juju/environs/imagemetadata"
35
"github.com/juju/juju/environs/simplestreams"
36
"github.com/juju/juju/environs/storage"
37
"github.com/juju/juju/environs/sync"
38
"github.com/juju/juju/environs/tools"
39
"github.com/juju/juju/mongo"
40
coretools "github.com/juju/juju/tools"
41
jujuversion "github.com/juju/juju/version"
44
const noToolsMessage = `Juju cannot bootstrap because no tools are available for your model.
45
You may want to use the 'agent-metadata-url' configuration setting to specify the tools location.
49
logger = loggo.GetLogger("juju.environs.bootstrap")
52
// BootstrapParams holds the parameters for bootstrapping an environment.
53
type BootstrapParams struct {
54
// ModelConstraints are merged with the bootstrap constraints
55
// to choose the initial instance, and will be stored in the
56
// initial models' states.
57
ModelConstraints constraints.Value
59
// BootstrapConstraints are used to choose the initial instance.
60
// BootstrapConstraints does not affect the model constraints.
61
BootstrapConstraints constraints.Value
63
// BootstrapSeries, if specified, is the series to use for the
64
// initial bootstrap machine.
65
BootstrapSeries string
67
// BootstrapImage, if specified, is the image ID to use for the
68
// initial bootstrap machine.
71
// CloudName is the name of the cloud that Juju will be bootstrapped in.
74
// Cloud contains the properties of the cloud that Juju will be
78
// CloudRegion is the name of the cloud region that Juju will be bootstrapped in.
81
// CloudCredentialName is the name of the cloud credential that Juju will be
82
// bootstrapped with. This may be empty, for clouds that do not require
84
CloudCredentialName string
86
// CloudCredential contains the cloud credential that Juju will be
87
// bootstrapped with. This may be nil, for clouds that do not require
89
CloudCredential *cloud.Credential
91
// ControllerConfig is the set of config attributes relevant
93
ControllerConfig controller.Config
95
// ControllerInheritedConfig is the set of config attributes to be shared
96
// across all models in the same controller.
97
ControllerInheritedConfig map[string]interface{}
99
// HostedModelConfig is the set of config attributes to be overlaid
100
// on the controller config to construct the initial hosted model
102
HostedModelConfig map[string]interface{}
104
// Placement, if non-empty, holds an environment-specific placement
105
// directive used to choose the initial instance.
108
// UploadTools reports whether we should upload the local tools and
109
// override the environment's specified agent-version. It is an error
110
// to specify UploadTools with a nil BuildToolsTarball.
113
// BuildToolsTarball, if non-nil, is a function that may be used to
114
// build tools to upload. If this is nil, tools uploading will never
116
BuildToolsTarball sync.BuildToolsTarballFunc
118
// MetadataDir is an optional path to a local directory containing
119
// tools and/or image metadata.
122
// AgentVersion, if set, determines the exact tools version that
123
// will be used to start the Juju agents.
124
AgentVersion *version.Number
126
// GUIDataSourceBaseURL holds the simplestreams data source base URL
127
// used to retrieve the Juju GUI archive installed in the controller.
128
// If not set, the Juju GUI is not installed from simplestreams.
129
GUIDataSourceBaseURL string
131
// AdminSecret contains the administrator password.
134
// CAPrivateKey is the controller's CA certificate private key.
137
// DialOpts contains the bootstrap dial options.
138
DialOpts environs.BootstrapDialOpts
141
// Validate validates the bootstrap parameters.
142
func (p BootstrapParams) Validate() error {
143
if p.AdminSecret == "" {
144
return errors.New("admin-secret is empty")
146
if p.ControllerConfig.ControllerUUID() == "" {
147
return errors.New("controller configuration has no controller UUID")
149
if _, hasCACert := p.ControllerConfig.CACert(); !hasCACert {
150
return errors.New("controller configuration has no ca-cert")
152
if p.CAPrivateKey == "" {
153
return errors.New("empty ca-private-key")
155
// TODO(axw) validate other things.
159
// Bootstrap bootstraps the given environment. The supplied constraints are
160
// used to provision the instance, and are also set within the bootstrapped
162
func Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args BootstrapParams) error {
163
if err := args.Validate(); err != nil {
164
return errors.Annotate(err, "validating bootstrap parameters")
167
cfg := environ.Config()
168
if authKeys := ssh.SplitAuthorisedKeys(cfg.AuthorizedKeys()); len(authKeys) == 0 {
169
// Apparently this can never happen, so it's not tested. But, one day,
170
// Config will act differently (it's pretty crazy that, AFAICT, the
171
// authorized-keys are optional config settings... but it's impossible
172
// to actually *create* a config without them)... and when it does,
173
// we'll be here to catch this problem early.
174
return errors.Errorf("model configuration has no authorized-keys")
177
// Set default tools metadata source, add image metadata source,
178
// then verify constraints. Providers may rely on image metadata
179
// for constraint validation.
180
var customImageMetadata []*imagemetadata.ImageMetadata
181
if args.MetadataDir != "" {
183
customImageMetadata, err = setPrivateMetadataSources(environ, args.MetadataDir)
188
if err := validateConstraints(environ, args.ModelConstraints); err != nil {
191
if err := validateConstraints(environ, args.BootstrapConstraints); err != nil {
195
constraintsValidator, err := environ.ConstraintsValidator()
199
bootstrapConstraints, err := constraintsValidator.Merge(
200
args.ModelConstraints, args.BootstrapConstraints,
206
_, supportsNetworking := environs.SupportsNetworking(environ)
208
var bootstrapSeries *string
209
if args.BootstrapSeries != "" {
210
bootstrapSeries = &args.BootstrapSeries
213
ctx.Infof("Bootstrapping model %q", cfg.Name())
214
logger.Debugf("model %q supports service/machine networks: %v", cfg.Name(), supportsNetworking)
215
disableNetworkManagement, _ := cfg.DisableNetworkManagement()
216
logger.Debugf("network management by juju enabled: %v", !disableNetworkManagement)
217
availableTools, err := findAvailableTools(
218
environ, args.AgentVersion, bootstrapConstraints.Arch,
219
bootstrapSeries, args.UploadTools, args.BuildToolsTarball != nil,
221
if errors.IsNotFound(err) {
222
return errors.New(noToolsMessage)
223
} else if err != nil {
227
imageMetadata, err := bootstrapImageMetadata(
228
environ, availableTools,
230
&customImageMetadata,
233
return errors.Trace(err)
236
// If we're uploading, we must override agent-version;
237
// if we're not uploading, we want to ensure we have an
238
// agent-version set anyway, to appease FinishInstanceConfig.
239
// In the latter case, setBootstrapTools will later set
240
// agent-version to the correct thing.
241
agentVersion := jujuversion.Current
242
if args.AgentVersion != nil {
243
agentVersion = *args.AgentVersion
245
if cfg, err = cfg.Apply(map[string]interface{}{
246
"agent-version": agentVersion.String(),
250
if err = environ.SetConfig(cfg); err != nil {
254
ctx.Infof("Starting new instance for initial controller")
256
result, err := environ.Bootstrap(ctx, environs.BootstrapParams{
257
ControllerConfig: args.ControllerConfig,
258
ModelConstraints: args.ModelConstraints,
259
BootstrapConstraints: args.BootstrapConstraints,
260
BootstrapSeries: args.BootstrapSeries,
261
Placement: args.Placement,
262
AvailableTools: availableTools,
263
ImageMetadata: imageMetadata,
269
matchingTools, err := availableTools.Match(coretools.Filter{
271
Series: result.Series,
276
selectedToolsList, err := setBootstrapTools(environ, matchingTools)
280
havePrepackaged := false
281
for i, selectedTools := range selectedToolsList {
282
if selectedTools.URL != "" {
283
havePrepackaged = true
286
ctx.Infof("Building tools to upload (%s)", selectedTools.Version)
287
builtTools, err := args.BuildToolsTarball(&selectedTools.Version.Number, cfg.AgentStream())
289
return errors.Annotate(err, "cannot upload bootstrap tools")
291
defer os.RemoveAll(builtTools.Dir)
292
filename := filepath.Join(builtTools.Dir, builtTools.StorageName)
293
selectedTools.URL = fmt.Sprintf("file://%s", filename)
294
selectedTools.Size = builtTools.Size
295
selectedTools.SHA256 = builtTools.Sha256Hash
296
selectedToolsList[i] = selectedTools
298
if !havePrepackaged && !args.UploadTools {
299
// There are no prepackaged agents, so we must upload
300
// even though the user didn't ask for it. We only do
301
// this when the image-stream is not "released" and
302
// the agent version hasn't been specified.
303
logger.Infof("no prepackaged tools available")
306
ctx.Infof("Installing Juju agent on bootstrap instance")
307
publicKey, err := userPublicSigningKey()
311
instanceConfig, err := instancecfg.NewBootstrapInstanceConfig(
312
args.ControllerConfig,
313
args.BootstrapConstraints,
314
args.ModelConstraints,
321
if err := instanceConfig.SetTools(selectedToolsList); err != nil {
322
return errors.Trace(err)
324
// Make sure we have the most recent environ config as the specified
325
// tools version has been updated there.
326
cfg = environ.Config()
327
if err := finalizeInstanceBootstrapConfig(ctx, instanceConfig, args, cfg, customImageMetadata); err != nil {
328
return errors.Annotate(err, "finalizing bootstrap instance config")
330
if err := result.Finalize(ctx, instanceConfig, args.DialOpts); err != nil {
333
ctx.Infof("Bootstrap agent installed")
337
func finalizeInstanceBootstrapConfig(
338
ctx environs.BootstrapContext,
339
icfg *instancecfg.InstanceConfig,
340
args BootstrapParams,
342
customImageMetadata []*imagemetadata.ImageMetadata,
344
if icfg.APIInfo != nil || icfg.Controller.MongoInfo != nil {
345
return errors.New("machine configuration already has api/state info")
347
controllerCfg := icfg.Controller.Config
348
caCert, hasCACert := controllerCfg.CACert()
350
return errors.New("controller configuration has no ca-cert")
352
icfg.APIInfo = &api.Info{
353
Password: args.AdminSecret,
355
ModelTag: names.NewModelTag(cfg.UUID()),
357
icfg.Controller.MongoInfo = &mongo.MongoInfo{
358
Password: args.AdminSecret,
359
Info: mongo.Info{CACert: caCert},
362
// These really are directly relevant to running a controller.
363
// Initially, generate a controller certificate with no host IP
364
// addresses in the SAN field. Once the controller is up and the
365
// NIC addresses become known, the certificate can be regenerated.
366
cert, key, err := controller.GenerateControllerCertAndKey(caCert, args.CAPrivateKey, nil)
368
return errors.Annotate(err, "cannot generate controller certificate")
370
icfg.Bootstrap.StateServingInfo = params.StateServingInfo{
371
StatePort: controllerCfg.StatePort(),
372
APIPort: controllerCfg.APIPort(),
374
PrivateKey: string(key),
375
CAPrivateKey: args.CAPrivateKey,
377
if _, ok := cfg.AgentVersion(); !ok {
378
return errors.New("controller model configuration has no agent-version")
381
icfg.Bootstrap.ControllerModelConfig = cfg
382
icfg.Bootstrap.CustomImageMetadata = customImageMetadata
383
icfg.Bootstrap.ControllerCloudName = args.CloudName
384
icfg.Bootstrap.ControllerCloud = args.Cloud
385
icfg.Bootstrap.ControllerCloudRegion = args.CloudRegion
386
icfg.Bootstrap.ControllerCloudCredential = args.CloudCredential
387
icfg.Bootstrap.ControllerCloudCredentialName = args.CloudCredentialName
388
icfg.Bootstrap.ControllerConfig = args.ControllerConfig
389
icfg.Bootstrap.ControllerInheritedConfig = args.ControllerInheritedConfig
390
icfg.Bootstrap.HostedModelConfig = args.HostedModelConfig
391
icfg.Bootstrap.Timeout = args.DialOpts.Timeout
392
icfg.Bootstrap.GUI = guiArchive(args.GUIDataSourceBaseURL, func(msg string) {
398
func userPublicSigningKey() (string, error) {
399
signingKeyFile := os.Getenv("JUJU_STREAMS_PUBLICKEY_FILE")
401
if signingKeyFile != "" {
402
path, err := utils.NormalizePath(signingKeyFile)
404
return "", errors.Annotatef(err, "cannot expand key file path: %s", signingKeyFile)
406
b, err := ioutil.ReadFile(path)
408
return "", errors.Annotatef(err, "invalid public key file: %s", path)
410
signingKey = string(b)
412
return signingKey, nil
415
// bootstrapImageMetadata returns the image metadata to use for bootstrapping
416
// the given environment. If the environment provider does not make use of
417
// simplestreams, no metadata will be returned.
419
// If a bootstrap image ID is specified, image metadata will be synthesised
420
// using that image ID, and the architecture and series specified by the
421
// initiator. In addition, the custom image metadata that is saved into the
422
// state database will have the synthesised image metadata added to it.
423
func bootstrapImageMetadata(
424
environ environs.Environ,
425
availableTools coretools.List,
426
bootstrapImageId string,
427
customImageMetadata *[]*imagemetadata.ImageMetadata,
428
) ([]*imagemetadata.ImageMetadata, error) {
430
hasRegion, ok := environ.(simplestreams.HasRegion)
432
if bootstrapImageId != "" {
433
// We only support specifying image IDs for providers
434
// that use simplestreams for now.
435
return nil, errors.NotSupportedf(
436
"specifying bootstrap image for %q provider",
437
environ.Config().Type(),
440
// No region, no metadata.
443
region, err := hasRegion.Region()
445
return nil, errors.Trace(err)
448
if bootstrapImageId != "" {
449
arches := availableTools.Arches()
450
if len(arches) != 1 {
451
return nil, errors.NotValidf("multiple architectures with bootstrap image")
453
allSeries := availableTools.AllSeries()
454
if len(allSeries) != 1 {
455
return nil, errors.NotValidf("multiple series with bootstrap image")
457
seriesVersion, err := series.SeriesVersion(allSeries[0])
459
return nil, errors.Trace(err)
461
// The returned metadata does not have information about the
462
// storage or virtualisation type. Any provider that wants to
463
// filter on those properties should allow for empty values.
464
meta := &imagemetadata.ImageMetadata{
465
Id: bootstrapImageId,
467
Version: seriesVersion,
468
RegionName: region.Region,
469
Endpoint: region.Endpoint,
470
Stream: environ.Config().ImageStream(),
472
*customImageMetadata = append(*customImageMetadata, meta)
473
return []*imagemetadata.ImageMetadata{meta}, nil
476
// For providers that support making use of simplestreams
477
// image metadata, search public image metadata. We need
478
// to pass this onto Bootstrap for selecting images.
479
sources, err := environs.ImageMetadataSources(environ)
481
return nil, errors.Trace(err)
483
imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{
485
Series: availableTools.AllSeries(),
486
Arches: availableTools.Arches(),
487
Stream: environ.Config().ImageStream(),
489
logger.Debugf("constraints for image metadata lookup %v", imageConstraint)
491
// Get image metadata from all data sources.
492
// Since order of data source matters, order of image metadata matters too. Append is important here.
493
var publicImageMetadata []*imagemetadata.ImageMetadata
494
for _, source := range sources {
495
sourceMetadata, _, err := imagemetadata.Fetch([]simplestreams.DataSource{source}, imageConstraint)
497
logger.Debugf("ignoring image metadata in %s: %v", source.Description(), err)
498
// Just keep looking...
501
logger.Debugf("found %d image metadata in %s", len(sourceMetadata), source.Description())
502
publicImageMetadata = append(publicImageMetadata, sourceMetadata...)
505
logger.Debugf("found %d image metadata from all image data sources", len(publicImageMetadata))
506
if len(publicImageMetadata) == 0 {
507
return nil, errors.New("no image metadata found")
509
return publicImageMetadata, nil
512
// setBootstrapTools returns the newest tools from the given tools list,
513
// and updates the agent-version configuration attribute.
514
func setBootstrapTools(environ environs.Environ, possibleTools coretools.List) (coretools.List, error) {
515
if len(possibleTools) == 0 {
516
return nil, fmt.Errorf("no bootstrap tools available")
518
var newVersion version.Number
519
newVersion, toolsList := possibleTools.Newest()
520
logger.Infof("newest version: %s", newVersion)
521
cfg := environ.Config()
522
if agentVersion, _ := cfg.AgentVersion(); agentVersion != newVersion {
523
cfg, err := cfg.Apply(map[string]interface{}{
524
"agent-version": newVersion.String(),
527
err = environ.SetConfig(cfg)
530
return nil, fmt.Errorf("failed to update model configuration: %v", err)
533
bootstrapVersion := newVersion
534
// We should only ever bootstrap the exact same version as the client,
535
// or we risk bootstrap incompatibility. We still set agent-version to
536
// the newest version, so the agent will immediately upgrade itself.
537
if !isCompatibleVersion(newVersion, jujuversion.Current) {
538
compatibleVersion, compatibleTools := findCompatibleTools(possibleTools, jujuversion.Current)
539
if len(compatibleTools) == 0 {
541
"failed to find %s tools, will attempt to use %s",
542
jujuversion.Current, newVersion,
545
bootstrapVersion, toolsList = compatibleVersion, compatibleTools
548
logger.Infof("picked bootstrap tools version: %s", bootstrapVersion)
549
return toolsList, nil
552
// findCompatibleTools finds tools in the list that have the same major, minor
553
// and patch level as jujuversion.Current.
555
// Build number is not important to match; uploaded tools will have
556
// incremented build number, and we want to match them.
557
func findCompatibleTools(possibleTools coretools.List, version version.Number) (version.Number, coretools.List) {
558
var compatibleTools coretools.List
559
for _, tools := range possibleTools {
560
if isCompatibleVersion(tools.Version.Number, version) {
561
compatibleTools = append(compatibleTools, tools)
564
return compatibleTools.Newest()
567
func isCompatibleVersion(v1, v2 version.Number) bool {
570
return v1.Compare(v2) == 0
573
// setPrivateMetadataSources sets the default tools metadata source
574
// for tools syncing, and adds an image metadata source after verifying
576
func setPrivateMetadataSources(env environs.Environ, metadataDir string) ([]*imagemetadata.ImageMetadata, error) {
577
logger.Infof("Setting default tools and image metadata sources: %s", metadataDir)
578
tools.DefaultBaseURL = metadataDir
580
imageMetadataDir := filepath.Join(metadataDir, storage.BaseImagesPath)
581
if _, err := os.Stat(imageMetadataDir); err != nil {
582
if !os.IsNotExist(err) {
583
return nil, errors.Annotate(err, "cannot access image metadata")
588
baseURL := fmt.Sprintf("file://%s", filepath.ToSlash(imageMetadataDir))
589
publicKey, _ := simplestreams.UserPublicSigningKey()
590
datasource := simplestreams.NewURLSignedDataSource("bootstrap metadata", baseURL, publicKey, utils.NoVerifySSLHostnames, simplestreams.CUSTOM_CLOUD_DATA, false)
592
// Read the image metadata, as we'll want to upload it to the environment.
593
imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{})
594
existingMetadata, _, err := imagemetadata.Fetch([]simplestreams.DataSource{datasource}, imageConstraint)
595
if err != nil && !errors.IsNotFound(err) {
596
return nil, errors.Annotate(err, "cannot read image metadata")
599
// Add an image metadata datasource for constraint validation, etc.
600
environs.RegisterUserImageDataSourceFunc("bootstrap metadata", func(environs.Environ) (simplestreams.DataSource, error) {
601
return datasource, nil
603
logger.Infof("custom image metadata added to search path")
604
return existingMetadata, nil
607
func validateConstraints(env environs.Environ, cons constraints.Value) error {
608
validator, err := env.ConstraintsValidator()
610
return errors.Trace(err)
612
unsupported, err := validator.Validate(cons)
613
return errors.Annotatef(err, "unsupported constraints: %v", unsupported)
616
// guiArchive returns information on the GUI archive that will be uploaded
617
// to the controller. Possible errors in retrieving the GUI archive information
618
// do not prevent the model to be bootstrapped. If dataSourceBaseURL is
619
// non-empty, remote GUI archive info is retrieved from simplestreams using it
620
// as the base URL. The given logProgress function is used to inform users
621
// about errors or progress in setting up the Juju GUI.
622
func guiArchive(dataSourceBaseURL string, logProgress func(string)) *coretools.GUIArchive {
623
// The environment variable is only used for development purposes.
624
path := os.Getenv("JUJU_GUI")
626
vers, err := guiVersion(path)
628
logProgress(fmt.Sprintf("Cannot use Juju GUI at %q: %s", path, err))
631
hash, size, err := hashAndSize(path)
633
logProgress(fmt.Sprintf("Cannot use Juju GUI at %q: %s", path, err))
636
logProgress(fmt.Sprintf("Preparing for Juju GUI %s installation from local archive", vers))
637
return &coretools.GUIArchive{
639
URL: "file://" + filepath.ToSlash(path),
644
// Check if the user requested to bootstrap with no GUI.
645
if dataSourceBaseURL == "" {
646
logProgress("Juju GUI installation has been disabled")
649
// Fetch GUI archives info from simplestreams.
650
source := gui.NewDataSource(dataSourceBaseURL)
651
allMeta, err := guiFetchMetadata(gui.ReleasedStream, source)
653
logProgress(fmt.Sprintf("Unable to fetch Juju GUI info: %s", err))
656
if len(allMeta) == 0 {
657
logProgress("No available Juju GUI archives found")
660
// Metadata info are returned in descending version order.
661
logProgress(fmt.Sprintf("Preparing for Juju GUI %s release installation", allMeta[0].Version))
662
return &coretools.GUIArchive{
663
Version: allMeta[0].Version,
664
URL: allMeta[0].FullPath,
665
SHA256: allMeta[0].SHA256,
666
Size: allMeta[0].Size,
670
// guiFetchMetadata is defined for testing purposes.
671
var guiFetchMetadata = gui.FetchMetadata
673
// guiVersion retrieves the GUI version from the juju-gui-* directory included
674
// in the bz2 archive at the given path.
675
func guiVersion(path string) (version.Number, error) {
676
var number version.Number
677
f, err := os.Open(path)
679
return number, errors.Annotate(err, "cannot open Juju GUI archive")
683
r := tar.NewReader(bzip2.NewReader(f))
690
return number, errors.New("cannot read Juju GUI archive")
692
info := hdr.FileInfo()
693
if !info.IsDir() || !strings.HasPrefix(hdr.Name, prefix) {
696
n := info.Name()[len(prefix):]
697
number, err = version.Parse(n)
699
return number, errors.Errorf("cannot parse version %q", n)
703
return number, errors.New("cannot find Juju GUI version")
706
// hashAndSize calculates and returns the SHA256 hash and the size of the file
707
// located at the given path.
708
func hashAndSize(path string) (hash string, size int64, err error) {
709
f, err := os.Open(path)
711
return "", 0, errors.Mask(err)
715
size, err = io.Copy(h, f)
717
return "", 0, errors.Mask(err)
719
return fmt.Sprintf("%x", h.Sum(nil)), size, nil