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