Permalink
Switch branches/tags
Find file
Fetching contributors…
Cannot retrieve contributors at this time
797 lines (727 sloc) 27.4 KB
// Copyright 2012, 2013 Canonical Ltd.
// Licensed under the AGPLv3, see LICENCE file for details.
package bootstrap
import (
"archive/tar"
"compress/bzip2"
"crypto/sha256"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
"github.com/juju/errors"
"github.com/juju/loggo"
"github.com/juju/utils"
"github.com/juju/utils/arch"
"github.com/juju/utils/series"
"github.com/juju/utils/set"
"github.com/juju/utils/ssh"
"github.com/juju/version"
"gopkg.in/juju/names.v2"
"github.com/juju/juju/api"
"github.com/juju/juju/apiserver/params"
"github.com/juju/juju/cloud"
"github.com/juju/juju/cloudconfig/instancecfg"
"github.com/juju/juju/constraints"
"github.com/juju/juju/controller"
"github.com/juju/juju/environs"
"github.com/juju/juju/environs/config"
"github.com/juju/juju/environs/gui"
"github.com/juju/juju/environs/imagemetadata"
"github.com/juju/juju/environs/simplestreams"
"github.com/juju/juju/environs/storage"
"github.com/juju/juju/environs/sync"
"github.com/juju/juju/environs/tools"
"github.com/juju/juju/mongo"
coretools "github.com/juju/juju/tools"
jujuversion "github.com/juju/juju/version"
)
const noToolsMessage = `Juju cannot bootstrap because no agent binaries are available for your model.
You may want to use the 'agent-metadata-url' configuration setting to specify the binaries' location.
`
var (
logger = loggo.GetLogger("juju.environs.bootstrap")
)
// BootstrapParams holds the parameters for bootstrapping an environment.
type BootstrapParams struct {
// ModelConstraints are merged with the bootstrap constraints
// to choose the initial instance, and will be stored in the
// initial models' states.
ModelConstraints constraints.Value
// BootstrapConstraints are used to choose the initial instance.
// BootstrapConstraints does not affect the model constraints.
BootstrapConstraints constraints.Value
// BootstrapSeries, if specified, is the series to use for the
// initial bootstrap machine.
BootstrapSeries string
// BootstrapImage, if specified, is the image ID to use for the
// initial bootstrap machine.
BootstrapImage string
// Cloud contains the properties of the cloud that Juju will be
// bootstrapped in.
Cloud cloud.Cloud
// CloudRegion is the name of the cloud region that Juju will be bootstrapped in.
CloudRegion string
// CloudCredentialName is the name of the cloud credential that Juju will be
// bootstrapped with. This may be empty, for clouds that do not require
// credentials.
CloudCredentialName string
// CloudCredential contains the cloud credential that Juju will be
// bootstrapped with. This may be nil, for clouds that do not require
// credentialis.
CloudCredential *cloud.Credential
// ControllerConfig is the set of config attributes relevant
// to a controller.
ControllerConfig controller.Config
// ControllerInheritedConfig is the set of config attributes to be shared
// across all models in the same controller.
ControllerInheritedConfig map[string]interface{}
// RegionInheritedConfig holds region specific configuration attributes to
// be shared across all models in the same controller on a particular
// cloud.
RegionInheritedConfig cloud.RegionConfig
// HostedModelConfig is the set of config attributes to be overlaid
// on the controller config to construct the initial hosted model
// config.
HostedModelConfig map[string]interface{}
// Placement, if non-empty, holds an environment-specific placement
// directive used to choose the initial instance.
Placement string
// BuildAgent reports whether we should build and upload the local agent
// binary and override the environment's specified agent-version.
// It is an error to specify BuildAgent with a nil BuildAgentTarball.
BuildAgent bool
// BuildAgentTarball, if non-nil, is a function that may be used to
// build tools to upload. If this is nil, tools uploading will never
// take place.
BuildAgentTarball sync.BuildAgentTarballFunc
// MetadataDir is an optional path to a local directory containing
// tools and/or image metadata.
MetadataDir string
// AgentVersion, if set, determines the exact tools version that
// will be used to start the Juju agents.
AgentVersion *version.Number
// GUIDataSourceBaseURL holds the simplestreams data source base URL
// used to retrieve the Juju GUI archive installed in the controller.
// If not set, the Juju GUI is not installed from simplestreams.
GUIDataSourceBaseURL string
// AdminSecret contains the administrator password.
AdminSecret string
// CAPrivateKey is the controller's CA certificate private key.
CAPrivateKey string
// DialOpts contains the bootstrap dial options.
DialOpts environs.BootstrapDialOpts
}
// Validate validates the bootstrap parameters.
func (p BootstrapParams) Validate() error {
if p.AdminSecret == "" {
return errors.New("admin-secret is empty")
}
if p.ControllerConfig.ControllerUUID() == "" {
return errors.New("controller configuration has no controller UUID")
}
if _, hasCACert := p.ControllerConfig.CACert(); !hasCACert {
return errors.New("controller configuration has no ca-cert")
}
if p.CAPrivateKey == "" {
return errors.New("empty ca-private-key")
}
// TODO(axw) validate other things.
return nil
}
// withDefaultControllerConstraints returns the given constraints,
// updated to choose a default instance type appropriate for a
// controller machine. We use this only if the user does not specify
// any constraints that would otherwise control the instance type
// selection.
func withDefaultControllerConstraints(cons constraints.Value) constraints.Value {
if !cons.HasInstanceType() && !cons.HasCpuCores() && !cons.HasCpuPower() && !cons.HasMem() {
// A default of 3.5GiB will result in machines with up to 4GiB of memory, eg
// - 3.75GiB on AWS, Google
// - 3.5GiB on Azure
// - 4GiB on Rackspace etc
var mem uint64 = 3.5 * 1024
cons.Mem = &mem
}
return cons
}
// Bootstrap bootstraps the given environment. The supplied constraints are
// used to provision the instance, and are also set within the bootstrapped
// environment.
func Bootstrap(ctx environs.BootstrapContext, environ environs.Environ, args BootstrapParams) error {
if err := args.Validate(); err != nil {
return errors.Annotate(err, "validating bootstrap parameters")
}
cfg := environ.Config()
if authKeys := ssh.SplitAuthorisedKeys(cfg.AuthorizedKeys()); len(authKeys) == 0 {
// Apparently this can never happen, so it's not tested. But, one day,
// Config will act differently (it's pretty crazy that, AFAICT, the
// authorized-keys are optional config settings... but it's impossible
// to actually *create* a config without them)... and when it does,
// we'll be here to catch this problem early.
return errors.Errorf("model configuration has no authorized-keys")
}
_, supportsNetworking := environs.SupportsNetworking(environ)
logger.Debugf("model %q supports service/machine networks: %v", cfg.Name(), supportsNetworking)
disableNetworkManagement, _ := cfg.DisableNetworkManagement()
logger.Debugf("network management by juju enabled: %v", !disableNetworkManagement)
// Set default tools metadata source, add image metadata source,
// then verify constraints. Providers may rely on image metadata
// for constraint validation.
var customImageMetadata []*imagemetadata.ImageMetadata
if args.MetadataDir != "" {
var err error
customImageMetadata, err = setPrivateMetadataSources(args.MetadataDir)
if err != nil {
return err
}
}
var bootstrapSeries *string
if args.BootstrapSeries != "" {
bootstrapSeries = &args.BootstrapSeries
}
var bootstrapArchForImageSearch string
if args.BootstrapConstraints.Arch != nil {
bootstrapArchForImageSearch = *args.BootstrapConstraints.Arch
} else if args.ModelConstraints.Arch != nil {
bootstrapArchForImageSearch = *args.ModelConstraints.Arch
} else {
bootstrapArchForImageSearch = arch.HostArch()
// We no longer support i386.
if bootstrapArchForImageSearch == arch.I386 {
bootstrapArchForImageSearch = arch.AMD64
}
}
ctx.Verbosef("Loading image metadata")
imageMetadata, err := bootstrapImageMetadata(environ,
bootstrapSeries,
bootstrapArchForImageSearch,
args.BootstrapImage,
&customImageMetadata,
)
if err != nil {
return errors.Trace(err)
}
// We want to determine a list of valid architectures for which to pick tools and images.
// This includes architectures from custom and other available image metadata.
architectures := set.NewStrings()
if len(customImageMetadata) > 0 {
for _, customMetadata := range customImageMetadata {
architectures.Add(customMetadata.Arch)
}
}
if len(imageMetadata) > 0 {
for _, iMetadata := range imageMetadata {
architectures.Add(iMetadata.Arch)
}
}
constraintsValidator, err := environ.ConstraintsValidator()
if err != nil {
return err
}
constraintsValidator.UpdateVocabulary(constraints.Arch, architectures.SortedValues())
bootstrapConstraints, err := constraintsValidator.Merge(
args.ModelConstraints, args.BootstrapConstraints,
)
if err != nil {
return errors.Trace(err)
}
bootstrapConstraints = withDefaultControllerConstraints(args.BootstrapConstraints)
// The arch we use to find tools isn't the boostrapConstraints arch.
// We copy the constraints arch to a separate variable and
// update it from the host arch if not specified.
// (axw) This is still not quite right:
// For e.g. if there is a MAAS with only ARM64 machines,
// on an AMD64 client, we're going to look for only AMD64 tools,
// limiting what the provider can bootstrap anyway.
var bootstrapArch string
if bootstrapConstraints.Arch != nil {
bootstrapArch = *bootstrapConstraints.Arch
} else {
// If no arch is specified as a constraint, we'll bootstrap
// on the same arch as the client used to bootstrap.
bootstrapArch = arch.HostArch()
// We no longer support controllers on i386.
// If we are bootstrapping from an i386 client,
// we'll look for amd64 tools.
if bootstrapArch == arch.I386 {
bootstrapArch = arch.AMD64
}
}
var availableTools coretools.List
if !args.BuildAgent {
ctx.Infof("Looking for packaged Juju agent version %s for %s", args.AgentVersion, bootstrapArch)
availableTools, err = findPackagedTools(environ, args.AgentVersion, &bootstrapArch, bootstrapSeries)
if err != nil && !errors.IsNotFound(err) {
return err
}
}
// If there are no prepackaged tools and a specific version has not been
// requested, look for or build a local binary.
var builtTools *sync.BuiltAgent
if len(availableTools) == 0 && (args.AgentVersion == nil || isCompatibleVersion(*args.AgentVersion, jujuversion.Current)) {
if args.BuildAgentTarball == nil {
return errors.New("cannot build agent binary to upload")
}
if err := validateUploadAllowed(environ, &bootstrapArch, bootstrapSeries, constraintsValidator); err != nil {
return err
}
if args.BuildAgent {
ctx.Infof("Building local Juju agent binary version %s for %s", args.AgentVersion, bootstrapArch)
} else {
ctx.Infof("No packaged binary found, preparing local Juju agent binary")
}
var forceVersion version.Number
availableTools, forceVersion = locallyBuildableTools(bootstrapSeries)
builtTools, err = args.BuildAgentTarball(args.BuildAgent, &forceVersion, cfg.AgentStream())
if err != nil {
return errors.Annotate(err, "cannot package bootstrap agent binary")
}
defer os.RemoveAll(builtTools.Dir)
for i, tool := range availableTools {
if tool.URL != "" {
continue
}
filename := filepath.Join(builtTools.Dir, builtTools.StorageName)
tool.URL = fmt.Sprintf("file://%s", filename)
tool.Size = builtTools.Size
tool.SHA256 = builtTools.Sha256Hash
availableTools[i] = tool
}
}
if len(availableTools) == 0 {
return errors.New(noToolsMessage)
}
// If we're uploading, we must override agent-version;
// if we're not uploading, we want to ensure we have an
// agent-version set anyway, to appease FinishInstanceConfig.
// In the latter case, setBootstrapTools will later set
// agent-version to the correct thing.
agentVersion := jujuversion.Current
if args.AgentVersion != nil {
agentVersion = *args.AgentVersion
}
if cfg, err = cfg.Apply(map[string]interface{}{
"agent-version": agentVersion.String(),
}); err != nil {
return err
}
if err = environ.SetConfig(cfg); err != nil {
return err
}
ctx.Verbosef("Starting new instance for initial controller")
result, err := environ.Bootstrap(ctx, environs.BootstrapParams{
CloudName: args.Cloud.Name,
CloudRegion: args.CloudRegion,
ControllerConfig: args.ControllerConfig,
ModelConstraints: args.ModelConstraints,
BootstrapConstraints: bootstrapConstraints,
BootstrapSeries: args.BootstrapSeries,
Placement: args.Placement,
AvailableTools: availableTools,
ImageMetadata: imageMetadata,
})
if err != nil {
return err
}
matchingTools, err := availableTools.Match(coretools.Filter{
Arch: result.Arch,
Series: result.Series,
})
if err != nil {
return err
}
selectedToolsList, err := getBootstrapToolsVersion(matchingTools)
if err != nil {
return err
}
// We set agent-version to the newest version, so the agent will immediately upgrade itself.
// Note that this only is relevant if a specific agent version has not been requested, since
// in that case the specific version will be the only version available.
newestVersion, _ := matchingTools.Newest()
if err := setBootstrapToolsVersion(environ, newestVersion); err != nil {
return err
}
logger.Infof("Installing Juju agent on bootstrap instance")
publicKey, err := userPublicSigningKey()
if err != nil {
return err
}
instanceConfig, err := instancecfg.NewBootstrapInstanceConfig(
args.ControllerConfig,
bootstrapConstraints,
args.ModelConstraints,
result.Series,
publicKey,
)
if err != nil {
return err
}
if err := instanceConfig.SetTools(selectedToolsList); err != nil {
return errors.Trace(err)
}
// Make sure we have the most recent environ config as the specified
// tools version has been updated there.
cfg = environ.Config()
if err := finalizeInstanceBootstrapConfig(ctx, instanceConfig, args, cfg, customImageMetadata); err != nil {
return errors.Annotate(err, "finalizing bootstrap instance config")
}
if err := result.Finalize(ctx, instanceConfig, args.DialOpts); err != nil {
return err
}
ctx.Infof("Bootstrap agent now started")
return nil
}
func finalizeInstanceBootstrapConfig(
ctx environs.BootstrapContext,
icfg *instancecfg.InstanceConfig,
args BootstrapParams,
cfg *config.Config,
customImageMetadata []*imagemetadata.ImageMetadata,
) error {
if icfg.APIInfo != nil || icfg.Controller.MongoInfo != nil {
return errors.New("machine configuration already has api/state info")
}
controllerCfg := icfg.Controller.Config
caCert, hasCACert := controllerCfg.CACert()
if !hasCACert {
return errors.New("controller configuration has no ca-cert")
}
icfg.APIInfo = &api.Info{
Password: args.AdminSecret,
CACert: caCert,
ModelTag: names.NewModelTag(cfg.UUID()),
}
icfg.Controller.MongoInfo = &mongo.MongoInfo{
Password: args.AdminSecret,
Info: mongo.Info{CACert: caCert},
}
// These really are directly relevant to running a controller.
// Initially, generate a controller certificate with no host IP
// addresses in the SAN field. Once the controller is up and the
// NIC addresses become known, the certificate can be regenerated.
cert, key, err := controller.GenerateControllerCertAndKey(caCert, args.CAPrivateKey, nil)
if err != nil {
return errors.Annotate(err, "cannot generate controller certificate")
}
icfg.Bootstrap.StateServingInfo = params.StateServingInfo{
StatePort: controllerCfg.StatePort(),
APIPort: controllerCfg.APIPort(),
Cert: string(cert),
PrivateKey: string(key),
CAPrivateKey: args.CAPrivateKey,
}
if _, ok := cfg.AgentVersion(); !ok {
return errors.New("controller model configuration has no agent-version")
}
icfg.Bootstrap.ControllerModelConfig = cfg
icfg.Bootstrap.CustomImageMetadata = customImageMetadata
icfg.Bootstrap.ControllerCloud = args.Cloud
icfg.Bootstrap.ControllerCloudRegion = args.CloudRegion
icfg.Bootstrap.ControllerCloudCredential = args.CloudCredential
icfg.Bootstrap.ControllerCloudCredentialName = args.CloudCredentialName
icfg.Bootstrap.ControllerConfig = args.ControllerConfig
icfg.Bootstrap.ControllerInheritedConfig = args.ControllerInheritedConfig
icfg.Bootstrap.RegionInheritedConfig = args.Cloud.RegionConfig
icfg.Bootstrap.HostedModelConfig = args.HostedModelConfig
icfg.Bootstrap.Timeout = args.DialOpts.Timeout
icfg.Bootstrap.GUI = guiArchive(args.GUIDataSourceBaseURL, func(msg string) {
ctx.Infof(msg)
})
return nil
}
func userPublicSigningKey() (string, error) {
signingKeyFile := os.Getenv("JUJU_STREAMS_PUBLICKEY_FILE")
signingKey := ""
if signingKeyFile != "" {
path, err := utils.NormalizePath(signingKeyFile)
if err != nil {
return "", errors.Annotatef(err, "cannot expand key file path: %s", signingKeyFile)
}
b, err := ioutil.ReadFile(path)
if err != nil {
return "", errors.Annotatef(err, "invalid public key file: %s", path)
}
signingKey = string(b)
}
return signingKey, nil
}
// bootstrapImageMetadata returns the image metadata to use for bootstrapping
// the given environment. If the environment provider does not make use of
// simplestreams, no metadata will be returned.
//
// If a bootstrap image ID is specified, image metadata will be synthesised
// using that image ID, and the architecture and series specified by the
// initiator. In addition, the custom image metadata that is saved into the
// state database will have the synthesised image metadata added to it.
func bootstrapImageMetadata(
environ environs.Environ,
bootstrapSeries *string,
bootstrapArch string,
bootstrapImageId string,
customImageMetadata *[]*imagemetadata.ImageMetadata,
) ([]*imagemetadata.ImageMetadata, error) {
hasRegion, ok := environ.(simplestreams.HasRegion)
if !ok {
if bootstrapImageId != "" {
// We only support specifying image IDs for providers
// that use simplestreams for now.
return nil, errors.NotSupportedf(
"specifying bootstrap image for %q provider",
environ.Config().Type(),
)
}
// No region, no metadata.
return nil, nil
}
region, err := hasRegion.Region()
if err != nil {
return nil, errors.Trace(err)
}
if bootstrapImageId != "" {
if bootstrapSeries == nil {
return nil, errors.NotValidf("no series specified with bootstrap image")
}
seriesVersion, err := series.SeriesVersion(*bootstrapSeries)
if err != nil {
return nil, errors.Trace(err)
}
// The returned metadata does not have information about the
// storage or virtualisation type. Any provider that wants to
// filter on those properties should allow for empty values.
meta := &imagemetadata.ImageMetadata{
Id: bootstrapImageId,
Arch: bootstrapArch,
Version: seriesVersion,
RegionName: region.Region,
Endpoint: region.Endpoint,
Stream: environ.Config().ImageStream(),
}
*customImageMetadata = append(*customImageMetadata, meta)
return []*imagemetadata.ImageMetadata{meta}, nil
}
// For providers that support making use of simplestreams
// image metadata, search public image metadata. We need
// to pass this onto Bootstrap for selecting images.
sources, err := environs.ImageMetadataSources(environ)
if err != nil {
return nil, errors.Trace(err)
}
// This constraint will search image metadata for all supported architectures and series.
imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{
CloudSpec: region,
Stream: environ.Config().ImageStream(),
})
logger.Debugf("constraints for image metadata lookup %v", imageConstraint)
// Get image metadata from all data sources.
// Since order of data source matters, order of image metadata matters too. Append is important here.
var publicImageMetadata []*imagemetadata.ImageMetadata
for _, source := range sources {
sourceMetadata, _, err := imagemetadata.Fetch([]simplestreams.DataSource{source}, imageConstraint)
if err != nil {
logger.Debugf("ignoring image metadata in %s: %v", source.Description(), err)
// Just keep looking...
continue
}
logger.Debugf("found %d image metadata in %s", len(sourceMetadata), source.Description())
publicImageMetadata = append(publicImageMetadata, sourceMetadata...)
}
logger.Debugf("found %d image metadata from all image data sources", len(publicImageMetadata))
if len(publicImageMetadata) == 0 {
return nil, errors.New("no image metadata found")
}
return publicImageMetadata, nil
}
// getBootstrapToolsVersion returns the newest tools from the given tools list.
func getBootstrapToolsVersion(possibleTools coretools.List) (coretools.List, error) {
if len(possibleTools) == 0 {
return nil, errors.New("no bootstrap tools available")
}
var newVersion version.Number
newVersion, toolsList := possibleTools.Newest()
logger.Infof("newest version: %s", newVersion)
bootstrapVersion := newVersion
// We should only ever bootstrap the exact same version as the client,
// or we risk bootstrap incompatibility.
if !isCompatibleVersion(newVersion, jujuversion.Current) {
compatibleVersion, compatibleTools := findCompatibleTools(possibleTools, jujuversion.Current)
if len(compatibleTools) == 0 {
logger.Infof(
"failed to find %s tools, will attempt to use %s",
jujuversion.Current, newVersion,
)
} else {
bootstrapVersion, toolsList = compatibleVersion, compatibleTools
}
}
logger.Infof("picked bootstrap tools version: %s", bootstrapVersion)
return toolsList, nil
}
// setBootstrapToolsVersion updates the agent-version configuration attribute.
func setBootstrapToolsVersion(environ environs.Environ, toolsVersion version.Number) error {
cfg := environ.Config()
if agentVersion, _ := cfg.AgentVersion(); agentVersion != toolsVersion {
cfg, err := cfg.Apply(map[string]interface{}{
"agent-version": toolsVersion.String(),
})
if err == nil {
err = environ.SetConfig(cfg)
}
if err != nil {
return errors.Errorf("failed to update model configuration: %v", err)
}
}
return nil
}
// findCompatibleTools finds tools in the list that have the same major, minor
// and patch level as jujuversion.Current.
//
// Build number is not important to match; uploaded tools will have
// incremented build number, and we want to match them.
func findCompatibleTools(possibleTools coretools.List, version version.Number) (version.Number, coretools.List) {
var compatibleTools coretools.List
for _, tools := range possibleTools {
if isCompatibleVersion(tools.Version.Number, version) {
compatibleTools = append(compatibleTools, tools)
}
}
return compatibleTools.Newest()
}
func isCompatibleVersion(v1, v2 version.Number) bool {
v1.Build = 0
v2.Build = 0
return v1.Compare(v2) == 0
}
// setPrivateMetadataSources sets the default tools metadata source
// for tools syncing, and adds an image metadata source after verifying
// the contents.
func setPrivateMetadataSources(metadataDir string) ([]*imagemetadata.ImageMetadata, error) {
logger.Infof("Setting default tools and image metadata sources: %s", metadataDir)
tools.DefaultBaseURL = metadataDir
imageMetadataDir := filepath.Join(metadataDir, storage.BaseImagesPath)
if _, err := os.Stat(imageMetadataDir); err != nil {
if !os.IsNotExist(err) {
return nil, errors.Annotate(err, "cannot access image metadata")
}
return nil, nil
}
baseURL := fmt.Sprintf("file://%s", filepath.ToSlash(imageMetadataDir))
publicKey, _ := simplestreams.UserPublicSigningKey()
datasource := simplestreams.NewURLSignedDataSource("bootstrap metadata", baseURL, publicKey, utils.NoVerifySSLHostnames, simplestreams.CUSTOM_CLOUD_DATA, false)
// Read the image metadata, as we'll want to upload it to the environment.
imageConstraint := imagemetadata.NewImageConstraint(simplestreams.LookupParams{})
existingMetadata, _, err := imagemetadata.Fetch([]simplestreams.DataSource{datasource}, imageConstraint)
if err != nil && !errors.IsNotFound(err) {
return nil, errors.Annotate(err, "cannot read image metadata")
}
// Add an image metadata datasource for constraint validation, etc.
environs.RegisterUserImageDataSourceFunc("bootstrap metadata", func(environs.Environ) (simplestreams.DataSource, error) {
return datasource, nil
})
logger.Infof("custom image metadata added to search path")
return existingMetadata, nil
}
// guiArchive returns information on the GUI archive that will be uploaded
// to the controller. Possible errors in retrieving the GUI archive information
// do not prevent the model to be bootstrapped. If dataSourceBaseURL is
// non-empty, remote GUI archive info is retrieved from simplestreams using it
// as the base URL. The given logProgress function is used to inform users
// about errors or progress in setting up the Juju GUI.
func guiArchive(dataSourceBaseURL string, logProgress func(string)) *coretools.GUIArchive {
// The environment variable is only used for development purposes.
path := os.Getenv("JUJU_GUI")
if path != "" {
vers, err := guiVersion(path)
if err != nil {
logProgress(fmt.Sprintf("Cannot use Juju GUI at %q: %s", path, err))
return nil
}
hash, size, err := hashAndSize(path)
if err != nil {
logProgress(fmt.Sprintf("Cannot use Juju GUI at %q: %s", path, err))
return nil
}
logProgress(fmt.Sprintf("Fetching Juju GUI %s from local archive", vers))
return &coretools.GUIArchive{
Version: vers,
URL: "file://" + filepath.ToSlash(path),
SHA256: hash,
Size: size,
}
}
// Check if the user requested to bootstrap with no GUI.
if dataSourceBaseURL == "" {
logProgress("Juju GUI installation has been disabled")
return nil
}
// Fetch GUI archives info from simplestreams.
source := gui.NewDataSource(dataSourceBaseURL)
allMeta, err := guiFetchMetadata(gui.ReleasedStream, source)
if err != nil {
logProgress(fmt.Sprintf("Unable to fetch Juju GUI info: %s", err))
return nil
}
if len(allMeta) == 0 {
logProgress("No available Juju GUI archives found")
return nil
}
// Metadata info are returned in descending version order.
logProgress(fmt.Sprintf("Fetching Juju GUI %s", allMeta[0].Version))
return &coretools.GUIArchive{
Version: allMeta[0].Version,
URL: allMeta[0].FullPath,
SHA256: allMeta[0].SHA256,
Size: allMeta[0].Size,
}
}
// guiFetchMetadata is defined for testing purposes.
var guiFetchMetadata = gui.FetchMetadata
// guiVersion retrieves the GUI version from the juju-gui-* directory included
// in the bz2 archive at the given path.
func guiVersion(path string) (version.Number, error) {
var number version.Number
f, err := os.Open(path)
if err != nil {
return number, errors.Annotate(err, "cannot open Juju GUI archive")
}
defer f.Close()
prefix := "jujugui-"
r := tar.NewReader(bzip2.NewReader(f))
for {
hdr, err := r.Next()
if err == io.EOF {
break
}
if err != nil {
return number, errors.New("cannot read Juju GUI archive")
}
info := hdr.FileInfo()
if !info.IsDir() || !strings.HasPrefix(hdr.Name, prefix) {
continue
}
n := info.Name()[len(prefix):]
number, err = version.Parse(n)
if err != nil {
return number, errors.Errorf("cannot parse version %q", n)
}
return number, nil
}
return number, errors.New("cannot find Juju GUI version")
}
// hashAndSize calculates and returns the SHA256 hash and the size of the file
// located at the given path.
func hashAndSize(path string) (hash string, size int64, err error) {
f, err := os.Open(path)
if err != nil {
return "", 0, errors.Mask(err)
}
defer f.Close()
h := sha256.New()
size, err = io.Copy(h, f)
if err != nil {
return "", 0, errors.Mask(err)
}
return fmt.Sprintf("%x", h.Sum(nil)), size, nil
}