github.com/axw/juju@v0.0.0-20161005053422-4bd6544d08d4/environs/sync/sync.go (about)

     1  // Copyright 2012, 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package sync
     5  
     6  import (
     7  	"bytes"
     8  	"io"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  
    13  	"github.com/juju/errors"
    14  	"github.com/juju/loggo"
    15  	"github.com/juju/utils"
    16  	jujuseries "github.com/juju/utils/series"
    17  	"github.com/juju/version"
    18  
    19  	"github.com/juju/juju/environs/filestorage"
    20  	"github.com/juju/juju/environs/simplestreams"
    21  	"github.com/juju/juju/environs/storage"
    22  	envtools "github.com/juju/juju/environs/tools"
    23  	"github.com/juju/juju/juju/keys"
    24  	coretools "github.com/juju/juju/tools"
    25  	jujuversion "github.com/juju/juju/version"
    26  )
    27  
    28  var logger = loggo.GetLogger("juju.environs.sync")
    29  
    30  // SyncContext describes the context for tool synchronization.
    31  type SyncContext struct {
    32  	// TargetToolsFinder is a ToolsFinder provided to find existing
    33  	// tools in the target destination.
    34  	TargetToolsFinder ToolsFinder
    35  
    36  	// TargetToolsUploader is a ToolsUploader provided to upload
    37  	// tools to the target destination.
    38  	TargetToolsUploader ToolsUploader
    39  
    40  	// AllVersions controls the copy of all versions, not only the latest.
    41  	AllVersions bool
    42  
    43  	// Copy tools with major version, if MajorVersion > 0.
    44  	MajorVersion int
    45  
    46  	// Copy tools with minor version, if MinorVersion > 0.
    47  	MinorVersion int
    48  
    49  	// DryRun controls that nothing is copied. Instead it's logged
    50  	// what would be coppied.
    51  	DryRun bool
    52  
    53  	// Stream specifies the simplestreams stream to use (defaults to "Released").
    54  	Stream string
    55  
    56  	// Source, if non-empty, specifies a directory in the local file system
    57  	// to use as a source.
    58  	Source string
    59  }
    60  
    61  // ToolsFinder provides an interface for finding tools of a specified version.
    62  type ToolsFinder interface {
    63  	// FindTools returns a list of tools with the specified major version in the specified stream.
    64  	FindTools(major int, stream string) (coretools.List, error)
    65  }
    66  
    67  // ToolsUploader provides an interface for uploading tools and associated
    68  // metadata.
    69  type ToolsUploader interface {
    70  	// UploadTools uploads the tools with the specified version and tarball contents.
    71  	UploadTools(toolsDir, stream string, tools *coretools.Tools, data []byte) error
    72  }
    73  
    74  // SyncTools copies the Juju tools tarball from the official bucket
    75  // or a specified source directory into the user's environment.
    76  func SyncTools(syncContext *SyncContext) error {
    77  	sourceDataSource, err := selectSourceDatasource(syncContext)
    78  	if err != nil {
    79  		return err
    80  	}
    81  
    82  	logger.Infof("listing available tools")
    83  	if syncContext.MajorVersion == 0 && syncContext.MinorVersion == 0 {
    84  		syncContext.MajorVersion = jujuversion.Current.Major
    85  		syncContext.MinorVersion = -1
    86  		if !syncContext.AllVersions {
    87  			syncContext.MinorVersion = jujuversion.Current.Minor
    88  		}
    89  	}
    90  
    91  	toolsDir := syncContext.Stream
    92  	// If no stream has been specified, assume "released" for non-devel versions of Juju.
    93  	if syncContext.Stream == "" {
    94  		// We now store the tools in a directory named after their stream, but the
    95  		// legacy behaviour is to store all tools in a single "releases" directory.
    96  		toolsDir = envtools.LegacyReleaseDirectory
    97  		syncContext.Stream = envtools.PreferredStream(&jujuversion.Current, false, syncContext.Stream)
    98  	}
    99  	sourceTools, err := envtools.FindToolsForCloud(
   100  		[]simplestreams.DataSource{sourceDataSource}, simplestreams.CloudSpec{},
   101  		syncContext.Stream, syncContext.MajorVersion, syncContext.MinorVersion, coretools.Filter{})
   102  	// For backwards compatibility with cloud storage, if there are no tools in the specified stream,
   103  	// double check the release stream.
   104  	// TODO - remove this when we no longer need to support cloud storage upgrades.
   105  	if err == envtools.ErrNoTools {
   106  		sourceTools, err = envtools.FindToolsForCloud(
   107  			[]simplestreams.DataSource{sourceDataSource}, simplestreams.CloudSpec{},
   108  			envtools.ReleasedStream, syncContext.MajorVersion, syncContext.MinorVersion, coretools.Filter{})
   109  	}
   110  	if err != nil {
   111  		return err
   112  	}
   113  
   114  	logger.Infof("found %d tools", len(sourceTools))
   115  	if !syncContext.AllVersions {
   116  		var latest version.Number
   117  		latest, sourceTools = sourceTools.Newest()
   118  		logger.Infof("found %d recent tools (version %s)", len(sourceTools), latest)
   119  	}
   120  	for _, tool := range sourceTools {
   121  		logger.Debugf("found source tool: %v", tool)
   122  	}
   123  
   124  	logger.Infof("listing target tools storage")
   125  	targetTools, err := syncContext.TargetToolsFinder.FindTools(syncContext.MajorVersion, syncContext.Stream)
   126  	switch err {
   127  	case nil, coretools.ErrNoMatches, envtools.ErrNoTools:
   128  	default:
   129  		return err
   130  	}
   131  	for _, tool := range targetTools {
   132  		logger.Debugf("found target tool: %v", tool)
   133  	}
   134  
   135  	missing := sourceTools.Exclude(targetTools)
   136  	logger.Infof("found %d tools in target; %d tools to be copied", len(targetTools), len(missing))
   137  	if syncContext.DryRun {
   138  		for _, tools := range missing {
   139  			logger.Infof("copying %s from %s", tools.Version, tools.URL)
   140  		}
   141  		return nil
   142  	}
   143  
   144  	err = copyTools(toolsDir, syncContext.Stream, missing, syncContext.TargetToolsUploader)
   145  	if err != nil {
   146  		return err
   147  	}
   148  	logger.Infof("copied %d tools", len(missing))
   149  	return nil
   150  }
   151  
   152  // selectSourceDatasource returns a storage reader based on the source setting.
   153  func selectSourceDatasource(syncContext *SyncContext) (simplestreams.DataSource, error) {
   154  	source := syncContext.Source
   155  	if source == "" {
   156  		source = envtools.DefaultBaseURL
   157  	}
   158  	sourceURL, err := envtools.ToolsURL(source)
   159  	if err != nil {
   160  		return nil, err
   161  	}
   162  	logger.Infof("using sync tools source: %v", sourceURL)
   163  	return simplestreams.NewURLSignedDataSource("sync tools source", sourceURL, keys.JujuPublicKey, utils.VerifySSLHostnames, simplestreams.CUSTOM_CLOUD_DATA, false), nil
   164  }
   165  
   166  // copyTools copies a set of tools from the source to the target.
   167  func copyTools(toolsDir, stream string, tools []*coretools.Tools, u ToolsUploader) error {
   168  	for _, tool := range tools {
   169  		logger.Infof("copying %s from %s", tool.Version, tool.URL)
   170  		if err := copyOneToolsPackage(toolsDir, stream, tool, u); err != nil {
   171  			return err
   172  		}
   173  	}
   174  	return nil
   175  }
   176  
   177  // copyOneToolsPackage copies one tool from the source to the target.
   178  func copyOneToolsPackage(toolsDir, stream string, tools *coretools.Tools, u ToolsUploader) error {
   179  	toolsName := envtools.StorageName(tools.Version, toolsDir)
   180  	logger.Infof("downloading %q %v (%v)", stream, toolsName, tools.URL)
   181  	resp, err := utils.GetValidatingHTTPClient().Get(tools.URL)
   182  	if err != nil {
   183  		return err
   184  	}
   185  	defer resp.Body.Close()
   186  	// Verify SHA-256 hash.
   187  	var buf bytes.Buffer
   188  	sha256, size, err := utils.ReadSHA256(io.TeeReader(resp.Body, &buf))
   189  	if err != nil {
   190  		return err
   191  	}
   192  	if tools.SHA256 == "" {
   193  		logger.Errorf("no SHA-256 hash for %v", tools.SHA256) // TODO(dfc) can you spot the bug ?
   194  	} else if sha256 != tools.SHA256 {
   195  		return errors.Errorf("SHA-256 hash mismatch (%v/%v)", sha256, tools.SHA256)
   196  	}
   197  	sizeInKB := (size + 512) / 1024
   198  	logger.Infof("uploading %v (%dkB) to model", toolsName, sizeInKB)
   199  	return u.UploadTools(toolsDir, stream, tools, buf.Bytes())
   200  }
   201  
   202  // UploadFunc is the type of Upload, which may be
   203  // reassigned to control the behaviour of tools
   204  // uploading.
   205  type UploadFunc func(stor storage.Storage, stream string, forceVersion *version.Number, series ...string) (*coretools.Tools, error)
   206  
   207  // Exported for testing.
   208  var Upload UploadFunc = upload
   209  
   210  // upload builds whatever version of github.com/juju/juju is in $GOPATH,
   211  // uploads it to the given storage, and returns a Tools instance describing
   212  // them. If forceVersion is not nil, the uploaded tools bundle will report
   213  // the given version number; if any fakeSeries are supplied, additional copies
   214  // of the built tools will be uploaded for use by machines of those series.
   215  // Juju tools built for one series do not necessarily run on another, but this
   216  // func exists only for development use cases.
   217  func upload(stor storage.Storage, stream string, forceVersion *version.Number, fakeSeries ...string) (*coretools.Tools, error) {
   218  	builtTools, err := BuildAgentTarball(true, forceVersion, stream)
   219  	if err != nil {
   220  		return nil, err
   221  	}
   222  	defer os.RemoveAll(builtTools.Dir)
   223  	logger.Debugf("Uploading agent binaries for %v", fakeSeries)
   224  	return syncBuiltTools(stor, stream, builtTools, fakeSeries...)
   225  }
   226  
   227  // cloneToolsForSeries copies the built tools tarball into a tarball for the specified
   228  // stream and series and generates corresponding metadata.
   229  func cloneToolsForSeries(toolsInfo *BuiltAgent, stream string, series ...string) error {
   230  	// Copy the tools to the target storage, recording a Tools struct for each one.
   231  	var targetTools coretools.List
   232  	targetTools = append(targetTools, &coretools.Tools{
   233  		Version: toolsInfo.Version,
   234  		Size:    toolsInfo.Size,
   235  		SHA256:  toolsInfo.Sha256Hash,
   236  	})
   237  	putTools := func(vers version.Binary) (string, error) {
   238  		name := envtools.StorageName(vers, stream)
   239  		src := filepath.Join(toolsInfo.Dir, toolsInfo.StorageName)
   240  		dest := filepath.Join(toolsInfo.Dir, name)
   241  		destDir := filepath.Dir(dest)
   242  		if err := os.MkdirAll(destDir, 0755); err != nil {
   243  			return "", err
   244  		}
   245  		if err := utils.CopyFile(dest, src); err != nil {
   246  			return "", err
   247  		}
   248  		// Append to targetTools the attributes required to write out tools metadata.
   249  		targetTools = append(targetTools, &coretools.Tools{
   250  			Version: vers,
   251  			Size:    toolsInfo.Size,
   252  			SHA256:  toolsInfo.Sha256Hash,
   253  		})
   254  		return name, nil
   255  	}
   256  	logger.Debugf("generating tarballs for %v", series)
   257  	for _, series := range series {
   258  		_, err := jujuseries.SeriesVersion(series)
   259  		if err != nil {
   260  			return err
   261  		}
   262  		if series != toolsInfo.Version.Series {
   263  			fakeVersion := toolsInfo.Version
   264  			fakeVersion.Series = series
   265  			if _, err := putTools(fakeVersion); err != nil {
   266  				return err
   267  			}
   268  		}
   269  	}
   270  	// The tools have been copied to a temp location from which they will be uploaded,
   271  	// now write out the matching simplestreams metadata so that SyncTools can find them.
   272  	metadataStore, err := filestorage.NewFileStorageWriter(toolsInfo.Dir)
   273  	if err != nil {
   274  		return err
   275  	}
   276  	logger.Debugf("generating tools metadata")
   277  	return envtools.MergeAndWriteMetadata(metadataStore, stream, stream, targetTools, false)
   278  }
   279  
   280  // BuiltAgent contains metadata for a tools tarball resulting from
   281  // a call to BundleTools.
   282  type BuiltAgent struct {
   283  	Version     version.Binary
   284  	Dir         string
   285  	StorageName string
   286  	Sha256Hash  string
   287  	Size        int64
   288  }
   289  
   290  // BuildAgentTarballFunc is a function which can build an agent tarball.
   291  type BuildAgentTarballFunc func(build bool, forceVersion *version.Number, stream string) (*BuiltAgent, error)
   292  
   293  // Override for testing.
   294  var BuildAgentTarball BuildAgentTarballFunc = buildAgentTarball
   295  
   296  // BuildAgentTarball bundles an agent tarball and places it in a temp directory in
   297  // the expected agent path.
   298  func buildAgentTarball(build bool, forceVersion *version.Number, stream string) (_ *BuiltAgent, err error) {
   299  	// TODO(rog) find binaries from $PATH when not using a development
   300  	// version of juju within a $GOPATH.
   301  
   302  	logger.Debugf("Making agent binary tarball")
   303  	// We create the entire archive before asking the environment to
   304  	// start uploading so that we can be sure we have archived
   305  	// correctly.
   306  	f, err := ioutil.TempFile("", "juju-tgz")
   307  	if err != nil {
   308  		return nil, err
   309  	}
   310  	defer f.Close()
   311  	defer os.Remove(f.Name())
   312  	toolsVersion, sha256Hash, err := envtools.BundleTools(build, f, forceVersion)
   313  	if err != nil {
   314  		return nil, err
   315  	}
   316  	// Built agent version needs to match the client used to bootstrap.
   317  	builtVersion := toolsVersion
   318  	builtVersion.Build = 0
   319  	clientVersion := jujuversion.Current
   320  	clientVersion.Build = 0
   321  	if builtVersion.Number.Compare(clientVersion) != 0 {
   322  		return nil, errors.Errorf("agent binary %v not compatibile with bootstrap client %v", toolsVersion.Number, jujuversion.Current)
   323  	}
   324  	fileInfo, err := f.Stat()
   325  	if err != nil {
   326  		return nil, errors.Errorf("cannot stat newly made tools archive: %v", err)
   327  	}
   328  	size := fileInfo.Size()
   329  	reportedVersion := toolsVersion
   330  	if forceVersion != nil {
   331  		reportedVersion.Number = *forceVersion
   332  	}
   333  	logger.Infof("using agent binary %v aliased to %v (%dkB)", toolsVersion, reportedVersion, (size+512)/1024)
   334  	baseToolsDir, err := ioutil.TempDir("", "juju-tools")
   335  	if err != nil {
   336  		return nil, err
   337  	}
   338  
   339  	// If we exit with an error, clean up the built tools directory.
   340  	defer func() {
   341  		if err != nil {
   342  			os.RemoveAll(baseToolsDir)
   343  		}
   344  	}()
   345  
   346  	err = os.MkdirAll(filepath.Join(baseToolsDir, storage.BaseToolsPath, stream), 0755)
   347  	if err != nil {
   348  		return nil, err
   349  	}
   350  	storageName := envtools.StorageName(toolsVersion, stream)
   351  	err = utils.CopyFile(filepath.Join(baseToolsDir, storageName), f.Name())
   352  	if err != nil {
   353  		return nil, err
   354  	}
   355  	return &BuiltAgent{
   356  		Version:     toolsVersion,
   357  		Dir:         baseToolsDir,
   358  		StorageName: storageName,
   359  		Size:        size,
   360  		Sha256Hash:  sha256Hash,
   361  	}, nil
   362  }
   363  
   364  // syncBuiltTools copies to storage a tools tarball and cloned copies for each series.
   365  func syncBuiltTools(stor storage.Storage, stream string, builtTools *BuiltAgent, fakeSeries ...string) (*coretools.Tools, error) {
   366  	if err := cloneToolsForSeries(builtTools, stream, fakeSeries...); err != nil {
   367  		return nil, err
   368  	}
   369  	syncContext := &SyncContext{
   370  		Source:              builtTools.Dir,
   371  		TargetToolsFinder:   StorageToolsFinder{stor},
   372  		TargetToolsUploader: StorageToolsUploader{stor, false, false},
   373  		AllVersions:         true,
   374  		Stream:              stream,
   375  		MajorVersion:        builtTools.Version.Major,
   376  		MinorVersion:        -1,
   377  	}
   378  	logger.Debugf("uploading agent binaries to cloud storage")
   379  	err := SyncTools(syncContext)
   380  	if err != nil {
   381  		return nil, err
   382  	}
   383  	url, err := stor.URL(builtTools.StorageName)
   384  	if err != nil {
   385  		return nil, err
   386  	}
   387  	return &coretools.Tools{
   388  		Version: builtTools.Version,
   389  		URL:     url,
   390  		Size:    builtTools.Size,
   391  		SHA256:  builtTools.Sha256Hash,
   392  	}, nil
   393  }
   394  
   395  // StorageToolsFinder is an implementation of ToolsFinder
   396  // that searches for tools in the specified storage.
   397  type StorageToolsFinder struct {
   398  	Storage storage.StorageReader
   399  }
   400  
   401  func (f StorageToolsFinder) FindTools(major int, stream string) (coretools.List, error) {
   402  	return envtools.ReadList(f.Storage, stream, major, -1)
   403  }
   404  
   405  // StorageToolsUplader is an implementation of ToolsUploader that
   406  // writes tools to the provided storage and then writes merged
   407  // metadata, optionally with mirrors.
   408  type StorageToolsUploader struct {
   409  	Storage       storage.Storage
   410  	WriteMetadata bool
   411  	WriteMirrors  envtools.ShouldWriteMirrors
   412  }
   413  
   414  func (u StorageToolsUploader) UploadTools(toolsDir, stream string, tools *coretools.Tools, data []byte) error {
   415  	toolsName := envtools.StorageName(tools.Version, toolsDir)
   416  	if err := u.Storage.Put(toolsName, bytes.NewReader(data), int64(len(data))); err != nil {
   417  		return err
   418  	}
   419  	if !u.WriteMetadata {
   420  		return nil
   421  	}
   422  	err := envtools.MergeAndWriteMetadata(u.Storage, toolsDir, stream, coretools.List{tools}, u.WriteMirrors)
   423  	if err != nil {
   424  		logger.Errorf("error writing tools metadata: %v", err)
   425  		return err
   426  	}
   427  	return nil
   428  }