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  }