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 }