Skip to content

Commit

Permalink
cmd: add tool to migrate objects from Peapod to FSTree
Browse files Browse the repository at this point in the history
Add `cmd/peapod-to-fstree` application which accepts YAML
configuration file of the storage node, for each configured shard,
overtakes data from Peapod to FSTree that has already been configured and
updates metabase `StorageId` indexes.

The tool is going to be used for phased and safe rejection of the
Peapod and the transition to FSTree.

Closes #2924.

Signed-off-by: Andrey Butusov <andrey@nspcc.io>
  • Loading branch information
End-rey committed Nov 15, 2024
1 parent 75830fe commit 4fb969a
Show file tree
Hide file tree
Showing 3 changed files with 514 additions and 0 deletions.
18 changes: 18 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ attribute, which is used for container domain name in NNS contracts (#2954)
- Docs files for cli commands to the `docs/cli-commands` folder (#2983)
- `logger.encoding` config option (#2999)
- Reloading morph endpoints with SIGHUP (#2998)
- New `peapod-to-fstree` tool providing peapod-to-fstree data migration (#3013)

### Fixed
- Do not search for tombstones when handling their expiration, use local indexes instead (#2929)
Expand All @@ -43,6 +44,7 @@ attribute, which is used for container domain name in NNS contracts (#2954)
- Reject configuration with unknown fields (#2981)
- Log sampling is disabled by default now (#3011)
- EACL is no longer considered for system role (#2972)
- Deprecate peapod substorage (#3013)

### Removed
- Support for node.key configuration (#2959)
Expand All @@ -61,6 +63,22 @@ introduced in version 0.22.3 and support for binary keys was removed from
other components in 0.33.0 and 0.37.0. Please migrate to wallets (see 0.37.0
notes) if you've not done it previously.

To migrate data from Peapods to FSTree:
```shell
$ peapod-to-fstree -config </path/to/storage/node/config>
```
For any shard, the data from the configured Peapod is copied into
a FSTree that must be already configured.
Notice that peapod DB is not deleted during migration. Configuration is also
updated, for example, `/etc/neofs/config.yaml` -> `/etc/neofs/config.yaml.migrated`.
WARN: carefully review the updated config before using it in the application!

Now there is an FSTree in which files of any size will be more efficient and easier to store.
We support both `fstree` and `peapod` sub-storages,
but `peapod` can be removed in future versions. We recommend using `fstree`.
If you want to use only `fstree` and storage node already stores some data,
don't forget to make data migration described above.

## [0.43.0] - 2024-08-20 - Jukdo

### Added
Expand Down
327 changes: 327 additions & 0 deletions cmd/peapod-to-fstree/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,327 @@
package main

import (
"bytes"
"errors"
"flag"
"fmt"
"log"
"os"
"path/filepath"
"slices"
"time"

"github.com/nspcc-dev/neofs-node/cmd/neofs-node/config"
engineconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/engine"
shardconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/engine/shard"
fstreeconfig "github.com/nspcc-dev/neofs-node/cmd/neofs-node/config/engine/shard/blobstor/fstree"
objectcore "github.com/nspcc-dev/neofs-node/pkg/core/object"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor/common"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor/compression"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor/fstree"
"github.com/nspcc-dev/neofs-node/pkg/local_object_storage/blobstor/peapod"
meta "github.com/nspcc-dev/neofs-node/pkg/local_object_storage/metabase"
"go.etcd.io/bbolt"
"gopkg.in/yaml.v3"
)

type epochState struct{}

func (s epochState) CurrentEpoch() uint64 {
return 0

Check warning on line 31 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L30-L31

Added lines #L30 - L31 were not covered by tests
}

func main() {
nodeCfgPath := flag.String("config", "", "Path to storage node's YAML configuration file")

flag.Parse()

if *nodeCfgPath == "" {
log.Fatal("missing storage node config flag")
}

Check warning on line 41 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L34-L41

Added lines #L34 - L41 were not covered by tests

srcPath := *nodeCfgPath

dstPath := srcPath + ".migrated"

log.Printf("migrating configuration to '%s' file...\n", dstPath)

err := migrateConfigToFstree(dstPath, srcPath)
if err != nil {
log.Fatal(err)
}

Check warning on line 52 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L43-L52

Added lines #L43 - L52 were not covered by tests

log.Println("configuration successfully migrated, migrating data in shards...")

appCfg := config.New(config.Prm{}, config.WithConfigFile(*nodeCfgPath))

i := uint64(0)
err = engineconfig.IterateShards(appCfg, false, func(sc *shardconfig.Config) error {
log.Printf("processing shard %d...\n", i)

var ppd, fstr common.Storage
storagesCfg := sc.BlobStor().Storages()

for i := range storagesCfg {
switch storagesCfg[i].Type() {
case fstree.Type:
sub := fstreeconfig.From((*config.Config)(storagesCfg[i]))

fstr = fstree.New(
fstree.WithPath(storagesCfg[i].Path()),
fstree.WithPerm(storagesCfg[i].Perm()),
fstree.WithDepth(sub.Depth()),
fstree.WithNoSync(sub.NoSync()),
fstree.WithCombinedCountLimit(sub.CombinedCountLimit()),
fstree.WithCombinedSizeLimit(sub.CombinedSizeLimit()),
fstree.WithCombinedSizeThreshold(sub.CombinedSizeThreshold()),
fstree.WithCombinedWriteInterval(storagesCfg[i].FlushInterval()),
)

Check warning on line 79 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L54-L79

Added lines #L54 - L79 were not covered by tests

case peapod.Type:
ppd = peapod.New(
storagesCfg[i].Path(),
storagesCfg[i].Perm(),
storagesCfg[i].FlushInterval(),
)
default:
return fmt.Errorf("invalid storage type: %s", storagesCfg[i].Type())

Check warning on line 88 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L81-L88

Added lines #L81 - L88 were not covered by tests
}
}

if ppd == nil {
log.Printf("Peapod is not configured for the shard %d, going to next one...\n", i)
return nil
}

Check warning on line 95 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L92-L95

Added lines #L92 - L95 were not covered by tests

if fstr == nil {
return fmt.Errorf("FSTree is not configured for the shard %d, please configure some fstree for this shard, going to next one...\n", i)
}

Check warning on line 99 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L97-L99

Added lines #L97 - L99 were not covered by tests

ppdPath := ppd.Path()
if !filepath.IsAbs(ppdPath) {
log.Fatalf("Peapod path '%s' is not absolute, make it like this in the config file first\n", ppdPath)
}

Check warning on line 104 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L101-L104

Added lines #L101 - L104 were not covered by tests

var compressCfg compression.Config
compressCfg.Enabled = sc.Compress()
compressCfg.UncompressableContentTypes = sc.UncompressableContentTypes()

err := compressCfg.Init()
if err != nil {
log.Fatalf("init compression config for the shard %d: %v", i, err)
}

Check warning on line 113 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L106-L113

Added lines #L106 - L113 were not covered by tests

ppd.SetCompressor(&compressCfg)
fstr.SetCompressor(&compressCfg)

log.Printf("migrating data from Peapod '%s' to Fstree '%s'...\n", ppd.Path(), fstr.Path())

err = common.Copy(fstr, ppd)
if err != nil {
log.Fatal("migration failed: ", err)
}

Check warning on line 123 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L115-L123

Added lines #L115 - L123 were not covered by tests

log.Println("updating metabase indexes...")

readOnly := false
metabase := meta.New(
meta.WithPath(sc.Metabase().Path()),
meta.WithBoltDBOptions(&bbolt.Options{
ReadOnly: readOnly,
Timeout: time.Second,
}),
meta.WithEpochState(epochState{}),
)
if err := metabase.Open(readOnly); err != nil {
return fmt.Errorf("could not open metabase in shard %d: %w", i, err)
}

Check warning on line 138 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L125-L138

Added lines #L125 - L138 were not covered by tests

var cursor *meta.Cursor
var addrs []objectcore.AddressWithType
for {
addrs, cursor, err = metabase.ListWithCursor(1024, cursor)
if err != nil {
if errors.Is(err, meta.ErrEndOfListing) {
break

Check warning on line 146 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L140-L146

Added lines #L140 - L146 were not covered by tests
}

return fmt.Errorf("listing objects in metabase: %w", err)

Check warning on line 149 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L149

Added line #L149 was not covered by tests
}

for _, obj := range addrs {
addr := obj.Address

storageId, err := metabase.StorageID(addr)
if err != nil {
return fmt.Errorf("could not get storage id for address %s: %w", addr, err)
}

Check warning on line 158 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L152-L158

Added lines #L152 - L158 were not covered by tests

if bytes.Equal(storageId, []byte("peapod")) {
err = metabase.UpdateStorageID(addr, []byte{})
if err != nil {
return fmt.Errorf("could not update storage id for address %s: %w", addr, err)
}

Check warning on line 164 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L160-L164

Added lines #L160 - L164 were not covered by tests
}
}
}

log.Printf("data successfully migrated in the shard %d, going to the next one...\n", i)

i++
return nil

Check warning on line 172 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L169-L172

Added lines #L169 - L172 were not covered by tests
})
if err != nil {
log.Fatal(err)
}

Check warning on line 176 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L174-L176

Added lines #L174 - L176 were not covered by tests

log.Println("data successfully migrated in all shards")

Check warning on line 178 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L178

Added line #L178 was not covered by tests
}

func migrateConfigToFstree(dstPath, srcPath string) error {
fData, err := os.ReadFile(srcPath)
if err != nil {
return fmt.Errorf("read source config file: %w", err)
}

Check warning on line 185 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L184-L185

Added lines #L184 - L185 were not covered by tests

var mConfig map[any]any

err = yaml.Unmarshal(fData, &mConfig)
if err != nil {
return fmt.Errorf("decode config from YAML: %w", err)
}

Check warning on line 192 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L191-L192

Added lines #L191 - L192 were not covered by tests

v, ok := mConfig["storage"]
if !ok {
return errors.New("missing 'storage' section")
}

Check warning on line 197 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L196-L197

Added lines #L196 - L197 were not covered by tests

mStorage, ok := v.(map[string]any)
if !ok {
return fmt.Errorf("unexpected 'storage' section type: %T instead of %T", v, mStorage)
}

Check warning on line 202 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L201-L202

Added lines #L201 - L202 were not covered by tests

v, ok = mStorage["shard"]
if !ok {
return errors.New("missing 'storage.shard' section")
}

Check warning on line 207 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L206-L207

Added lines #L206 - L207 were not covered by tests

mShards, ok := v.(map[any]any)
if !ok {
return fmt.Errorf("unexpected 'storage.shard' section type: %T instead of %T", v, mShards)
}

Check warning on line 212 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L211-L212

Added lines #L211 - L212 were not covered by tests

replacePeapodWithFstree := func(mShard map[string]any, shardDesc any) error {
v, ok := mShard["blobstor"]
if !ok {
return fmt.Errorf("missing 'blobstor' section in shard '%v' config", shardDesc)
}

Check warning on line 218 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L217-L218

Added lines #L217 - L218 were not covered by tests

sBlobStor, ok := v.([]any)
if !ok {
return fmt.Errorf("unexpected 'blobstor' section type in shard '%v': %T instead of %T", shardDesc, v, sBlobStor)
}

Check warning on line 223 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L222-L223

Added lines #L222 - L223 were not covered by tests

var ppdSubStorage map[string]any
var ppdSubStorageIndex int
var fstreeExist bool

for i := range sBlobStor {
mSubStorage, ok := sBlobStor[i].(map[string]any)
if !ok {
return fmt.Errorf("unexpected sub-storage #%d type in shard '%v': %T instead of %T", i, shardDesc, v, mStorage)
}

Check warning on line 233 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L232-L233

Added lines #L232 - L233 were not covered by tests

v, ok := mSubStorage["type"]
if ok {
typ, ok := v.(string)
if !ok {
return fmt.Errorf("unexpected type of sub-storage name: %T instead of %T", v, typ)
}

Check warning on line 240 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L239-L240

Added lines #L239 - L240 were not covered by tests

if typ == peapod.Type {
ppdSubStorage = mSubStorage
ppdSubStorageIndex = i
}

if typ == fstree.Type {
fstreeExist = true
}

continue
}

// in 'default' section 'type' may be missing

_, withDepth := mSubStorage["depth"]
_, withNoSync := mSubStorage["no_sync"]
_, withCountLimit := mSubStorage["combined_count_limit"]
_, withSizeLimit := mSubStorage["combined_size_limit"]
_, withSizeThreshold := mSubStorage["combined_size_threshold"]

if !(withDepth || withNoSync || withCountLimit || withSizeLimit || withSizeThreshold) {
ppdSubStorage = mSubStorage
ppdSubStorageIndex = i
}
fstreeExist = true
}

if ppdSubStorage == nil {
log.Printf("peapod is not configured for the shard '%v', skip\n", shardDesc)
return nil
}

if !fstreeExist {
return fmt.Errorf("fstree is not configured for the shard '%v', please configure some fstree for this shard, skip\n", shardDesc)
}

mShard["blobstor"] = slices.Delete(sBlobStor, ppdSubStorageIndex, ppdSubStorageIndex+1)

return nil
}

v, ok = mShards["default"]
if ok {
mShard, ok := v.(map[string]any)
if !ok {
return fmt.Errorf("unexpected 'storage.shard.default' section type: %T instead of %T", v, mShard)
}

Check warning on line 288 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L287-L288

Added lines #L287 - L288 were not covered by tests

err = replacePeapodWithFstree(mShard, "default")
if err != nil {
return err
}

Check warning on line 293 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L292-L293

Added lines #L292 - L293 were not covered by tests
}

for i := 0; ; i++ {
v, ok = mShards[i]
if !ok {
if i == 0 {
return errors.New("missing numbered shards")
}

Check warning on line 301 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L300-L301

Added lines #L300 - L301 were not covered by tests
break
}

mShard, ok := v.(map[string]any)
if !ok {
return fmt.Errorf("unexpected 'storage.shard.%d' section type: %T instead of %T", i, v, mStorage)
}

Check warning on line 308 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L307-L308

Added lines #L307 - L308 were not covered by tests

err = replacePeapodWithFstree(mShard, i)
if err != nil {
return err
}
}

data, err := yaml.Marshal(mConfig)
if err != nil {
return fmt.Errorf("encode modified config into YAML: %w", err)
}

Check warning on line 319 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L318-L319

Added lines #L318 - L319 were not covered by tests

err = os.WriteFile(dstPath, data, 0o640)
if err != nil {
return fmt.Errorf("write resulting config to the destination file: %w", err)
}

Check warning on line 324 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L323-L324

Added lines #L323 - L324 were not covered by tests

return nil
}
Loading

0 comments on commit 4fb969a

Please sign in to comment.