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