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  }