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  }