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 }