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