influxdb/restore/restore.go

348 lines
9.2 KiB
Go

package restore
import (
"compress/gzip"
"context"
"encoding/json"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sort"
"strings"
"github.com/influxdata/influxdb/v2/kit/platform"
"github.com/influxdata/influxdb/v2/kit/platform/errors"
"github.com/influxdata/influxdb/v2"
"github.com/influxdata/influxdb/v2/bolt"
"github.com/influxdata/influxdb/v2/tenant"
"github.com/influxdata/influxdb/v2/v1/services/meta"
"go.uber.org/zap"
)
type Request struct {
// Path to local backup data created using `influx backup`
Path string
// Original ID/name of the organization to restore.
// If not set, all orgs will be restored.
OrgID platform.ID
Org string
// New name to use for the restored organization.
// If not set, the org will be restored using its backed-up name.
NewOrgName string
// Original ID/name of the bucket to restore.
// If not set, all buckets within the org filter will be restored.
BucketID platform.ID
Bucket string
// New name to use for the restored bucket.
// If not set, the bucket will be restored using its backed-up name.
NewBucketName string
// If true, replace all data on the server with the local backup.
// Otherwise only restore the requested org/bucket, leaving other data untouched.
Full bool
}
type Services struct {
RestoreService influxdb.RestoreService
OrgService influxdb.OrganizationService
BucketService influxdb.BucketService
}
type restoreRunner struct {
Services
kvManifest *influxdb.ManifestKVEntry
shardManifests map[uint64]*influxdb.ManifestEntry
tenantService influxdb.TenantService
metaClient *meta.Client
log *zap.Logger
}
func RunRestore(ctx context.Context, req Request, svcs Services, log *zap.Logger) error {
runner := restoreRunner{
Services: svcs,
log: log,
}
if err := runner.loadManifests(req.Path); err != nil {
return err
}
if req.Full {
return runner.fullRestore(ctx, req)
}
return runner.partialRestore(ctx, req)
}
func (r *restoreRunner) loadManifests(path string) error {
// Read all manifest files from path, sort in descending time.
manifests, err := filepath.Glob(filepath.Join(path, "*.manifest"))
if err != nil {
return fmt.Errorf("failed to find backup manifests at %q: %w", path, err)
} else if len(manifests) == 0 {
return fmt.Errorf("no backup manifests found at %q", path)
}
sort.Sort(sort.Reverse(sort.StringSlice(manifests)))
r.shardManifests = make(map[uint64]*influxdb.ManifestEntry)
for _, filename := range manifests {
// Skip file if it is a directory.
if fi, err := os.Stat(filename); err != nil {
return fmt.Errorf("failed to inspect local manifest at %q: %w", filename, err)
} else if fi.IsDir() {
continue
}
// Read manifest file for backup.
var manifest influxdb.Manifest
if buf, err := ioutil.ReadFile(filename); err != nil {
return fmt.Errorf("failed to read local manifest at %q: %w", filename, err)
} else if err := json.Unmarshal(buf, &manifest); err != nil {
return fmt.Errorf("read manifest: %v", err)
}
// Save latest KV entry (first in the sorted slice).
if r.kvManifest == nil {
r.kvManifest = &manifest.KV
}
// Load most recent backup per shard.
for i := range manifest.Files {
sh := manifest.Files[i]
if _, err := os.Stat(filepath.Join(path, sh.FileName)); err != nil {
continue
}
entry := r.shardManifests[sh.ShardID]
if entry == nil || sh.LastModified.After(entry.LastModified) {
r.shardManifests[sh.ShardID] = &sh
}
}
}
return nil
}
func (r *restoreRunner) fullRestore(ctx context.Context, req Request) error {
if err := r.restoreKV(ctx, req.Path); err != nil {
return err
}
for _, m := range r.shardManifests {
if err := r.restoreShard(ctx, req.Path, m); err != nil {
return err
}
}
r.log.Info("Full restore complete", zap.String("path", req.Path))
return nil
}
func (r *restoreRunner) partialRestore(ctx context.Context, req Request) error {
// Open meta store so we can iterate over metadata.
kvStore := bolt.NewKVStore(r.log, filepath.Join(req.Path, r.kvManifest.FileName))
if err := kvStore.Open(ctx); err != nil {
return err
}
defer kvStore.Close()
r.tenantService = tenant.NewService(tenant.NewStore(kvStore))
r.metaClient = meta.NewClient(meta.NewConfig(), kvStore)
if err := r.metaClient.Open(); err != nil {
return err
}
defer r.metaClient.Close()
if err := r.restoreOrganizations(ctx, req); err != nil {
return err
}
r.log.Info("Partial restore complete", zap.String("path", req.Path))
return nil
}
func (r *restoreRunner) restoreKV(ctx context.Context, path string) error {
kvPath := filepath.Join(path, r.kvManifest.FileName)
r.log.Info("Restoring full metadata from local backup", zap.String("path", kvPath))
f, err := os.Open(kvPath)
if err != nil {
return fmt.Errorf("failed to open local KV backup at %q: %w", kvPath, err)
}
defer f.Close()
if err := r.RestoreService.RestoreKVStore(ctx, f); err != nil {
return fmt.Errorf("failed to upload local KV backup at %q: %w", kvPath, err)
}
r.log.Info("Full metadata restored", zap.String("path", kvPath))
return nil
}
func (r *restoreRunner) restoreShard(ctx context.Context, path string, manifest *influxdb.ManifestEntry) error {
shardPath := filepath.Join(path, manifest.FileName)
r.log.Info("Restoring shard from local backup", zap.Uint64("id", manifest.ShardID), zap.String("path", shardPath))
f, err := os.Open(shardPath)
if err != nil {
return fmt.Errorf("failed to open local shard backup at %q: %w", shardPath, err)
}
defer f.Close()
gr, err := gzip.NewReader(f)
if err != nil {
return fmt.Errorf("failed to open gzip reader for local shard backup: %w", err)
}
defer gr.Close()
if err := r.RestoreService.RestoreShard(ctx, manifest.ShardID, gr); err != nil {
return fmt.Errorf("failed to upload local shard backup at %q: %w", shardPath, err)
}
return nil
}
func (r *restoreRunner) restoreOrganizations(ctx context.Context, req Request) error {
var filter influxdb.OrganizationFilter
if req.OrgID.Valid() {
filter.ID = &req.OrgID
}
if req.Org != "" {
filter.Name = &req.Org
}
orgs, _, err := r.tenantService.FindOrganizations(ctx, filter)
if err != nil {
return err
}
for _, org := range orgs {
if err := r.restoreOrganization(ctx, org, req); err != nil {
return err
}
}
return nil
}
func (r *restoreRunner) restoreOrganization(ctx context.Context, org *influxdb.Organization, req Request) error {
newOrg := *org
if req.NewOrgName != "" {
newOrg.Name = req.NewOrgName
}
r.log.Info(
"Restoring organization",
zap.String("backup_id", org.ID.String()),
zap.String("backup_name", org.Name),
zap.String("restored_name", newOrg.Name),
)
// Create organization on server if it doesn't already exist.
if o, err := r.OrgService.FindOrganization(ctx, influxdb.OrganizationFilter{Name: &newOrg.Name}); errors.ErrorCode(err) == errors.ENotFound {
if err := r.OrgService.CreateOrganization(ctx, &newOrg); err != nil {
return fmt.Errorf("failed to create organization %q: %w", newOrg.Name, err)
}
} else if err != nil {
return fmt.Errorf("failed to check existence of organization %q: %w", newOrg.Name, err)
} else {
newOrg.ID = o.ID
}
filter := influxdb.BucketFilter{OrganizationID: &org.ID}
if req.BucketID.Valid() {
filter.ID = &req.BucketID
}
if req.Bucket != "" {
filter.Name = &req.Bucket
}
buckets, _, err := r.tenantService.FindBuckets(ctx, filter)
if err != nil {
return err
}
for _, bkt := range buckets {
// Skip internal buckets.
if strings.HasPrefix(bkt.Name, "_") {
continue
}
bkt = bkt.Clone()
bkt.OrgID = newOrg.ID
if err := r.restoreBucket(ctx, bkt, req); err != nil {
return err
}
}
return nil
}
func (r *restoreRunner) restoreBucket(ctx context.Context, bkt *influxdb.Bucket, req Request) error {
newBucket := *bkt
if req.NewBucketName != "" {
newBucket.Name = req.NewBucketName
}
r.log.Info(
"Restoring bucket",
zap.String("backup_id", bkt.ID.String()),
zap.String("backup_name", bkt.Name),
zap.String("restored_name", newBucket.Name),
)
// Lookup matching database from the meta store.
// Search using bucket ID from backup.
dbi := r.metaClient.Database(bkt.ID.String())
if dbi == nil {
return fmt.Errorf("database for bucket %q not found in local backup", bkt.ID.String())
}
// Serialize to protobufs.
buf, err := dbi.MarshalBinary()
if err != nil {
return fmt.Errorf("failed to marshal database info for bucket %q: %w", bkt.ID.String(), err)
}
// TODO: Only create if it doesn't already exist.
if err := r.BucketService.CreateBucket(ctx, &newBucket); err != nil {
return fmt.Errorf("failed to create bucket %q: %w", newBucket.Name, err)
}
shardIDMap, err := r.RestoreService.RestoreBucket(ctx, newBucket.ID, buf)
if err != nil {
return fmt.Errorf("failed to restore bucket %q: %w", newBucket.Name, err)
}
// Restore each shard for the bucket.
for _, m := range r.shardManifests {
if bkt.ID.String() != m.BucketID {
continue
}
// Skip if shard metadata was not imported.
newID, ok := shardIDMap[m.ShardID]
if !ok {
r.log.Warn(
"Meta info not found, skipping shard",
zap.Uint64("shard_id", m.ShardID),
zap.String("bucket_id", newBucket.ID.String()),
zap.String("path", filepath.Join(req.Path, m.FileName)),
)
continue
}
m.ShardID = newID
if err := r.restoreShard(ctx, req.Path, m); err != nil {
return err
}
}
return nil
}