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