608 lines
17 KiB
Go
608 lines
17 KiB
Go
// Package restore is the restore subcommand for the influxd command,
|
|
// for restoring from a backup.
|
|
package restore
|
|
|
|
import (
|
|
"archive/tar"
|
|
"bytes"
|
|
"encoding/binary"
|
|
"flag"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/influxdata/influxdb/cmd/influxd/backup_util"
|
|
tarstream "github.com/influxdata/influxdb/pkg/tar"
|
|
"github.com/influxdata/influxdb/services/meta"
|
|
"github.com/influxdata/influxdb/services/snapshotter"
|
|
gzip "github.com/klauspost/pgzip"
|
|
)
|
|
|
|
// Command represents the program execution for "influxd restore".
|
|
type Command struct {
|
|
// The logger passed to the ticker during execution.
|
|
StdoutLogger *log.Logger
|
|
StderrLogger *log.Logger
|
|
|
|
// Standard input/output, overridden for testing.
|
|
Stderr io.Writer
|
|
Stdout io.Writer
|
|
|
|
host string
|
|
client *snapshotter.Client
|
|
|
|
backupFilesPath string
|
|
metadir string
|
|
datadir string
|
|
destinationDatabase string
|
|
sourceDatabase string
|
|
backupRetention string
|
|
restoreRetention string
|
|
shard uint64
|
|
portable bool
|
|
online bool
|
|
manifestMeta *backup_util.MetaEntry
|
|
manifestFiles map[uint64]*backup_util.Entry
|
|
|
|
// TODO: when the new meta stuff is done this should not be exported or be gone
|
|
MetaConfig *meta.Config
|
|
|
|
shardIDMap map[uint64]uint64
|
|
}
|
|
|
|
// NewCommand returns a new instance of Command with default settings.
|
|
func NewCommand() *Command {
|
|
return &Command{
|
|
Stdout: os.Stdout,
|
|
Stderr: os.Stderr,
|
|
MetaConfig: meta.NewConfig(),
|
|
}
|
|
}
|
|
|
|
// Run executes the program.
|
|
func (cmd *Command) Run(args ...string) error {
|
|
// Set up logger.
|
|
cmd.StdoutLogger = log.New(cmd.Stdout, "", log.LstdFlags)
|
|
cmd.StderrLogger = log.New(cmd.Stderr, "", log.LstdFlags)
|
|
if err := cmd.parseFlags(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
if cmd.portable {
|
|
return cmd.runOnlinePortable()
|
|
} else if cmd.online {
|
|
return cmd.runOnlineLegacy()
|
|
} else {
|
|
return cmd.runOffline()
|
|
}
|
|
}
|
|
|
|
func (cmd *Command) runOffline() error {
|
|
if cmd.metadir != "" {
|
|
if err := cmd.unpackMeta(); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
if cmd.shard != 0 {
|
|
return cmd.unpackShard(cmd.shard)
|
|
} else if cmd.restoreRetention != "" {
|
|
return cmd.unpackRetention()
|
|
} else if cmd.datadir != "" {
|
|
return cmd.unpackDatabase()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cmd *Command) runOnlinePortable() error {
|
|
err := cmd.updateMetaPortable()
|
|
if err != nil {
|
|
cmd.StderrLogger.Printf("error updating meta: %v", err)
|
|
return err
|
|
}
|
|
err = cmd.uploadShardsPortable()
|
|
if err != nil {
|
|
cmd.StderrLogger.Printf("error updating shards: %v", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (cmd *Command) runOnlineLegacy() error {
|
|
err := cmd.updateMetaLegacy()
|
|
if err != nil {
|
|
cmd.StderrLogger.Printf("error updating meta: %v", err)
|
|
return err
|
|
}
|
|
err = cmd.uploadShardsLegacy()
|
|
if err != nil {
|
|
cmd.StderrLogger.Printf("error updating shards: %v", err)
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// parseFlags parses and validates the command line arguments.
|
|
func (cmd *Command) parseFlags(args []string) error {
|
|
fs := flag.NewFlagSet("", flag.ContinueOnError)
|
|
fs.StringVar(&cmd.host, "host", "localhost:8088", "")
|
|
fs.StringVar(&cmd.metadir, "metadir", "", "")
|
|
fs.StringVar(&cmd.datadir, "datadir", "", "")
|
|
|
|
fs.StringVar(&cmd.sourceDatabase, "database", "", "")
|
|
fs.StringVar(&cmd.sourceDatabase, "db", "", "")
|
|
fs.StringVar(&cmd.destinationDatabase, "newdb", "", "")
|
|
|
|
fs.StringVar(&cmd.backupRetention, "retention", "", "")
|
|
fs.StringVar(&cmd.backupRetention, "rp", "", "")
|
|
fs.StringVar(&cmd.restoreRetention, "newrp", "", "")
|
|
|
|
fs.Uint64Var(&cmd.shard, "shard", 0, "")
|
|
fs.BoolVar(&cmd.online, "online", false, "")
|
|
fs.BoolVar(&cmd.portable, "portable", false, "")
|
|
fs.SetOutput(cmd.Stdout)
|
|
fs.Usage = cmd.printUsage
|
|
if err := fs.Parse(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd.MetaConfig = meta.NewConfig()
|
|
cmd.MetaConfig.Dir = cmd.metadir
|
|
cmd.client = snapshotter.NewClient(cmd.host)
|
|
|
|
// Require output path.
|
|
cmd.backupFilesPath = fs.Arg(0)
|
|
if cmd.backupFilesPath == "" {
|
|
return fmt.Errorf("path with backup files required")
|
|
}
|
|
|
|
fi, err := os.Stat(cmd.backupFilesPath)
|
|
if err != nil || !fi.IsDir() {
|
|
return fmt.Errorf("backup path should be a valid directory: %s", cmd.backupFilesPath)
|
|
}
|
|
|
|
if cmd.destinationDatabase != "" && cmd.sourceDatabase == "" {
|
|
return fmt.Errorf("must specify a database to be restored into new database %s", cmd.destinationDatabase)
|
|
}
|
|
|
|
if cmd.portable || cmd.online {
|
|
// validate the arguments
|
|
|
|
if cmd.metadir != "" {
|
|
return fmt.Errorf("offline parameter metadir found, not compatible with -portable")
|
|
}
|
|
|
|
if cmd.datadir != "" {
|
|
return fmt.Errorf("offline parameter datadir found, not compatible with -portable")
|
|
}
|
|
|
|
if cmd.restoreRetention == "" {
|
|
cmd.restoreRetention = cmd.backupRetention
|
|
}
|
|
|
|
if cmd.portable {
|
|
var err error
|
|
|
|
cmd.manifestMeta, cmd.manifestFiles, err = backup_util.LoadIncremental(cmd.backupFilesPath)
|
|
if err != nil {
|
|
return fmt.Errorf("restore failed while processing manifest files: %s", err.Error())
|
|
} else if cmd.manifestMeta == nil {
|
|
// No manifest files found.
|
|
return fmt.Errorf("No manifest files found in: %s\n", cmd.backupFilesPath)
|
|
|
|
}
|
|
}
|
|
} else {
|
|
// validate the arguments
|
|
if cmd.destinationDatabase == "" {
|
|
cmd.destinationDatabase = cmd.sourceDatabase
|
|
}
|
|
|
|
if cmd.metadir == "" && cmd.destinationDatabase == "" {
|
|
return fmt.Errorf("-metadir or -newdb are required to restore")
|
|
}
|
|
|
|
if cmd.destinationDatabase != "" && cmd.datadir == "" {
|
|
return fmt.Errorf("-datadir is required to restore")
|
|
}
|
|
|
|
if cmd.shard != 0 {
|
|
if cmd.destinationDatabase == "" {
|
|
return fmt.Errorf("-newdb is required to restore shard")
|
|
}
|
|
if cmd.backupRetention == "" {
|
|
return fmt.Errorf("-retention is required to restore shard")
|
|
}
|
|
} else if cmd.backupRetention != "" && cmd.destinationDatabase == "" {
|
|
return fmt.Errorf("-newdb is required to restore retention policy")
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// unpackMeta reads the metadata from the backup directory and initializes a raft
|
|
// cluster and replaces the root metadata.
|
|
func (cmd *Command) unpackMeta() error {
|
|
// find the meta file
|
|
metaFiles, err := filepath.Glob(filepath.Join(cmd.backupFilesPath, backup_util.Metafile+".*"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(metaFiles) == 0 {
|
|
return fmt.Errorf("no metastore backups in %s", cmd.backupFilesPath)
|
|
}
|
|
|
|
latest := metaFiles[len(metaFiles)-1]
|
|
|
|
fmt.Fprintf(cmd.Stdout, "Using metastore snapshot: %v\n", latest)
|
|
// Read the metastore backup
|
|
f, err := os.Open(latest)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var buf bytes.Buffer
|
|
if _, err := io.Copy(&buf, f); err != nil {
|
|
return fmt.Errorf("copy: %s", err)
|
|
}
|
|
|
|
b := buf.Bytes()
|
|
var i int
|
|
|
|
// Make sure the file is actually a meta store backup file
|
|
magic := binary.BigEndian.Uint64(b[:8])
|
|
if magic != snapshotter.BackupMagicHeader {
|
|
return fmt.Errorf("invalid metadata file")
|
|
}
|
|
i += 8
|
|
|
|
// Size of the meta store bytes
|
|
length := int(binary.BigEndian.Uint64(b[i : i+8]))
|
|
i += 8
|
|
metaBytes := b[i : i+length]
|
|
i += int(length)
|
|
|
|
// Size of the node.json bytes
|
|
length = int(binary.BigEndian.Uint64(b[i : i+8]))
|
|
i += 8
|
|
nodeBytes := b[i : i+length]
|
|
|
|
// Unpack into metadata.
|
|
var data meta.Data
|
|
if err := data.UnmarshalBinary(metaBytes); err != nil {
|
|
return fmt.Errorf("unmarshal: %s", err)
|
|
}
|
|
|
|
// Copy meta config and remove peers so it starts in single mode.
|
|
c := cmd.MetaConfig
|
|
c.Dir = cmd.metadir
|
|
|
|
// Create the meta dir
|
|
if err := os.MkdirAll(c.Dir, 0700); err != nil {
|
|
return err
|
|
}
|
|
|
|
// Write node.json back to meta dir
|
|
if err := os.WriteFile(filepath.Join(c.Dir, "node.json"), nodeBytes, 0655); err != nil {
|
|
return err
|
|
}
|
|
|
|
client := meta.NewClient(c)
|
|
if err := client.Open(); err != nil {
|
|
return err
|
|
}
|
|
defer client.Close()
|
|
|
|
// Force set the full metadata.
|
|
if err := client.SetData(&data); err != nil {
|
|
return fmt.Errorf("set data: %s", err)
|
|
}
|
|
|
|
// remove the raft.db file if it exists
|
|
err = os.Remove(filepath.Join(cmd.metadir, "raft.db"))
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
// remove the node.json file if it exists
|
|
err = os.Remove(filepath.Join(cmd.metadir, "node.json"))
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
return nil
|
|
}
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func (cmd *Command) updateMetaPortable() error {
|
|
var metaBytes []byte
|
|
fileName := filepath.Join(cmd.backupFilesPath, cmd.manifestMeta.FileName)
|
|
|
|
fileBytes, err := os.ReadFile(fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
var ep backup_util.PortablePacker
|
|
ep.UnmarshalBinary(fileBytes)
|
|
|
|
metaBytes = ep.Data
|
|
|
|
req := &snapshotter.Request{
|
|
Type: snapshotter.RequestMetaStoreUpdate,
|
|
BackupDatabase: cmd.sourceDatabase,
|
|
RestoreDatabase: cmd.destinationDatabase,
|
|
BackupRetentionPolicy: cmd.backupRetention,
|
|
RestoreRetentionPolicy: cmd.restoreRetention,
|
|
UploadSize: int64(len(metaBytes)),
|
|
}
|
|
|
|
shardIDMap, err := cmd.client.UpdateMeta(req, bytes.NewReader(metaBytes))
|
|
cmd.shardIDMap = shardIDMap
|
|
return err
|
|
|
|
}
|
|
|
|
// updateMetaLive takes a metadata backup and sends it to the influx server
|
|
// for a live merger of metadata.
|
|
func (cmd *Command) updateMetaLegacy() error {
|
|
|
|
var metaBytes []byte
|
|
|
|
// find the meta file
|
|
metaFiles, err := filepath.Glob(filepath.Join(cmd.backupFilesPath, backup_util.Metafile+".*"))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(metaFiles) == 0 {
|
|
return fmt.Errorf("no metastore backups in %s", cmd.backupFilesPath)
|
|
}
|
|
|
|
fileName := metaFiles[len(metaFiles)-1]
|
|
cmd.StdoutLogger.Printf("Using metastore snapshot: %v\n", fileName)
|
|
metaBytes, err = backup_util.GetMetaBytes(fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
req := &snapshotter.Request{
|
|
Type: snapshotter.RequestMetaStoreUpdate,
|
|
BackupDatabase: cmd.sourceDatabase,
|
|
RestoreDatabase: cmd.destinationDatabase,
|
|
BackupRetentionPolicy: cmd.backupRetention,
|
|
RestoreRetentionPolicy: cmd.restoreRetention,
|
|
UploadSize: int64(len(metaBytes)),
|
|
}
|
|
|
|
shardIDMap, err := cmd.client.UpdateMeta(req, bytes.NewReader(metaBytes))
|
|
cmd.shardIDMap = shardIDMap
|
|
return err
|
|
}
|
|
|
|
func (cmd *Command) uploadShardsPortable() error {
|
|
for _, file := range cmd.manifestFiles {
|
|
if cmd.sourceDatabase == "" || cmd.sourceDatabase == file.Database {
|
|
if cmd.backupRetention == "" || cmd.backupRetention == file.Policy {
|
|
if cmd.shard == 0 || cmd.shard == file.ShardID {
|
|
oldID := file.ShardID
|
|
// if newID not found then this shard's metadata was NOT imported
|
|
// and should be skipped
|
|
newID, ok := cmd.shardIDMap[oldID]
|
|
if !ok {
|
|
cmd.StdoutLogger.Printf("Meta info not found for shard %d on database %s. Skipping shard file %s", oldID, file.Database, file.FileName)
|
|
continue
|
|
}
|
|
cmd.StdoutLogger.Printf("Restoring shard %d live from backup %s\n", file.ShardID, file.FileName)
|
|
f, err := os.Open(filepath.Join(cmd.backupFilesPath, file.FileName))
|
|
if err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
gr, err := gzip.NewReader(f)
|
|
if err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
tr := tar.NewReader(gr)
|
|
targetDB := cmd.destinationDatabase
|
|
if targetDB == "" {
|
|
targetDB = file.Database
|
|
}
|
|
|
|
if err := cmd.client.UploadShard(oldID, newID, targetDB, cmd.restoreRetention, tr); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
f.Close()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// unpackFiles will look for backup files matching the pattern and restore them to the data dir
|
|
func (cmd *Command) uploadShardsLegacy() error {
|
|
// find the destinationDatabase backup files
|
|
pat := fmt.Sprintf("%s.*", filepath.Join(cmd.backupFilesPath, cmd.sourceDatabase))
|
|
cmd.StdoutLogger.Printf("Restoring live from backup %s\n", pat)
|
|
backupFiles, err := filepath.Glob(pat)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(backupFiles) == 0 {
|
|
return fmt.Errorf("no backup files in %s", cmd.backupFilesPath)
|
|
}
|
|
|
|
for _, fn := range backupFiles {
|
|
parts := strings.Split(filepath.Base(fn), ".")
|
|
|
|
if len(parts) != 4 {
|
|
cmd.StderrLogger.Printf("Skipping mis-named backup file: %s", fn)
|
|
}
|
|
shardID, err := strconv.ParseUint(parts[2], 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// if newID not found then this shard's metadata was NOT imported
|
|
// and should be skipped
|
|
newID, ok := cmd.shardIDMap[shardID]
|
|
if !ok {
|
|
cmd.StdoutLogger.Printf("Meta info not found for shard %d. Skipping shard file %s", shardID, fn)
|
|
continue
|
|
}
|
|
f, err := os.Open(fn)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tr := tar.NewReader(f)
|
|
if err := cmd.client.UploadShard(shardID, newID, cmd.destinationDatabase, cmd.restoreRetention, tr); err != nil {
|
|
f.Close()
|
|
return err
|
|
}
|
|
f.Close()
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// unpackDatabase will look for all backup files in the path matching this destinationDatabase
|
|
// and restore them to the data dir
|
|
func (cmd *Command) unpackDatabase() error {
|
|
// make sure the shard isn't already there so we don't clobber anything
|
|
restorePath := filepath.Join(cmd.datadir, cmd.sourceDatabase)
|
|
if _, err := os.Stat(restorePath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("database already present: %s", restorePath)
|
|
}
|
|
|
|
// find the database backup files
|
|
pat := filepath.Join(cmd.backupFilesPath, cmd.sourceDatabase)
|
|
return cmd.unpackFiles(pat + ".*")
|
|
}
|
|
|
|
// unpackRetention will look for all backup files in the path matching this retention
|
|
// and restore them to the data dir
|
|
func (cmd *Command) unpackRetention() error {
|
|
// make sure the shard isn't already there so we don't clobber anything
|
|
restorePath := filepath.Join(cmd.datadir, cmd.sourceDatabase, cmd.backupRetention)
|
|
if _, err := os.Stat(restorePath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("retention already present: %s", restorePath)
|
|
}
|
|
|
|
// find the retention backup files
|
|
pat := filepath.Join(cmd.backupFilesPath, cmd.sourceDatabase)
|
|
return cmd.unpackFiles(fmt.Sprintf("%s.%s.*", pat, cmd.backupRetention))
|
|
}
|
|
|
|
// unpackShard will look for all backup files in the path matching this shard ID
|
|
// and restore them to the data dir
|
|
func (cmd *Command) unpackShard(shard uint64) error {
|
|
shardID := strconv.FormatUint(shard, 10)
|
|
// make sure the shard isn't already there so we don't clobber anything
|
|
restorePath := filepath.Join(cmd.datadir, cmd.sourceDatabase, cmd.backupRetention, shardID)
|
|
if _, err := os.Stat(restorePath); err != nil && !os.IsNotExist(err) {
|
|
return fmt.Errorf("shard already present: %s", restorePath)
|
|
}
|
|
|
|
id, err := strconv.ParseUint(shardID, 10, 64)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// find the shard backup files
|
|
pat := filepath.Join(cmd.backupFilesPath, fmt.Sprintf(backup_util.BackupFilePattern, cmd.sourceDatabase, cmd.backupRetention, id))
|
|
return cmd.unpackFiles(pat + ".*")
|
|
}
|
|
|
|
// unpackFiles will look for backup files matching the pattern and restore them to the data dir
|
|
func (cmd *Command) unpackFiles(pat string) error {
|
|
cmd.StdoutLogger.Printf("Restoring offline from backup %s\n", pat)
|
|
|
|
backupFiles, err := filepath.Glob(pat)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
if len(backupFiles) == 0 {
|
|
return fmt.Errorf("no backup files for %s in %s", pat, cmd.backupFilesPath)
|
|
}
|
|
|
|
for _, fn := range backupFiles {
|
|
if err := cmd.unpackTar(fn); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// unpackTar will restore a single tar archive to the data dir
|
|
func (cmd *Command) unpackTar(tarFile string) error {
|
|
f, err := os.Open(tarFile)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
// should get us ["db","rp", "00001", "00"]
|
|
pathParts := strings.Split(filepath.Base(tarFile), ".")
|
|
if len(pathParts) != 4 {
|
|
return fmt.Errorf("backup tarfile name incorrect format")
|
|
}
|
|
|
|
shardPath := filepath.Join(cmd.datadir, pathParts[0], pathParts[1], strings.Trim(pathParts[2], "0"))
|
|
os.MkdirAll(shardPath, 0755)
|
|
|
|
return tarstream.Restore(f, shardPath)
|
|
}
|
|
|
|
// printUsage prints the usage message to STDERR.
|
|
func (cmd *Command) printUsage() {
|
|
fmt.Fprintf(cmd.Stdout, `
|
|
Uses backup copies from the specified PATH to restore databases or specific shards from InfluxDB OSS
|
|
or InfluxDB Enterprise to an InfluxDB OSS instance.
|
|
|
|
Usage: influxd restore -portable [options] PATH
|
|
|
|
Note: Restore using the '-portable' option consumes files in an improved Enterprise-compatible
|
|
format that includes a file manifest.
|
|
|
|
Options:
|
|
-portable
|
|
Required to activate the portable restore mode. If not specified, the legacy restore mode is used.
|
|
-host <host:port>
|
|
InfluxDB OSS host to connect to where the data will be restored. Defaults to '127.0.0.1:8088'.
|
|
-db <name>
|
|
Name of database to be restored from the backup (InfluxDB OSS or InfluxDB Enterprise)
|
|
-newdb <name>
|
|
Name of the InfluxDB OSS database into which the archived data will be imported on the target system.
|
|
Optional. If not given, then the value of '-db <db_name>' is used. The new database name must be unique
|
|
to the target system.
|
|
-rp <name>
|
|
Name of retention policy from the backup that will be restored. Optional.
|
|
Requires that '-db <db_name>' is specified.
|
|
-newrp <name>
|
|
Name of the retention policy to be created on the target system. Optional. Requires that '-rp <rp_name>'
|
|
is set. If not given, the '-rp <rp_name>' value is used.
|
|
-shard <id>
|
|
Identifier of the shard to be restored. Optional. If specified, then '-db <db_name>' and '-rp <rp_name>' are
|
|
required.
|
|
PATH
|
|
Path to directory containing the backup files.
|
|
|
|
`)
|
|
}
|