Left: | ||
Right: |
OLD | NEW |
---|---|
(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 } | |
OLD | NEW |