github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/environs/tools/simplestreams.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // The tools package supports locating, parsing, and filtering Ubuntu tools metadata in simplestreams format.
     5  // See http://launchpad.net/simplestreams and in particular the doc/README file in that project for more information
     6  // about the file formats.
     7  package tools
     8  
     9  import (
    10  	"bytes"
    11  	"crypto/sha256"
    12  	"encoding/json"
    13  	"fmt"
    14  	"hash"
    15  	"io"
    16  	"io/ioutil"
    17  	"path"
    18  	"sort"
    19  	"strings"
    20  	"time"
    21  
    22  	"github.com/juju/errors"
    23  	"github.com/juju/utils/arch"
    24  	"github.com/juju/utils/series"
    25  	"github.com/juju/utils/set"
    26  	"github.com/juju/version"
    27  
    28  	"github.com/juju/juju/environs/simplestreams"
    29  	"github.com/juju/juju/environs/storage"
    30  	coretools "github.com/juju/juju/tools"
    31  )
    32  
    33  func init() {
    34  	simplestreams.RegisterStructTags(ToolsMetadata{})
    35  }
    36  
    37  const (
    38  	// ImageIds is the simplestreams tools content type.
    39  	ContentDownload = "content-download"
    40  
    41  	// StreamsVersionV1 is used to construct the path for accessing streams data.
    42  	StreamsVersionV1 = "v1"
    43  
    44  	// IndexFileVersion is used to construct the streams index file.
    45  	IndexFileVersion = 2
    46  )
    47  
    48  var currentStreamsVersion = StreamsVersionV1
    49  
    50  // This needs to be a var so we can override it for testing.
    51  var DefaultBaseURL = "https://streams.canonical.com/juju/tools"
    52  
    53  const (
    54  	// Legacy release directory for Juju < 1.21.
    55  	LegacyReleaseDirectory = "releases"
    56  
    57  	// Used to specify the released tools metadata.
    58  	ReleasedStream = "released"
    59  
    60  	// Used to specify metadata for testing tools.
    61  	TestingStream = "testing"
    62  
    63  	// Used to specify the proposed tools metadata.
    64  	ProposedStream = "proposed"
    65  
    66  	// Used to specify the devel tools metadata.
    67  	DevelStream = "devel"
    68  )
    69  
    70  // ToolsConstraint defines criteria used to find a tools metadata record.
    71  type ToolsConstraint struct {
    72  	simplestreams.LookupParams
    73  	Version      version.Number
    74  	MajorVersion int
    75  	MinorVersion int
    76  }
    77  
    78  // NewVersionedToolsConstraint returns a ToolsConstraint for a tools with a specific version.
    79  func NewVersionedToolsConstraint(vers version.Number, params simplestreams.LookupParams) *ToolsConstraint {
    80  	return &ToolsConstraint{LookupParams: params, Version: vers}
    81  }
    82  
    83  // NewGeneralToolsConstraint returns a ToolsConstraint for tools with matching major/minor version numbers.
    84  func NewGeneralToolsConstraint(majorVersion, minorVersion int, params simplestreams.LookupParams) *ToolsConstraint {
    85  	return &ToolsConstraint{LookupParams: params, Version: version.Zero,
    86  		MajorVersion: majorVersion, MinorVersion: minorVersion}
    87  }
    88  
    89  // IndexIds generates a string array representing product ids formed similarly to an ISCSI qualified name (IQN).
    90  func (tc *ToolsConstraint) IndexIds() []string {
    91  	if tc.Stream == "" {
    92  		return nil
    93  	}
    94  	return []string{ToolsContentId(tc.Stream)}
    95  }
    96  
    97  // ProductIds generates a string array representing product ids formed similarly to an ISCSI qualified name (IQN).
    98  func (tc *ToolsConstraint) ProductIds() ([]string, error) {
    99  	var allIds []string
   100  	for _, ser := range tc.Series {
   101  		version, err := series.SeriesVersion(ser)
   102  		if err != nil {
   103  			if series.IsUnknownSeriesVersionError(err) {
   104  				logger.Debugf("ignoring unknown series %q", ser)
   105  				continue
   106  			}
   107  			return nil, err
   108  		}
   109  		ids := make([]string, len(tc.Arches))
   110  		for i, arch := range tc.Arches {
   111  			ids[i] = fmt.Sprintf("com.ubuntu.juju:%s:%s", version, arch)
   112  		}
   113  		allIds = append(allIds, ids...)
   114  	}
   115  	return allIds, nil
   116  }
   117  
   118  // ToolsMetadata holds information about a particular tools tarball.
   119  type ToolsMetadata struct {
   120  	Release  string `json:"release"`
   121  	Version  string `json:"version"`
   122  	Arch     string `json:"arch"`
   123  	Size     int64  `json:"size"`
   124  	Path     string `json:"path"`
   125  	FullPath string `json:"-"`
   126  	FileType string `json:"ftype"`
   127  	SHA256   string `json:"sha256"`
   128  }
   129  
   130  func (t *ToolsMetadata) String() string {
   131  	return fmt.Sprintf("%+v", *t)
   132  }
   133  
   134  // sortString is used by byVersion to sort a list of ToolsMetadata.
   135  func (t *ToolsMetadata) sortString() string {
   136  	return fmt.Sprintf("%v-%s-%s", t.Version, t.Release, t.Arch)
   137  }
   138  
   139  // binary returns the tools metadata's binary version, which may be used for
   140  // map lookup.
   141  func (t *ToolsMetadata) binary() (version.Binary, error) {
   142  	num, err := version.Parse(t.Version)
   143  	if err != nil {
   144  		return version.Binary{}, errors.Trace(err)
   145  	}
   146  	return version.Binary{
   147  		Number: num,
   148  		Series: t.Release,
   149  		Arch:   t.Arch,
   150  	}, nil
   151  }
   152  
   153  func (t *ToolsMetadata) productId() (string, error) {
   154  	seriesVersion, err := series.SeriesVersion(t.Release)
   155  	if err != nil {
   156  		return "", err
   157  	}
   158  	return fmt.Sprintf("com.ubuntu.juju:%s:%s", seriesVersion, t.Arch), nil
   159  }
   160  
   161  // Fetch returns a list of tools for the specified cloud matching the constraint.
   162  // The base URL locations are as specified - the first location which has a file is the one used.
   163  // Signed data is preferred, but if there is no signed data available and onlySigned is false,
   164  // then unsigned data is used.
   165  func Fetch(
   166  	sources []simplestreams.DataSource, cons *ToolsConstraint,
   167  ) ([]*ToolsMetadata, *simplestreams.ResolveInfo, error) {
   168  
   169  	params := simplestreams.GetMetadataParams{
   170  		StreamsVersion:   currentStreamsVersion,
   171  		LookupConstraint: cons,
   172  		ValueParams: simplestreams.ValueParams{
   173  			DataType:        ContentDownload,
   174  			FilterFunc:      appendMatchingTools,
   175  			MirrorContentId: ToolsContentId(cons.Stream),
   176  			ValueTemplate:   ToolsMetadata{},
   177  		},
   178  	}
   179  	items, resolveInfo, err := simplestreams.GetMetadata(sources, params)
   180  	if err != nil {
   181  		return nil, nil, err
   182  	}
   183  	metadata := make([]*ToolsMetadata, len(items))
   184  	for i, md := range items {
   185  		metadata[i] = md.(*ToolsMetadata)
   186  	}
   187  	// Sorting the metadata is not strictly necessary, but it ensures consistent ordering for
   188  	// all compilers, and it just makes it easier to look at the data.
   189  	Sort(metadata)
   190  	return metadata, resolveInfo, nil
   191  }
   192  
   193  // Sort sorts a slice of ToolsMetadata in ascending order of their version
   194  // in order to ensure the results of Fetch are ordered deterministically.
   195  func Sort(metadata []*ToolsMetadata) {
   196  	sort.Sort(byVersion(metadata))
   197  }
   198  
   199  type byVersion []*ToolsMetadata
   200  
   201  func (b byVersion) Len() int           { return len(b) }
   202  func (b byVersion) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
   203  func (b byVersion) Less(i, j int) bool { return b[i].sortString() < b[j].sortString() }
   204  
   205  // appendMatchingTools updates matchingTools with tools metadata records from tools which belong to the
   206  // specified series. If a tools record already exists in matchingTools, it is not overwritten.
   207  func appendMatchingTools(source simplestreams.DataSource, matchingTools []interface{},
   208  	tools map[string]interface{}, cons simplestreams.LookupConstraint) ([]interface{}, error) {
   209  
   210  	toolsMap := make(map[version.Binary]*ToolsMetadata, len(matchingTools))
   211  	for _, val := range matchingTools {
   212  		tm := val.(*ToolsMetadata)
   213  		binary, err := tm.binary()
   214  		if err != nil {
   215  			return nil, errors.Trace(err)
   216  		}
   217  		toolsMap[binary] = tm
   218  	}
   219  	for _, val := range tools {
   220  		tm := val.(*ToolsMetadata)
   221  		if !set.NewStrings(cons.Params().Series...).Contains(tm.Release) {
   222  			continue
   223  		}
   224  		if toolsConstraint, ok := cons.(*ToolsConstraint); ok {
   225  			tmNumber := version.MustParse(tm.Version)
   226  			if toolsConstraint.Version == version.Zero {
   227  				if toolsConstraint.MajorVersion >= 0 && toolsConstraint.MajorVersion != tmNumber.Major {
   228  					continue
   229  				}
   230  				if toolsConstraint.MinorVersion >= 0 && toolsConstraint.MinorVersion != tmNumber.Minor {
   231  					continue
   232  				}
   233  			} else {
   234  				if toolsConstraint.Version != tmNumber {
   235  					continue
   236  				}
   237  			}
   238  		}
   239  		binary, err := tm.binary()
   240  		if err != nil {
   241  			return nil, errors.Trace(err)
   242  		}
   243  		if _, ok := toolsMap[binary]; !ok {
   244  			tm.FullPath, _ = source.URL(tm.Path)
   245  			matchingTools = append(matchingTools, tm)
   246  		}
   247  	}
   248  	return matchingTools, nil
   249  }
   250  
   251  type MetadataFile struct {
   252  	Path string
   253  	Data []byte
   254  }
   255  
   256  // MetadataFromTools returns a tools metadata list derived from the
   257  // given tools list. The size and sha256 will not be computed if
   258  // missing.
   259  func MetadataFromTools(toolsList coretools.List, toolsDir string) []*ToolsMetadata {
   260  	metadata := make([]*ToolsMetadata, len(toolsList))
   261  	for i, t := range toolsList {
   262  		path := fmt.Sprintf("%s/juju-%s-%s-%s.tgz", toolsDir, t.Version.Number, t.Version.Series, t.Version.Arch)
   263  		metadata[i] = &ToolsMetadata{
   264  			Release:  t.Version.Series,
   265  			Version:  t.Version.Number.String(),
   266  			Arch:     t.Version.Arch,
   267  			Path:     path,
   268  			FileType: "tar.gz",
   269  			Size:     t.Size,
   270  			SHA256:   t.SHA256,
   271  		}
   272  	}
   273  	return metadata
   274  }
   275  
   276  // ResolveMetadata resolves incomplete metadata
   277  // by fetching the tools from storage and computing
   278  // the size and hash locally.
   279  func ResolveMetadata(stor storage.StorageReader, toolsDir string, metadata []*ToolsMetadata) error {
   280  	for _, md := range metadata {
   281  		if md.Size != 0 {
   282  			continue
   283  		}
   284  		binary, err := md.binary()
   285  		if err != nil {
   286  			return errors.Annotate(err, "cannot resolve metadata")
   287  		}
   288  		logger.Infof("Fetching tools from dir %q to generate hash: %v", toolsDir, binary)
   289  		size, sha256hash, err := fetchToolsHash(stor, toolsDir, binary)
   290  		// Older versions of Juju only know about ppc64, not ppc64el,
   291  		// so if there's no metadata for ppc64, dd metadata for that arch.
   292  		if errors.IsNotFound(err) && binary.Arch == arch.LEGACY_PPC64 {
   293  			ppc64elBinary := binary
   294  			ppc64elBinary.Arch = arch.PPC64EL
   295  			md.Path = strings.Replace(md.Path, binary.Arch, ppc64elBinary.Arch, -1)
   296  			size, sha256hash, err = fetchToolsHash(stor, toolsDir, ppc64elBinary)
   297  		}
   298  		if err != nil {
   299  			return err
   300  		}
   301  		md.Size = size
   302  		md.SHA256 = fmt.Sprintf("%x", sha256hash.Sum(nil))
   303  	}
   304  	return nil
   305  }
   306  
   307  // MergeMetadata merges the given tools metadata.
   308  // If metadata for the same tools version exists in both lists,
   309  // an entry with non-empty size/SHA256 takes precedence; if
   310  // the two entries have different sizes/hashes, then an error is
   311  // returned.
   312  func MergeMetadata(tmlist1, tmlist2 []*ToolsMetadata) ([]*ToolsMetadata, error) {
   313  	merged := make(map[version.Binary]*ToolsMetadata)
   314  	for _, tm := range tmlist1 {
   315  		binary, err := tm.binary()
   316  		if err != nil {
   317  			return nil, errors.Annotate(err, "cannot merge metadata")
   318  		}
   319  		merged[binary] = tm
   320  	}
   321  	for _, tm := range tmlist2 {
   322  		binary, err := tm.binary()
   323  		if err != nil {
   324  			return nil, errors.Annotate(err, "cannot merge metadata")
   325  		}
   326  		if existing, ok := merged[binary]; ok {
   327  			if tm.Size != 0 {
   328  				if existing.Size == 0 {
   329  					merged[binary] = tm
   330  				} else if existing.Size != tm.Size || existing.SHA256 != tm.SHA256 {
   331  					return nil, fmt.Errorf(
   332  						"metadata mismatch for %s: sizes=(%v,%v) sha256=(%v,%v)",
   333  						binary.String(),
   334  						existing.Size, tm.Size,
   335  						existing.SHA256, tm.SHA256,
   336  					)
   337  				}
   338  			}
   339  		} else {
   340  			merged[binary] = tm
   341  		}
   342  	}
   343  	list := make([]*ToolsMetadata, 0, len(merged))
   344  	for _, metadata := range merged {
   345  		list = append(list, metadata)
   346  	}
   347  	Sort(list)
   348  	return list, nil
   349  }
   350  
   351  // ReadMetadata returns the tools metadata from the given storage for the specified stream.
   352  func ReadMetadata(store storage.StorageReader, stream string) ([]*ToolsMetadata, error) {
   353  	dataSource := storage.NewStorageSimpleStreamsDataSource("existing metadata", store, storage.BaseToolsPath, simplestreams.EXISTING_CLOUD_DATA, false)
   354  	toolsConstraint, err := makeToolsConstraint(simplestreams.CloudSpec{}, stream, -1, -1, coretools.Filter{})
   355  	if err != nil {
   356  		return nil, err
   357  	}
   358  	metadata, _, err := Fetch([]simplestreams.DataSource{dataSource}, toolsConstraint)
   359  	if err != nil && !errors.IsNotFound(err) {
   360  		return nil, err
   361  	}
   362  	return metadata, nil
   363  }
   364  
   365  // AllMetadataStreams is the set of streams for which there will be simplestreams tools metadata.
   366  var AllMetadataStreams = []string{ReleasedStream, ProposedStream, TestingStream, DevelStream}
   367  
   368  // ReadAllMetadata returns the tools metadata from the given storage for all streams.
   369  // The result is a map of metadata slices, keyed on stream.
   370  func ReadAllMetadata(store storage.StorageReader) (map[string][]*ToolsMetadata, error) {
   371  	streamMetadata := make(map[string][]*ToolsMetadata)
   372  	for _, stream := range AllMetadataStreams {
   373  		metadata, err := ReadMetadata(store, stream)
   374  		if err != nil {
   375  			return nil, err
   376  		}
   377  		if len(metadata) == 0 {
   378  			continue
   379  		}
   380  		streamMetadata[stream] = metadata
   381  	}
   382  	return streamMetadata, nil
   383  }
   384  
   385  // removeMetadataUpdated unmarshalls simplestreams metadata, clears the
   386  // updated attribute, and then marshalls back to a string.
   387  func removeMetadataUpdated(metadataBytes []byte) (string, error) {
   388  	var metadata map[string]interface{}
   389  	err := json.Unmarshal(metadataBytes, &metadata)
   390  	if err != nil {
   391  		return "", err
   392  	}
   393  	delete(metadata, "updated")
   394  
   395  	metadataJson, err := json.Marshal(metadata)
   396  	if err != nil {
   397  		return "", err
   398  	}
   399  	return string(metadataJson), nil
   400  }
   401  
   402  // metadataUnchanged returns true if the content of metadata for stream in stor is the same
   403  // as generatedMetadata, ignoring the "updated" attribute.
   404  func metadataUnchanged(stor storage.Storage, stream string, generatedMetadata []byte) (bool, error) {
   405  	mdPath := ProductMetadataPath(stream)
   406  	filePath := path.Join(storage.BaseToolsPath, mdPath)
   407  	existingDataReader, err := stor.Get(filePath)
   408  	// If the file can't be retrieved, consider it has changed.
   409  	if err != nil {
   410  		return false, nil
   411  	}
   412  	defer existingDataReader.Close()
   413  	existingData, err := ioutil.ReadAll(existingDataReader)
   414  	if err != nil {
   415  		return false, err
   416  	}
   417  
   418  	// To do the comparison, we unmarshall the metadata, clear the
   419  	// updated value, and marshall back to a string.
   420  	existingMetadata, err := removeMetadataUpdated(existingData)
   421  	if err != nil {
   422  		return false, err
   423  	}
   424  	newMetadata, err := removeMetadataUpdated(generatedMetadata)
   425  	if err != nil {
   426  		return false, err
   427  	}
   428  	return existingMetadata == newMetadata, nil
   429  }
   430  
   431  // WriteMetadata writes the given tools metadata for the specified streams to the given storage.
   432  // streamMetadata contains all known metadata so that the correct index files can be written.
   433  // Only product files for the specified streams are written.
   434  func WriteMetadata(stor storage.Storage, streamMetadata map[string][]*ToolsMetadata, streams []string, writeMirrors ShouldWriteMirrors) error {
   435  	updated := time.Now()
   436  	index, legacyIndex, products, err := MarshalToolsMetadataJSON(streamMetadata, updated)
   437  	if err != nil {
   438  		return err
   439  	}
   440  	metadataInfo := []MetadataFile{
   441  		{simplestreams.UnsignedIndex(currentStreamsVersion, IndexFileVersion), index},
   442  	}
   443  	if legacyIndex != nil {
   444  		metadataInfo = append(metadataInfo, MetadataFile{
   445  			simplestreams.UnsignedIndex(currentStreamsVersion, 1), legacyIndex,
   446  		})
   447  	}
   448  	for _, stream := range streams {
   449  		if metadata, ok := products[stream]; ok {
   450  			// If metadata hasn't changed, do not overwrite.
   451  			unchanged, err := metadataUnchanged(stor, stream, metadata)
   452  			if err != nil {
   453  				return err
   454  			}
   455  			if unchanged {
   456  				logger.Infof("Metadata for stream %q unchanged", stream)
   457  				continue
   458  			}
   459  			// Metadata is different, so include it.
   460  			metadataInfo = append(metadataInfo, MetadataFile{ProductMetadataPath(stream), metadata})
   461  		}
   462  	}
   463  	if writeMirrors {
   464  		streamsMirrorsMetadata := make(map[string][]simplestreams.MirrorReference)
   465  		for stream := range streamMetadata {
   466  			streamsMirrorsMetadata[ToolsContentId(stream)] = []simplestreams.MirrorReference{{
   467  				Updated:  updated.Format("20060102"), // YYYYMMDD
   468  				DataType: ContentDownload,
   469  				Format:   simplestreams.MirrorFormat,
   470  				Path:     simplestreams.MirrorFile,
   471  			}}
   472  		}
   473  		mirrorsMetadata := map[string]map[string][]simplestreams.MirrorReference{
   474  			"mirrors": streamsMirrorsMetadata,
   475  		}
   476  		mirrorsInfo, err := json.MarshalIndent(&mirrorsMetadata, "", "    ")
   477  		if err != nil {
   478  			return err
   479  		}
   480  		metadataInfo = append(
   481  			metadataInfo, MetadataFile{simplestreams.UnsignedMirror(currentStreamsVersion), mirrorsInfo})
   482  	}
   483  	return writeMetadataFiles(stor, metadataInfo)
   484  }
   485  
   486  var writeMetadataFiles = func(stor storage.Storage, metadataInfo []MetadataFile) error {
   487  	for _, md := range metadataInfo {
   488  		filePath := path.Join(storage.BaseToolsPath, md.Path)
   489  		logger.Infof("Writing %s", filePath)
   490  		err := stor.Put(filePath, bytes.NewReader(md.Data), int64(len(md.Data)))
   491  		if err != nil {
   492  			return err
   493  		}
   494  	}
   495  	return nil
   496  }
   497  
   498  type ShouldWriteMirrors bool
   499  
   500  const (
   501  	WriteMirrors      = ShouldWriteMirrors(true)
   502  	DoNotWriteMirrors = ShouldWriteMirrors(false)
   503  )
   504  
   505  // MergeAndWriteMetadata reads the existing metadata from storage (if any),
   506  // and merges it with metadata generated from the given tools list. The
   507  // resulting metadata is written to storage.
   508  func MergeAndWriteMetadata(stor storage.Storage, toolsDir, stream string, tools coretools.List, writeMirrors ShouldWriteMirrors) error {
   509  	existing, err := ReadAllMetadata(stor)
   510  	if err != nil {
   511  		return err
   512  	}
   513  	metadata := MetadataFromTools(tools, toolsDir)
   514  	if metadata, err = MergeMetadata(metadata, existing[stream]); err != nil {
   515  		return err
   516  	}
   517  	existing[stream] = metadata
   518  	return WriteMetadata(stor, existing, []string{stream}, writeMirrors)
   519  }
   520  
   521  // fetchToolsHash fetches the tools from storage and calculates
   522  // its size in bytes and computes a SHA256 hash of its contents.
   523  func fetchToolsHash(stor storage.StorageReader, stream string, ver version.Binary) (size int64, sha256hash hash.Hash, err error) {
   524  	r, err := storage.Get(stor, StorageName(ver, stream))
   525  	if err != nil {
   526  		return 0, nil, err
   527  	}
   528  	defer r.Close()
   529  	sha256hash = sha256.New()
   530  	size, err = io.Copy(sha256hash, r)
   531  	return size, sha256hash, err
   532  }