~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/environs/bootstrap/bootstrap.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
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
        "gopkg.in/juju/names.v2"
 
24
 
 
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"
 
42
)
 
43
 
 
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.
 
46
`
 
47
 
 
48
var (
 
49
        logger = loggo.GetLogger("juju.environs.bootstrap")
 
50
)
 
51
 
 
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
 
58
 
 
59
        // BootstrapConstraints are used to choose the initial instance.
 
60
        // BootstrapConstraints does not affect the model constraints.
 
61
        BootstrapConstraints constraints.Value
 
62
 
 
63
        // BootstrapSeries, if specified, is the series to use for the
 
64
        // initial bootstrap machine.
 
65
        BootstrapSeries string
 
66
 
 
67
        // BootstrapImage, if specified, is the image ID to use for the
 
68
        // initial bootstrap machine.
 
69
        BootstrapImage string
 
70
 
 
71
        // CloudName is the name of the cloud that Juju will be bootstrapped in.
 
72
        CloudName string
 
73
 
 
74
        // Cloud contains the properties of the cloud that Juju will be
 
75
        // bootstrapped in.
 
76
        Cloud cloud.Cloud
 
77
 
 
78
        // CloudRegion is the name of the cloud region that Juju will be bootstrapped in.
 
79
        CloudRegion string
 
80
 
 
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
 
83
        // credentials.
 
84
        CloudCredentialName string
 
85
 
 
86
        // CloudCredential contains the cloud credential that Juju will be
 
87
        // bootstrapped with. This may be nil, for clouds that do not require
 
88
        // credentialis.
 
89
        CloudCredential *cloud.Credential
 
90
 
 
91
        // ControllerConfig is the set of config attributes relevant
 
92
        // to a controller.
 
93
        ControllerConfig controller.Config
 
94
 
 
95
        // ControllerInheritedConfig is the set of config attributes to be shared
 
96
        // across all models in the same controller.
 
97
        ControllerInheritedConfig map[string]interface{}
 
98
 
 
99
        // HostedModelConfig is the set of config attributes to be overlaid
 
100
        // on the controller config to construct the initial hosted model
 
101
        // config.
 
102
        HostedModelConfig map[string]interface{}
 
103
 
 
104
        // Placement, if non-empty, holds an environment-specific placement
 
105
        // directive used to choose the initial instance.
 
106
        Placement string
 
107
 
 
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.
 
111
        UploadTools bool
 
112
 
 
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
 
115
        // take place.
 
116
        BuildToolsTarball sync.BuildToolsTarballFunc
 
117
 
 
118
        // MetadataDir is an optional path to a local directory containing
 
119
        // tools and/or image metadata.
 
120
        MetadataDir string
 
121
 
 
122
        // AgentVersion, if set, determines the exact tools version that
 
123
        // will be used to start the Juju agents.
 
124
        AgentVersion *version.Number
 
125
 
 
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
 
130
 
 
131
        // AdminSecret contains the administrator password.
 
132
        AdminSecret string
 
133
 
 
134
        // CAPrivateKey is the controller's CA certificate private key.
 
135
        CAPrivateKey string
 
136
 
 
137
        // DialOpts contains the bootstrap dial options.
 
138
        DialOpts environs.BootstrapDialOpts
 
139
}
 
140
 
 
141
// Validate validates the bootstrap parameters.
 
142
func (p BootstrapParams) Validate() error {
 
143
        if p.AdminSecret == "" {
 
144
                return errors.New("admin-secret is empty")
 
145
        }
 
146
        if p.ControllerConfig.ControllerUUID() == "" {
 
147
                return errors.New("controller configuration has no controller UUID")
 
148
        }
 
149
        if _, hasCACert := p.ControllerConfig.CACert(); !hasCACert {
 
150
                return errors.New("controller configuration has no ca-cert")
 
151
        }
 
152
        if p.CAPrivateKey == "" {
 
153
                return errors.New("empty ca-private-key")
 
154
        }
 
155
        // TODO(axw) validate other things.
 
156
        return nil
 
157
}
 
158
 
 
159
// Bootstrap bootstraps the given environment. The supplied constraints are
 
160
// used to provision the instance, and are also set within the bootstrapped
 
161
// environment.
 
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")
 
165
        }
 
166
 
 
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")
 
175
        }
 
176
 
 
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 != "" {
 
182
                var err error
 
183
                customImageMetadata, err = setPrivateMetadataSources(environ, args.MetadataDir)
 
184
                if err != nil {
 
185
                        return err
 
186
                }
 
187
        }
 
188
        if err := validateConstraints(environ, args.ModelConstraints); err != nil {
 
189
                return err
 
190
        }
 
191
        if err := validateConstraints(environ, args.BootstrapConstraints); err != nil {
 
192
                return err
 
193
        }
 
194
 
 
195
        constraintsValidator, err := environ.ConstraintsValidator()
 
196
        if err != nil {
 
197
                return err
 
198
        }
 
199
        bootstrapConstraints, err := constraintsValidator.Merge(
 
200
                args.ModelConstraints, args.BootstrapConstraints,
 
201
        )
 
202
        if err != nil {
 
203
                return err
 
204
        }
 
205
 
 
206
        _, supportsNetworking := environs.SupportsNetworking(environ)
 
207
 
 
208
        var bootstrapSeries *string
 
209
        if args.BootstrapSeries != "" {
 
210
                bootstrapSeries = &args.BootstrapSeries
 
211
        }
 
212
 
 
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,
 
220
        )
 
221
        if errors.IsNotFound(err) {
 
222
                return errors.New(noToolsMessage)
 
223
        } else if err != nil {
 
224
                return err
 
225
        }
 
226
 
 
227
        imageMetadata, err := bootstrapImageMetadata(
 
228
                environ, availableTools,
 
229
                args.BootstrapImage,
 
230
                &customImageMetadata,
 
231
        )
 
232
        if err != nil {
 
233
                return errors.Trace(err)
 
234
        }
 
235
 
 
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
 
244
        }
 
245
        if cfg, err = cfg.Apply(map[string]interface{}{
 
246
                "agent-version": agentVersion.String(),
 
247
        }); err != nil {
 
248
                return err
 
249
        }
 
250
        if err = environ.SetConfig(cfg); err != nil {
 
251
                return err
 
252
        }
 
253
 
 
254
        ctx.Infof("Starting new instance for initial controller")
 
255
 
 
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,
 
264
        })
 
265
        if err != nil {
 
266
                return err
 
267
        }
 
268
 
 
269
        matchingTools, err := availableTools.Match(coretools.Filter{
 
270
                Arch:   result.Arch,
 
271
                Series: result.Series,
 
272
        })
 
273
        if err != nil {
 
274
                return err
 
275
        }
 
276
        selectedToolsList, err := setBootstrapTools(environ, matchingTools)
 
277
        if err != nil {
 
278
                return err
 
279
        }
 
280
        havePrepackaged := false
 
281
        for i, selectedTools := range selectedToolsList {
 
282
                if selectedTools.URL != "" {
 
283
                        havePrepackaged = true
 
284
                        continue
 
285
                }
 
286
                ctx.Infof("Building tools to upload (%s)", selectedTools.Version)
 
287
                builtTools, err := args.BuildToolsTarball(&selectedTools.Version.Number, cfg.AgentStream())
 
288
                if err != nil {
 
289
                        return errors.Annotate(err, "cannot upload bootstrap tools")
 
290
                }
 
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
 
297
        }
 
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")
 
304
        }
 
305
 
 
306
        ctx.Infof("Installing Juju agent on bootstrap instance")
 
307
        publicKey, err := userPublicSigningKey()
 
308
        if err != nil {
 
309
                return err
 
310
        }
 
311
        instanceConfig, err := instancecfg.NewBootstrapInstanceConfig(
 
312
                args.ControllerConfig,
 
313
                args.BootstrapConstraints,
 
314
                args.ModelConstraints,
 
315
                result.Series,
 
316
                publicKey,
 
317
        )
 
318
        if err != nil {
 
319
                return err
 
320
        }
 
321
        if err := instanceConfig.SetTools(selectedToolsList); err != nil {
 
322
                return errors.Trace(err)
 
323
        }
 
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")
 
329
        }
 
330
        if err := result.Finalize(ctx, instanceConfig, args.DialOpts); err != nil {
 
331
                return err
 
332
        }
 
333
        ctx.Infof("Bootstrap agent installed")
 
334
        return nil
 
335
}
 
336
 
 
337
func finalizeInstanceBootstrapConfig(
 
338
        ctx environs.BootstrapContext,
 
339
        icfg *instancecfg.InstanceConfig,
 
340
        args BootstrapParams,
 
341
        cfg *config.Config,
 
342
        customImageMetadata []*imagemetadata.ImageMetadata,
 
343
) error {
 
344
        if icfg.APIInfo != nil || icfg.Controller.MongoInfo != nil {
 
345
                return errors.New("machine configuration already has api/state info")
 
346
        }
 
347
        controllerCfg := icfg.Controller.Config
 
348
        caCert, hasCACert := controllerCfg.CACert()
 
349
        if !hasCACert {
 
350
                return errors.New("controller configuration has no ca-cert")
 
351
        }
 
352
        icfg.APIInfo = &api.Info{
 
353
                Password: args.AdminSecret,
 
354
                CACert:   caCert,
 
355
                ModelTag: names.NewModelTag(cfg.UUID()),
 
356
        }
 
357
        icfg.Controller.MongoInfo = &mongo.MongoInfo{
 
358
                Password: args.AdminSecret,
 
359
                Info:     mongo.Info{CACert: caCert},
 
360
        }
 
361
 
 
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)
 
367
        if err != nil {
 
368
                return errors.Annotate(err, "cannot generate controller certificate")
 
369
        }
 
370
        icfg.Bootstrap.StateServingInfo = params.StateServingInfo{
 
371
                StatePort:    controllerCfg.StatePort(),
 
372
                APIPort:      controllerCfg.APIPort(),
 
373
                Cert:         string(cert),
 
374
                PrivateKey:   string(key),
 
375
                CAPrivateKey: args.CAPrivateKey,
 
376
        }
 
377
        if _, ok := cfg.AgentVersion(); !ok {
 
378
                return errors.New("controller model configuration has no agent-version")
 
379
        }
 
380
 
 
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) {
 
393
                ctx.Infof(msg)
 
394
        })
 
395
        return nil
 
396
}
 
397
 
 
398
func userPublicSigningKey() (string, error) {
 
399
        signingKeyFile := os.Getenv("JUJU_STREAMS_PUBLICKEY_FILE")
 
400
        signingKey := ""
 
401
        if signingKeyFile != "" {
 
402
                path, err := utils.NormalizePath(signingKeyFile)
 
403
                if err != nil {
 
404
                        return "", errors.Annotatef(err, "cannot expand key file path: %s", signingKeyFile)
 
405
                }
 
406
                b, err := ioutil.ReadFile(path)
 
407
                if err != nil {
 
408
                        return "", errors.Annotatef(err, "invalid public key file: %s", path)
 
409
                }
 
410
                signingKey = string(b)
 
411
        }
 
412
        return signingKey, nil
 
413
}
 
414
 
 
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.
 
418
//
 
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) {
 
429
 
 
430
        hasRegion, ok := environ.(simplestreams.HasRegion)
 
431
        if !ok {
 
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(),
 
438
                        )
 
439
                }
 
440
                // No region, no metadata.
 
441
                return nil, nil
 
442
        }
 
443
        region, err := hasRegion.Region()
 
444
        if err != nil {
 
445
                return nil, errors.Trace(err)
 
446
        }
 
447
 
 
448
        if bootstrapImageId != "" {
 
449
                arches := availableTools.Arches()
 
450
                if len(arches) != 1 {
 
451
                        return nil, errors.NotValidf("multiple architectures with bootstrap image")
 
452
                }
 
453
                allSeries := availableTools.AllSeries()
 
454
                if len(allSeries) != 1 {
 
455
                        return nil, errors.NotValidf("multiple series with bootstrap image")
 
456
                }
 
457
                seriesVersion, err := series.SeriesVersion(allSeries[0])
 
458
                if err != nil {
 
459
                        return nil, errors.Trace(err)
 
460
                }
 
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,
 
466
                        Arch:       arches[0],
 
467
                        Version:    seriesVersion,
 
468
                        RegionName: region.Region,
 
469
                        Endpoint:   region.Endpoint,
 
470
                        Stream:     environ.Config().ImageStream(),
 
471
                }
 
472
                *customImageMetadata = append(*customImageMetadata, meta)
 
473
                return []*imagemetadata.ImageMetadata{meta}, nil
 
474
        }
 
475
 
 
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)
 
480
        if err != nil {
 
481
                return nil, errors.Trace(err)
 
482
        }
 
483
        imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{
 
484
                CloudSpec: region,
 
485
                Series:    availableTools.AllSeries(),
 
486
                Arches:    availableTools.Arches(),
 
487
                Stream:    environ.Config().ImageStream(),
 
488
        })
 
489
        logger.Debugf("constraints for image metadata lookup %v", imageConstraint)
 
490
 
 
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)
 
496
                if err != nil {
 
497
                        logger.Debugf("ignoring image metadata in %s: %v", source.Description(), err)
 
498
                        // Just keep looking...
 
499
                        continue
 
500
                }
 
501
                logger.Debugf("found %d image metadata in %s", len(sourceMetadata), source.Description())
 
502
                publicImageMetadata = append(publicImageMetadata, sourceMetadata...)
 
503
        }
 
504
 
 
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")
 
508
        }
 
509
        return publicImageMetadata, nil
 
510
}
 
511
 
 
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")
 
517
        }
 
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(),
 
525
                })
 
526
                if err == nil {
 
527
                        err = environ.SetConfig(cfg)
 
528
                }
 
529
                if err != nil {
 
530
                        return nil, fmt.Errorf("failed to update model configuration: %v", err)
 
531
                }
 
532
        }
 
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 {
 
540
                        logger.Infof(
 
541
                                "failed to find %s tools, will attempt to use %s",
 
542
                                jujuversion.Current, newVersion,
 
543
                        )
 
544
                } else {
 
545
                        bootstrapVersion, toolsList = compatibleVersion, compatibleTools
 
546
                }
 
547
        }
 
548
        logger.Infof("picked bootstrap tools version: %s", bootstrapVersion)
 
549
        return toolsList, nil
 
550
}
 
551
 
 
552
// findCompatibleTools finds tools in the list that have the same major, minor
 
553
// and patch level as jujuversion.Current.
 
554
//
 
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)
 
562
                }
 
563
        }
 
564
        return compatibleTools.Newest()
 
565
}
 
566
 
 
567
func isCompatibleVersion(v1, v2 version.Number) bool {
 
568
        v1.Build = 0
 
569
        v2.Build = 0
 
570
        return v1.Compare(v2) == 0
 
571
}
 
572
 
 
573
// setPrivateMetadataSources sets the default tools metadata source
 
574
// for tools syncing, and adds an image metadata source after verifying
 
575
// the contents.
 
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
 
579
 
 
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")
 
584
                }
 
585
                return nil, nil
 
586
        }
 
587
 
 
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)
 
591
 
 
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")
 
597
        }
 
598
 
 
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
 
602
        })
 
603
        logger.Infof("custom image metadata added to search path")
 
604
        return existingMetadata, nil
 
605
}
 
606
 
 
607
func validateConstraints(env environs.Environ, cons constraints.Value) error {
 
608
        validator, err := env.ConstraintsValidator()
 
609
        if err != nil {
 
610
                return errors.Trace(err)
 
611
        }
 
612
        unsupported, err := validator.Validate(cons)
 
613
        return errors.Annotatef(err, "unsupported constraints: %v", unsupported)
 
614
}
 
615
 
 
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")
 
625
        if path != "" {
 
626
                vers, err := guiVersion(path)
 
627
                if err != nil {
 
628
                        logProgress(fmt.Sprintf("Cannot use Juju GUI at %q: %s", path, err))
 
629
                        return nil
 
630
                }
 
631
                hash, size, err := hashAndSize(path)
 
632
                if err != nil {
 
633
                        logProgress(fmt.Sprintf("Cannot use Juju GUI at %q: %s", path, err))
 
634
                        return nil
 
635
                }
 
636
                logProgress(fmt.Sprintf("Preparing for Juju GUI %s installation from local archive", vers))
 
637
                return &coretools.GUIArchive{
 
638
                        Version: vers,
 
639
                        URL:     "file://" + filepath.ToSlash(path),
 
640
                        SHA256:  hash,
 
641
                        Size:    size,
 
642
                }
 
643
        }
 
644
        // Check if the user requested to bootstrap with no GUI.
 
645
        if dataSourceBaseURL == "" {
 
646
                logProgress("Juju GUI installation has been disabled")
 
647
                return nil
 
648
        }
 
649
        // Fetch GUI archives info from simplestreams.
 
650
        source := gui.NewDataSource(dataSourceBaseURL)
 
651
        allMeta, err := guiFetchMetadata(gui.ReleasedStream, source)
 
652
        if err != nil {
 
653
                logProgress(fmt.Sprintf("Unable to fetch Juju GUI info: %s", err))
 
654
                return nil
 
655
        }
 
656
        if len(allMeta) == 0 {
 
657
                logProgress("No available Juju GUI archives found")
 
658
                return nil
 
659
        }
 
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,
 
667
        }
 
668
}
 
669
 
 
670
// guiFetchMetadata is defined for testing purposes.
 
671
var guiFetchMetadata = gui.FetchMetadata
 
672
 
 
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)
 
678
        if err != nil {
 
679
                return number, errors.Annotate(err, "cannot open Juju GUI archive")
 
680
        }
 
681
        defer f.Close()
 
682
        prefix := "jujugui-"
 
683
        r := tar.NewReader(bzip2.NewReader(f))
 
684
        for {
 
685
                hdr, err := r.Next()
 
686
                if err == io.EOF {
 
687
                        break
 
688
                }
 
689
                if err != nil {
 
690
                        return number, errors.New("cannot read Juju GUI archive")
 
691
                }
 
692
                info := hdr.FileInfo()
 
693
                if !info.IsDir() || !strings.HasPrefix(hdr.Name, prefix) {
 
694
                        continue
 
695
                }
 
696
                n := info.Name()[len(prefix):]
 
697
                number, err = version.Parse(n)
 
698
                if err != nil {
 
699
                        return number, errors.Errorf("cannot parse version %q", n)
 
700
                }
 
701
                return number, nil
 
702
        }
 
703
        return number, errors.New("cannot find Juju GUI version")
 
704
}
 
705
 
 
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)
 
710
        if err != nil {
 
711
                return "", 0, errors.Mask(err)
 
712
        }
 
713
        defer f.Close()
 
714
        h := sha256.New()
 
715
        size, err = io.Copy(h, f)
 
716
        if err != nil {
 
717
                return "", 0, errors.Mask(err)
 
718
        }
 
719
        return fmt.Sprintf("%x", h.Sum(nil)), size, nil
 
720
}