github.com/mwhudson/juju@v0.0.0-20160512215208-90ff01f3497f/environs/imagemetadata/simplestreams.go (about)

     1  // Copyright 2013 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  // The imagemetadata package supports locating, parsing, and filtering Ubuntu image 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 imagemetadata
     8  
     9  import (
    10  	"fmt"
    11  	"sort"
    12  
    13  	"github.com/juju/utils"
    14  
    15  	"github.com/juju/juju/environs/simplestreams"
    16  	"github.com/juju/utils/arch"
    17  	"github.com/juju/utils/series"
    18  )
    19  
    20  func init() {
    21  	simplestreams.RegisterStructTags(ImageMetadata{})
    22  }
    23  
    24  const (
    25  	// ImageIds is the simplestreams image content type.
    26  	ImageIds = "image-ids"
    27  
    28  	// StreamsVersionV1 is used to construct the path for accessing streams data.
    29  	StreamsVersionV1 = "v1"
    30  
    31  	// IndexFileVersion is used to construct the streams index file.
    32  	IndexFileVersion = 1
    33  
    34  	// currentStreamsVersion is the current version of image simplestreams data.
    35  	currentStreamsVersion = StreamsVersionV1
    36  )
    37  
    38  // simplestreamsImagesPublicKey is the public key required to
    39  // authenticate the simple streams data on http://cloud-images.ubuntu.com.
    40  // Declared as a var so it can be overidden for testing.
    41  // See http://bazaar.launchpad.net/~smoser/simplestreams/trunk/view/head:/examples/keys/cloud-images.pub
    42  var SimplestreamsImagesPublicKey = `-----BEGIN PGP PUBLIC KEY BLOCK-----
    43  Version: GnuPG v1.4.12 (GNU/Linux)
    44  
    45  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
    93  =+MFT
    94  -----END PGP PUBLIC KEY BLOCK-----
    95  `
    96  
    97  const (
    98  	// The location where Ubuntu generic cloud image metadata is published for
    99  	// public consumption.
   100  	UbuntuCloudImagesURL = "http://cloud-images.ubuntu.com"
   101  
   102  	// The ;ocation of juju specific image metadata including non-Ubuntu images
   103  	// in public clouds.
   104  	JujuStreamsImagesURL = "https://streams.canonical.com/juju/images"
   105  
   106  	// The path where released image metadata is found.
   107  	ReleasedImagesPath = "releases"
   108  )
   109  
   110  // This needs to be a var so we can override it for testing and in bootstrap.
   111  var (
   112  	//
   113  	DefaultUbuntuBaseURL = UbuntuCloudImagesURL
   114  	DefaultJujuBaseURL   = JujuStreamsImagesURL
   115  )
   116  
   117  // OfficialDataSources returns the simplestreams datasources where official
   118  // image metadata can be found.
   119  func OfficialDataSources(stream string) ([]simplestreams.DataSource, error) {
   120  	var result []simplestreams.DataSource
   121  
   122  	// New images metadata for centos and windows and existing clouds.
   123  	defaultJujuURL, err := ImageMetadataURL(DefaultJujuBaseURL, stream)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	if defaultJujuURL != "" {
   128  		publicKey, err := simplestreams.UserPublicSigningKey()
   129  		if err != nil {
   130  			return nil, err
   131  		}
   132  		if publicKey == "" {
   133  			publicKey = SimplestreamsImagesPublicKey
   134  		}
   135  		result = append(
   136  			result,
   137  			simplestreams.NewURLSignedDataSource("default cloud images", defaultJujuURL, publicKey, utils.VerifySSLHostnames, simplestreams.DEFAULT_CLOUD_DATA, true))
   138  	}
   139  
   140  	// Fallback to image metadata for existing Ubuntu images.
   141  	defaultUbuntuURL, err := ImageMetadataURL(DefaultUbuntuBaseURL, stream)
   142  	if err != nil {
   143  		return nil, err
   144  	}
   145  	if defaultUbuntuURL != "" {
   146  		result = append(
   147  			result,
   148  			simplestreams.NewURLSignedDataSource("default ubuntu cloud images", defaultUbuntuURL, SimplestreamsImagesPublicKey, utils.VerifySSLHostnames, simplestreams.DEFAULT_CLOUD_DATA, true))
   149  	}
   150  
   151  	return result, nil
   152  }
   153  
   154  // ImageConstraint defines criteria used to find an image metadata record.
   155  type ImageConstraint struct {
   156  	simplestreams.LookupParams
   157  }
   158  
   159  func NewImageConstraint(params simplestreams.LookupParams) *ImageConstraint {
   160  	if len(params.Series) == 0 {
   161  		params.Series = series.SupportedSeries()
   162  	}
   163  	if len(params.Arches) == 0 {
   164  		params.Arches = arch.AllSupportedArches
   165  	}
   166  	return &ImageConstraint{LookupParams: params}
   167  }
   168  
   169  const (
   170  	// Used to specify the released image metadata.
   171  	ReleasedStream = "released"
   172  )
   173  
   174  // idStream returns the string to use in making a product id
   175  // for the given product stream.
   176  func idStream(stream string) string {
   177  	idstream := ""
   178  	if stream != "" && stream != ReleasedStream {
   179  		idstream = "." + stream
   180  	}
   181  	return idstream
   182  }
   183  
   184  // IndexIds generates a string array representing product ids formed similarly to an ISCSI qualified name (IQN).
   185  func (ic *ImageConstraint) IndexIds() []string {
   186  	// Image constraints do not filter on index ids.
   187  	return nil
   188  }
   189  
   190  // ProductIds generates a string array representing product ids formed similarly to an ISCSI qualified name (IQN).
   191  func (ic *ImageConstraint) ProductIds() ([]string, error) {
   192  	stream := idStream(ic.Stream)
   193  	nrArches := len(ic.Arches)
   194  	nrSeries := len(ic.Series)
   195  	ids := make([]string, nrArches*nrSeries)
   196  	for i, arch := range ic.Arches {
   197  		for j, ser := range ic.Series {
   198  			version, err := series.SeriesVersion(ser)
   199  			if err != nil {
   200  				return nil, err
   201  			}
   202  			ids[j*nrArches+i] = fmt.Sprintf("com.ubuntu.cloud%s:server:%s:%s", stream, version, arch)
   203  		}
   204  	}
   205  	return ids, nil
   206  }
   207  
   208  // ImageMetadata holds information about a particular cloud image.
   209  type ImageMetadata struct {
   210  	Id          string `json:"id"`
   211  	Storage     string `json:"root_store,omitempty"`
   212  	VirtType    string `json:"virt,omitempty"`
   213  	Arch        string `json:"arch,omitempty"`
   214  	Version     string `json:"version,omitempty"`
   215  	RegionAlias string `json:"crsn,omitempty"`
   216  	RegionName  string `json:"region,omitempty"`
   217  	Endpoint    string `json:"endpoint,omitempty"`
   218  	Stream      string `json:"-"`
   219  }
   220  
   221  func (im *ImageMetadata) String() string {
   222  	return fmt.Sprintf("%#v", im)
   223  }
   224  
   225  func (im *ImageMetadata) productId() string {
   226  	stream := idStream(im.Stream)
   227  	return fmt.Sprintf("com.ubuntu.cloud%s:server:%s:%s", stream, im.Version, im.Arch)
   228  }
   229  
   230  // Fetch returns a list of images for the specified cloud matching the constraint.
   231  // The base URL locations are as specified - the first location which has a file is the one used.
   232  // Signed data is preferred, but if there is no signed data available and onlySigned is false,
   233  // then unsigned data is used.
   234  func Fetch(
   235  	sources []simplestreams.DataSource, cons *ImageConstraint,
   236  ) ([]*ImageMetadata, *simplestreams.ResolveInfo, error) {
   237  
   238  	params := simplestreams.GetMetadataParams{
   239  		StreamsVersion:   currentStreamsVersion,
   240  		LookupConstraint: cons,
   241  		ValueParams: simplestreams.ValueParams{
   242  			DataType:      ImageIds,
   243  			FilterFunc:    appendMatchingImages,
   244  			ValueTemplate: ImageMetadata{},
   245  		},
   246  	}
   247  	items, resolveInfo, err := simplestreams.GetMetadata(sources, params)
   248  	if err != nil {
   249  		return nil, resolveInfo, err
   250  	}
   251  	metadata := make([]*ImageMetadata, len(items))
   252  	for i, md := range items {
   253  		metadata[i] = md.(*ImageMetadata)
   254  	}
   255  	// Sorting the metadata is not strictly necessary, but it ensures consistent ordering for
   256  	// all compilers, and it just makes it easier to look at the data.
   257  	Sort(metadata)
   258  	return metadata, resolveInfo, nil
   259  }
   260  
   261  // Sort sorts a slice of ImageMetadata in ascending order of their id
   262  // in order to ensure the results of Fetch are ordered deterministically.
   263  func Sort(metadata []*ImageMetadata) {
   264  	sort.Sort(byId(metadata))
   265  }
   266  
   267  type byId []*ImageMetadata
   268  
   269  func (b byId) Len() int           { return len(b) }
   270  func (b byId) Swap(i, j int)      { b[i], b[j] = b[j], b[i] }
   271  func (b byId) Less(i, j int) bool { return b[i].Id < b[j].Id }
   272  
   273  type imageKey struct {
   274  	vtype   string
   275  	arch    string
   276  	version string
   277  	region  string
   278  	storage string
   279  }
   280  
   281  // appendMatchingImages updates matchingImages with image metadata records from images which belong to the
   282  // specified region. If an image already exists in matchingImages, it is not overwritten.
   283  func appendMatchingImages(source simplestreams.DataSource, matchingImages []interface{},
   284  	images map[string]interface{}, cons simplestreams.LookupConstraint) ([]interface{}, error) {
   285  
   286  	imagesMap := make(map[imageKey]*ImageMetadata, len(matchingImages))
   287  	for _, val := range matchingImages {
   288  		im := val.(*ImageMetadata)
   289  		imagesMap[imageKey{im.VirtType, im.Arch, im.Version, im.RegionName, im.Storage}] = im
   290  	}
   291  	for _, val := range images {
   292  		im := val.(*ImageMetadata)
   293  		if cons != nil && cons.Params().Region != "" && cons.Params().Region != im.RegionName {
   294  			continue
   295  		}
   296  		if _, ok := imagesMap[imageKey{im.VirtType, im.Arch, im.Version, im.RegionName, im.Storage}]; !ok {
   297  			matchingImages = append(matchingImages, im)
   298  		}
   299  	}
   300  	return matchingImages, nil
   301  }
   302  
   303  // GetLatestImageIdMetadata is provided so it can be call by tests outside the imagemetadata package.
   304  func GetLatestImageIdMetadata(data []byte, source simplestreams.DataSource, cons *ImageConstraint) ([]*ImageMetadata, error) {
   305  	metadata, err := simplestreams.ParseCloudMetadata(data, simplestreams.ProductFormat, "<unknown>", ImageMetadata{})
   306  	if err != nil {
   307  		return nil, err
   308  	}
   309  	items, err := simplestreams.GetLatestMetadata(metadata, cons, source, appendMatchingImages)
   310  	if err != nil {
   311  		return nil, err
   312  	}
   313  	result := make([]*ImageMetadata, len(items))
   314  	for i, md := range items {
   315  		result[i] = md.(*ImageMetadata)
   316  	}
   317  	return result, nil
   318  }