~nskaggs/+junk/xenial-test

« back to all changes in this revision

Viewing changes to src/github.com/juju/juju/state/backups/create.go

  • Committer: Nicholas Skaggs
  • Date: 2016-10-24 20:56:05 UTC
  • Revision ID: nicholas.skaggs@canonical.com-20161024205605-z8lta0uvuhtxwzwl
Initi with beta15

Show diffs side-by-side

added added

removed removed

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