Rietveld Code Review Tool
Help | Bug tracker | Discussion group | Source code | Sign in
(414)

Side by Side Diff: environs/imagemetadata/simplestreams.go

Issue 9138044: Initial simple streams support (Closed)
Patch Set: Initial simple streams support Created 11 years, 11 months ago
Left:
Right:
Use n/p to move between diff chunks; N/P to move between comments. Please Sign in to add in-line comments.
Jump to:
View unified diff | Download patch
OLDNEW
(Empty)
1 // // The imagemetadata package supports locating, parsing, and filtering Ubuntu image metadata in simplestreams format.
2 // See http://launchpad.net/simplestreams and in particular the doc/README file in that project for more information
3 // about the file formats.
4 package imagemetadata
5
6 import (
7 "bufio"
8 "encoding/json"
9 "fmt"
10 "io"
11 "io/ioutil"
12 "net/http"
13 "os"
14 "reflect"
15 "sort"
16 "strings"
17 )
18
19 // CloudSpec uniquely defines a specific cloud deployment.
20 type CloudSpec struct {
21 Region string
22 Endpoint string
23 }
24
25 // ImageConstraint defines criteria used to find an image.
26 type ImageConstraint struct {
27 CloudSpec
28 Release string
fwereade 2013/05/16 07:54:54 Can we call this "Series" please, for consistency
wallyworld 2013/05/17 01:10:26 Done.
29 Arch string
fwereade 2013/05/16 07:54:54 This shouldn't be here, I think.
30 Stream string // may be "", typically "release", "daily" etc
31 // the id may be expensive to generate so cache it.
32 cachedId string
33 }
34
35 // NewImageConstraint creates a ImageConstraint.
36 func NewImageConstraint(region, endpoint, release, arch, stream string) ImageCon straint {
rog 2013/05/15 17:06:26 i'm not entirely convinced of the worth of this fu
wallyworld 2013/05/16 00:24:14 The reason for the function is because the returne
37 return ImageConstraint{
38 CloudSpec: CloudSpec{
39 Endpoint: endpoint,
40 Region: region,
41 },
42 Release: release,
43 Arch: arch,
fwereade 2013/05/16 07:54:54 We cannot assume that arch is known at this stage.
44 Stream: stream,
45 }
46 }
47
48 // Generates a string representing a product id formed similarly to an ISCSI qua lified name (IQN).
49 func (ic *ImageConstraint) Id() (string, error) {
50 if ic.cachedId != "" {
51 return ic.cachedId, nil
52 }
53 stream := ic.Stream
54 if stream != "" {
55 stream = "." + stream
56 }
57 // We need to find the release version eg 12.04 from the series eg preci se. Use the information found in
58 // /usr/share/distro-info/ubuntu.csv provided by distro-info-data packag e.
59 err := updateDistroInfo()
60 if err != nil {
61 return "", err
62 }
63 if version, ok := releaseVersions[ic.Release]; ok {
64 ic.cachedId = fmt.Sprintf("com.ubuntu.cloud%s:server:%s:%s", str eam, version, ic.Arch)
65 return ic.cachedId, nil
66 }
67 return "", fmt.Errorf("Invalid Ubuntu release %q", ic.Release)
fwereade 2013/05/16 07:54:54 "invalid series %q"?
wallyworld 2013/05/17 01:10:26 Done.
68 }
69
70 // releaseVersions provides a mapping between Ubuntu series names and version nu mbers.
71 // The values here are current as of the time of writing. On Ubuntu systems, we update
72 // these values from /usr/share/distro-info/ubuntu.csv to ensure we have the lat est values.
73 // On non-Ubuntu systems, these values provide a nice fallback option.
74 var releaseVersions = map[string]string{
rog 2013/05/15 17:06:26 since this is a mutable global, this should be gua
wallyworld 2013/05/16 00:24:14 Done.
75 "precise": "12.04",
76 "quantal": "12.10",
77 "raring": "13.04",
78 "saucy": "13.10",
79 }
80
81 // updateDistroInfo updates releaseVersions from /usr/share/distro-info/ubuntu.c sv if possible..
82 func updateDistroInfo() error {
83 // We need to find the release version eg 12.04 from the series eg preci se. Use the information found in
84 // /usr/share/distro-info/ubuntu.csv provided by distro-info-data packag e.
85 f, err := os.Open("/usr/share/distro-info/ubuntu.csv")
86 if err != nil {
87 // On non-Ubuntu systems this file won't exist butr that's expec ted.
jameinel 2013/05/16 06:18:52 typo "butr" Should this only trap for NoExist? Gi
wallyworld 2013/05/17 01:10:26 Yeah, the consensus seemed to be to not fail
88 return nil
89 }
90 defer f.Close()
91 bufRdr := bufio.NewReader(f)
jameinel 2013/05/16 06:18:52 Channeling my inner thumper, the name could be imp
92 for {
93 line, err := bufRdr.ReadString('\n')
94 if err == io.EOF {
95 break
96 }
97 if err != nil {
98 return fmt.Errorf("reading distro info file file: %v", e rr)
99 }
100 // lines are of the form: "12.04 LTS,Precise Pangolin,precise,20 11-10-13,2012-04-26,2017-04-26"
101 parts := strings.Split(line, ",")
102 // the numeric version may contain a LTS moniker so strip that o ut.
rog 2013/05/15 17:06:26 if len(parts) < 3 { return fmt.Errorf("syntax
wallyworld 2013/05/16 00:24:14 Done.
103 releaseInfo := strings.Split(parts[0], " ")
104 releaseVersions[parts[2]] = releaseInfo[0]
105 }
106 return nil
107 }
108
109 // The following structs define the data model used in the JSON image metadata f iles.
110 // Not every model attribute is defined here, only the ones we care about.
111 // See the doc/README file in lp:simplestreams for more information.
112
113 // These structs define the model used for image metadata.
114
115 // ImageMetadata attribute values may point to a map of attribute values (aka al iases) and these attributes
116 // are used to override/augment the existing ImageMetadata attributes.
117 type attributeValues map[string]string
118 type aliasesByAttribute map[string]attributeValues
119
120 type cloudImageMetadata struct {
121 Products map[string]imageMetadataCatalog `json:"products"`
122 Aliases map[string]aliasesByAttribute `json:"_aliases"`
123 Updated string `json:"updated"`
124 Format string `json:"format"`
125 }
126
127 type imagesByVersion map[string]*imageCollection
128
129 type imageMetadataCatalog struct {
130 Release string `json:"release"`
131 Version string `json:"version"`
132 Arch string `json:"arch"`
133 RegionName string `json:"region"`
134 Endpoint string `json:"endpoint"`
135 Images imagesByVersion `json:"versions"`
136 }
137
138 type imageCollection struct {
139 Images map[string]*ImageMetadata `json:"items"`
140 RegionName string `json:"region"`
141 Endpoint string `json:"endpoint"`
142 }
143
144 // This is the only struct we need to export. The goal of this package is to pro vide a list of
145 // ImageMetadata records matching the supplied region, arch etc.
rog 2013/05/15 17:06:26 this comment seems out of place. how about just:
wallyworld 2013/05/16 00:24:14 Done.
146 type ImageMetadata struct {
147 Id string `json:"id"`
148 Storage string `json:"root_store"`
149 VType string `json:"virt"`
150 RegionAlias string `json:"crsn"`
151 RegionName string `json:"region"`
152 Endpoint string `json:"endpoint"`
153 }
154
155 // These structs define the model used to image metadata indices.
156
157 type indices struct {
158 Indexes map[string]*indexMetadata `json:"index"`
159 Updated string `json:"updated"`
160 Format string `json:"format"`
161 }
162
163 type indexReference struct {
164 indices
165 baseURL string
166 }
167
168 type indexMetadata struct {
169 Updated string `json:"updated"`
170 Format string `json:"format"`
171 DataType string `json:"datatype"`
172 CloudName string `json:"cloudname"`
173 Clouds []CloudSpec `json:"clouds"`
174 ProductsFilePath string `json:"path"`
175 ProductIds []string `json:"products"`
176 }
177
178 const (
179 DefaultIndexPath = "streams/v1/index.json"
180 imageIds = "image-ids"
181 )
182
183 // GetImageIdMetadata returns a list of images for the specified cloud matching the product criteria.
184 // The index file location is as specified.
rog 2013/05/15 17:06:26 s/specified./specified. The usual file location is
wallyworld 2013/05/16 00:24:14 Done.
185 func GetImageIdMetadata(baseURL, indexPath string, imageConstraint *ImageConstra int) ([]*ImageMetadata, error) {
rog 2013/05/15 17:06:26 this name seems a little redundant now that the na
186 indexRef, err := getIndexWithFormat(baseURL, indexPath, "index:1.0")
187 if err != nil {
188 return nil, err
189 }
190 return indexRef.getLatestImageIdMetadataWithFormat(imageConstraint, "pro ducts:1.0")
191 }
192
193 // fetchData gets all the data from the given path relative to the given base UR L.
rog 2013/05/15 17:06:26 // It returns the data found and the URL used.
wallyworld 2013/05/16 00:24:14 Done.
194 func fetchData(baseURL, path string) ([]byte, string, error) {
195 dataURL := baseURL
196 if !strings.HasSuffix(dataURL, "/") {
197 dataURL += "/"
198 }
199 dataURL += path
200 resp, err := http.Get(dataURL)
201 if err != nil {
202 return nil, dataURL, err
203 }
204 defer resp.Body.Close()
205 if resp.StatusCode != 200 {
206 return nil, dataURL, fmt.Errorf("cannot access URL %s, %s", data URL, resp.Status)
207 }
208
209 data, err := ioutil.ReadAll(resp.Body)
210 if err != nil {
211 return nil, dataURL, fmt.Errorf("cannot read URL data, %s", err. Error())
212 }
213 return data, dataURL, nil
214 }
215
216 func getIndexWithFormat(baseURL, indexPath, format string) (*indexReference, err or) {
217 data, url, err := fetchData(baseURL, indexPath)
218 if err != nil {
219 return nil, fmt.Errorf("cannot read index data, %v", err)
220 }
221 var indices indices
222 err = json.Unmarshal(data, &indices)
223 if err != nil {
224 return nil, fmt.Errorf("cannot unmarshal JSON index metadata at URL %s: %s", url, err.Error())
rog 2013/05/15 17:06:26 return nil, fmt.Errorf("cannot unmarshal JSON inde
wallyworld 2013/05/16 00:24:14 Done.
225 }
226 if indices.Format != format {
227 return nil, fmt.Errorf("unexpected index file format %q, expecte d %s at URL %s", indices.Format, format, url)
rog 2013/05/15 17:06:26 s/%s/%q/g
wallyworld 2013/05/16 00:24:14 Done.
228 }
229 return &indexReference{
230 indices: indices,
231 baseURL: baseURL,
232 }, nil
233 }
234
235 // getImageIdsPath returns the path to the metadata file containing image ids fo r the specified
236 // cloud and product.
237 func (indexRef *indexReference) getImageIdsPath(imageConstraint *ImageConstraint ) (string, error) {
238 prodSpecId, err := imageConstraint.Id()
239 if err != nil {
240 return "", fmt.Errorf("cannot resolve Ubuntu version %q: %v", im ageConstraint.Release, err)
241 }
242 var containsImageIds bool
243 for _, metadata := range indexRef.Indexes {
244 if metadata.DataType != imageIds {
245 continue
246 }
247 containsImageIds = true
248 var cloudSpecMatches bool
249 for _, cs := range metadata.Clouds {
250 if cs == imageConstraint.CloudSpec {
251 cloudSpecMatches = true
252 break
253 }
254 }
255 var prodSpecMatches bool
256 for _, pid := range metadata.ProductIds {
257 if pid == prodSpecId {
fwereade 2013/05/16 07:54:54 I'm not sure this is correct, because of (1) arch
258 prodSpecMatches = true
259 break
260 }
261 }
262 if cloudSpecMatches && prodSpecMatches {
263 return metadata.ProductsFilePath, nil
264 }
265 }
266 if !containsImageIds {
267 return "", fmt.Errorf("index file missing %q data", imageIds)
268 }
269 return "", fmt.Errorf("index file missing data for cloud %v", imageConst raint.CloudSpec)
270 }
271
272 // To keep the metadata concise, attributes on ImageMetadata which have the same value for each
273 // item may be moved up to a higher level in the tree. denormaliseImageMetadata descends the tree
274 // and fills in any missing attributes with values from a higher level.
275 func (metadata *cloudImageMetadata) denormaliseImageMetadata() {
276 for _, metadataCatalog := range metadata.Products {
277 for _, imageCollection := range metadataCatalog.Images {
278 for _, im := range imageCollection.Images {
279 coll := *imageCollection
280 inherit(&coll, metadataCatalog)
281 inherit(im, &coll)
282 }
283 }
284 }
285 }
286
287 // inherit sets any blank fields in dst to their equivalent values in fields in src that have matching json tags.
288 // The dst parameter must be a pointer to a struct.
289 func inherit(dst, src interface{}) {
290 for tag := range tags(dst) {
291 setFieldByTag(dst, tag, fieldByTag(src, tag), false)
292 }
293 }
294
295 // processAliases looks through the image fields to see if
296 // any aliases apply, and sets attributes appropriately
297 // if so.
298 func (metadata *cloudImageMetadata) processAliases(im *ImageMetadata) {
299 for tag := range tags(im) {
300 aliases, ok := metadata.Aliases[tag]
301 if !ok {
302 continue
303 }
304 // We have found a set of aliases for one of the fields in the i mage.
305 // Now check to see if the field matches one of the defined alia ses.
306 fields, ok := aliases[fieldByTag(im, tag)]
307 if !ok {
308 continue
309 }
310 // The alias matches - set all the aliased fields in the image.
311 for attr, val := range fields {
312 setFieldByTag(im, attr, val, true)
313 }
314 }
315 }
316
317 // Apply any attribute aliases to the image metadata records.
318 func (metadata *cloudImageMetadata) applyAliases() {
319 for _, metadataCatalog := range metadata.Products {
320 for _, imageCollection := range metadataCatalog.Images {
321 for _, im := range imageCollection.Images {
322 metadata.processAliases(im)
323 }
324 }
325 }
326 }
327
328 var tagsForType = mkTags(imageMetadataCatalog{}, imageCollection{}, ImageMetadat a{})
329
330 func mkTags(vals ...interface{}) map[reflect.Type]map[string]int {
331 typeMap := make(map[reflect.Type]map[string]int)
332 for _, v := range vals {
333 t := reflect.TypeOf(v)
334 typeMap[t] = jsonTags(t)
335 }
336 return typeMap
337 }
338
339 // jsonTags returns a map from json tag to the field index for the string fields in the given type.
340 func jsonTags(t reflect.Type) map[string]int {
341 if t.Kind() != reflect.Struct {
342 panic(fmt.Errorf("cannot get json tags on type %s", t))
343 }
344 tags := make(map[string]int)
345 for i := 0; i < t.NumField(); i++ {
346 f := t.Field(i)
347 if f.Type != reflect.TypeOf("") {
348 continue
349 }
350 if tag := f.Tag.Get("json"); tag != "" {
351 if i := strings.Index(tag, ","); i >= 0 {
352 tag = tag[0:i]
353 }
354 if tag == "-" {
355 continue
356 }
357 if tag != "" {
358 f.Name = tag
359 }
360 }
361 tags[f.Name] = i
362 }
363 return tags
364 }
365
366 // tags returns the field offsets for the JSON tags defined by the given value, which must be
367 // a struct or a pointer to a struct.
368 func tags(x interface{}) map[string]int {
369 t := reflect.TypeOf(x)
370 if t.Kind() == reflect.Ptr {
371 t = t.Elem()
372 }
373 if t.Kind() != reflect.Struct {
374 panic(fmt.Errorf("expected struct, not %s", t))
375 }
376
377 if tagm := tagsForType[t]; tagm != nil {
378 return tagm
379 }
380 panic(fmt.Errorf("%s not found in type table", t))
381 }
382
383 // fieldByTag returns the value for the field in x with the given JSON tag, or " " if there is no such field.
384 func fieldByTag(x interface{}, tag string) string {
385 tagm := tags(x)
386 v := reflect.ValueOf(x)
387 if v.Kind() == reflect.Ptr {
388 v = v.Elem()
389 }
390 if i, ok := tagm[tag]; ok {
391 return v.Field(i).Interface().(string)
392 }
393 return ""
394 }
395
396 // setFieldByTag sets the value for the field in x with the given JSON tag to va l.
397 // The override parameter specifies whether the value will be set even if the or iginal value is non-empty.
398 func setFieldByTag(x interface{}, tag, val string, override bool) {
399 i, ok := tags(x)[tag]
400 if !ok {
401 return
402 }
403 v := reflect.ValueOf(x).Elem()
404 f := v.Field(i)
405 if override || f.Interface().(string) == "" {
406 f.Set(reflect.ValueOf(val))
407 }
408 }
409
410 type imageKey struct {
411 vtype string
412 storage string
413 }
414
415 // findMatchingImages updates matchingImages with image metadata records from im ages which belong to the
416 // specified region. If an image already exists in matchingImages, it is not ove rwritten.
417 func findMatchingImages(matchingImages []*ImageMetadata, images map[string]*Imag eMetadata, imageConstraint *ImageConstraint) []*ImageMetadata {
418 imagesMap := make(map[imageKey]*ImageMetadata, len(matchingImages))
419 for _, im := range matchingImages {
420 imagesMap[imageKey{im.VType, im.Storage}] = im
421 }
422 for _, im := range images {
423 if imageConstraint.Region != im.RegionName {
424 continue
425 }
426 if _, ok := imagesMap[imageKey{im.VType, im.Storage}]; !ok {
427 matchingImages = append(matchingImages, im)
428 }
429 }
430 return matchingImages
431 }
432
433 // getCloudMetadataWithFormat loads the entire cloud image metadata encoded usin g the specified format.
434 func (indexRef *indexReference) getCloudMetadataWithFormat(imageConstraint *Imag eConstraint, format string) (*cloudImageMetadata, error) {
435 productFilesPath, err := indexRef.getImageIdsPath(imageConstraint)
436 if err != nil {
437 return nil, fmt.Errorf("error finding product files path %s", er r.Error())
438 }
439 data, url, err := fetchData(indexRef.baseURL, productFilesPath)
440 if err != nil {
441 return nil, fmt.Errorf("cannot read product data, %v", err)
442 }
443 var imageMetadata cloudImageMetadata
444 err = json.Unmarshal(data, &imageMetadata)
445 if err != nil {
446 return nil, fmt.Errorf("cannot unmarshal JSON image metadata at URL %s: %s", url, err.Error())
447 }
448 if imageMetadata.Format != format {
449 return nil, fmt.Errorf("unexpected index file format %q, expecte d %q at URL %s", imageMetadata.Format, format, url)
450 }
451 imageMetadata.applyAliases()
452 imageMetadata.denormaliseImageMetadata()
453 return &imageMetadata, nil
454 }
455
456 // getLatestImageIdMetadataWithFormat loads the image metadata for the given clo ud and order the images
457 // starting with the most recent, and returns images which match the product cri teria, choosing from the
458 // latest versions first. The result is a list of images matching the criteria, but differing on type of storage etc.
459 func (indexRef *indexReference) getLatestImageIdMetadataWithFormat(imageConstrai nt *ImageConstraint, format string) ([]*ImageMetadata, error) {
460 imageMetadata, err := indexRef.getCloudMetadataWithFormat(imageConstrain t, format)
461 if err != nil {
462 return nil, err
463 }
464 prodSpecId, err := imageConstraint.Id()
465 if err != nil {
466 return nil, fmt.Errorf("cannot resolve Ubuntu version %q: %v", i mageConstraint.Release, err)
467 }
468 metadataCatalog, ok := imageMetadata.Products[prodSpecId]
469 if !ok {
470 return nil, fmt.Errorf("no image metadata for %s", prodSpecId)
471 }
472 var bv byVersionDesc = make(byVersionDesc, len(metadataCatalog.Images))
473 i := 0
474 for vers, imageColl := range metadataCatalog.Images {
475 bv[i] = imageCollectionVersion{vers, imageColl}
476 i++
477 }
478 sort.Sort(bv)
479 var matchingImages []*ImageMetadata
480 for _, imageCollVersion := range bv {
481 matchingImages = findMatchingImages(matchingImages, imageCollVer sion.imageCollection.Images, imageConstraint)
482 }
483 return matchingImages, nil
484 }
485
486 type imageCollectionVersion struct {
487 version string
488 imageCollection *imageCollection
489 }
490
491 // byVersionDesc is used to sort a slice of image collections in descending orde r of their
492 // version in YYYYMMDD.
493 type byVersionDesc []imageCollectionVersion
494
495 func (bv byVersionDesc) Len() int { return len(bv) }
496 func (bv byVersionDesc) Swap(i, j int) {
497 bv[i], bv[j] = bv[j], bv[i]
498 }
499 func (bv byVersionDesc) Less(i, j int) bool {
500 return bv[i].version > bv[j].version
501 }
OLDNEW

Powered by Google App Engine
RSS Feeds Recent Issues | This issue
This is Rietveld f62528b