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 }