github.com/juju/juju@v0.0.0-20240430160146-1752b71fcf00/state/backups/create.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  	"compress/gzip"
     8  	"crypto/sha1"
     9  	"fmt"
    10  	"io"
    11  	"os"
    12  	"path/filepath"
    13  	"time"
    14  
    15  	"github.com/juju/errors"
    16  	"github.com/juju/loggo"
    17  	"github.com/juju/utils/v3/hash"
    18  	"github.com/juju/utils/v3/tar"
    19  )
    20  
    21  // TODO(ericsnow) One concern is files that get out of date by the time
    22  // backup finishes running.  This is particularly a problem with log
    23  // files.
    24  
    25  const (
    26  	tempPrefix = "jujuBackup-"
    27  )
    28  
    29  type createArgs struct {
    30  	destinationDir string
    31  	filesToBackUp  []string
    32  	db             DBDumper
    33  	metadataReader io.Reader
    34  }
    35  
    36  type createResult struct {
    37  	archiveFile io.ReadCloser
    38  	size        int64
    39  	checksum    string
    40  	filename    string
    41  }
    42  
    43  // create builds a new backup archive file and returns it.  It also
    44  // updates the metadata with the file info.
    45  func create(args *createArgs) (_ *createResult, err error) {
    46  	// Prepare the backup builder.
    47  	builder, err := newBuilder(args.destinationDir, args.filesToBackUp, args.db)
    48  	if err != nil {
    49  		return nil, errors.Trace(err)
    50  	}
    51  	defer func() {
    52  		if cerr := builder.cleanUp(err != nil); cerr != nil {
    53  			cerr.Log(logger)
    54  			if err == nil {
    55  				err = cerr
    56  			}
    57  		}
    58  	}()
    59  
    60  	// Inject the metadata file.
    61  	if args.metadataReader == nil {
    62  		return nil, errors.New("missing metadataReader")
    63  	}
    64  	if err := builder.injectMetadataFile(args.metadataReader); err != nil {
    65  		return nil, errors.Trace(err)
    66  	}
    67  
    68  	// Build the backup.
    69  	if err := builder.buildAll(); err != nil {
    70  		return nil, errors.Trace(err)
    71  	}
    72  
    73  	// Get the result.
    74  	result, err := builder.result()
    75  	if err != nil {
    76  		return nil, errors.Trace(err)
    77  	}
    78  
    79  	// Return the result.  Note that the entire build workspace will be
    80  	// deleted at the end of this function.  This includes the backup
    81  	// archive file we built.  However, the handle to that file in the
    82  	// result will still be open and readable.
    83  	// If we ever support state machines on Windows, this will need to
    84  	// change (you can't delete open files on Windows).
    85  	return result, nil
    86  }
    87  
    88  // builder exposes the machinery for creating a backup of juju's state.
    89  type builder struct {
    90  	// destinationDir is where the backup archive is stored.
    91  	destinationDir string
    92  	// stagingDir is the root of the archive workspace.
    93  	stagingDir string
    94  	// archivePaths is the backups archive summary.
    95  	archivePaths ArchivePaths
    96  	// filename is the path to the archive file.
    97  	filename string
    98  	// filesToBackUp is the paths to every file to include in the archive.
    99  	filesToBackUp []string
   100  	// db is the wrapper around the DB dump command and args.
   101  	db DBDumper
   102  	// checksum is the checksum of the archive file.
   103  	checksum string
   104  	// archiveFile is the backup archive file.
   105  	archiveFile io.WriteCloser
   106  	// bundleFile is the inner archive file containing all the juju
   107  	// state-related files gathered during backup.
   108  	bundleFile io.WriteCloser
   109  }
   110  
   111  // newBuilder returns a new backup archive builder.  It creates the temp
   112  // directories which backup uses as its staging area while building the
   113  // archive.  It also creates the archive
   114  // (temp root, tarball root, DB dumpdir), along with any error.
   115  func newBuilder(destinationDir string, filesToBackUp []string, db DBDumper) (b *builder, err error) {
   116  	// Create the backups workspace root directory.
   117  	// The root directory will always be relative to the
   118  	// specified backup dir - by default we'll write to
   119  	// a directory under "/tmp".
   120  
   121  	stagingDir, err := os.MkdirTemp(destinationDir, tempPrefix)
   122  	if err != nil {
   123  		return nil, errors.Annotate(err, "while making backups staging directory")
   124  	}
   125  	if db.IsSnap() && destinationDir == os.TempDir() {
   126  		stagingDir = filepath.Join(snapTmpDir, stagingDir)
   127  	}
   128  
   129  	// TODO(hpidcock): lp:1558657
   130  	finalFilename := time.Now().Format(FilenameTemplate)
   131  	// Populate the builder.
   132  	b = &builder{
   133  		destinationDir: destinationDir,
   134  		stagingDir:     stagingDir,
   135  		archivePaths:   NewNonCanonicalArchivePaths(stagingDir),
   136  		filename:       filepath.Join(destinationDir, finalFilename),
   137  		filesToBackUp:  filesToBackUp,
   138  		db:             db,
   139  	}
   140  	defer func() {
   141  		if err != nil {
   142  			logger.Errorf("error creating backup, cleaning up: %v", err)
   143  			if cerr := b.cleanUp(true); cerr != nil {
   144  				cerr.Log(logger)
   145  			}
   146  		}
   147  	}()
   148  
   149  	// Create all the directories we need.  We go with user-only
   150  	// permissions on principle; the directories are short-lived so in
   151  	// practice it shouldn't matter much.
   152  	err = os.MkdirAll(b.archivePaths.DBDumpDir, 0700)
   153  	if err != nil {
   154  		return nil, errors.Annotate(err, "while creating temp directories")
   155  	}
   156  
   157  	// Create the archive files.  We do so here to fail as early as
   158  	// possible.
   159  	b.archiveFile, err = os.Create(b.filename)
   160  	if err != nil {
   161  		return nil, errors.Annotate(err, "while creating archive file")
   162  	}
   163  
   164  	b.bundleFile, err = os.Create(b.archivePaths.FilesBundle)
   165  	if err != nil {
   166  		return nil, errors.Annotate(err, `while creating bundle file`)
   167  	}
   168  
   169  	return b, nil
   170  }
   171  
   172  func (b *builder) closeArchiveFile() error {
   173  	// Currently this method isn't thread-safe (doesn't need to be).
   174  	if b.archiveFile == nil {
   175  		return nil
   176  	}
   177  
   178  	if err := b.archiveFile.Close(); err != nil {
   179  		return errors.Annotate(err, "while closing archive file")
   180  	}
   181  
   182  	b.archiveFile = nil
   183  	return nil
   184  }
   185  
   186  func (b *builder) closeBundleFile() error {
   187  	// Currently this method isn't thread-safe (doesn't need to be).
   188  	if b.bundleFile == nil {
   189  		return nil
   190  	}
   191  
   192  	if err := b.bundleFile.Close(); err != nil {
   193  		return errors.Annotate(err, "while closing bundle file")
   194  	}
   195  
   196  	b.bundleFile = nil
   197  	return nil
   198  }
   199  
   200  func (b *builder) removeStagingDir() error {
   201  	// Currently this method isn't thread-safe (doesn't need to be).
   202  	if b.stagingDir == "" {
   203  		return errors.Errorf("stagingDir is unexpected empty, filename(%s)", b.filename)
   204  	}
   205  
   206  	if err := os.RemoveAll(b.stagingDir); err != nil {
   207  		return errors.Annotate(err, "while removing backups staging dir")
   208  	}
   209  
   210  	return nil
   211  }
   212  
   213  type cleanupErrors struct {
   214  	Errors []error
   215  }
   216  
   217  func (e cleanupErrors) Error() string {
   218  	if len(e.Errors) == 1 {
   219  		return fmt.Sprintf("while cleaning up: %v", e.Errors[0])
   220  	} else {
   221  		return fmt.Sprintf("%d errors during cleanup", len(e.Errors))
   222  	}
   223  }
   224  
   225  func (e cleanupErrors) Log(logger loggo.Logger) {
   226  	logger.Errorf(e.Error())
   227  	for _, err := range e.Errors {
   228  		logger.Errorf(err.Error())
   229  	}
   230  }
   231  
   232  func (b *builder) cleanUp(removeBackup bool) *cleanupErrors {
   233  	var errors []error
   234  
   235  	if err := b.closeBundleFile(); err != nil {
   236  		errors = append(errors, err)
   237  	}
   238  	if err := b.closeArchiveFile(); err != nil {
   239  		errors = append(errors, err)
   240  	}
   241  	if err := b.removeStagingDir(); err != nil {
   242  		errors = append(errors, err)
   243  	}
   244  	if removeBackup {
   245  		if err := os.Remove(b.filename); err != nil && !os.IsNotExist(err) {
   246  			errors = append(errors, err)
   247  		}
   248  	}
   249  
   250  	if errors != nil {
   251  		return &cleanupErrors{errors}
   252  	}
   253  	return nil
   254  }
   255  
   256  func (b *builder) injectMetadataFile(source io.Reader) error {
   257  	err := writeAll(b.archivePaths.MetadataFile, source)
   258  	return errors.Trace(err)
   259  }
   260  
   261  func writeAll(targetname string, source io.Reader) error {
   262  	target, err := os.Create(targetname)
   263  	if err != nil {
   264  		return errors.Annotatef(err, "while creating file %q", targetname)
   265  	}
   266  	_, err = io.Copy(target, source)
   267  	if err != nil {
   268  		target.Close()
   269  		return errors.Annotatef(err, "while copying into file %q", targetname)
   270  	}
   271  	return errors.Trace(target.Close())
   272  }
   273  
   274  func (b *builder) buildFilesBundle() error {
   275  	logger.Infof("dumping juju state-related files")
   276  	if len(b.filesToBackUp) == 0 {
   277  		return errors.New("missing list of files to back up")
   278  	}
   279  	if b.bundleFile == nil {
   280  		return errors.New("missing bundleFile")
   281  	}
   282  
   283  	stripPrefix := string(os.PathSeparator)
   284  	_, err := tar.TarFiles(b.filesToBackUp, b.bundleFile, stripPrefix)
   285  	if err != nil {
   286  		return errors.Annotate(err, "while bundling state-critical files")
   287  	}
   288  
   289  	return nil
   290  }
   291  
   292  func (b *builder) buildDBDump() error {
   293  	logger.Infof("dumping database")
   294  	if b.db == nil {
   295  		logger.Infof("nothing to do")
   296  		return nil
   297  	}
   298  
   299  	dumpDir := b.archivePaths.DBDumpDir
   300  	if err := b.db.Dump(dumpDir); err != nil {
   301  		return errors.Annotate(err, "while dumping juju state database")
   302  	}
   303  
   304  	return nil
   305  }
   306  
   307  func (b *builder) buildArchive(outFile io.Writer) error {
   308  	tarball := gzip.NewWriter(outFile)
   309  	defer tarball.Close()
   310  
   311  	// We add a trailing slash (or whatever) to root so that everything
   312  	// in the path up to and including that slash is stripped off when
   313  	// each file is added to the tar file.
   314  	stripPrefix := b.stagingDir + string(os.PathSeparator)
   315  	filenames := []string{b.archivePaths.ContentDir}
   316  	if _, err := tar.TarFiles(filenames, tarball, stripPrefix); err != nil {
   317  		return errors.Annotate(err, "while bundling final archive")
   318  	}
   319  
   320  	return nil
   321  }
   322  
   323  func (b *builder) buildArchiveAndChecksum() error {
   324  	if b.archiveFile == nil {
   325  		return errors.New("missing archiveFile")
   326  	}
   327  	logger.Infof("building archive file %q", b.filename)
   328  
   329  	// Build the tarball, writing out to both the archive file and a
   330  	// SHA1 hash.  The hash will correspond to the gzipped file rather
   331  	// than to the uncompressed contents of the tarball.  This is so
   332  	// that users can compare the published checksum against the
   333  	// checksum of the file without having to decompress it first.
   334  	hasher := hash.NewHashingWriter(b.archiveFile, sha1.New())
   335  	if err := b.buildArchive(hasher); err != nil {
   336  		return errors.Trace(err)
   337  	}
   338  
   339  	// Save the SHA1 checksum.
   340  	// Gzip writers may buffer what they're writing so we must call
   341  	// Close() on the writer *before* getting the checksum from the
   342  	// hasher.
   343  	b.checksum = hasher.Base64Sum()
   344  
   345  	return nil
   346  }
   347  
   348  func (b *builder) buildAll() error {
   349  	// Dump the files.
   350  	if err := b.buildFilesBundle(); err != nil {
   351  		return errors.Trace(err)
   352  	}
   353  
   354  	// Dump the database.
   355  	if err := b.buildDBDump(); err != nil {
   356  		return errors.Trace(err)
   357  	}
   358  
   359  	// Bundle it all into a tarball.
   360  	if err := b.buildArchiveAndChecksum(); err != nil {
   361  		return errors.Trace(err)
   362  	}
   363  
   364  	return nil
   365  }
   366  
   367  // result returns a "create" result relative to the current state of the
   368  // builder.  create() uses this method to get the final backup result
   369  // from the builder it used.
   370  //
   371  // Note that create() calls builder.cleanUp() after it calls
   372  // builder.result().  cleanUp() causes the builder's workspace directory
   373  // to be deleted.  This means that while the file in the result is still
   374  // open, it no longer corresponds to any filename on the filesystem.
   375  // We do this to avoid leaving any temporary files around.  The
   376  // consequence is that we cannot simply return the temp filename, we
   377  // must leave the file open, and the caller is responsible for closing
   378  // the file (hence io.ReadCloser).
   379  func (b *builder) result() (*createResult, error) {
   380  	// Open the file in read-only mode.
   381  	file, err := os.Open(b.filename)
   382  	if err != nil {
   383  		return nil, errors.Annotate(err, "while opening archive file")
   384  	}
   385  
   386  	// Get the size.
   387  	stat, err := file.Stat()
   388  	if err != nil {
   389  		if err := file.Close(); err != nil {
   390  			// We don't want to just throw the error away.
   391  			err = errors.Annotate(err, "while closing file during handling of another error")
   392  			logger.Errorf(err.Error())
   393  		}
   394  		return nil, errors.Annotate(err, "while reading archive file info")
   395  	}
   396  	size := stat.Size()
   397  
   398  	// Get the checksum.
   399  	checksum := b.checksum
   400  
   401  	// Return the result.
   402  	result := createResult{
   403  		archiveFile: file,
   404  		size:        size,
   405  		checksum:    checksum,
   406  		filename:    b.filename,
   407  	}
   408  	return &result, nil
   409  }