github.com/mattyw/juju@v0.0.0-20140610034352-732aecd63861/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/loggo"
    15  	"github.com/juju/utils"
    16  
    17  	"github.com/juju/juju/environs/filestorage"
    18  	"github.com/juju/juju/environs/simplestreams"
    19  	"github.com/juju/juju/environs/storage"
    20  	envtools "github.com/juju/juju/environs/tools"
    21  	coretools "github.com/juju/juju/tools"
    22  	"github.com/juju/juju/version"
    23  	"github.com/juju/juju/version/ubuntu"
    24  )
    25  
    26  var logger = loggo.GetLogger("juju.environs.sync")
    27  
    28  // SyncContext describes the context for tool synchronization.
    29  type SyncContext struct {
    30  	// Target holds the destination for the tool synchronization
    31  	Target storage.Storage
    32  
    33  	// AllVersions controls the copy of all versions, not only the latest.
    34  	AllVersions bool
    35  
    36  	// Copy tools with major version, if MajorVersion > 0.
    37  	MajorVersion int
    38  
    39  	// Copy tools with minor version, if MinorVersion > 0.
    40  	MinorVersion int
    41  
    42  	// DryRun controls that nothing is copied. Instead it's logged
    43  	// what would be coppied.
    44  	DryRun bool
    45  
    46  	// Dev controls the copy of development versions as well as released ones.
    47  	Dev bool
    48  
    49  	// Tools are being synced for a public cloud so include mirrors information.
    50  	Public bool
    51  
    52  	// Source, if non-empty, specifies a directory in the local file system
    53  	// to use as a source.
    54  	Source string
    55  }
    56  
    57  // SyncTools copies the Juju tools tarball from the official bucket
    58  // or a specified source directory into the user's environment.
    59  func SyncTools(syncContext *SyncContext) error {
    60  	sourceDataSource, err := selectSourceDatasource(syncContext)
    61  	if err != nil {
    62  		return err
    63  	}
    64  
    65  	logger.Infof("listing available tools")
    66  	if syncContext.MajorVersion == 0 && syncContext.MinorVersion == 0 {
    67  		syncContext.MajorVersion = version.Current.Major
    68  		syncContext.MinorVersion = -1
    69  		if !syncContext.AllVersions {
    70  			syncContext.MinorVersion = version.Current.Minor
    71  		}
    72  	} else if !syncContext.Dev && syncContext.MinorVersion != -1 {
    73  		// If a major.minor version is specified, we allow dev versions.
    74  		// If Dev is already true, leave it alone.
    75  		syncContext.Dev = true
    76  	}
    77  
    78  	released := !syncContext.Dev && !version.Current.IsDev()
    79  	sourceTools, err := envtools.FindToolsForCloud(
    80  		[]simplestreams.DataSource{sourceDataSource}, simplestreams.CloudSpec{},
    81  		syncContext.MajorVersion, syncContext.MinorVersion, coretools.Filter{Released: released})
    82  	if err != nil {
    83  		return err
    84  	}
    85  
    86  	logger.Infof("found %d tools", len(sourceTools))
    87  	if !syncContext.AllVersions {
    88  		var latest version.Number
    89  		latest, sourceTools = sourceTools.Newest()
    90  		logger.Infof("found %d recent tools (version %s)", len(sourceTools), latest)
    91  	}
    92  	for _, tool := range sourceTools {
    93  		logger.Debugf("found source tool: %v", tool)
    94  	}
    95  
    96  	logger.Infof("listing target tools storage")
    97  	targetStorage := syncContext.Target
    98  	targetTools, err := envtools.ReadList(targetStorage, syncContext.MajorVersion, -1)
    99  	switch err {
   100  	case nil, coretools.ErrNoMatches, envtools.ErrNoTools:
   101  	default:
   102  		return err
   103  	}
   104  	for _, tool := range targetTools {
   105  		logger.Debugf("found target tool: %v", tool)
   106  	}
   107  
   108  	missing := sourceTools.Exclude(targetTools)
   109  	logger.Infof("found %d tools in target; %d tools to be copied", len(targetTools), len(missing))
   110  	err = copyTools(missing, syncContext, targetStorage)
   111  	if err != nil {
   112  		return err
   113  	}
   114  	logger.Infof("copied %d tools", len(missing))
   115  
   116  	logger.Infof("generating tools metadata")
   117  	if !syncContext.DryRun {
   118  		targetTools = append(targetTools, missing...)
   119  		writeMirrors := envtools.DoNotWriteMirrors
   120  		if syncContext.Public {
   121  			writeMirrors = envtools.WriteMirrors
   122  		}
   123  		err = envtools.MergeAndWriteMetadata(targetStorage, targetTools, writeMirrors)
   124  		if err != nil {
   125  			return err
   126  		}
   127  	}
   128  	logger.Infof("tools metadata written")
   129  	return nil
   130  }
   131  
   132  // selectSourceDatasource returns a storage reader based on the source setting.
   133  func selectSourceDatasource(syncContext *SyncContext) (simplestreams.DataSource, error) {
   134  	source := syncContext.Source
   135  	if source == "" {
   136  		source = envtools.DefaultBaseURL
   137  	}
   138  	sourceURL, err := envtools.ToolsURL(source)
   139  	if err != nil {
   140  		return nil, err
   141  	}
   142  	logger.Infof("using sync tools source: %v", sourceURL)
   143  	return simplestreams.NewURLDataSource("sync tools source", sourceURL, utils.VerifySSLHostnames), nil
   144  }
   145  
   146  // copyTools copies a set of tools from the source to the target.
   147  func copyTools(tools []*coretools.Tools, syncContext *SyncContext, dest storage.Storage) error {
   148  	for _, tool := range tools {
   149  		logger.Infof("copying %s from %s", tool.Version, tool.URL)
   150  		if syncContext.DryRun {
   151  			continue
   152  		}
   153  		if err := copyOneToolsPackage(tool, dest); err != nil {
   154  			return err
   155  		}
   156  	}
   157  	return nil
   158  }
   159  
   160  // copyOneToolsPackage copies one tool from the source to the target.
   161  func copyOneToolsPackage(tool *coretools.Tools, dest storage.Storage) error {
   162  	toolsName := envtools.StorageName(tool.Version)
   163  	logger.Infof("copying %v", toolsName)
   164  	resp, err := utils.GetValidatingHTTPClient().Get(tool.URL)
   165  	if err != nil {
   166  		return err
   167  	}
   168  	buf := &bytes.Buffer{}
   169  	srcFile := resp.Body
   170  	defer srcFile.Close()
   171  	tool.SHA256, tool.Size, err = utils.ReadSHA256(io.TeeReader(srcFile, buf))
   172  	if err != nil {
   173  		return err
   174  	}
   175  	sizeInKB := (tool.Size + 512) / 1024
   176  	logger.Infof("downloaded %v (%dkB), uploading", toolsName, sizeInKB)
   177  	logger.Infof("download %dkB, uploading", sizeInKB)
   178  	return dest.Put(toolsName, buf, tool.Size)
   179  }
   180  
   181  // UploadFunc is the type of Upload, which may be
   182  // reassigned to control the behaviour of tools
   183  // uploading.
   184  type UploadFunc func(stor storage.Storage, forceVersion *version.Number, series ...string) (*coretools.Tools, error)
   185  
   186  // Upload builds whatever version of github.com/juju/juju is in $GOPATH,
   187  // uploads it to the given storage, and returns a Tools instance describing
   188  // them. If forceVersion is not nil, the uploaded tools bundle will report
   189  // the given version number; if any fakeSeries are supplied, additional copies
   190  // of the built tools will be uploaded for use by machines of those series.
   191  // Juju tools built for one series do not necessarily run on another, but this
   192  // func exists only for development use cases.
   193  var Upload UploadFunc = upload
   194  
   195  func upload(stor storage.Storage, forceVersion *version.Number, fakeSeries ...string) (*coretools.Tools, error) {
   196  	builtTools, err := BuildToolsTarball(forceVersion)
   197  	if err != nil {
   198  		return nil, err
   199  	}
   200  	defer os.RemoveAll(builtTools.Dir)
   201  	logger.Debugf("Uploading tools for %v", fakeSeries)
   202  	return SyncBuiltTools(stor, builtTools, fakeSeries...)
   203  }
   204  
   205  // cloneToolsForSeries copies the built tools tarball into a tarball for the specified
   206  // series and generates corresponding metadata.
   207  func cloneToolsForSeries(toolsInfo *BuiltTools, series ...string) error {
   208  	// Copy the tools to the target storage, recording a Tools struct for each one.
   209  	var targetTools coretools.List
   210  	targetTools = append(targetTools, &coretools.Tools{
   211  		Version: toolsInfo.Version,
   212  		Size:    toolsInfo.Size,
   213  		SHA256:  toolsInfo.Sha256Hash,
   214  	})
   215  	putTools := func(vers version.Binary) (string, error) {
   216  		name := envtools.StorageName(vers)
   217  		src := filepath.Join(toolsInfo.Dir, toolsInfo.StorageName)
   218  		dest := filepath.Join(toolsInfo.Dir, name)
   219  		err := utils.CopyFile(dest, src)
   220  		if err != nil {
   221  			return "", err
   222  		}
   223  		// Append to targetTools the attributes required to write out tools metadata.
   224  		targetTools = append(targetTools, &coretools.Tools{
   225  			Version: vers,
   226  			Size:    toolsInfo.Size,
   227  			SHA256:  toolsInfo.Sha256Hash,
   228  		})
   229  		return name, nil
   230  	}
   231  	logger.Debugf("generating tarballs for %v", series)
   232  	for _, series := range series {
   233  		_, err := ubuntu.SeriesVersion(series)
   234  		if err != nil {
   235  			return err
   236  		}
   237  		if series != toolsInfo.Version.Series {
   238  			fakeVersion := toolsInfo.Version
   239  			fakeVersion.Series = series
   240  			if _, err := putTools(fakeVersion); err != nil {
   241  				return err
   242  			}
   243  		}
   244  	}
   245  	// The tools have been copied to a temp location from which they will be uploaded,
   246  	// now write out the matching simplestreams metadata so that SyncTools can find them.
   247  	metadataStore, err := filestorage.NewFileStorageWriter(toolsInfo.Dir)
   248  	if err != nil {
   249  		return err
   250  	}
   251  	logger.Debugf("generating tools metadata")
   252  	return envtools.MergeAndWriteMetadata(metadataStore, targetTools, false)
   253  }
   254  
   255  // BuiltTools contains metadata for a tools tarball resulting from
   256  // a call to BundleTools.
   257  type BuiltTools struct {
   258  	Version     version.Binary
   259  	Dir         string
   260  	StorageName string
   261  	Sha256Hash  string
   262  	Size        int64
   263  }
   264  
   265  // BuildToolsTarballFunc is a function which can build a tools tarball.
   266  type BuildToolsTarballFunc func(forceVersion *version.Number) (*BuiltTools, error)
   267  
   268  // Override for testing.
   269  var BuildToolsTarball BuildToolsTarballFunc = buildToolsTarball
   270  
   271  // buildToolsTarball bundles a tools tarball and places it in a temp directory in
   272  // the expected tools path.
   273  func buildToolsTarball(forceVersion *version.Number) (builtTools *BuiltTools, err error) {
   274  	// TODO(rog) find binaries from $PATH when not using a development
   275  	// version of juju within a $GOPATH.
   276  
   277  	logger.Debugf("Building tools")
   278  	// We create the entire archive before asking the environment to
   279  	// start uploading so that we can be sure we have archived
   280  	// correctly.
   281  	f, err := ioutil.TempFile("", "juju-tgz")
   282  	if err != nil {
   283  		return nil, err
   284  	}
   285  	defer f.Close()
   286  	defer os.Remove(f.Name())
   287  	toolsVersion, sha256Hash, err := envtools.BundleTools(f, forceVersion)
   288  	if err != nil {
   289  		return nil, err
   290  	}
   291  	fileInfo, err := f.Stat()
   292  	if err != nil {
   293  		return nil, fmt.Errorf("cannot stat newly made tools archive: %v", err)
   294  	}
   295  	size := fileInfo.Size()
   296  	logger.Infof("built tools %v (%dkB)", toolsVersion, (size+512)/1024)
   297  	baseToolsDir, err := ioutil.TempDir("", "juju-tools")
   298  	if err != nil {
   299  		return nil, err
   300  	}
   301  
   302  	// If we exit with an error, clean up the built tools directory.
   303  	defer func() {
   304  		if err != nil {
   305  			os.RemoveAll(baseToolsDir)
   306  		}
   307  	}()
   308  
   309  	err = os.MkdirAll(filepath.Join(baseToolsDir, storage.BaseToolsPath, "releases"), 0755)
   310  	if err != nil {
   311  		return nil, err
   312  	}
   313  	storageName := envtools.StorageName(toolsVersion)
   314  	err = utils.CopyFile(filepath.Join(baseToolsDir, storageName), f.Name())
   315  	if err != nil {
   316  		return nil, err
   317  	}
   318  	return &BuiltTools{
   319  		Version:     toolsVersion,
   320  		Dir:         baseToolsDir,
   321  		StorageName: storageName,
   322  		Size:        size,
   323  		Sha256Hash:  sha256Hash,
   324  	}, nil
   325  }
   326  
   327  // SyncBuiltTools copies to storage a tools tarball and cloned copies for each series.
   328  func SyncBuiltTools(stor storage.Storage, builtTools *BuiltTools, fakeSeries ...string) (*coretools.Tools, error) {
   329  	if err := cloneToolsForSeries(builtTools, fakeSeries...); err != nil {
   330  		return nil, err
   331  	}
   332  	syncContext := &SyncContext{
   333  		Source:       builtTools.Dir,
   334  		Target:       stor,
   335  		AllVersions:  true,
   336  		Dev:          builtTools.Version.IsDev(),
   337  		MajorVersion: builtTools.Version.Major,
   338  		MinorVersion: -1,
   339  	}
   340  	logger.Debugf("uploading tools to cloud storage")
   341  	err := SyncTools(syncContext)
   342  	if err != nil {
   343  		return nil, err
   344  	}
   345  	url, err := stor.URL(builtTools.StorageName)
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  	return &coretools.Tools{
   350  		Version: builtTools.Version,
   351  		URL:     url,
   352  		Size:    builtTools.Size,
   353  		SHA256:  builtTools.Sha256Hash,
   354  	}, nil
   355  }