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 39f1a79
Show file tree
Hide file tree
Showing 3 changed files with 515 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_fstree.yaml`.
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
328 changes: 328 additions & 0 deletions cmd/peapod-to-fstree/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,328 @@
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

extension := filepath.Ext(srcPath)
dstPath := srcPath[0:len(srcPath)-len(extension)] + "_fstree" + extension

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

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L43 - L53 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 80 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L55-L80

Added lines #L55 - L80 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 89 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L82-L89

Added lines #L82 - L89 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 96 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L93-L96

Added lines #L93 - L96 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 100 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L98-L100

Added lines #L98 - L100 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 105 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L102-L105

Added lines #L102 - L105 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 114 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L107-L114

Added lines #L107 - L114 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 124 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L116-L124

Added lines #L116 - L124 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 139 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L126-L139

Added lines #L126 - L139 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 147 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L141-L147

Added lines #L141 - L147 were not covered by tests
}

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L150 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 159 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L153-L159

Added lines #L153 - L159 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 165 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L161-L165

Added lines #L161 - L165 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 173 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L170-L173

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

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

View check run for this annotation

Codecov / codecov/patch

cmd/peapod-to-fstree/main.go#L175-L177

Added lines #L175 - L177 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added line #L179 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 186 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L185 - L186 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 193 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L192 - L193 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L197 - L198 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 203 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L202 - L203 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L207 - L208 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 213 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L212 - L213 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 219 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L218 - L219 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 224 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L223 - L224 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 234 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L233 - L234 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 241 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L240 - L241 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 289 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L288 - L289 were not covered by tests

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

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

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L293 - L294 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 302 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L301 - L302 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 309 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L308 - L309 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 320 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L319 - L320 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 325 in cmd/peapod-to-fstree/main.go

View check run for this annotation

Codecov / codecov/patch

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

Added lines #L324 - L325 were not covered by tests

return nil
}
Loading

0 comments on commit 39f1a79

Please sign in to comment.