github.com/makyo/juju@v0.0.0-20160425123129-2608902037e9/state/cloudimagemetadata/image.go (about) 1 // Copyright 2015 Canonical Ltd. 2 // Licensed under the AGPLv3, see LICENCE file for details. 3 4 package cloudimagemetadata 5 6 import ( 7 "fmt" 8 "time" 9 10 "github.com/juju/errors" 11 "github.com/juju/loggo" 12 jujutxn "github.com/juju/txn" 13 "github.com/juju/utils/series" 14 "gopkg.in/mgo.v2" 15 "gopkg.in/mgo.v2/bson" 16 "gopkg.in/mgo.v2/txn" 17 ) 18 19 var logger = loggo.GetLogger("juju.state.cloudimagemetadata") 20 21 type storage struct { 22 modelUUID string 23 collection string 24 store DataStore 25 } 26 27 var _ Storage = (*storage)(nil) 28 29 // NewStorage constructs a new Storage that stores image metadata 30 // in the provided data store. 31 func NewStorage(modelUUID, collectionName string, store DataStore) Storage { 32 return &storage{modelUUID, collectionName, store} 33 } 34 35 var emptyMetadata = Metadata{} 36 37 // SaveMetadata implements Storage.SaveMetadata and behaves as save-or-update. 38 func (s *storage) SaveMetadata(metadata []Metadata) error { 39 if len(metadata) == 0 { 40 return nil 41 } 42 43 newDocs := make([]imagesMetadataDoc, len(metadata)) 44 for i, m := range metadata { 45 newDoc := s.mongoDoc(m) 46 if err := validateMetadata(&newDoc); err != nil { 47 return err 48 } 49 newDocs[i] = newDoc 50 } 51 52 buildTxn := func(attempt int) ([]txn.Op, error) { 53 var ops []txn.Op 54 for _, newDoc := range newDocs { 55 newDocCopy := newDoc 56 op := txn.Op{ 57 C: s.collection, 58 Id: newDocCopy.Id, 59 } 60 61 // Check if this image metadata is already known. 62 existing, err := s.getMetadata(newDocCopy.Id) 63 if errors.IsNotFound(err) { 64 op.Assert = txn.DocMissing 65 op.Insert = &newDocCopy 66 ops = append(ops, op) 67 logger.Debugf("inserting cloud image metadata for %v", newDocCopy.Id) 68 } else if err != nil { 69 return nil, errors.Trace(err) 70 } else if existing.ImageId != newDocCopy.ImageId { 71 // need to update imageId 72 op.Assert = txn.DocExists 73 op.Update = bson.D{{"$set", bson.D{{"image_id", newDocCopy.ImageId}}}} 74 ops = append(ops, op) 75 logger.Debugf("updating cloud image id for metadata %v", newDocCopy.Id) 76 } 77 } 78 if len(ops) == 0 { 79 return nil, jujutxn.ErrNoOperations 80 } 81 return ops, nil 82 } 83 84 err := s.store.RunTransaction(buildTxn) 85 if err != nil { 86 return errors.Annotate(err, "cannot save cloud image metadata") 87 } 88 return nil 89 } 90 91 // DeleteMetadata implements Storage.DeleteMetadata. 92 func (s *storage) DeleteMetadata(imageId string) error { 93 deleteOperation := func(docId string) txn.Op { 94 logger.Debugf("deleting metadata (ID=%v) for image (ID=%v)", docId, imageId) 95 return txn.Op{ 96 C: s.collection, 97 Id: docId, 98 Assert: txn.DocExists, 99 Remove: true, 100 } 101 } 102 103 noOp := func() ([]txn.Op, error) { 104 logger.Debugf("no metadata for image ID %v to delete", imageId) 105 return nil, jujutxn.ErrNoOperations 106 } 107 108 buildTxn := func(attempt int) ([]txn.Op, error) { 109 // find all metadata docs with given image id 110 imageMetadata, err := s.metadataForImageId(imageId) 111 if err != nil { 112 if err == mgo.ErrNotFound { 113 return noOp() 114 } 115 return nil, err 116 } 117 if len(imageMetadata) == 0 { 118 return noOp() 119 } 120 121 allTxn := make([]txn.Op, len(imageMetadata)) 122 for i, doc := range imageMetadata { 123 allTxn[i] = deleteOperation(doc.Id) 124 } 125 return allTxn, nil 126 } 127 128 err := s.store.RunTransaction(buildTxn) 129 if err != nil { 130 return errors.Annotatef(err, "cannot delete metadata for cloud image %v", imageId) 131 } 132 return nil 133 } 134 135 func (s *storage) metadataForImageId(imageId string) ([]imagesMetadataDoc, error) { 136 coll, closer := s.store.GetCollection(s.collection) 137 defer closer() 138 139 var docs []imagesMetadataDoc 140 query := bson.D{{"image_id", imageId}} 141 if err := coll.Find(query).All(&docs); err != nil { 142 return nil, err 143 } 144 return docs, nil 145 } 146 147 func (s *storage) getMetadata(id string) (Metadata, error) { 148 coll, closer := s.store.GetCollection(s.collection) 149 defer closer() 150 151 var old imagesMetadataDoc 152 if err := coll.Find(bson.D{{"_id", id}}).One(&old); err != nil { 153 if err == mgo.ErrNotFound { 154 return Metadata{}, errors.NotFoundf("image metadata with ID %q", id) 155 } 156 return emptyMetadata, errors.Trace(err) 157 } 158 return old.metadata(), nil 159 } 160 161 // imagesMetadataDoc results in immutable records. Updates are effectively 162 // a delate and an insert. 163 type imagesMetadataDoc struct { 164 // ModelUUID is the model identifier. 165 ModelUUID string `bson:"model-uuid"` 166 167 // Id contains unique key for cloud image metadata. 168 // This is an amalgamation of all deterministic metadata attributes to ensure 169 // that there can be a public and custom image for the same attributes set. 170 Id string `bson:"_id"` 171 172 // ImageId is an image identifier. 173 ImageId string `bson:"image_id"` 174 175 // Stream contains reference to a particular stream, 176 // for e.g. "daily" or "released" 177 Stream string `bson:"stream"` 178 179 // Region is the name of cloud region associated with the image. 180 Region string `bson:"region"` 181 182 // Version is OS version, for e.g. "12.04". 183 Version string `bson:"version"` 184 185 // Series is OS series, for e.g. "trusty". 186 Series string `bson:"series"` 187 188 // Arch is the architecture for this cloud image, for e.g. "amd64" 189 Arch string `bson:"arch"` 190 191 // VirtType contains virtualisation type of the cloud image, for e.g. "pv", "hvm". "kvm". 192 VirtType string `bson:"virt_type,omitempty"` 193 194 // RootStorageType contains type of root storage, for e.g. "ebs", "instance". 195 RootStorageType string `bson:"root_storage_type,omitempty"` 196 197 // RootStorageSize contains size of root storage in gigabytes (GB). 198 RootStorageSize uint64 `bson:"root_storage_size"` 199 200 // DateCreated is the date/time when this doc was created. 201 DateCreated int64 `bson:"date_created"` 202 203 // Source describes where this image is coming from: is it public? custom? 204 Source string `bson:"source"` 205 206 // Priority is an importance factor for image metadata. 207 // Higher number means higher priority. 208 // This will allow to sort metadata by importance. 209 Priority int `bson:"priority"` 210 } 211 212 func (m imagesMetadataDoc) metadata() Metadata { 213 r := Metadata{ 214 MetadataAttributes{ 215 Source: m.Source, 216 Stream: m.Stream, 217 Region: m.Region, 218 Version: m.Version, 219 Series: m.Series, 220 Arch: m.Arch, 221 RootStorageType: m.RootStorageType, 222 VirtType: m.VirtType, 223 }, 224 m.Priority, 225 m.ImageId, 226 } 227 if m.RootStorageSize != 0 { 228 r.RootStorageSize = &m.RootStorageSize 229 } 230 return r 231 } 232 233 func (s *storage) mongoDoc(m Metadata) imagesMetadataDoc { 234 r := imagesMetadataDoc{ 235 ModelUUID: s.modelUUID, 236 Id: buildKey(m), 237 Stream: m.Stream, 238 Region: m.Region, 239 Version: m.Version, 240 Series: m.Series, 241 Arch: m.Arch, 242 VirtType: m.VirtType, 243 RootStorageType: m.RootStorageType, 244 ImageId: m.ImageId, 245 // TODO(fwereade): 2016-03-17 lp:1558657 246 DateCreated: time.Now().UnixNano(), 247 Source: m.Source, 248 Priority: m.Priority, 249 } 250 if m.RootStorageSize != nil { 251 r.RootStorageSize = *m.RootStorageSize 252 } 253 return r 254 } 255 256 func buildKey(m Metadata) string { 257 return fmt.Sprintf("%s:%s:%s:%s:%s:%s:%s", 258 m.Stream, 259 m.Region, 260 m.Series, 261 m.Arch, 262 m.VirtType, 263 m.RootStorageType, 264 m.Source) 265 } 266 267 func validateMetadata(m *imagesMetadataDoc) error { 268 // series must be supplied. 269 if m.Series == "" { 270 return errors.NotValidf("missing series: metadata for image %v", m.ImageId) 271 } 272 273 v, err := series.SeriesVersion(m.Series) 274 if err != nil { 275 return err 276 } 277 278 m.Version = v 279 return nil 280 } 281 282 // FindMetadata implements Storage.FindMetadata. 283 // Results are sorted by date created and grouped by source. 284 func (s *storage) FindMetadata(criteria MetadataFilter) (map[string][]Metadata, error) { 285 coll, closer := s.store.GetCollection(s.collection) 286 defer closer() 287 288 logger.Debugf("searching for image metadata %#v", criteria) 289 searchCriteria := buildSearchClauses(criteria) 290 var docs []imagesMetadataDoc 291 if err := coll.Find(searchCriteria).Sort("date_created").All(&docs); err != nil { 292 return nil, errors.Trace(err) 293 } 294 if len(docs) == 0 { 295 return nil, errors.NotFoundf("matching cloud image metadata") 296 } 297 298 metadata := make(map[string][]Metadata) 299 for _, doc := range docs { 300 one := doc.metadata() 301 metadata[one.Source] = append(metadata[one.Source], one) 302 } 303 return metadata, nil 304 } 305 306 func buildSearchClauses(criteria MetadataFilter) bson.D { 307 all := bson.D{} 308 309 if criteria.Stream != "" { 310 all = append(all, bson.DocElem{"stream", criteria.Stream}) 311 } 312 313 if criteria.Region != "" { 314 all = append(all, bson.DocElem{"region", criteria.Region}) 315 } 316 317 if len(criteria.Series) != 0 { 318 all = append(all, bson.DocElem{"series", bson.D{{"$in", criteria.Series}}}) 319 } 320 321 if len(criteria.Arches) != 0 { 322 all = append(all, bson.DocElem{"arch", bson.D{{"$in", criteria.Arches}}}) 323 } 324 325 if criteria.VirtType != "" { 326 all = append(all, bson.DocElem{"virt_type", criteria.VirtType}) 327 } 328 329 if criteria.RootStorageType != "" { 330 all = append(all, bson.DocElem{"root_storage_type", criteria.RootStorageType}) 331 } 332 333 if len(all.Map()) == 0 { 334 return nil 335 } 336 return all 337 } 338 339 // MetadataFilter contains all metadata attributes that alow to find a particular 340 // cloud image metadata. Since size and source are not discriminating attributes 341 // for cloud image metadata, they are not included in search criteria. 342 type MetadataFilter struct { 343 // Region stores metadata region. 344 Region string `json:"region,omitempty"` 345 346 // Series stores all desired series. 347 Series []string `json:"series,omitempty"` 348 349 // Arches stores all desired architectures. 350 Arches []string `json:"arches,omitempty"` 351 352 // Stream can be "" or "released" for the default "released" stream, 353 // or "daily" for daily images, or any other stream that the available 354 // simplestreams metadata supports. 355 Stream string `json:"stream,omitempty"` 356 357 // VirtType stores virtualisation type. 358 VirtType string `json:"virt_type,omitempty"` 359 360 // RootStorageType stores storage type. 361 RootStorageType string `json:"root-storage-type,omitempty"` 362 } 363 364 // SupportedArchitectures implements Storage.SupportedArchitectures. 365 func (s *storage) SupportedArchitectures(criteria MetadataFilter) ([]string, error) { 366 coll, closer := s.store.GetCollection(s.collection) 367 defer closer() 368 369 var arches []string 370 if err := coll.Find(buildSearchClauses(criteria)).Distinct("arch", &arches); err != nil { 371 return nil, errors.Trace(err) 372 } 373 return arches, nil 374 } 375 376 // MetadataArchitectureQuerier isolates querying supported architectures. 377 type MetadataArchitectureQuerier struct { 378 Storage Storage 379 } 380 381 // SupportedArchitectures implements state policy SupportedArchitecturesQuerier.SupportedArchitectures. 382 func (q *MetadataArchitectureQuerier) SupportedArchitectures(stream, region string) ([]string, error) { 383 return q.Storage.SupportedArchitectures(MetadataFilter{ 384 Stream: stream, 385 Region: region, 386 }) 387 }