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