From 2dc9a1ee4ed042471784982c4459ffd52dc524f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C5=81ukasz=20Magiera?= Date: Thu, 1 Oct 2020 13:58:26 +0200 Subject: [PATCH] lotus-miner backup command --- api/api_storage.go | 6 ++ api/apistruct/struct.go | 6 ++ cmd/lotus-storage-miner/backup.go | 42 ++++++++ cmd/lotus-storage-miner/main.go | 1 + lib/backupds/backupds.go | 165 ++++++++++++++++++++++++++++++ node/impl/storminer.go | 67 +++++++++++- node/modules/storage.go | 8 +- 7 files changed, 292 insertions(+), 3 deletions(-) create mode 100644 cmd/lotus-storage-miner/backup.go create mode 100644 lib/backupds/backupds.go diff --git a/api/api_storage.go b/api/api_storage.go index 824772181..529224f6e 100644 --- a/api/api_storage.go +++ b/api/api_storage.go @@ -101,6 +101,12 @@ type StorageMiner interface { PiecesListCidInfos(ctx context.Context) ([]cid.Cid, error) PiecesGetPieceInfo(ctx context.Context, pieceCid cid.Cid) (*piecestore.PieceInfo, error) PiecesGetCIDInfo(ctx context.Context, payloadCid cid.Cid) (*piecestore.CIDInfo, error) + + // CreateBackup creates node backup onder the specified file name. The + // method requires that the lotus-miner is running with the + // LOTUS_BACKUP_BASE_PATH environment variable set to some path, and that + // the path specified when calling CreateBackup is within the base path + CreateBackup(ctx context.Context, fpath string) error } type SealRes struct { diff --git a/api/apistruct/struct.go b/api/apistruct/struct.go index c79d0841c..6a3d709a7 100644 --- a/api/apistruct/struct.go +++ b/api/apistruct/struct.go @@ -319,6 +319,8 @@ type StorageMinerStruct struct { PiecesListCidInfos func(ctx context.Context) ([]cid.Cid, error) `perm:"read"` PiecesGetPieceInfo func(ctx context.Context, pieceCid cid.Cid) (*piecestore.PieceInfo, error) `perm:"read"` PiecesGetCIDInfo func(ctx context.Context, payloadCid cid.Cid) (*piecestore.CIDInfo, error) `perm:"read"` + + CreateBackup func(ctx context.Context, fpath string) error `perm:"admin"` } } @@ -1265,6 +1267,10 @@ func (c *StorageMinerStruct) PiecesGetCIDInfo(ctx context.Context, payloadCid ci return c.Internal.PiecesGetCIDInfo(ctx, payloadCid) } +func (c *StorageMinerStruct) CreateBackup(ctx context.Context, fpath string) error { + return c.Internal.CreateBackup(ctx, fpath) +} + // WorkerStruct func (w *WorkerStruct) Version(ctx context.Context) (build.Version, error) { diff --git a/cmd/lotus-storage-miner/backup.go b/cmd/lotus-storage-miner/backup.go new file mode 100644 index 000000000..779fd1fb4 --- /dev/null +++ b/cmd/lotus-storage-miner/backup.go @@ -0,0 +1,42 @@ +package main + +import ( + "fmt" + + "github.com/urfave/cli/v2" + "golang.org/x/xerrors" + + lcli "github.com/filecoin-project/lotus/cli" +) + +var backupCmd = &cli.Command{ + Name: "backup", + Usage: "Create node metadata backup", + Description: `The backup command writes a copy of node metadata under the specified path + +For security reasons, the daemon must be have LOTUS_BACKUP_BASE_PATH env var set +to a path where backup files are supposed to be saved, and the path specified in +this command must be within this base path`, + ArgsUsage: "[backup file path]", + Flags: []cli.Flag{}, + Action: func(cctx *cli.Context) error { + api, closer, err := lcli.GetStorageMinerAPI(cctx) + if err != nil { + return err + } + defer closer() + + if cctx.Args().Len() != 1 { + return xerrors.Errorf("expected 1 argument") + } + + err = api.CreateBackup(lcli.ReqContext(cctx), cctx.Args().First()) + if err != nil { + return err + } + + fmt.Println("Success") + + return nil + }, +} diff --git a/cmd/lotus-storage-miner/main.go b/cmd/lotus-storage-miner/main.go index cee64f077..671f75cf0 100644 --- a/cmd/lotus-storage-miner/main.go +++ b/cmd/lotus-storage-miner/main.go @@ -35,6 +35,7 @@ func main() { runCmd, stopCmd, configCmd, + backupCmd, lcli.WithCategory("chain", actorCmd), lcli.WithCategory("chain", infoCmd), lcli.WithCategory("market", storageDealsCmd), diff --git a/lib/backupds/backupds.go b/lib/backupds/backupds.go new file mode 100644 index 000000000..8845eacc7 --- /dev/null +++ b/lib/backupds/backupds.go @@ -0,0 +1,165 @@ +package backupds + +import ( + "io" + "sync" + + logging "github.com/ipfs/go-log/v2" + cbg "github.com/whyrusleeping/cbor-gen" + "golang.org/x/xerrors" + + "github.com/ipfs/go-datastore" + "github.com/ipfs/go-datastore/query" +) + +var log = logging.Logger("backupds") + +type Datastore struct { + child datastore.Batching + + backupLk sync.RWMutex +} + +func Wrap(child datastore.Batching) *Datastore { + return &Datastore{ + child: child, + } +} + +// Writes a datastore dump into the provided writer as indefinite length cbor +// array of [key, value] tuples +func (d *Datastore) Backup(out io.Writer) error { + // write indefinite length array header + if _, err := out.Write([]byte{0x9f}); err != nil { + return xerrors.Errorf("writing header: %w", err) + } + + d.backupLk.Lock() + defer d.backupLk.Unlock() + + log.Info("Starting datastore backup") + defer log.Info("Datastore backup done") + + qr, err := d.child.Query(query.Query{}) + if err != nil { + return xerrors.Errorf("query: %w", err) + } + defer func() { + if err := qr.Close(); err != nil { + log.Errorf("query close error: %+v", err) + return + } + }() + + scratch := make([]byte, 9) + + for result := range qr.Next() { + if err := cbg.WriteMajorTypeHeaderBuf(scratch, out, cbg.MajArray, 2); err != nil { + return xerrors.Errorf("writing tuple header: %w", err) + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, out, cbg.MajByteString, uint64(len([]byte(result.Key)))); err != nil { + return xerrors.Errorf("writing key header: %w", err) + } + + if _, err := out.Write([]byte(result.Key)[:]); err != nil { + return xerrors.Errorf("writing key: %w", err) + } + + if err := cbg.WriteMajorTypeHeaderBuf(scratch, out, cbg.MajByteString, uint64(len(result.Value))); err != nil { + return xerrors.Errorf("writing value header: %w", err) + } + + if _, err := out.Write(result.Value[:]); err != nil { + return xerrors.Errorf("writing value: %w", err) + } + } + + // array break + if _, err := out.Write([]byte{0xff}); err != nil { + return xerrors.Errorf("writing array 'break': %w", err) + } + + return nil +} + +// proxy + +func (d *Datastore) Get(key datastore.Key) (value []byte, err error) { + return d.child.Get(key) +} + +func (d *Datastore) Has(key datastore.Key) (exists bool, err error) { + return d.child.Has(key) +} + +func (d *Datastore) GetSize(key datastore.Key) (size int, err error) { + return d.child.GetSize(key) +} + +func (d *Datastore) Query(q query.Query) (query.Results, error) { + return d.child.Query(q) +} + +func (d *Datastore) Put(key datastore.Key, value []byte) error { + d.backupLk.RLock() + defer d.backupLk.RUnlock() + + return d.child.Put(key, value) +} + +func (d *Datastore) Delete(key datastore.Key) error { + d.backupLk.RLock() + defer d.backupLk.RUnlock() + + return d.child.Delete(key) +} + +func (d *Datastore) Sync(prefix datastore.Key) error { + d.backupLk.RLock() + defer d.backupLk.RUnlock() + + return d.child.Sync(prefix) +} + +func (d *Datastore) Close() error { + d.backupLk.RLock() + defer d.backupLk.RUnlock() + + return d.child.Close() +} + +func (d *Datastore) Batch() (datastore.Batch, error) { + b, err := d.child.Batch() + if err != nil { + return nil, err + } + + return &bbatch{ + b: b, + rlk: d.backupLk.RLocker(), + }, nil +} + +type bbatch struct { + b datastore.Batch + rlk sync.Locker +} + +func (b *bbatch) Put(key datastore.Key, value []byte) error { + return b.b.Put(key, value) +} + +func (b *bbatch) Delete(key datastore.Key) error { + return b.b.Delete(key) +} + +func (b *bbatch) Commit() error { + b.rlk.Lock() + defer b.rlk.Unlock() + + return b.b.Commit() +} + +var _ datastore.Batch = &bbatch{} +var _ datastore.Batching = &Datastore{} diff --git a/node/impl/storminer.go b/node/impl/storminer.go index 5634c140b..c0cbe0081 100644 --- a/node/impl/storminer.go +++ b/node/impl/storminer.go @@ -5,21 +5,24 @@ import ( "encoding/json" "net/http" "os" + "path/filepath" "strconv" + "strings" "time" - datatransfer "github.com/filecoin-project/go-data-transfer" - "github.com/filecoin-project/go-state-types/big" "github.com/ipfs/go-cid" "github.com/libp2p/go-libp2p-core/host" + "github.com/mitchellh/go-homedir" "golang.org/x/xerrors" "github.com/filecoin-project/go-address" + datatransfer "github.com/filecoin-project/go-data-transfer" "github.com/filecoin-project/go-fil-markets/piecestore" retrievalmarket "github.com/filecoin-project/go-fil-markets/retrievalmarket" storagemarket "github.com/filecoin-project/go-fil-markets/storagemarket" "github.com/filecoin-project/go-jsonrpc/auth" "github.com/filecoin-project/go-state-types/abi" + "github.com/filecoin-project/go-state-types/big" sectorstorage "github.com/filecoin-project/lotus/extern/sector-storage" "github.com/filecoin-project/lotus/extern/sector-storage/ffiwrapper" @@ -31,9 +34,11 @@ import ( "github.com/filecoin-project/lotus/api" "github.com/filecoin-project/lotus/api/apistruct" "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/lib/backupds" "github.com/filecoin-project/lotus/miner" "github.com/filecoin-project/lotus/node/impl/common" "github.com/filecoin-project/lotus/node/modules/dtypes" + "github.com/filecoin-project/lotus/node/repo" "github.com/filecoin-project/lotus/storage" "github.com/filecoin-project/lotus/storage/sectorblocks" ) @@ -56,6 +61,9 @@ type StorageMinerAPI struct { DataTransfer dtypes.ProviderDataTransfer Host host.Host + DS dtypes.MetadataDS + Repo repo.LockedRepo + ConsiderOnlineStorageDealsConfigFunc dtypes.ConsiderOnlineStorageDealsConfigFunc SetConsiderOnlineStorageDealsConfigFunc dtypes.SetConsiderOnlineStorageDealsConfigFunc ConsiderOnlineRetrievalDealsConfigFunc dtypes.ConsiderOnlineRetrievalDealsConfigFunc @@ -516,4 +524,59 @@ func (sm *StorageMinerAPI) PiecesGetCIDInfo(ctx context.Context, payloadCid cid. return &ci, nil } +func (sm *StorageMinerAPI) CreateBackup(ctx context.Context, fpath string) error { + // TODO: Config + bb, ok := os.LookupEnv("LOTUS_BACKUP_BASE_PATH") + if !ok { + return xerrors.Errorf("LOTUS_BACKUP_BASE_PATH env var not set") + } + + bds, ok := sm.DS.(*backupds.Datastore) + if !ok { + return xerrors.Errorf("expected a backup datastore") + } + + bb, err := homedir.Expand(bb) + if err != nil { + return xerrors.Errorf("expanding base path: %w", err) + } + + bb, err = filepath.Abs(bb) + if err != nil { + return xerrors.Errorf("getting absolute base path: %w", err) + } + + fpath, err = homedir.Expand(fpath) + if err != nil { + return xerrors.Errorf("expanding file path: %w", err) + } + + fpath, err = filepath.Abs(fpath) + if err != nil { + return xerrors.Errorf("getting absolute file path: %w", err) + } + + if !strings.HasPrefix(fpath, bb) { + return xerrors.Errorf("backup file name (%s) must be inside base path (%s)", fpath, bb) + } + + out, err := os.OpenFile(fpath, os.O_CREATE|os.O_WRONLY, 0644) + if err != nil { + return xerrors.Errorf("open %s: %w", fpath, err) + } + + if err := bds.Backup(out); err != nil { + if cerr := out.Close(); cerr != nil { + log.Errorw("error closing backup file while handling backup error", "closeErr", cerr, "backupErr", err) + } + return xerrors.Errorf("backup error: %w", err) + } + + if err := out.Close(); err != nil { + return xerrors.Errorf("closing backup file: %w", err) + } + + return nil +} + var _ api.StorageMiner = &StorageMinerAPI{} diff --git a/node/modules/storage.go b/node/modules/storage.go index 1bdce1d2f..9c1a18368 100644 --- a/node/modules/storage.go +++ b/node/modules/storage.go @@ -6,6 +6,7 @@ import ( "go.uber.org/fx" "github.com/filecoin-project/lotus/chain/types" + "github.com/filecoin-project/lotus/lib/backupds" "github.com/filecoin-project/lotus/node/modules/dtypes" "github.com/filecoin-project/lotus/node/repo" ) @@ -27,5 +28,10 @@ func KeyStore(lr repo.LockedRepo) (types.KeyStore, error) { } func Datastore(r repo.LockedRepo) (dtypes.MetadataDS, error) { - return r.Datastore("/metadata") + mds, err := r.Datastore("/metadata") + if err != nil { + return nil, err + } + + return backupds.Wrap(mds), nil }