github.com/mhilton/juju-juju@v0.0.0-20150901100907-a94dd2c73455/state/backups/storage.go (about)

     1  // Copyright 2014 Canonical Ltd.
     2  // Licensed under the AGPLv3, see LICENCE file for details.
     3  
     4  package backups
     5  
     6  import (
     7  	"io"
     8  	"path"
     9  	"time"
    10  
    11  	"github.com/juju/blobstore"
    12  	"github.com/juju/errors"
    13  	"github.com/juju/names"
    14  	jujutxn "github.com/juju/txn"
    15  	"github.com/juju/utils/filestorage"
    16  	"gopkg.in/mgo.v2"
    17  	"gopkg.in/mgo.v2/bson"
    18  	"gopkg.in/mgo.v2/txn"
    19  
    20  	"github.com/juju/juju/version"
    21  )
    22  
    23  // backupIDTimstamp is used to format the timestamp from a backup
    24  // metadata into a string. The result is used as the first half of the
    25  // corresponding backup ID.
    26  const backupIDTimestamp = "20060102-150405"
    27  
    28  //---------------------------
    29  // Backup metadata document
    30  
    31  // storageMetaDoc is a mirror of backups.Metadata, used just for DB storage.
    32  type storageMetaDoc struct {
    33  	ID string `bson:"_id"`
    34  
    35  	// blob storage
    36  
    37  	Checksum       string `bson:"checksum"`
    38  	ChecksumFormat string `bson:"checksumformat"`
    39  	Size           int64  `bson:"size,minsize"`
    40  	Stored         int64  `bson:"stored,minsize"`
    41  
    42  	// backup
    43  
    44  	Started  int64  `bson:"started,minsize"`
    45  	Finished int64  `bson:"finished,minsize"`
    46  	Notes    string `bson:"notes,omitempty"`
    47  
    48  	// origin
    49  
    50  	Environment string         `bson:"environment"`
    51  	Machine     string         `bson:"machine"`
    52  	Hostname    string         `bson:"hostname"`
    53  	Version     version.Number `bson:"version"`
    54  }
    55  
    56  func (doc *storageMetaDoc) isFileInfoComplete() bool {
    57  	if doc.Checksum == "" {
    58  		return false
    59  	}
    60  	if doc.ChecksumFormat == "" {
    61  		return false
    62  	}
    63  	if doc.Size == 0 {
    64  		return false
    65  	}
    66  	return true
    67  }
    68  
    69  func (doc *storageMetaDoc) validate() error {
    70  	if doc.ID == "" {
    71  		return errors.New("missing ID")
    72  	}
    73  	if doc.Started == 0 {
    74  		return errors.New("missing Started")
    75  	}
    76  	// We don't check doc.Finished because it doesn't have to be set.
    77  	if doc.Environment == "" {
    78  		return errors.New("missing Environment")
    79  	}
    80  	if doc.Machine == "" {
    81  		return errors.New("missing Machine")
    82  	}
    83  	if doc.Hostname == "" {
    84  		return errors.New("missing Hostname")
    85  	}
    86  	if doc.Version.Major == 0 {
    87  		return errors.New("missing Version")
    88  	}
    89  
    90  	// We don't check doc.Stored because it doesn't have to be set.
    91  
    92  	// Check the file-related fields.
    93  	if !doc.isFileInfoComplete() {
    94  		// Don't check the file-related fields.
    95  		return nil
    96  	}
    97  	if doc.Checksum == "" {
    98  		return errors.New("missing Checksum")
    99  	}
   100  	if doc.ChecksumFormat == "" {
   101  		return errors.New("missing ChecksumFormat")
   102  	}
   103  	if doc.Size == 0 {
   104  		return errors.New("missing Size")
   105  	}
   106  
   107  	return nil
   108  }
   109  
   110  func metadocUnixToTime(t int64) time.Time {
   111  	return time.Unix(t, 0).UTC()
   112  }
   113  
   114  func metadocTimeToUnix(t time.Time) int64 {
   115  	return t.UTC().Unix()
   116  }
   117  
   118  // docAsMetadata returns a new backups.Metadata based on the storageMetaDoc.
   119  func docAsMetadata(doc *storageMetaDoc) *Metadata {
   120  	meta := NewMetadata()
   121  	meta.Started = metadocUnixToTime(doc.Started)
   122  	meta.Notes = doc.Notes
   123  
   124  	meta.Origin.Environment = doc.Environment
   125  	meta.Origin.Machine = doc.Machine
   126  	meta.Origin.Hostname = doc.Hostname
   127  	meta.Origin.Version = doc.Version
   128  
   129  	meta.SetID(doc.ID)
   130  
   131  	if doc.Finished != 0 {
   132  		finished := metadocUnixToTime(doc.Finished)
   133  		meta.Finished = &finished
   134  	}
   135  
   136  	if doc.isFileInfoComplete() {
   137  		// Set the file-related fields.
   138  
   139  		// The doc should have already been validated when stored.
   140  		meta.FileMetadata.Raw.Size = doc.Size
   141  		meta.FileMetadata.Raw.Checksum = doc.Checksum
   142  		meta.FileMetadata.Raw.ChecksumFormat = doc.ChecksumFormat
   143  	}
   144  
   145  	if doc.Stored != 0 {
   146  		stored := metadocUnixToTime(doc.Stored)
   147  		meta.SetStored(&stored)
   148  	}
   149  
   150  	return meta
   151  }
   152  
   153  // newStorageMetaDoc creates a storageMetaDoc using the corresponding
   154  // values from the backup Metadata.
   155  func newStorageMetaDoc(meta *Metadata) storageMetaDoc {
   156  	var doc storageMetaDoc
   157  
   158  	// Ignore metadata.ID. It will be set by storage later.
   159  
   160  	doc.Checksum = meta.Checksum()
   161  	doc.ChecksumFormat = meta.ChecksumFormat()
   162  	doc.Size = meta.Size()
   163  	if meta.Stored() != nil {
   164  		stored := meta.Stored()
   165  		doc.Stored = metadocTimeToUnix(*stored)
   166  	}
   167  
   168  	doc.Started = metadocTimeToUnix(meta.Started)
   169  	if meta.Finished != nil {
   170  		doc.Finished = metadocTimeToUnix(*meta.Finished)
   171  	}
   172  	doc.Notes = meta.Notes
   173  
   174  	doc.Environment = meta.Origin.Environment
   175  	doc.Machine = meta.Origin.Machine
   176  	doc.Hostname = meta.Origin.Hostname
   177  	doc.Version = meta.Origin.Version
   178  
   179  	return doc
   180  }
   181  
   182  //---------------------------
   183  // DB operations
   184  
   185  // TODO(ericsnow) Merge storageDBWrapper with the storage implementation in
   186  // state/storage.go (also see state/toolstorage).
   187  
   188  // storageDBWrapper performs all state database operations needed for backups.
   189  type storageDBWrapper struct {
   190  	session   *mgo.Session
   191  	db        *mgo.Database
   192  	metaColl  *mgo.Collection
   193  	txnRunner jujutxn.Runner
   194  	envUUID   string
   195  }
   196  
   197  // newStorageDBWrapper returns a DB operator for the , with its own session.
   198  func newStorageDBWrapper(db *mgo.Database, metaColl, envUUID string) *storageDBWrapper {
   199  	session := db.Session.Copy()
   200  	db = db.With(session)
   201  
   202  	coll := db.C(metaColl)
   203  	txnRunner := jujutxn.NewRunner(jujutxn.RunnerParams{Database: db})
   204  	dbWrap := storageDBWrapper{
   205  		session:   session,
   206  		db:        db,
   207  		metaColl:  coll,
   208  		txnRunner: txnRunner,
   209  		envUUID:   envUUID,
   210  	}
   211  	return &dbWrap
   212  }
   213  
   214  // metadata populates doc with the document matching the ID.
   215  func (b *storageDBWrapper) metadata(id string, doc interface{}) error {
   216  	err := b.metaColl.FindId(id).One(doc)
   217  	if err == mgo.ErrNotFound {
   218  		return errors.NotFoundf("backup metadata %q", id)
   219  	}
   220  	return errors.Trace(err)
   221  }
   222  
   223  // allMetadata populates docs with the list of documents in storage.
   224  func (b *storageDBWrapper) allMetadata(docs interface{}) error {
   225  	err := b.metaColl.Find(nil).All(docs)
   226  	return errors.Trace(err)
   227  }
   228  
   229  // removeMetadataID removes the identified metadata from storage.
   230  func (b *storageDBWrapper) removeMetadataID(id string) error {
   231  	err := b.metaColl.RemoveId(id)
   232  	return errors.Trace(err)
   233  }
   234  
   235  // txnOp returns a single transaction operation populated with the id
   236  // and the metadata collection name. The caller should set other op
   237  // values as needed.
   238  func (b *storageDBWrapper) txnOpBase(id string) txn.Op {
   239  	op := txn.Op{
   240  		C:  b.metaColl.Name,
   241  		Id: id,
   242  	}
   243  	return op
   244  }
   245  
   246  // txnOpInsert returns a single transaction operation that will insert
   247  // the document into storage.
   248  func (b *storageDBWrapper) txnOpInsert(id string, doc interface{}) txn.Op {
   249  	op := b.txnOpBase(id)
   250  	op.Assert = txn.DocMissing
   251  	op.Insert = doc
   252  	return op
   253  }
   254  
   255  // txnOpInsert returns a single transaction operation that will update
   256  // the already stored document.
   257  func (b *storageDBWrapper) txnOpUpdate(id string, updates ...bson.DocElem) txn.Op {
   258  	op := b.txnOpBase(id)
   259  	op.Assert = txn.DocExists
   260  	op.Update = bson.D{{"$set", bson.D(updates)}}
   261  	return op
   262  }
   263  
   264  // runTransaction runs the DB operations within a single transaction.
   265  func (b *storageDBWrapper) runTransaction(ops []txn.Op) error {
   266  	err := b.txnRunner.RunTransaction(ops)
   267  	return errors.Trace(err)
   268  }
   269  
   270  // blobStorage returns a ManagedStorage matching the env storage and the blobDB.
   271  func (b *storageDBWrapper) blobStorage(blobDB string) blobstore.ManagedStorage {
   272  	dataStore := blobstore.NewGridFS(blobDB, b.envUUID, b.session)
   273  	return blobstore.NewManagedStorage(b.db, dataStore)
   274  }
   275  
   276  // Copy returns a copy of the operator.
   277  func (b *storageDBWrapper) Copy() *storageDBWrapper {
   278  	session := b.session.Copy()
   279  
   280  	coll := b.metaColl.With(session)
   281  	db := coll.Database
   282  	txnRunner := jujutxn.NewRunner(jujutxn.RunnerParams{Database: db})
   283  	dbWrap := storageDBWrapper{
   284  		session:   session,
   285  		db:        db,
   286  		metaColl:  coll,
   287  		txnRunner: txnRunner,
   288  		envUUID:   b.envUUID,
   289  	}
   290  	return &dbWrap
   291  }
   292  
   293  // Close releases the DB connection resources.
   294  func (b *storageDBWrapper) Close() error {
   295  	b.session.Close()
   296  	return nil
   297  }
   298  
   299  // getStorageMetadata returns the backup metadata associated with "id".
   300  // If "id" does not match any stored records, an error satisfying
   301  // juju/errors.IsNotFound() is returned.
   302  func getStorageMetadata(dbWrap *storageDBWrapper, id string) (*storageMetaDoc, error) {
   303  	var doc storageMetaDoc
   304  	err := dbWrap.metadata(id, &doc)
   305  	if errors.IsNotFound(err) {
   306  		return nil, errors.Trace(err)
   307  	} else if err != nil {
   308  		return nil, errors.Annotate(err, "while getting metadata")
   309  	}
   310  
   311  	if err := doc.validate(); err != nil {
   312  		return nil, errors.Trace(err)
   313  	}
   314  	return &doc, nil
   315  }
   316  
   317  // newStorageID returns a new ID for a state backup.  The format is the
   318  // UTC timestamp from the metadata followed by the environment ID:
   319  // "YYYYMMDD-hhmmss.<env ID>".  This makes the ID a little more human-
   320  // consumable (in contrast to a plain UUID string).  Ideally we would
   321  // use some form of environment name rather than the UUID, but for now
   322  // the raw env ID is sufficient.
   323  var newStorageID = func(doc *storageMetaDoc) string {
   324  	started := metadocUnixToTime(doc.Started)
   325  	timestamp := started.Format(backupIDTimestamp)
   326  	return timestamp + "." + doc.Environment
   327  }
   328  
   329  // addStorageMetadata stores metadata for a backup where it can be
   330  // accessed later. It returns a new ID that is associated with the
   331  // backup. If the provided metadata already has an ID set, it is
   332  // ignored. The new ID is set on the doc, even when there is an error.
   333  func addStorageMetadata(dbWrap *storageDBWrapper, doc *storageMetaDoc) (string, error) {
   334  	// We use our own mongo _id value since the auto-generated one from
   335  	// mongo may contain sensitive data (see bson.ObjectID).
   336  	id := newStorageID(doc)
   337  
   338  	doc.ID = id
   339  	if err := doc.validate(); err != nil {
   340  		return "", errors.Trace(err)
   341  	}
   342  
   343  	op := dbWrap.txnOpInsert(id, doc)
   344  
   345  	if err := dbWrap.runTransaction([]txn.Op{op}); err != nil {
   346  		if errors.Cause(err) == txn.ErrAborted {
   347  			return "", errors.AlreadyExistsf("backup metadata %q", doc.ID)
   348  		}
   349  		return "", errors.Annotate(err, "while running transaction")
   350  	}
   351  
   352  	return id, nil
   353  }
   354  
   355  // setStorageStoredTime updates the backup metadata associated with "id"
   356  // to indicate that a backup archive has been stored.  If "id" does
   357  // not match any stored records, an error satisfying
   358  // juju/errors.IsNotFound() is returned.
   359  func setStorageStoredTime(dbWrap *storageDBWrapper, id string, stored time.Time) error {
   360  	op := dbWrap.txnOpUpdate(id, bson.DocElem{"stored", metadocTimeToUnix(stored)})
   361  	if err := dbWrap.runTransaction([]txn.Op{op}); err != nil {
   362  		if errors.Cause(err) == txn.ErrAborted {
   363  			return errors.NotFoundf("backup metadata %q", id)
   364  		}
   365  		return errors.Annotate(err, "while running transaction")
   366  	}
   367  	return nil
   368  }
   369  
   370  //---------------------------
   371  // metadata storage
   372  
   373  type backupsDocStorage struct {
   374  	dbWrap *storageDBWrapper
   375  }
   376  
   377  type backupsMetadataStorage struct {
   378  	filestorage.MetadataDocStorage
   379  	db      *mgo.Database
   380  	envUUID string
   381  }
   382  
   383  func newMetadataStorage(dbWrap *storageDBWrapper) *backupsMetadataStorage {
   384  	dbWrap = dbWrap.Copy()
   385  
   386  	docStor := backupsDocStorage{dbWrap}
   387  	stor := backupsMetadataStorage{
   388  		MetadataDocStorage: filestorage.MetadataDocStorage{&docStor},
   389  		db:                 dbWrap.db,
   390  		envUUID:            dbWrap.envUUID,
   391  	}
   392  	return &stor
   393  }
   394  
   395  // AddDoc adds the document to storage and returns the new ID.
   396  func (s *backupsDocStorage) AddDoc(doc filestorage.Document) (string, error) {
   397  	metadata, ok := doc.(*Metadata)
   398  	if !ok {
   399  		return "", errors.Errorf("doc must be of type *backups.Metadata")
   400  	}
   401  	metaDoc := newStorageMetaDoc(metadata)
   402  
   403  	dbWrap := s.dbWrap.Copy()
   404  	defer dbWrap.Close()
   405  
   406  	id, err := addStorageMetadata(dbWrap, &metaDoc)
   407  	return id, errors.Trace(err)
   408  }
   409  
   410  // Doc returns the stored document associated with the given ID.
   411  func (s *backupsDocStorage) Doc(id string) (filestorage.Document, error) {
   412  	dbWrap := s.dbWrap.Copy()
   413  	defer dbWrap.Close()
   414  
   415  	doc, err := getStorageMetadata(dbWrap, id)
   416  	if err != nil {
   417  		return nil, errors.Trace(err)
   418  	}
   419  
   420  	metadata := docAsMetadata(doc)
   421  	return metadata, nil
   422  }
   423  
   424  // ListDocs returns the list of all stored documents.
   425  func (s *backupsDocStorage) ListDocs() ([]filestorage.Document, error) {
   426  	dbWrap := s.dbWrap.Copy()
   427  	defer dbWrap.Close()
   428  
   429  	var docs []storageMetaDoc
   430  	if err := dbWrap.allMetadata(&docs); err != nil {
   431  		return nil, errors.Trace(err)
   432  	}
   433  
   434  	list := make([]filestorage.Document, len(docs))
   435  	for i, doc := range docs {
   436  		meta := docAsMetadata(&doc)
   437  		list[i] = meta
   438  	}
   439  	return list, nil
   440  }
   441  
   442  // RemoveDoc removes the identified document from storage.
   443  func (s *backupsDocStorage) RemoveDoc(id string) error {
   444  	dbWrap := s.dbWrap.Copy()
   445  	defer dbWrap.Close()
   446  
   447  	return errors.Trace(dbWrap.removeMetadataID(id))
   448  }
   449  
   450  // Close releases the DB resources.
   451  func (s *backupsDocStorage) Close() error {
   452  	return s.dbWrap.Close()
   453  }
   454  
   455  // SetStored records in the metadata the fact that the file was stored.
   456  func (s *backupsMetadataStorage) SetStored(id string) error {
   457  	dbWrap := newStorageDBWrapper(s.db, storageMetaName, s.envUUID)
   458  	defer dbWrap.Close()
   459  
   460  	err := setStorageStoredTime(dbWrap, id, time.Now())
   461  	return errors.Trace(err)
   462  }
   463  
   464  //---------------------------
   465  // raw file storage
   466  
   467  const backupStorageRoot = "backups"
   468  
   469  type backupBlobStorage struct {
   470  	dbWrap *storageDBWrapper
   471  
   472  	envUUID   string
   473  	storeImpl blobstore.ManagedStorage
   474  	root      string
   475  }
   476  
   477  func newFileStorage(dbWrap *storageDBWrapper, root string) filestorage.RawFileStorage {
   478  	dbWrap = dbWrap.Copy()
   479  
   480  	managed := dbWrap.blobStorage(dbWrap.db.Name)
   481  	stor := backupBlobStorage{
   482  		dbWrap:    dbWrap,
   483  		envUUID:   dbWrap.envUUID,
   484  		storeImpl: managed,
   485  		root:      root,
   486  	}
   487  	return &stor
   488  }
   489  
   490  func (s *backupBlobStorage) path(id string) string {
   491  	// Use of path.Join instead of filepath.Join is intentional - this
   492  	// is an environment storage path not a filesystem path.
   493  	return path.Join(s.root, id)
   494  }
   495  
   496  // File returns the identified file from storage.
   497  func (s *backupBlobStorage) File(id string) (io.ReadCloser, error) {
   498  	file, _, err := s.storeImpl.GetForEnvironment(s.envUUID, s.path(id))
   499  	return file, errors.Trace(err)
   500  }
   501  
   502  // AddFile adds the file to storage.
   503  func (s *backupBlobStorage) AddFile(id string, file io.Reader, size int64) error {
   504  	return s.storeImpl.PutForEnvironment(s.envUUID, s.path(id), file, size)
   505  }
   506  
   507  // RemoveFile removes the identified file from storage.
   508  func (s *backupBlobStorage) RemoveFile(id string) error {
   509  	return s.storeImpl.RemoveForEnvironment(s.envUUID, s.path(id))
   510  }
   511  
   512  // Close closes the storage.
   513  func (s *backupBlobStorage) Close() error {
   514  	return s.dbWrap.Close()
   515  }
   516  
   517  //---------------------------
   518  // backup storage
   519  
   520  const (
   521  	storageDBName   = "backups"
   522  	storageMetaName = "metadata"
   523  )
   524  
   525  // DB represents the set of methods required to perform a backup.
   526  // It exists to break the strict dependency between state and this package,
   527  // and those that depend on this package.
   528  type DB interface {
   529  	// MongoSession returns the underlying mongodb session.
   530  	MongoSession() *mgo.Session
   531  
   532  	// EnvironTag is the concrete environ tag for this database.
   533  	EnvironTag() names.EnvironTag
   534  }
   535  
   536  // NewStorage returns a new FileStorage to use for storing backup
   537  // archives (and metadata).
   538  func NewStorage(st DB) filestorage.FileStorage {
   539  	envUUID := st.EnvironTag().Id()
   540  	db := st.MongoSession().DB(storageDBName)
   541  	dbWrap := newStorageDBWrapper(db, storageMetaName, envUUID)
   542  	defer dbWrap.Close()
   543  
   544  	files := newFileStorage(dbWrap, backupStorageRoot)
   545  	docs := newMetadataStorage(dbWrap)
   546  	return filestorage.NewFileStorage(docs, files)
   547  }