sophia ea87b6824d
Upgrade bolt to bbolt
boltdb/bolt is no longer a maintained project. bbolt is the CoreOS
fork that the author of boltdb suggests using as a replacement.
2022-04-25 12:24:34 -05:00

463 lines
12 KiB
Go

package state
import (
"bufio"
"compress/gzip"
"crypto/sha256"
"encoding/hex"
"fmt"
"hash"
"io"
"os"
"path/filepath"
"strings"
"time"
"github.com/gofrs/flock"
"github.com/hashicorp/go-hclog"
"github.com/natefinch/atomic"
bolt "go.etcd.io/bbolt"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
"github.com/hashicorp/vagrant/internal/pkg/protowriter"
"github.com/hashicorp/vagrant/internal/protocolversion"
"github.com/hashicorp/vagrant/internal/server/proto/vagrant_server"
)
// CreateSnapshot creates a database snapshot and writes it to the given writer.
//
// This will NOT buffer data to w, so you should wrap w in a bufio.Writer
// if you want buffering.
func (s *State) CreateSnapshot(w io.Writer) error {
// We build up the checksum using a multiwriter from the protowriter.
// This lets us figure out the checksum after the proto bytes are marshalled
// but before gzip.
checksum := sha256.New()
gzw := gzip.NewWriter(w)
defer gzw.Close()
dw := protowriter.NewDelimitedWriter(io.MultiWriter(gzw, checksum))
defer dw.Close()
// Write our header
if err := dw.WriteMsg(&vagrant_server.Snapshot_Header{
Version: protocolversion.Current(),
Format: vagrant_server.Snapshot_Header_BOLT,
}); err != nil {
return err
}
return s.db.View(func(dbTxn *bolt.Tx) error {
if err := dbTxn.ForEach(func(name []byte, b *bolt.Bucket) error {
const chunkLenMax = 1024 * 1024 // 1 MB
chunkLen := 0
chunkItems := map[string][]byte{}
// flushChunk is a function to flush the current chunk of data
flushChunk := func() error {
if err := dw.WriteMsg(&vagrant_server.Snapshot_BoltChunk{
Bucket: string(name),
Items: chunkItems,
}); err != nil {
return err
}
chunkItems = map[string][]byte{}
chunkLen = 0
return nil
}
// Iterate and write the data
if err := b.ForEach(func(k, v []byte) error {
if len(v)+chunkLen > chunkLenMax {
if err := flushChunk(); err != nil {
return err
}
}
chunkLen += len(v)
chunkItems[string(k)] = v
return nil
}); err != nil {
return err
}
// Write any final values
return flushChunk()
}); err != nil {
return err
}
// Write our footer chunk
if err := dw.WriteMsg(&vagrant_server.Snapshot_BoltChunk{Final: true}); err != nil {
return err
}
// Write our trailer
if err := dw.WriteMsg(&vagrant_server.Snapshot_Trailer{
Checksum: &vagrant_server.Snapshot_Trailer_Sha256{
Sha256: hex.EncodeToString(checksum.Sum(nil)),
},
}); err != nil {
return err
}
return nil
})
}
// StageRestoreSnapshot stages a database restore for the next server restart.
// This will create a temporary file alongside the data file so we must have
// write access to the directory containing the database.
func (s *State) StageRestoreSnapshot(r io.Reader) error {
log := s.log.Named("restore")
log.Warn("beginning to stage snapshot restore")
ri := newRestoreInfo(log, s.db)
if err := ri.Lock(); err != nil {
return err
}
defer ri.Unlock()
// Get our file info
fi, err := os.Stat(s.db.Path())
if err != nil {
return err
}
// Open our temporary file and copy the restore contents into it.
log.Info("copying the snapshot data to a temporary path", "path", ri.StageTempPath)
tempF, err := os.OpenFile(ri.StageTempPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, fi.Mode())
if err != nil {
log.Error("error creating temporary path", "err", err)
return err
}
n, err := io.Copy(tempF, r)
log.Info("copied snapshot data", "len", n)
tempF.Close()
if err != nil {
log.Error("error copying snapshot data", "err", err)
return err
}
// We do want to check that this appears valid because if we can't open
// the file then the server will never start again until it is cleaned up.
log.Info("validating restore file")
sr, closer, err := snapshotReader(ri.StageTempPath, nil)
if err != nil {
log.Error("error while validating restore file", "err", err)
return fmt.Errorf("error validating restore data: %s", err)
}
var header vagrant_server.Snapshot_Header
err = sr.ReadMsg(&header)
if cerr := closer(); cerr != nil {
err = cerr
}
if err != nil {
log.Error("error while validating restore file", "err", err)
return fmt.Errorf("error validating restore data: %s", err)
}
// Replace our file
log.Info("atomically replacing file", "src", ri.StageTempPath, "dest", ri.StagePath)
if err := atomic.ReplaceFile(ri.StageTempPath, ri.StagePath); err != nil {
log.Error("error replacing file", "err", err)
return err
}
// Open the new database
log.Warn("snapshot staged for restore on next startup")
return nil
}
// finalizeRestore checks for any staged restore and moves it into place.
// This will error if it fails for any reason which may prevent startup
// but we have to prevent startup because the user wanted a restore.
func finalizeRestore(log hclog.Logger, db *bolt.DB) (*bolt.DB, error) {
log.Debug("checking if DB restore is requested")
ri := newRestoreInfo(log, db)
if err := ri.Lock(); err != nil {
return db, err
}
defer ri.Unlock()
_, err := os.Stat(ri.StagePath)
if os.IsNotExist(err) {
log.Debug("no restore file found, no DB restore requested")
return db, nil
}
if err != nil {
log.Error("error checking for restore file", "err", err)
return db, err
}
log.Warn("restore file found, will initiate database restore", "path", ri.StagePath)
// The file on disk should be in the form of vagrant_server.Snapshot. So what we need
// to do next is parse out the header to determine the format, and then
// based on the format, write the correct data to disk.
log.Info("inspecting restore file to determine header")
checksum := sha256.New()
sr, closer, err := snapshotReader(ri.StagePath, checksum)
if err != nil {
log.Error("error while opening restore file", "err", err)
return db, err
}
defer closer()
// Get our header first, guaranteed first message
var header vagrant_server.Snapshot_Header
if err := sr.ReadMsg(&header); err != nil {
log.Error("error while parsing restore header", "err", err)
return db, fmt.Errorf("error reading restore header data: %s", err)
}
log.Info("snapshot header info",
"created_by", header.Version.Version,
"format", header.Format.String(),
)
// We currently only support bolt
if header.Format != vagrant_server.Snapshot_Header_BOLT {
return db, fmt.Errorf("invalid snapshot format (got code: %d)", header.Format)
}
// Open a temporary file to write our raw bolt data
log.Info("reading bolt information and writing it into a new bolt db", "path", ri.StageTempPath)
tempDb, err := bolt.Open(ri.StageTempPath, 0600, &bolt.Options{Timeout: 2 * time.Second})
if err != nil {
return db, err
}
for {
var chunk vagrant_server.Snapshot_BoltChunk
err := sr.ReadMsg(&chunk)
if err == io.EOF {
break
}
if err != nil {
tempDb.Close()
return db, err
}
if len(chunk.Bucket) > 0 && len(chunk.Items) > 0 {
if err := tempDb.Update(func(dbTxn *bolt.Tx) error {
b, err := dbTxn.CreateBucketIfNotExists([]byte(chunk.Bucket))
if err != nil {
return err
}
for k, v := range chunk.Items {
if err := b.Put([]byte(k), v); err != nil {
return err
}
}
return nil
}); err != nil {
tempDb.Close()
return db, err
}
}
if chunk.Final {
tempDb.Close()
break
}
}
// Determine our checksum. It is very important to do this here before
// we read the trailer because the checksum is up to but not including
// the trailer.
finalChecksum := hex.EncodeToString(checksum.Sum(nil))
// Read the trailer
var trailer vagrant_server.Snapshot_Trailer
if err := sr.ReadMsg(&trailer); err != nil {
log.Error("error while parsing restore trailer", "err", err)
return db, fmt.Errorf("error reading restore trailer data: %s", err)
}
// Validate the checksum
switch v := trailer.Checksum.(type) {
case *vagrant_server.Snapshot_Trailer_Sha256:
if strings.ToLower(finalChecksum) != strings.ToLower(v.Sha256) {
log.Error("checksum mismatch",
"expected", v.Sha256,
"actual", finalChecksum,
)
return db, fmt.Errorf("checksum mismatch, expected %s got %s", v.Sha256, finalChecksum)
}
default:
log.Error("unknown checksum type", "type", fmt.Sprintf("%T", trailer.Checksum))
return db, fmt.Errorf("error reading restore trailer data: unknown checksum type")
}
// Close our DB, we will reopen with the new one
if err := db.Close(); err != nil {
log.Error("failed to close db for restore", "err", err)
return db, err
}
// Get our file info
var mode os.FileMode = 0666
if fi, err := os.Stat(ri.DBPath); err == nil {
mode = fi.Mode()
} else if !os.IsNotExist(err) {
return db, err
}
// Replace our file
log.Info("atomically replacing db file", "src", ri.StagePath, "dest", ri.DBPath)
if err := atomic.ReplaceFile(ri.StageTempPath, ri.DBPath); err != nil {
log.Error("error replacing file", "err", err)
return db, err
}
// Delete our restore data
log.Info("finished reading restore data, removing now")
if err := os.Remove(ri.StagePath); err != nil {
log.Error("error removing restore data", "err", err)
return db, err
}
// Reopen the DB
log.Info("reopening database", "path", ri.DBPath)
db, err = bolt.Open(ri.DBPath, mode, &bolt.Options{
Timeout: 2 * time.Second,
})
if err != nil {
log.Error("error reopening db", "err", err)
return db, err
}
log.Warn("database restore successful")
return db, nil
}
// snapshotReader opens the delimited reader for a snapshot.
func snapshotReader(path string, h hash.Hash) (protowriter.Reader, func() error, error) {
f, err := os.Open(path)
if err != nil {
return nil, nil, err
}
gzr, err := gzip.NewReader(f)
if err != nil {
f.Close()
return nil, nil, err
}
if h == nil {
h = sha256.New()
}
hbr := &hashedBufferedReader{
R: bufio.NewReader(gzr),
H: h,
}
const maxSize = 4096 * 1024 // 4MB
dr := protowriter.NewDelimitedReader(hbr, maxSize)
return dr, func() error {
if err := dr.Close(); err != nil {
return err
}
if err := gzr.Close(); err != nil {
return err
}
return f.Close()
}, nil
}
type restoreInfo struct {
// DBPath is the final database path.
DBPath string
// StagePath is the final path where the staged restore data should
// be placed. If this path exists, the data is expected to be valid
// and not corrupted.
StagePath string
// StageTempPath is where the staged restore data should be temporarily
// written to while it is still being loaded. This data if it exists
// may be corrupted or incomplete until it is atomically moved to
// StagePath.
StageTempPath string
fl *flock.Flock
log hclog.Logger
}
// newRestoreInfo gets the restore info from the given DB.
func newRestoreInfo(log hclog.Logger, db *bolt.DB) *restoreInfo {
// Get our current directory
destPath := db.Path()
dir := filepath.Dir(destPath)
// Paths to our restore file
stagePath := filepath.Join(dir, "vagrant-restore.db")
tempPath := stagePath + ".temp"
lockPath := stagePath + ".lock"
return &restoreInfo{
DBPath: destPath,
StagePath: stagePath,
StageTempPath: tempPath,
fl: flock.New(lockPath),
log: log,
}
}
// Lock locks the restore lockfile or returns an error if this failed.
// If the return value is nil, then Unlock must be called to unlock.
func (r *restoreInfo) Lock() error {
// Create a file lock to ensure only one restore is happening at a time
r.log.Trace("acquiring file lock for restore", "path", r.fl.String())
locked, err := r.fl.TryLock()
if err != nil {
r.log.Error("error acquiring file lock", "err", err)
return err
}
if !locked {
r.log.Error("error acquiring file lock, lock already held")
return status.Errorf(codes.Aborted,
"failed to acquire file lock for restore, another restore may already be active")
}
return nil
}
// Unlock unlocks the file lock. This is only safe to call if a lock
// was successfully acquired.
func (r *restoreInfo) Unlock() error {
r.log.Trace("releasing file lock for restore", "path", r.fl.String())
return r.fl.Unlock()
}
// hashedBufferReader implements io.Reader and io.ByteReader for use
// with protowriter.Reader, and allows a checksum to be calculated
// as part of the read process.
type hashedBufferedReader struct {
R *bufio.Reader
H hash.Hash
}
func (r *hashedBufferedReader) ReadByte() (b byte, err error) {
b, err = r.R.ReadByte()
r.H.Write([]byte{b})
return
}
func (r *hashedBufferedReader) Read(p []byte) (n int, err error) {
n, err = r.R.Read(p)
if n > 0 {
r.H.Write(p[:n])
}
return
}