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  }