github.com/niedbalski/juju@v0.0.0-20190215020005-8ff100488e47/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  	jujuseries "github.com/juju/os/series"
    16  	"github.com/juju/utils"
    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 errors.Trace(err)
    80  	}
    81  
    82  	logger.Infof("listing available agent binaries")
    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.ReleasedStream
    97  		// Always use the primary stream here - the user can specify
    98  		// to override that decision.
    99  		syncContext.Stream = envtools.PreferredStreams(&jujuversion.Current, false, "")[0]
   100  	}
   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  	streams := []string{syncContext.Stream, envtools.ReleasedStream}
   105  	sourceTools, err := envtools.FindToolsForCloud(
   106  		[]simplestreams.DataSource{sourceDataSource}, simplestreams.CloudSpec{},
   107  		streams, syncContext.MajorVersion, syncContext.MinorVersion, coretools.Filter{})
   108  	if err != nil {
   109  		return errors.Trace(err)
   110  	}
   111  
   112  	logger.Infof("found %d agent binaries", len(sourceTools))
   113  	if !syncContext.AllVersions {
   114  		var latest version.Number
   115  		latest, sourceTools = sourceTools.Newest()
   116  		logger.Infof("found %d recent agent binaries (version %s)", len(sourceTools), latest)
   117  	}
   118  	for _, tool := range sourceTools {
   119  		logger.Debugf("found source agent binary: %v", tool)
   120  	}
   121  
   122  	logger.Infof("listing target agent binaries storage")
   123  	targetTools, err := syncContext.TargetToolsFinder.FindTools(syncContext.MajorVersion, syncContext.Stream)
   124  	switch err {
   125  	case nil, coretools.ErrNoMatches, envtools.ErrNoTools:
   126  	default:
   127  		return errors.Trace(err)
   128  	}
   129  	for _, tool := range targetTools {
   130  		logger.Debugf("found target agent binary: %v", tool)
   131  	}
   132  
   133  	missing := sourceTools.Exclude(targetTools)
   134  	logger.Infof("found %d agent binaries in target; %d agent binaries to be copied", len(targetTools), len(missing))
   135  	if syncContext.DryRun {
   136  		for _, tools := range missing {
   137  			logger.Infof("copying %s from %s", tools.Version, tools.URL)
   138  		}
   139  		return nil
   140  	}
   141  
   142  	err = copyTools(toolsDir, syncContext.Stream, missing, syncContext.TargetToolsUploader)
   143  	if err != nil {
   144  		return err
   145  	}
   146  	logger.Infof("copied %d agent binaries", len(missing))
   147  	return nil
   148  }
   149  
   150  // selectSourceDatasource returns a storage reader based on the source setting.
   151  func selectSourceDatasource(syncContext *SyncContext) (simplestreams.DataSource, error) {
   152  	source := syncContext.Source
   153  	if source == "" {
   154  		source = envtools.DefaultBaseURL
   155  	}
   156  	sourceURL, err := envtools.ToolsURL(source)
   157  	if err != nil {
   158  		return nil, err
   159  	}
   160  	logger.Infof("source for sync of agent binaries: %v", sourceURL)
   161  	return simplestreams.NewURLSignedDataSource("sync agent binaries source", sourceURL, keys.JujuPublicKey, utils.VerifySSLHostnames, simplestreams.CUSTOM_CLOUD_DATA, false), nil
   162  }
   163  
   164  // copyTools copies a set of tools from the source to the target.
   165  func copyTools(toolsDir, stream string, tools []*coretools.Tools, u ToolsUploader) error {
   166  	for _, tool := range tools {
   167  		logger.Infof("copying %s from %s", tool.Version, tool.URL)
   168  		if err := copyOneToolsPackage(toolsDir, stream, tool, u); err != nil {
   169  			return err
   170  		}
   171  	}
   172  	return nil
   173  }
   174  
   175  // copyOneToolsPackage copies one tool from the source to the target.
   176  func copyOneToolsPackage(toolsDir, stream string, tools *coretools.Tools, u ToolsUploader) error {
   177  	toolsName := envtools.StorageName(tools.Version, toolsDir)
   178  	logger.Infof("downloading %q %v (%v)", stream, toolsName, tools.URL)
   179  	resp, err := utils.GetValidatingHTTPClient().Get(tools.URL)
   180  	if err != nil {
   181  		return err
   182  	}
   183  	defer resp.Body.Close()
   184  	// Verify SHA-256 hash.
   185  	var buf bytes.Buffer
   186  	sha256, size, err := utils.ReadSHA256(io.TeeReader(resp.Body, &buf))
   187  	if err != nil {
   188  		return err
   189  	}
   190  	if tools.SHA256 == "" {
   191  		logger.Errorf("no SHA-256 hash for %v", tools.SHA256) // TODO(dfc) can you spot the bug ?
   192  	} else if sha256 != tools.SHA256 {
   193  		return errors.Errorf("SHA-256 hash mismatch (%v/%v)", sha256, tools.SHA256)
   194  	}
   195  	sizeInKB := (size + 512) / 1024
   196  	logger.Infof("uploading %v (%dkB) to model", toolsName, sizeInKB)
   197  	return u.UploadTools(toolsDir, stream, tools, buf.Bytes())
   198  }
   199  
   200  // UploadFunc is the type of Upload, which may be
   201  // reassigned to control the behaviour of tools
   202  // uploading.
   203  type UploadFunc func(stor storage.Storage, stream string, forceVersion *version.Number, series ...string) (*coretools.Tools, error)
   204  
   205  // Exported for testing.
   206  var Upload UploadFunc = upload
   207  
   208  // upload builds whatever version of github.com/juju/juju is in $GOPATH,
   209  // uploads it to the given storage, and returns a Tools instance describing
   210  // them. If forceVersion is not nil, the uploaded tools bundle will report
   211  // the given version number; if any fakeSeries are supplied, additional copies
   212  // of the built tools will be uploaded for use by machines of those series.
   213  // Juju tools built for one series do not necessarily run on another, but this
   214  // func exists only for development use cases.
   215  func upload(stor storage.Storage, stream string, forceVersion *version.Number, fakeSeries ...string) (*coretools.Tools, error) {
   216  	builtTools, err := BuildAgentTarball(true, forceVersion, stream)
   217  	if err != nil {
   218  		return nil, err
   219  	}
   220  	defer os.RemoveAll(builtTools.Dir)
   221  	logger.Debugf("Uploading agent binaries for %v", fakeSeries)
   222  	return syncBuiltTools(stor, stream, builtTools, fakeSeries...)
   223  }
   224  
   225  // cloneToolsForSeries copies the built tools tarball into a tarball for the specified
   226  // stream and series and generates corresponding metadata.
   227  func cloneToolsForSeries(toolsInfo *BuiltAgent, stream string, series ...string) error {
   228  	// Copy the tools to the target storage, recording a Tools struct for each one.
   229  	var targetTools coretools.List
   230  	targetTools = append(targetTools, &coretools.Tools{
   231  		Version: toolsInfo.Version,
   232  		Size:    toolsInfo.Size,
   233  		SHA256:  toolsInfo.Sha256Hash,
   234  	})
   235  	putTools := func(vers version.Binary) (string, error) {
   236  		name := envtools.StorageName(vers, stream)
   237  		src := filepath.Join(toolsInfo.Dir, toolsInfo.StorageName)
   238  		dest := filepath.Join(toolsInfo.Dir, name)
   239  		destDir := filepath.Dir(dest)
   240  		if err := os.MkdirAll(destDir, 0755); err != nil {
   241  			return "", err
   242  		}
   243  		if err := utils.CopyFile(dest, src); err != nil {
   244  			return "", err
   245  		}
   246  		// Append to targetTools the attributes required to write out tools metadata.
   247  		targetTools = append(targetTools, &coretools.Tools{
   248  			Version: vers,
   249  			Size:    toolsInfo.Size,
   250  			SHA256:  toolsInfo.Sha256Hash,
   251  		})
   252  		return name, nil
   253  	}
   254  	logger.Debugf("generating tarballs for %v", series)
   255  	for _, series := range series {
   256  		_, err := jujuseries.SeriesVersion(series)
   257  		if err != nil {
   258  			return err
   259  		}
   260  		if series != toolsInfo.Version.Series {
   261  			fakeVersion := toolsInfo.Version
   262  			fakeVersion.Series = series
   263  			if _, err := putTools(fakeVersion); err != nil {
   264  				return err
   265  			}
   266  		}
   267  	}
   268  	// The tools have been copied to a temp location from which they will be uploaded,
   269  	// now write out the matching simplestreams metadata so that SyncTools can find them.
   270  	metadataStore, err := filestorage.NewFileStorageWriter(toolsInfo.Dir)
   271  	if err != nil {
   272  		return err
   273  	}
   274  	logger.Debugf("generating agent metadata")
   275  	return envtools.MergeAndWriteMetadata(metadataStore, stream, stream, targetTools, false)
   276  }
   277  
   278  // BuiltAgent contains metadata for a tools tarball resulting from
   279  // a call to BundleTools.
   280  type BuiltAgent struct {
   281  	Version     version.Binary
   282  	Official    bool
   283  	Dir         string
   284  	StorageName string
   285  	Sha256Hash  string
   286  	Size        int64
   287  }
   288  
   289  // BuildAgentTarballFunc is a function which can build an agent tarball.
   290  type BuildAgentTarballFunc func(build bool, forceVersion *version.Number, stream string) (*BuiltAgent, error)
   291  
   292  // Override for testing.
   293  var BuildAgentTarball BuildAgentTarballFunc = buildAgentTarball
   294  
   295  // BuildAgentTarball bundles an agent tarball and places it in a temp directory in
   296  // the expected agent path.
   297  func buildAgentTarball(build bool, forceVersion *version.Number, stream string) (_ *BuiltAgent, 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("Making agent binary tarball")
   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, official, sha256Hash, err := envtools.BundleTools(build, f, forceVersion)
   312  	if err != nil {
   313  		return nil, err
   314  	}
   315  	// Built agent version needs to match the client used to bootstrap.
   316  	builtVersion := toolsVersion
   317  	builtVersion.Build = 0
   318  	clientVersion := jujuversion.Current
   319  	clientVersion.Build = 0
   320  	if builtVersion.Number.Compare(clientVersion) != 0 {
   321  		return nil, errors.Errorf("agent binary %v not compatible with bootstrap client %v", toolsVersion.Number, jujuversion.Current)
   322  	}
   323  	fileInfo, err := f.Stat()
   324  	if err != nil {
   325  		return nil, errors.Errorf("cannot stat newly made agent binary archive: %v", err)
   326  	}
   327  	size := fileInfo.Size()
   328  	reportedVersion := toolsVersion
   329  	if !official && forceVersion != nil {
   330  		reportedVersion.Number = *forceVersion
   331  	}
   332  	if official {
   333  		logger.Infof("using official agent binary %v (%dkB)", toolsVersion, (size+512)/1024)
   334  	} else {
   335  		logger.Infof("using agent binary %v aliased to %v (%dkB)", toolsVersion, reportedVersion, (size+512)/1024)
   336  	}
   337  	baseToolsDir, err := ioutil.TempDir("", "juju-tools")
   338  	if err != nil {
   339  		return nil, err
   340  	}
   341  
   342  	// If we exit with an error, clean up the built tools directory.
   343  	defer func() {
   344  		if err != nil {
   345  			os.RemoveAll(baseToolsDir)
   346  		}
   347  	}()
   348  
   349  	err = os.MkdirAll(filepath.Join(baseToolsDir, storage.BaseToolsPath, stream), 0755)
   350  	if err != nil {
   351  		return nil, err
   352  	}
   353  	storageName := envtools.StorageName(toolsVersion, stream)
   354  	err = utils.CopyFile(filepath.Join(baseToolsDir, storageName), f.Name())
   355  	if err != nil {
   356  		return nil, err
   357  	}
   358  	return &BuiltAgent{
   359  		Version:     toolsVersion,
   360  		Official:    official,
   361  		Dir:         baseToolsDir,
   362  		StorageName: storageName,
   363  		Size:        size,
   364  		Sha256Hash:  sha256Hash,
   365  	}, nil
   366  }
   367  
   368  // syncBuiltTools copies to storage a tools tarball and cloned copies for each series.
   369  func syncBuiltTools(stor storage.Storage, stream string, builtTools *BuiltAgent, fakeSeries ...string) (*coretools.Tools, error) {
   370  	if err := cloneToolsForSeries(builtTools, stream, fakeSeries...); err != nil {
   371  		return nil, err
   372  	}
   373  	syncContext := &SyncContext{
   374  		Source:              builtTools.Dir,
   375  		TargetToolsFinder:   StorageToolsFinder{stor},
   376  		TargetToolsUploader: StorageToolsUploader{stor, false, false},
   377  		AllVersions:         true,
   378  		Stream:              stream,
   379  		MajorVersion:        builtTools.Version.Major,
   380  		MinorVersion:        -1,
   381  	}
   382  	logger.Debugf("uploading agent binaries to cloud storage")
   383  	err := SyncTools(syncContext)
   384  	if err != nil {
   385  		return nil, err
   386  	}
   387  	url, err := stor.URL(builtTools.StorageName)
   388  	if err != nil {
   389  		return nil, err
   390  	}
   391  	return &coretools.Tools{
   392  		Version: builtTools.Version,
   393  		URL:     url,
   394  		Size:    builtTools.Size,
   395  		SHA256:  builtTools.Sha256Hash,
   396  	}, nil
   397  }
   398  
   399  // StorageToolsFinder is an implementation of ToolsFinder
   400  // that searches for tools in the specified storage.
   401  type StorageToolsFinder struct {
   402  	Storage storage.StorageReader
   403  }
   404  
   405  func (f StorageToolsFinder) FindTools(major int, stream string) (coretools.List, error) {
   406  	return envtools.ReadList(f.Storage, stream, major, -1)
   407  }
   408  
   409  // StorageToolsUplader is an implementation of ToolsUploader that
   410  // writes tools to the provided storage and then writes merged
   411  // metadata, optionally with mirrors.
   412  type StorageToolsUploader struct {
   413  	Storage       storage.Storage
   414  	WriteMetadata bool
   415  	WriteMirrors  envtools.ShouldWriteMirrors
   416  }
   417  
   418  func (u StorageToolsUploader) UploadTools(toolsDir, stream string, tools *coretools.Tools, data []byte) error {
   419  	toolsName := envtools.StorageName(tools.Version, toolsDir)
   420  	if err := u.Storage.Put(toolsName, bytes.NewReader(data), int64(len(data))); err != nil {
   421  		return err
   422  	}
   423  	if !u.WriteMetadata {
   424  		return nil
   425  	}
   426  	err := envtools.MergeAndWriteMetadata(u.Storage, toolsDir, stream, coretools.List{tools}, u.WriteMirrors)
   427  	if err != nil {
   428  		logger.Errorf("error writing agent metadata: %v", err)
   429  		return err
   430  	}
   431  	return nil
   432  }